利用Electron构建桌面应用

3,998 阅读7分钟

只需五分钟,就会Electron

关于Electron

Electron Logo

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

核心理念:

  • 为了保持Electron的小 (文件体积) 和可持续性 (依赖和API的扩展) ,Electron限制了使用的核心项目的范围。
  • Electron只用了Chromium渲染库 而不是 全部
  • Electron所添加的的新特性应主要用于原生API(node.js)。

Chromium是什么?

Chromium是由Google主导开发的网页浏览器,说明白点:

Chromium是Chrome浏览器背后的引擎。

几个无聊的联想

Chromium + Node.js = ?

如果把 浏览器Node.js 联系在一起,一般想到的一个web服务,TCP以及其衍生协议充当了页面服务器通信的桥梁。

Web + Native = ?

浏览器可以调用原生功能,很容易让人联想到Hybrid,web通过自定义的桥接协议与Native通信,Native拦截请求从而得知web需要调用的功能。

Electron的原理是什么?

在一个Electron桌面应用分成两个进程:

  • 主进程 —— MainProcess
  • 渲染进程 —— RenderProcess

主进程负责管理原生操作,例如: 文件读写,图形处理,程序开关等,同时还管理渲染进程以及应用的生命周期。

渲染进程通过 Chromium 绘制前端界面,用户可以在页面上触发事件。

那么,Node.js 如何与Chromium通信?

答案是 IPC——进程间通信。

Electron IPC通信模型

Electron IPC提供了基于事件的API,在渲染进程和后台进程中都可以向对方发送事件,也可以在事件处理函数中通过发送的新的事件回复对方:

  • 通过EventEmitter实例向对方发送事件
  • 发送事件需要制定对方句柄,不支持全局广播
  • 支持同步事件和异步事件

创建一个桌面应用

Step 1. 环境

  • Node.js = 8.x | 9.x

Step 2. 安装electron包

npm install electron --save-dev --save-exact

如果下的慢,可以尝试淘宝镜像

Step 3. 项目结构

.
└── app
  ├── main.js
  ├── index.js
  ├── index.html
  ├── package.json

main.js 是主进程执行文件, index.html 和 index.js 是渲染进程的视图,和平时写的网页一样。

需要在package.json写明App名称等信息,同时 需要指定入口文件(即main.js),举个例子:

{
  "name": "app",
  "productName": "app",
  "version": "0.0.1",
  "main": "./app/index.js"
}

更详细的范例可以参照官方示例:electron-quick-start

Step 4. 启动electron

主进程服务加上npm scripts:

"dev:electron-main": "cross-env NODE_ENV='development' electron -r babel-register ./",

启动主进程:

npm run  dev:electron-main

Step 5. 开发

electron主进程文件修改之后需要重启应用才能生效,每次手动重启不方便,推荐使用鄙人抖机灵撸的一个包:

electron-watch

食用方法,在主进程文件中插入下面代码片段:

if (process.env.NODE_ENV === 'development') {
  require('electron-watch')(
    __dirname,
    'dev:electron-main', // means: npm run dev:electron-main
    path.join(__dirname, './'),
  );
}

** PS: 该库Windows上有bug,正确的食用发放是只按一次Ctrl+S,同时按几次会重启多个应用。 **


electron模块

electron提供了丰富的API让你调用原生的功能,参见electron文档

但是过一下下面这几个模块,就可以快速撸出一个Electron App。

1. app

一个桌面应用对象,提供API控制应用,同时可以监控应用程序的事件生命周期。

例如在最后一个窗口被关闭时退出应用:

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

可以监控的生命周期事件:

  • will-finish-launching:当应用程序完成基础的启动的时候被触发
  • ready: 当 Electron`: 完成初始化时被触发
  • window-all-closed:当所有的窗口都被关闭时触发
  • before-quit:在应用程序开始关闭窗口之前触发
  • will-quit: 当所有窗口都已关闭并且应用程序将退出时发出
  • quit:在应用程序退出时发出

2. BrowserWindow

一个浏览器窗口

创建一个浏览器窗口:

import { app, BrowserWindow } from 'electron';

app.on('ready', () => {
  const win = new BrowserWindow({width: 800, height: 600});

  // 加载远程URL
  win.loadURL('https://www.coolecho.net'); 

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

父子窗口:

const {BrowserWindow} = require('electron');
  
const top = new BrowserWindow();
/**
 * 加上model属性,子窗口为模态窗口,父窗口被禁用
 */
const child = new BrowserWindow({parent: top})
child.show()
top.show()

child 窗口将总是显示在 top 窗口的顶部,长得像这个样子:

window

3. ipcMain & ipcRender

ipcMainEventEmitter类的一个实例。 当在主进程中使用时,ipcRender处理从 渲染器进程(网页) 发送出来的异步和同步信息。 从渲染器进程发送的消息将被发送到该模块。

ipcRenderer也是一个 EventEmitter 的实例。 你可以使用它提供的一些方法从渲染进程 (web 页面) 发送同步或异步的消息到主进程。 也可以接收主进程回复的消息。

下面是在渲染和主进程之间发送和处理消息的一个例子:

// 在主进程中
const {ipcMain} = require('electron');

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

// 同步
ipcMain.on('synchronous-message', (event, arg) => {
  console.log(arg)  // prints "ping"
  event.returnValue = 'pong'
});
//在渲染器进程 (网页) 中
const {ipcRenderer} = require('electron');

// 同步请求
console.log(ipcRenderer.sendSync('synchronous-message', 'ping'));

// 异步发送请求
ipcRenderer.on('asynchronous-reply', (event, arg) => {
    console.log(arg) // prints "pong"
});
// 异步接收返回
ipcRenderer.send('asynchronous-message', 'ping');

你可以通过ipcMainipcRenderer模块,在渲染页面中调用原生功能处理一些事物,当然electron做到的远不止这些,它还提供了remote模块。使用 remote 模块, 你可以调用 主进程对象 的方法, 而不必显式发送进程间消息。

4. Menu

创建原生应用菜单和上下文菜单。

import { Menu } from 'electron';

const menuTemplete = [{
    label: 'File',
    submenu: [{
      label: 'New Note',
      accelerator: 'CmdOrCtrl+N',
      enabled: false,
      // role: 'new file',
      click: () => mainWindow.webContents.send('new-file', 1),
    }, {
      label: 'New Project',
      accelerator: 'Shift+CmdOrCtrl+N',
      enabled: false,
      // role: 'new project',
      click: () => mainWindow.webContents.send('new-project'),
    }, {
      type: 'separator',
    }, {
      label: 'Save',
      accelerator: 'CmdOrCtrl+S',
      role: 'save',
      enabled: false,
      click: () => mainWindow.webContents.send('save-content'),
    }],
 }];

const menu = Menu.buildFromTemplate(menuTemplete);

Menu.setApplicationMenu(menu);

效果:

menu

还可以创建Context菜单:

const menu = new Menu();

menu.append(new MenuItem({
    label: 'Rename',
    click: () => mainWindow.webContents.send('rename-project'),
}));
menu.append(new MenuItem({
  label: 'Delete',
  click: () => mainWindow.webContents.send('delete-project'),
}));
menu.append(new MenuItem({
  type: 'separator',
}));
menu.append(new MenuItem({
  label: 'New Notebook', 
  click: () => mainWindow.webContents.send('new-project'),
}));

// 项目右键菜单
ipcMain.on('show-context-menu-project-item', (event) => {
  const win = BrowserWindow.fromWebContents(event.sender);
  menu.popup(win);
});

效果:

menucontext

菜单项

Electron提供了很多属性来定制一个菜单项,例如上面例子中的labelclick,role等等:

  • click: 点击菜单项的回调方法
  • role: Electron定制的内置事件,有click的时候,此项将被忽略
  • type: 可以是 normalseparatorsubmenucheckboxradio
  • label: 菜单名称,当设置role时默认为role
  • accelerator: 定义快捷键
  • icon
  • sublabel
  • enabled: 如果为 false,该菜单项将会置灰且不可点击
  • visible: 控制菜单项是否可见
  • checked: 控制菜单项是否选中,typechckboxradio时有效
  • submenu: 定义子菜单项
  • id: 可以通过它来引用该菜单项
  • position: 允许对给定菜单中的特定位置进行细粒度定义(没试过)

其他模块

基本上知道上诉三个模块,就可以开发一个Electron APP了,但Electron的模块远远不止这些,它还囊括网络、电源、通知、进程、菜单、本地化、协议、会话、Shell等等模块,详细的介绍参阅Electron文档


分发应用

应用开发完了,接下来得打包了。

有三种种打包方式:

  1. 手动打包
  2. 打包工具
  3. 通过重编译源代码来进行重新定制

1,3两种方式有点繁琐,这里介绍几个常用的打包工具:

如果想在APP STORE上架应用,,这需使用XCode填写证书和开发者信息,手动打包。

我用的是electron-packer,打包起来十分方便,看看npm script

"packager:mac": "electron-packager ./lib Yosoro --overwrite --platform=darwin --arch=x64 --out=out --icon=assets/icons/osx/app.icns",
"packager:win": "electron-packager ./lib Yosoro --overwrite --platform=win32 --arch=ia32 --out=out --icon=assets/icons/win/app.ico",
"packager:linux": "electron-packager ./lib Yosoro --overwrite --platform=linux --arch=x64 --out=out",

--icon参数是用来指定应用的logo的,macOS下是.icns格式, Windows下是.ico格式。

为啥Linux的没有这个命令行参数呢?

因为Linux的Logo需要在初始化Electron应用的时候指定,格式为PNGJPEG,建议使用PNG

const options = {
    title: 'Yosoro',
    width: 1180,
    height: 786,
};
if (process.platform === 'linux') { // 加上logo
  options.icon = path.join(__dirname, './resource/app.png');
}
mainWindow = new BrowserWindow(options);

options.icon这个属性在macOSWindows下是无效的。

最后,你以为直接npm run这三条命令就完事了吗?

electron-packager只在对的地方干对的事,例如,在macOS上只打macOS上运行的包,Windows上只打Windows上运行的包,从不会干多余的打包工作。比如在macOS下会直接打成一个APP包:

packager

如果你想分别打三端的包,需要分别在macOSWindowsLinux这三个环境下打包。

总结

关于Electron还有非常强大的功能,例如热更新等等,本文只是菜鸡随手笔记,有错误的地方欢迎指正。

最后,关于Electron结合react的实例 戳这里,求star。