# 核心概念

# 主进程与渲染进程

  • 主进程与渲染进程

    • 主进程:启动项目时运行的 main.js 脚本就是我们说的主进程。在主进程运行的脚本可以以创建 web 页面的形式展示 GUI,主进程只有一个
    • 渲染进程: 每个 Electron 的页面都在运行着自己的进程,这样的进程称之为渲染进程(基于Chromium的多进程结构)。
  • Electron的架构

    image-20220129172505667

说明:

  • 主进程使用BrowserWindow创建实例;
  • 主进程销毁后,对应的渲染进程会被终止;
  • 主进程与渲染进程通过IPC方式(事件驱动)进行通信;

# 如何调试Electon

Electron中集成了Chromium内核,可以通过菜单打开devTools窗口与chrome中一样。

image-20220129173713968

还可以在开发的时候,通过设置主窗口自动打开devTools:

win.webContents.openDevTools()

还可以配合着nodemon对Electron的代码进行调试(自动重启)。

还可以使用VSCode的调试功能进行单步调试,新增文件.vscode/launch.json配置如下:

{
  // 使用 IntelliSense 了解相关属性。 
  // 悬停以查看现有属性的描述。
  // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Electron Main",
      "program": "${workspaceFolder}/index.js",
      "request": "launch",
      "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
      "skipFiles": [
        "<node_internals>/**"
      ],
      "type": "pwa-node"
    }
    
  ]
}

# 常见API

# 主进程生命周期

官方文档:https://www.electronjs.org/zh/docs/latest/api/app (opens new window)

const { app } = require('electron')
app.on('window-all-closed', () => {
  app.quit()
})

常见的事件:

  • 'will-finish-launching':当应用程序完成基础的启动的时候被触发。;
  • 'ready':当 Electron 完成初始化时,发出一次;
  • 'window-all-closed':当所有的窗口都被关闭时触发;
  • 'before-quit':在程序关闭窗口前发信号;
  • 'will-quit':当所有窗口被关闭后触发,同时应用程序将退出;
  • 'quit':在应用程序退出时发出。

# 渲染进程使用Node

后续会介绍安全原则 ,一般不推荐直接在渲染进程中使用node,因为这样不安全。

一般在渲染进程中,通过映射的方式,把主进程中的node相关的接口进行映射使用,不是直接调用node。

示例代码:

const createWindow = () => {
	const win = new BrowerWindow({
    // ...
    webPreferences: {
      nodeIntergration: true,
      // 最新版本的electron v15需要添加
      contextIsolation: false,
    }
  })
}

上面的改造方式,适合老的项目改造成Electron桌面端的应用的。

# 通信

# 主进程与渲染进程

修改index.js项目的入口文件:

// 在主进程中.
const { ipcMain } = require('electron')
ipcMain.on('asynchronous-message', (event, arg) => {
  console.log(arg) // prints "ping"
  event.reply('asynchronous-reply', 'pong')
})

ipcMain.on('synchronous-message', (event, arg) => {
  console.log(arg) // prints "ping"
  event.returnValue = 'pong'
})

修改index.html中的scripts部分

//在渲染器进程 (网页) 中。
// 注意: Electron 的 API 在预加载后即可访问,直到上下文隔离被禁用
// 详见: https://www.electronjs.org/docs/tutorial/process-model#preload-scripts
const { ipcRenderer } = require('electron')
console.log(ipcRenderer.sendSync('synchronous-message', 'ping')) // prints "pong"

ipcRenderer.on('asynchronous-reply', (event, arg) => {
  console.log(arg) // prints "pong"
})
ipcRenderer.send('asynchronous-message', 'ping')

说明:

  • 使用ipc通信的一些应用场景:

    • 使用主进程进行请求可以解决跨域问题;
    • 背景音频&视频;
    • 获取数据任务(定时任务);
    • canvas渲染;
    • 后台任务等;
  • 在Electron v14之后,已经移除了remote模块,没有了remote通信方式了。

# 渲染进程之间

这个部分可能不太好理解。

ScreenFlow

主进程index.js文件

const { app, BrowserWindow, ipcMain, MessageChannelMain } = require('electron');

// 主进程
const createWindow = async () => {
  // 创建浏览器窗口
  const worker = new BrowserWindow({
    show: false,
    webPreferences: {
      nodeIntegration: true,
      // 最新版本的Electron v15
      contextIsolation: false,
    },
  });

  // 加载index.html文件
  await worker.loadFile('child.html');

  // 创建浏览器窗口
  const win = new BrowserWindow({
    width: 1200,
    height: 600,
    webPreferences: {
      nodeIntegration: true,
      // 最新版本的Electron v15
      contextIsolation: false,
    },
  });

  // 加载index.html文件
  win.loadFile('index.html');

  win.webContents.openDevTools();

  // 主进程与渲染进程通信
  ipcMain.on('my-channel', (event, args) => {
    // console.log('🚀 ~ file: index.js ~ line 55 ~ ipcMain.on ~ args', args);
    // console.log('🚀 ~ file: index.js ~ line 55 ~ ipcMain.on ~ event', event);
    // event.reply('child-channel', { event: 'msg', data: 'hello child!' });
    // For security reasons, let's make sure only the frames we expect can
    // access the worker.
    if (event.senderFrame === win.webContents.mainFrame) {
      // Create a new channel ...
      const { port1, port2 } = new MessageChannelMain();
      // ... send one end to the worker ...
      worker.webContents.postMessage('new-work', null, [port1]);
      // ... and the other end to the main window.
      event.senderFrame.postMessage('child-channel', null, [port2]);
      // Now the main window and the worker can communicate with each other
      // without going through the main process!
    }
  });
};

app.on('window-all-closed', () => {
  console.log('window-all-closed');
  // 对于MacOS系统 -> 关闭窗口时,不会直接退出应用
  if (process.platform !== 'darwin') app.quit();
});
app.on('will-finish-launching', () => {
  console.log('will-finish-launching');
});
app.on('ready', () => {
  console.log('ready');
});
app.on('will-quit', () => {
  console.log('will-quit');
});
app.on('before-quit', () => {
  console.log('before-quit');
});
app.on('quit', () => {
  console.log('quit');
});

app.whenReady().then(() => {
  createWindow();

  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) {
      createWindow();
    }
  });
});

渲染进程1index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Electorn Demo</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.14/vue.min.js"></script>
  </head>
  <body>
    Hello Electron
    <div id="app">
      hello1
      <p>Electron Version:{{electronVersion}}</p>
      <p>Node Version: {{nodeVersion}}</p>
      <p>Chromium version: {{chromeVersion}}</p>
      <p>count: {{count}}</p>
      <p>result: {{result}}</p>
      <button @click="sendMsg">发送消息给main</button>
    </div>
    <script>
      // const path = require('path');
      // console.log(path.resolve(__dirname, 'test.js'));
      // console.log(process.versions);
      const { ipcRenderer } = require('electron');

      ipcRenderer.on('child-channel', (event, arg) => {
        const [port] = event.ports;

        port.onmessage = (event) => {
          // 取worker返回的处理过后的数据
          app.result = event.data;
        };

        port.postMessage(app.count);
      });

      const app = new Vue({
        el: '#app',
        data: {
          electronVersion: process.versions.electron,
          nodeVersion: process.versions.node,
          chromeVersion: process.versions.chrome,
          count: 10,
          result: 0,
        },
        methods: {
          sendMsg: function () {
            // 准备的测试数据 -> 发送给worker渲染进程 -> 接收处理完成的数据
            this.count += 10;
            ipcRenderer.send('my-channel');
          },
        },
      });
    </script>
  </body>
</html>

渲染进程2index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      // ipcRenderer -> 建立与主ipc的通信
      // onmessage -> 接收通道的消息
      // postMessage -> 发送处理完成的数据消息 -> result
      const { ipcRenderer } = require('electron');
      function doWork(input) {
        return input * 2;
      }

      ipcRenderer.on('new-work', (event, args) => {
        const [port] = event.ports;
        port.onmessage = (event) => {
          // 从index渲染进行(通道1)传递过来的数据
          const result = doWork(event.data);
          // 响应通道数据 -> 通道2 发送数据
          port.postMessage(result);
        };
      });
    </script>
  </body>
</html>

# 安全原则

这一块,参考官方文档:安全性,原生能力和你的责任 (opens new window)

为加强程序安全性,你至少应当遵循下列规则:

  1. 只加载安全的内容 (opens new window)
  2. 禁止在所有渲染器中使用Node.js集成显示远程内容 (opens new window)
  3. 在所有显示远程内容的渲染器中启用上下文隔离。 (opens new window)
  4. Enable sandboxing (opens new window)
  5. 在所有加载远程内容的会话中使用 ses.setPermissionRequestHandler(). (opens new window)
  6. 不要禁用 webSecurity (opens new window)
  7. 定义一个Content-Security-Policy (opens new window)并设置限制规则(如:script-src 'self')
  8. 不要设置 allowRunningInsecureContent 为 true. (opens new window)
  9. 不要开启实验性功能 (opens new window)
  10. 不要使用enableBlinkFeatures (opens new window)
  11. webview:不要使用 allowpopups (opens new window)
  12. <webview>:验证选项与参数 (opens new window)
  13. 禁用或限制网页跳转 (opens new window)
  14. 禁用或限制新窗口创建 (opens new window)
  15. 不要对不可信的内容使用 openExternal (opens new window)
  16. 使用当前版本的 Electron (opens new window)