Electron - 蓦然回首

1,239 阅读12分钟

一、Electron 是什么?

Electron 是由 Github 开发,用 HTMLCSSJavaScript 来构建跨平台桌面应用程序的一个开源库。 Electron 通过将 ChromiumNode.js 合并到同一个运行时环境中,并将其打包为 MacWindowsLinux 系统下的应用来实现这一目的。



二、快速启动

# 克隆示例项目的仓库
$ git clone https://github.com/electron/electron-quick-start
# 进入这个仓库
$ cd electron-quick-start
# 安装依赖并运行
$ npm install && npm start

三、简单示例

项目启动后,你会看到一个 Hello World! 应用窗口,很简单,就不细说了。我更感兴趣的是 Electron API Demos app ,大家可以去官网下载

Electron API Demos app

Electron API Demos app 中,我们可以查看官方 API 和 一些 Demo 演示,强烈建议去实际感受一下。只有你知道了它的强大之处,你才会兴奋不已,点燃学习的激情。


四、目录结构


这里我们主要关注如下这三个文件:
1.main.js,也就是整个应用的入口,主要用来创建桌面应用窗口,并监听主进程的事件。

const {
    app,
    BrowserWindow
} = require('electron')
const path = require('path')
const url = require('url')
let mainWindow

function createWindow() {
    mainWindow = new BrowserWindow({
        width: 800,
        height: 600
    })
    // 启动文件入口,如 index.html
    mainWindow.loadURL(url.format({
        pathname: path.join(__dirname, 'index.html'),
        protocol: 'file:',
        slashes: true
    }))
    // 开启 Chromium DevTools
    mainWindow.webContents.openDevTools()
    // 监听窗口关闭事件
    mainWindow.on('closed', function () {
        mainWindow = null
    })
}
// 加载就绪
app.on('ready', createWindow)

// 监听所有窗口关闭的事件
app.on('window-all-closed', function () {
    if (process.platform !== 'darwin') {
        app.quit()
    }
})
// 激活窗口
app.on('activate', function () {
    if (mainWindow === null) {
        createWindow()
    }
})


2.index.html ,属于渲染进程,单纯的页面而已。

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
			<title>Hello World!</title>
		</head>
		<body>
			<h1>Hello World!</h1>
			<!-- 在渲染进程中可以使用所有的 Node.js APIs -->
    We are using Node.js 
			<script>document.write(process.versions.node)</script>,
    Chromium 
			<script>document.write(process.versions.chrome)</script>,
    and Electron 
			<script>document.write(process.versions.electron)</script>.

    
			<script>
      // 当然,我们也可以引入其他 js 文件
      require('./renderer.js')
    </script>
		</body>
	</html>

3.renderer.js ,属于渲染进程,也就是我们页面的业务逻辑所要加载的 JS,简单的例子可能如下。

let myNotification = new Notification('Jartto', {
    body: 'Hello Everybody!'
})

myNotification.onclick = () => {
    console.log('Click!')
}

五、整装代发

在开始 Coding 前,我们需要认识一下桌面应用的基本构成,以及基础模块。这里以 OS X 示例:

base modules

上图列出了一个桌面应用应该涵盖的基本特征,当然,还有更多无法视觉感知,但却依然重要的底层模块。下文将一一罗列,并尽量用示例说明。


六、主进程与渲染进程

Electron 中,由 package.json 中的 main.js 运行出来的进程为主进程(Main Process)。主进程用于创建 GUI 界面以便 web 页面的展示。ElectronChromium 负责页面的显示,所以当创建一个页面时,就会对应的创建渲染进程(Renderer Process)。


Electron Process

主进程通过创建 BrowserWindow 对象来创建 web 显示页面,BrowserWindow 运行在他自己的渲染进程中。当 BrowserWindow 被销毁时,对应的渲染进程也会终止。

七、进程间通信

既然有两个进程,那么肯定会涉及到进程间相互通讯,这里介绍两个概念:
1.Electron ipcMain 模块
ipcMain 模块是类 EventEmitter 的实例。当在主进程中使用它的时候,它控制着由渲染进程(web page)发送过来的异步或同步消息。从渲染进程发送过来的消息将触发事件。

2.Electron ipcRenderer 模块
pcRenderer 模块是一个 EventEmitter 类的实例。它提供了有限的方法,你可以从渲染进程向主进程发送同步或异步消息。也可以收到主进程的相应。

在主进程中,也就是 main.js 中,添加如下代码:

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

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

在渲染进程,也就是 ipc/index.js 文件中添加如下代码:

// 渲染进程 (web page).
const ipcRenderer = require('electron').ipcRenderer;
console.log(ipcRenderer.sendSync('synchronous-message', 'sync-ping')); // prints "pong"

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

详细代码可以查看代码示例:ipc

还有一种进程间的通讯方式:Electron remote 模块。

remote 模块提供了一种在渲染进程(网页)和主进程之间进行进程间通讯(IPC)的简便途径。

通过 remote 调用主进程中 BrowserWindow 来创建应用窗口:

// 渲染进程
const remote = require('electron').remote;
const BrowserWindow = remote.BrowserWindow;

let win = new BrowserWindow({
    width: 100,
    height: 100
});
win.loadURL('http://www.jartto.wang');

详细代码可以查看代码示例:remote

八、BrowserWindow 模块

在主进程 main.js 中,我们也看到了 BrowserWindow 的使用,简单的调用窗口很容易,更复杂的应用可能需要我们去翻阅文档,熟读 API

// 在主进程中.
const {
    BrowserWindow
} = require('electron')

// 或者从渲染进程中使用 `remote`.
// const {BrowserWindow} = require('electron').remote

let win = new BrowserWindow({
    width: 800,
    height: 600
})
win.on('closed', () => {
    win = null
})

// 加载远程 URL
win.loadURL('http://jartto.wang')

// 或加载本地 HTML 文件
win.loadURL(`file://${__dirname}/app/index.html`)


九、使用 Webview

iframe 不同,webview 和你的应用运行的是不同的进程。它不拥有渲染进程的权限,并且应用和嵌入内容之间的交互全部都是异步的,因为这能保证应用的安全性不受嵌入内容的影响。

webview 运行在渲染进程中,我们添加如下的 js 代码:

window.onload = function () {
    var webview = document.getElementById("foo");
    var indicator = document.querySelector(".indicator");

    var loadstart = function () {
        indicator.innerText = "loading...";
    }
    var loadstop = function () {
        indicator.innerText = "";
    }

    webview.addEventListener("did-start-loading", loadstart);
    webview.addEventListener("did-stop-loading", loadstop);
    webview.addEventListener("dom-ready", function () {
        // webview.openDevTools();
        console.log(webview.getTitle());
    });
}


并在 html 中添加两个容器:

<div class="indicator"></div>

<webview id="foo" src="http://www.jartto.wang/"></webview>

为了避免太过丑陋,我们增加如下 css 代码:

body {
    margin: 0;
}
#foo {
    width: 100%;
    height: 100vh;
}

ok,webview 的使用就这么简单的搞定了。

详细代码可以查看代码示例:webview

十、Dialog 模块

Dialog 模块提供了 API 来展示原生的系统对话框,例如打开文件框,alert 框,这样 web 应用可以给用户带来跟系统应用相同的体验。下面给出简单的示例:

1.打开选择文件和目录

var win = ...; // BrowserWindow in which to show the dialog
const dialog = require('electron').dialog;
console.log(dialog.showOpenDialog({
    properties: ['openFile', 'openDirectory', 'multiSelections']
}));

2.错误框提示

const dialog = require('electron').dialog;
dialog.showErrorBox('Jartto', 'Hello world');
// 或者传入更多的参数
// console.log(dialog.showMessageBox({
//   browserWindow: mainWindow,
//   options: {
//     type: 'error',
//     title: 'Jartto',
//     message: 'Hello World',
//   }
// }));

详细代码可以查看代码示例:main.js

十一、Global-Shortcut 模块

global-shortcut 模块可以便捷的为您设置(注册/注销)各种自定义操作的快捷键,下面我们来为应用注册一个 ctrl+x 的全局快捷键,当我们按下快捷键,系统弹窗消息反馈给我们

const globalShortcut = require('electron').globalShortcut;
const dialog = require('electron').dialog;

app.on('ready', function () {
    createWindow();
    // Register a 'ctrl+x' shortcut listener.
    var ret = globalShortcut.register('ctrl+x', function () {
        dialog.showErrorBox('Jartto', 'ctrl+x is pressed')
    })

    if (!ret) {
        dialog.showErrorBox('Jartto', 'registration failed')
    }

    // Check whether a shortcut is registered.
    console.log(globalShortcut.isRegistered('ctrl+x'));
})

app.on('will-quit', function () {
    // Unregister a shortcut.
    globalShortcut.unregister('ctrl+x');

    // Unregister all shortcuts.
    globalShortcut.unregisterAll();
});

详细代码可以查看代码示例:main.js

十二、Menu 模块

Menu 分为三种,每种菜单的用法也不一样,下面我们分别举例说明:
1.系统菜单

// 窗口菜单
const remote = require('electron').remote;
const Menu = remote.Menu;

let template = [...]; // 省略参数,具体请查看 demo
let menu = Menu.buildFromTemplate(template);

Menu.setApplicationMenu(menu);

2.上下文菜单

const remote = require('electron').remote;
const Menu = remote.Menu;
const MenuItem = remote.MenuItem;

// 右键菜单
var menu = new Menu();
menu.append(new MenuItem({
    label: 'MenuItem1',
    click: function () {
        console.log('item 1 clicked');
    }
}));
menu.append(new MenuItem({
    type: 'separator'
}));
menu.append(new MenuItem({
    label: 'MenuItem2',
    type: 'checkbox',
    checked: true
}));

window.addEventListener('contextmenu', function (e) {
    e.preventDefault();
    menu.popup(remote.getCurrentWindow());
}, false);

3.托盘菜单

let appIcon = new Tray(path.normalize(__dirname + '/image/icon.png'));
let contextMenu = Menu.buildFromTemplate([
    {
        label: '菜单一',
        type: 'radio'
    },
    {
        label: '菜单二',
        type: 'radio'
    },
    {
        label: '菜单三',
        type: 'radio',
        checked: true
    },
    {
        label: '关于',
        type: 'radio'
    }
]);

appIcon.setContextMenu(contextMenu);

详细代码可以查看代码示例:menu


十三、Electron power-monitor

power-monitor 模块是用来监听能源区改变的。只能在主进程中使用。在 app 模块的 ready 事件触发之后就不能使用这个模块了。

// 能源区变化,系统挂起和恢复,交流电和电池切换时
require('electron').powerMonitor.on('suspend', function () {
    console.log('The system is going to sleep');
});
require('electron').powerMonitor.on('on-ac', function () {
    console.log('The system is using AC power');
});

详细代码可以查看代码示例:main.js

十四、系统托盘 Tray

在 OS X 中,托盘一般会出现在显示器右上角的位置,一个简单的示例可能如下:

appIcon = new Tray(path.normalize(__dirname + '/image/icon.png'));
var contextMenu = Menu.buildFromTemplate([
    {
        label: 'Item1',
        type: 'radio'
    },
    {
        label: 'Item2',
        type: 'radio'
    },
    {
        label: 'Item3',
        type: 'radio',
        checked: true
    },
    {
        label: 'Item4',
        type: 'radio'
    }
]);
console.log(appIcon);
appIcon.setToolTip('This is my application.');
appIcon.setContextMenu(contextMenu);

如果想同时支持展示不同分辨率的图片,你可以将拥有不同 size 的图片放在同一个文件夹下,不用 DPI 后缀.例如 :

images/├── icon.png├── icon@2x.png└── icon@3x.png

详细代码可以查看代码示例:main.js

十五、Electron desktopCapturer 模块

desktopCapturer 模块可用来获取可用资源,这个资源可通过 getUserMedia 捕获得到。这样,我们就可以随便捕获其他的窗口,以及显示屏内容了。这里我们来抓取名称是 Screen 2 的屏幕,代码如下:

// 在渲染进程中.
var desktopCapturer = require('electron').desktopCapturer;

desktopCapturer.getSources({
    types: ['window', 'screen']
}, function (error, sources) {
    console.log('sources', sources);
    if (error) throw error;
    for (var i = 0; i < sources.length; ++i) {
        if (sources[i].name == "Screen 2") {
            navigator.webkitGetUserMedia({
                audio: false,
                video: {
                    mandatory: {
                        chromeMediaSource: 'desktop',
                        chromeMediaSourceId: sources[i].id,
                        minWidth: 1280,
                        maxWidth: 1280,
                        minHeight: 720,
                        maxHeight: 720
                    }
                }
            }, gotStream, getUserMediaError);
            return;
        }
    }
});

function gotStream(stream) {
    console.log(stream, 'stream');
    document.querySelector('video').src = URL.createObjectURL(stream);
}

function getUserMediaError(e) {
    console.log('getUserMediaError');
}

html 中,需要添加 video 元素:

<video> desktop capturer </video>

十六、Electron webFrame 模块

接着继续上面的话题,我们要将所有窗口都捕获在一个窗口显示,那么你可能会用到 web-frame ,该模块允许你自定义如何渲染当前网页。例如,放大当前页到 200%

let webFrame = require('electron').webFrame;

webFrame.setZoomFactor(2);

详细代码可以查看代码示例:webFrame

十七、Electron clipboard 模块

clipboard 模块提供方法来供复制和粘贴操作,非常实用的一个功能。下面例子展示了如何将一个字符串写入 clipboard :

const clipboard = require('electron').clipboard;

clipboard.writeText('Hello Jartto!');

详细代码可以查看代码示例:clipboard

十八、Electron crashReporter 模块

crash-reporter 模块开启发送应用崩溃报告,这个就没什么好说了,收集反馈信息为了更好的用户体验

const crashReporter = require('electron').crashReporter;

crashReporter.start({
    productName: 'Jartto',
    companyName: 'Jartto blog',
    submitURL: 'http://jartto.com/url-to-submit',
    autoSubmit: true
});

十九、Electron screen 模块

screen 模块检索屏幕的 size,显示,鼠标位置等的信息。在 app 模块的 ready 事件触发之前不可使用这个模块。

const electron = require('electron');
const app = electron.app;
const BrowserWindow = electron.BrowserWindow;

var mainWindow;

app.on('ready', function () {
    var electronScreen = electron.screen;
    var size = electronScreen.getPrimaryDisplay().workAreaSize;
    mainWindow = new BrowserWindow({
        width: size.width,
        height: size.height
    });
});

二十、Electron shell 模块

shell 模块提供了集成其他桌面客户端的关联功能。

在渲染进程中使用
const {
    shell
} = require('electron')
// 播放声音
shell.beep();
// shell.openItem('/Users/Jartto/Documents/my-demo');
shell.openExternal('http://jartto.wang');

二十一、Notification 通知模块

所有三个操作系统都提供了应用程序向用户发送通知的手段。 Electron 允许开发者使用 HTML5 Notification API 发送通知,并使用当前运行的操作系统的本地通知 API 来显示它。

注意: 由于这是一个 HTML5 API,它只能在渲染器进程中使用。 如果你想在主进程中显示通知,请查看 Notification 模块.

let myNotification = new Notification('Jartto', {
    body: 'Hello Everybody!'
})

myNotification.onclick = () => {
    console.log('通知被点击')
}

二十二、在 Dock 中显示

OS XDock 默认会被打开,我们还可以在主进程中做一些额外的控制,如隐藏、设置菜单等操作。

app.dock.hide();
app.dock.setMenu(menu);

二十三、Demo 汇总

所有的示例代码都在这里,为了方便切换启动页面,我们通过不同的目录来区分,你只需要更改 main.jspath 中的路径即可,如下:

ainWindow.loadURL(url.format({
    pathname: path.join(__dirname, '${dir-name}/index.html'),
    protocol: 'file:',
    slashes: true
}))

Demo 目录结构如下:


你可以更改 ${dir-name} 来切换不同的启动项,查看演示。

二十四、代码调试

要开发一个完整的 Electron 应用,肯定会用到代码调试,这里暂且分为两部分调试:
1.对 GUI 的调试,也就是主进程的调试,可以借助开发工具,这里我用 VS Code 来举例说明:

  • .vscode 目录下添加 launch.json 文件,如果有的话,直接第二步;
  • 打开 launch.json,写入如下配置;

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "Electron Main",
            "protocol": "auto",
            "program": "${workspaceFolder}/electron-quick-start/main.js"
    },
  ]
}

  • 运行 debug,就可以了;

2.渲染进程调试,可以开启调试工具:

// 主进程中使用

mainWindow.webContents.openDevTools()

之后的调试就向你在 Chrome 中使用 DevTool 一样,简单而又高效。

二十五、打包发布

Electron 应用开发完成之后,打包生产相应平台应用,通常推荐的工具有 electron-packagerasar

1.用 electron-packager 为所有系统打包你的应用很简单。简单来说,electron-packager 帮你完成所有用Electron 打包你应用的工作,最终生成你要发布的平台的安装包。

它可以作为 CLI 应用或构建过程的一部分,更复杂的构建情况不在本文所涉及范围内,但我们如果能用打包脚本,会使打包更简单。用 electron-packager 比较麻烦,打包应用的基本命令是:

electron-packager <location of project> <name of project> <platform> <architecture> <electron version> <optional options>

具体的参数说明如下:

  • location of project 是你项目文件夹的位置;
  • name of project 定义你的项目名;
  • platform 决定要构建的平台(
    all
    包括 Windows ,Mac 和 Linux );
  • architecture 决定构建哪个构架下(x86或x64,all表示两者);
  • electron version 让你选择要用的 Electron 版本;

第一次打包用时比较久,因为要下载平台的二进制文件,随后的打包将会快的多。

OS X 下打包应用的命令如下:

electron-packager ~/Projects/jartto-demo MyElectron --all --version=0.30.2 --out=~/Desktop --overwrite --icon=~/Projects/jartto-demo/app/img/app-icon.icns

命令的选项理解起来都比较简单。为了获得精美的图标,你首先要找一款类似这个软件可以把 PNG 文件转换到这些格式的工具,把它转换成 .icns 格式( Mac 用)或者 .ico 格式( Window 用)。如果在非 Windows 系统给Windows 平台的应用打包,你需要安装 wineMac 用户用 brewLinux 用户用 apt-get)。

每次都打这么长的命令很不方便,可以在 package.json 中加另一个脚本。首先,把 electron-packager 作为开发依赖安装:

npm install --save-dev electron-packager

现在我们可以在 package.json 中添加脚本:

"scripts": {
    "start": "electron .",
    "package": "electron-packager ./ MyElectron --all --out ~/Desktop/jartto-demo --version 0.30.2 --overwrite --icon=./app/img/app-icon.icns"
}

在命令行里执行下面的命令:

npm run-script package

这个打包命令会启动 electron-packager,在当前目录下找到目标应用文件,打包,保存到桌面。如果你用的是Windows 系统,需要修改脚本,不过改动很小。

2.使用 asar
安装 asar

npm i asar -g


npm i asar --save-dev

编辑 package.json

"scripts": {
    "start": "electron .",
    "build": "electron-packager . MyFirstApp --ignore=node_modules/electron-* --platform=win32 --arch=x64 --version=0.32.3",
    "package": "asar pack MyFirstApp-win32-x64/resources/app MyFirstApp-win32-x64/resources/app.asar && rm -rf MyFirstApp-win32-x64/resources/app"
}

然后执行:

npm run package

二十六、Electron 源代码结构

当然,如果你需要深入了解 Electron ,官方也提供了一些帮助,我们可以通过如下的结构来深入学习


二十七、学习资源

学习过程中,发现了很多不错的资源,分享给大家,共同进步吧:
Electron 官方文档
Electron 中文文档
Electron 的入门学习
用 Electron 开发桌面应用
Electron-Docs
vue,electron 实现网易云音乐客户端
awesome electron
使用 Electron 构建跨平台的抓取桌面程序
electron-builder
gulp-electron


二十八、调用dll底层模块

安装环境(mac环境无需安装第1步)

  1. 管理员权限执行npm install --global windows-build-tools 安装python和C++环境
    如果无法安装可以单独安装
  • python(v2.7 ,3.x不支持)
  • visual C++ Build Tools,或者vs2015及以上
  1. npm install -g node-gyp 编译原生node模块(electron使用node原生模块需要编译)
  2. node-gyp list 查看是否缺少node.lib 库,并按照提示安装 node-gyp install
  3. src/main/index.js中启用渲染层node集成

通过 node-ffi 模块调用C/C++ dll

  1. npm install ffi-napi --save 安装调用C/C++ dll的模块

    • 如果找不到python路径,设置python环境变量或者npm config set python你的python路径
    • 如果还不行尝试使用yarn安装
  2. npm install electron-rebuild ---save-dev 安装自动编译原生node模块

    • 如果不安装的话可以进行手动编译,方法如下
    • 进入node_module/ffi-napi文件夹执行node-gyp rebuild --target='当前electron的版本' --arch=x64 --dist-url=https://atom.io/download/atom-shell编译node原生模块
    • arch : 计算机的架构(x64或者ia32),如果node环境是32位,那么这里就是ia32,如果是node环境是64位,那么这里就是x64。
    • 如果路径错误,换成国内镜像路径 --dist-url=https://npm.taobao.org/mirrors/atom-shell
    • 再进入node_module/ref-napi文件夹执行node-gyp rebuild --target='当前electron的版本' --arch=x64 --dist-url=https://atom.io/download/atom-shell编译node原生模块
  3. 如果自动编译的话执行npx electron-rebuild,上步手动编译则不需要这一步了

  4. 剩下的就是在主进程里面调用native方法即可,渲染进程调用通过ipcRendereripcMain通信调用即可,举个小例子

// 主进程
// 调用示例
const ffi = require('ffi-napi')
const CTEST = ffi.Library('dll文件路径', {
    // 文件内的方法和参数类型
    'Add': ['float', ['float', 'float']],
    'Hello': ['string', []],
    'StrLength': ['int', ['string']]
})
// 同步调用
CTEST.Hello()

// 异步调用
CTEST.StrLength.async('1234', (error, res) => {
    console.log(error, res)
})

这里有两点需要注意一下

  • 如果没有使用electron-vue这个框架(electron-vue已经处理好静态文件路径),需要注意打包后的静态文件路径会有些问题,可以在在package.json文件中配置build后dll的文件存放

"extraFiles": [
    {
        "from": "",
        "to": ""
    }
]

  • 64位的dll不能再32位机器上调用,会报错

通过 electron-edge-js 模块调用C# dll

  1. npm install electron-edge-js --save 安装调用C# dll的模块
  2. 这里electron的版本不能为8.x,版本7.x的页面刷新有bug,目前这个模块支持情况

* Electron 1.6.x - Node.js v7.4.0.
* Electron 1.7.x - Node.js v7.9.0.
* Electron 1.8.x - Node.js v8.2.1.
* Electron 2.x - Node.js v8.9.3.
* Electron 3.x - Node.js v10.2.0.
* Electron 4.0.4+ - Node.js v10.11.0.
* Electron 5.x - Node.js v12.0.0.
* Electron 6.x - Node.js v12.4.0.
* Electron 7.x - Node.js v12.8.1

// 主进程
// 调用示例

const edge = require('electron-edge-js')
const edgeDll = edge.func({
    assemblyFile: 'dll文件路径',
    typeName: "Edge_test.Class1",
    methodName: "Concat"
})

edgeDll({
    first: 'aaa',
    second: 'bb'
}, function (error, result) {
    if (error) throw error
    console.log('C# DLL:', result)
})

通过robotjs控制鼠标键盘、node-serialport进行串口通讯等

    1. 使用方法这里不再赘述,文档查询即可。这些模块也是node原生模块,因此需要编译
    2. 自动编译同上npx electron-build,有时候自动编译不好使,就需要手动编译
    3. 手动编译 npm rebuild --runtime=electron --disturl=https://atom.io/download/atom-shell --target=<electron版本> --abi=<对应node版本的abi>或者直接进入模块文件夹中通过node-gyp编译:node-gyp rebuild
    4. abi查询网址:github.com/mapbox/node…