早期桌面应用的开发主要借助原生 C/C++ API
进行,由于需要反复经历编译过程,且无法分离界面 UI
与业务代码,开发调试极为不便。后期出现的 QT 和 WPF
在一定程度上解决了界面代码分离和跨平台的问题,却依然无法避免较长时间的编译过程。近几年伴随互联网行业的迅猛发展,尤其是
NodeJS、Chromium 这类基于 W3C 标准开源应用的不断涌现,原生代码与 Web
浏览器开发逐步走向融合,Electron 正是在这种背景下诞生的。
Electron 是由 Github 开发,通过将Chromium 和NodeJS 整合为一个运行时环境,实现使用
HTML、CSS、JavaScript 构建跨平台的桌面应用程序的目的。Electron 源于 2013
年 Github 社区提供的开源编辑器 Atom,后于 2014 年在社区开源,并在 2016
年的 5 月和 8 月,通过了 Mac App Store 和 Windows Store
的上架许可,VSCode、Skype 等著名开源或商业应用程序,都是基于 Electron
打造。为了方便编写测试用例,笔者在 Github 搭建了一个简单的 Electron
种子项目Octopus ,读者可以基于此来运行本文涉及的示例代码。
construction
construction
Getting Start
首先,让我们通过npm init
和git init
新建一个项目,然后通过如下npm
语句安装最新的
Electron 稳定版。
1 ➜ npm i -D electron@latest
然后向项目目录下的package.json
文件添加一条scripts
语句,便于后面通过npm start
命令启动
Electron 应用。
1 2 3 4 5 6 7 8 9 10 11 { "author" : "Hank" , "main" : "resource/main.js" , "scripts" : { "start" : "electron ." } , "devDependencies" : { "electron" : "^3.0.7" } }
然后在项目根目录下新建resource
文件夹,里面分别再建立index.html
和main.js
两个文件,最终形成如下的项目结构:
1 2 3 4 5 6 7 8 electron-demo ├── node_modules ├── package.json ├── package-lock.json ├── README.md └── resource ├── index.html └── main.js
main.js
是 Electron
应用程序的主入口点,当在命令行运行这段程序的时候,就会启动一个 Electron
的主进程 ,主进程当中可以通过代码打开指定的 Web
页面去展示 UI。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 const { app, BrowserWindow } = require ("electron" );let mainWindow;app.on ("ready" , () => { mainWindow = new BrowserWindow ({ width : 800 , height : 500 }); mainWindow.setMenu (null ); mainWindow.on ("closed" , () => { mainWindow = null ; }); }); app.on ("window-all-closed" , () => { if (process.platform !== "darwin" ) { app.quit (); } }); app.on ("activate" , () => { if (mainWindow === null ) { createWindow (); } });
Web
页面index.html
运行在自己的渲染进程 当中,但是能够通过
NodeJS 提供的 API
去访问操作系统的原生资源(例如下面代码中的process.versions
语句 ),这正是
Electron 能够跨平台执行的原因所在。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <!DOCTYPE html > <html > <head > <meta charset ="UTF-8" /> <title > Hello Electron</title > </head > <body > <h1 > 你好,Electron!</h1 > <h2 > 当前Electron版本: <script > document .write (process.versions .electron ); </script > </h2 > <h2 > 当前NodeJS版本: <script > document .write (process.versions .node ); </script > </h2 > <h2 > 当前Chromium版本: <script > document .write (process.versions .chrome ); </script > </h2 > <script > require ("./renderer.js" ); </script > </body > </html >
使用命令行工具执行npm start
命令之后,上述 HTML
代码在笔者 Linux
操作系统内被渲染为如下界面。应用当中,可以通过CTRL+R
重新加载页面,或者使用CTRL+SHIFT+I
打开浏览器控制台。
hello-electron
hello-electron
一个 Electron
应用的主进程 只会有一个,渲染进程 则会有多个。
主进程与渲染进程
主进程 (main process )管理所有的 web
页面以及相应的渲染进程,它通过BrowserWindow
来创建视图页面。
渲染进程 (renderer
processes )用来运行页面,每个渲染进程都对应自己的BrowserWindow
实例,如果实例被销毁那么渲染进程就会被终止。
structure
structure
Electron
分别在主进程 和渲染进程 提供了大量
API,可以通过require
语句方便的将这些 API
包含在当前模块使用。但是 Electron 提供的 API
只能用于指定进程类型,即某些 API
只能用于渲染进程,而某些只能用于主进程,例如上面提到的BrowserWindow
就只能用于主进程。
1 2 3 const { BrowserWindow } = require ("electron" );ccc = new BrowserWindow ();
Electron 通过remote
模块暴露一些主进程的
API,如果需要在渲染进程中创建一个BrowserWindow
实例,那么就可以借助这个
remote
模块:
1 2 3 4 const { remote } = require ("electron" ); const { BrowserWindow } = remote; const browserWindow = new BrowserWindow ();
Electron 可以使用所有 NodeJS 上提供的
API,同样只需要简单的require
一下。
1 2 3 const fs = require ("fs" );const root = fs.readdirSync ("/" );
当然,NodeJS 上数以万计的 npm 包也同样在 Electron
可用,当然,如果是涉及到底层
C/C++的模块还需要单独进行编译,虽然这样的模块在 npm 仓库里并不多。
1 const S3 = require ("aws-sdk/clients/s3" );
既然 Electron
本质是一个浏览器 + 跨平台中间件
的组合,因此常用的前端调试技术也适用于
Electron,这里可以通过CTRL+SHIFT+I
手动开启 Chromium
的调试控制台,或者通过下面代码在开发模式下自动打开:
1 mainWindow.webContents .openDevTools ();
核心模块
本节将对require("electron")
所获取的模块进行概述,便于后期进行分类查找。
app 模块
Electron
提供的app 模块即提供了可用于区分开发和生产环境的app.isPackaged
属性,也提供了关闭窗口的app.quit()
和用于退出程序的app.exit()
方法,以及window-all-closed
和ready
等
Electron 程序事件。
1 2 3 4 const { app } = require ("electron" );app.on ("window-all-closed" , () => { app.quit (); });
可以使用app.getLocale()
获取当前操作系统的国际化信息。
BrowserWindow 模块
工作在主进程,用于创建和控制浏览器窗口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const { BrowserWindow } = require ("electron" );const { BrowserWindow } = require ("electron" ).remote ;let window = new BrowserWindow ({ width : 800 , height : 600 });window .on ("closed" , () => { win = null ; }); window .loadURL ("https://uinika.github.io/" );window .loadURL (`file://${__dirname} /app/index.html` );
例如需要创建一个无边框窗口的 Electron
应用程序,只需将BrowserWindow
配置对象中的frame
属性设置为false
即可:
1 2 3 const { BrowserWindow } = require ("electron" );let window = new BrowserWindow ({ width : 800 , height : 600 , frame : false });window .show ();
例如加载页面时,渲染进程第一次完成绘制时BrowserWindow
会发出ready-to-show
事件。
1 2 3 4 5 const { BrowserWindow } = require ("electron" );let win = new BrowserWindow ({ show : false });win.once ("ready-to-show" , () => { win.show (); });
对于较为复杂的应用程序,ready-to-show
事件的发出可能较晚,会让应用程序的打开显得缓慢。
这种情况下,建议通过backgroundColor
属性设置接近应用程序背景色的方式显示窗口,从而获取更佳的用户体验。
1 2 3 4 const { BrowserWindow } = require ("electron" );let window = new BrowserWindow ({ backgroundColor : "#272822" });window .loadURL ("https://uinika.github.io/" );
如果想要创建子窗口,那么可以使用parent
选项,此时子窗口将总是显示在父窗口的顶部。
1 2 3 4 5 6 7 const { BrowserWindow } = require ("electron" );let top = new BrowserWindow ();let child = new BrowserWindow ({ parent : top });child.show (); top.show ();
创建子窗口时,如果需要禁用父窗口,那么可以同时设置modal
选项。
1 2 3 4 5 6 7 8 const { BrowserWindow } = require ("electron" );let child = new BrowserWindow ({ parent : top, modal : true , show : false });child.loadURL ("https://uinika.github.io/" ); child.once ("ready-to-show" , () => { child.show (); });
globalShortcut 模块
使用globalShortcut
模块中的register()
方法注册快捷键。
1 2 3 4 5 6 7 8 const { app, globalShortcut } = require ("electron" );app.on ("ready" , () => { globalShortcut.register ("CommandOrControl+Y" , () => { }); });
Linux 和 Windows 上【Command】键会失效, 所以要使用
CommandOrControl(既 MacOS 上是【Command】键 ,Linux 和 Windows
上是【Control】键)。
clipboard 模块
用于在系统剪贴板上执行复制和粘贴操作,包含有readText()
、writeText()
、readHTML()
、writeHTML()
、readImage()
、writeImage()
等方法。
1 2 const { clipboard } = require ("electron" );clipboard.writeText ("一些字符串内容" );
globalShortcut 模块
用于在 Electron
应用程序失去键盘焦点时监听全局键盘事件,即在操作系统中注册或注销全局快捷键。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const { app, globalShortcut } = require ("electron" );app.on ("ready" , () => { const regist = globalShortcut.register ("CommandOrControl+A" , () => { console .log ("快捷键被摁下!" ); }); if (!regist) { console .log ("注册失败!" ); } console .log (globalShortcut.isRegistered ("CommandOrControl+A" )); }); app.on ("will-quit" , () => { globalShortcut.unregister ("CommandOrControl+A" ); globalShortcut.unregisterAll (); });
ipcMain 与 ipcRenderer 模块
用于主进程到渲染进程的异步通信,下面是一个主进程与渲染进程之间发送和处理消息的例子:
1 2 3 4 5 6 7 8 9 10 11 const { ipcMain } = require ("electron" );ipcMain.on ("asynchronous-message" , (event, arg ) => { console .log (arg); event.sender .send ("asynchronous-reply" , "pong" ); }); ipcMain.on ("synchronous-message" , (event, arg ) => { console .log (arg); event.returnValue = "pong" ; });
1 2 3 4 5 6 7 8 const { ipcRenderer } = require ("electron" );console .log (ipcRenderer.sendSync ("synchronous-message" , "ping" )); ipcRenderer.on ("asynchronous-reply" , (event, arg ) => { console .log (arg); }); ipcRenderer.send ("asynchronous-message" , "ping" );
如果需要完成渲染器进程到主进程的异步通信,可以选择使用ipcRenderer
对象。
用于主进程,用于创建原生应用菜单和上下文菜单。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const { app, BrowserWindow , Menu } = require ("electron" );let mainWindow;const template = [ { label : "自定义菜单" , submenu : [{ label : "菜单项-1" }, { label : "菜单项-2" }], }, ]; app.on ("ready" , () => { mainWindow = new BrowserWindow ({ width : 800 , height : 500 }); mainWindow.setMenu (Menu .buildFromTemplate (template)); mainWindow.loadFile ("resource/index.html" ); });
使用MenuItem
类可以添加菜单项至 Electron
应用程序菜单和上下文菜单当中。
menu
menu
netLog 模块
用于记录网络日志。
1 2 3 4 5 6 7 const { netLog } = require ("electron" );netLog.startLogging ("/user/log.info" ); netLog.stopLogging ((path ) => { console .log ("网络日志log.info保存在" , path); });
powerMonitor 模块
通过 Electron
提供的powerMonitor
模块监视当前电脑电源状态的改变,值得注意的是,在app
模块的ready
事件被触发之前,
不能引用或使用该模块。
1 2 3 4 5 6 7 8 const electron = require ("electron" );const { app } = electron;app.on ("ready" , () => { electron.powerMonitor .on ("suspend" , () => { console .log ("系统将要休眠了!" ); }); });
powerSaveBlocker 模块
阻止操作系统进入低功耗 (休眠) 模式。
1 2 3 4 5 6 const { powerSaveBlocker } = require ("electron" );const ID = powerSaveBlocker.start ("prevent-display-sleep" );console .log (powerSaveBlocker.isStarted (ID ));powerSaveBlocker.stop (ID );
protocol 模块
注册自定义协议并拦截基于现有协议的请求,例如下面代码实现了一个与[file://]
协议等效的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const { app, protocol } = require ("electron" );const path = require ("path" );app.on ("ready" , () => { protocol.registerFileProtocol ( "uinika" , (request, callback ) => { const url = request.url .substr (7 ); callback ({ path : path.normalize (`${__dirname} /${url} ` ) }); }, (error ) => { if (error) console .error ("协议注册失败!" ); } ); });
net 模块
net
模块是一个发送 HTTP(S) 请求的客户端 API,类似于
NodeJS 的 HTTP 和 HTTPS 模块 ,但底层使用的是 Chromium 原生网络库。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const { app } = require ("electron" );app.on ("ready" , () => { const { net } = require ("electron" ); const request = net.request ("https://zhihu.com/people/uinika/activities" ); request.on ("response" , (response ) => { console .log (`STATUS: ${response.statusCode} ` ); console .log (`HEADERS: ${JSON .stringify(response.headers)} ` ); response.on ("data" , (chunk ) => { console .log (`BODY: ${chunk} ` ); }); response.on ("end" , () => { console .log ("没有更多数据!" ); }); }); request.end (); });
Electron 中提供的ClientRequest
类用来发起 HTTP/HTTPS
请求,IncomingMessage
类则用于响应 HTTP/HTTPS 请求。
remote 模块
remote
模块返回的每个对象都表示主进程中的一个对象,调用这个对象实质是在发送同步进程消息。因为
Electron 当中 GUI 相关的模块 (如 dialog
、menu
等) 仅在主进程中可用,
在渲染进程中不可用,所以remote
模块提供了一种渲染进程(Web
页面 )与主进程(IPC )通信的简单方法。remote
模块包含了一个remote.require(module)
remote.process
:主进程中的process
对象,与remote.getGlobal("process")
作用相同,
但结果已经被缓存。
remote.getCurrentWindow()
:返回BrowserWindow
,即该网页所属的窗口。
remote.getCurrentWebContents()
:返回WebContents
,即该网页的
Web 内容
remote.getGlobal(name)
:该方法返回主进程中名为name
的全局变量。
remote.require(module)
:返回主进程内执行require(module)
时返回的对象,参数module
指定的模块相对路径将会相对于主进程入口点进行解析。
1 2 3 4 5 6 7 project/ ├── main │ ├── helper.js │ └── index.js ├── package.json └── renderer └── index.js
1 2 3 4 5 6 7 8 9 10 11 const { app } = require ("electron" );app.on ("ready" , () => { }); module .exports = "This is a test!" ;const helper = require ("electron" ).remote .require ("./helper" );
remote
模块提供的主进程与渲染进程通信方法比ipcMain
/ipcRenderer
更加易于使用。
screen 模块
检索有关屏幕大小、显示器、光标位置等信息,应用的ready
事件触发之前,不能使用该模块。下面的示例代码,创建了一个可以自动全屏窗口的应用:
1 2 3 4 5 6 7 8 9 10 const electron = require ("electron" );const { app, BrowserWindow } = electron;let window ;app.on ("ready" , () => { const { width, height } = electron.screen .getPrimaryDisplay ().workAreaSize ; window = new BrowserWindow ({ width, height }); window .loadURL ("https://github.com" ); });
shell 模块
提供与桌面集成相关的功能,例如可以通过调用操作系统默认的应用程序管理文件或Url
。
1 2 3 const { shell } = require ("electron" );shell.openExternal ("https://github.com" );
systemPreferences 模块
获取操作系统特定的偏好信息,例如在 Mac
下可以通过下面代码获取当前是否开启系统 Dark 模式的信息。
1 2 const { systemPreferences } = require ("electron" );console .log (systemPreferences.isDarkMode ());
Tray 模块
用于主进程,添加图标和上下文菜单至操作系统通知区域。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const { app, Menu , Tray } = require ("electron" );let tray = null ;app.on ("ready" , () => { tray = new Tray ("/images/icon" ); const contextMenu = Menu .buildFromTemplate ([ { label : "Item1" , type : "radio" }, { label : "Item2" , type : "radio" }, { label : "Item3" , type : "radio" , checked : true }, { label : "Item4" , type : "radio" }, ]); tray.setToolTip ("This is my application." ); tray.setContextMenu (contextMenu); });
webFrame 模块
定义当前网页渲染的一些属性,比如缩放比例、缩放等级、设置拼写检查、执行
JavaScript 脚本等等。
1 2 const { webFrame } = require ("electron" );webFrame.setZoomFactor (5 );
session 模块
Electron
的session
模块可以创建新的session
对象,主要用来管理浏览器会话、cookie、缓存、代理设置等等。
如果需要访问现有页面的session
,那么可以通过BrowserWindow
对象的webContents
的session
属性来获取。
1 2 3 4 5 6 7 const { BrowserWindow } = require ("electron" );let window = new BrowserWindow ({ width : 600 , height : 900 });window .loadURL ("https://uinika.github.io/web/server/electron.html" );const mySession = window .webContents .session ;console .log (mySession.getUserAgent ());
Electron
里也可以通过session
模块的cookies
属性来访问浏览器的
Cookie 实例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const { session } = require ("electron" );session.defaultSession .cookies .get ({}, (error, cookies ) => { console .log (error, cookies); }); session.defaultSession .cookies .get ( { url : "http://www.github.com" }, (error, cookies ) => { console .log (error, cookies); } ); const cookie = { url : "https://www.zhihu.com/people/uinika/posts" , name : "hank" , value : "zhihu" , }; session.defaultSession .cookies .set (cookie, (error ) => { if (error) console .error (error); });
使用Session
的WebRequest
属性可以访问WebRequest
类的实例,WebRequest
类可以在
HTTP 请求生命周期的不同阶段修改相关内容,例如下面代码为 HTTP
请求添加了一个User-Agent
协议头:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const { session } = require ("electron" );const filter = { urls : ["https://*.github.com/*" , "*://electron.github.io" ], }; session.defaultSession .webRequest .onBeforeSendHeaders ( filter, (details, callback ) => { details.requestHeaders ["User-Agent" ] = "MyAgent" ; callback ({ cancel : false , requestHeaders : details.requestHeaders }); } );
desktopCapturer 模块
用于捕获桌面窗口里的内容,该模块只拥有一个方法:desktopCapturer.getSources(options, callback)
。
options
对象
types
:字符串数组,列出需要捕获的桌面类型是screen
还是window
。
thumbnailSize
:媒体源缩略图的大小,默认为150x150
。
callback
回调函数,拥有如下 2 个参数:
error
:错误信息。
sources
:捕获的资源数组。
如下代码工作在渲染进程当中,作用是将桌面窗口捕获为视频:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 const { desktopCapturer } = require ("electron" );desktopCapturer.getSources ( { types : ["window" , "screen" ] }, (error, sources ) => { if (error) throw error; for (let i = 0 ; i < sources.length ; ++i) { if (sources[i].name === "Electron" ) { navigator.mediaDevices .getUserMedia ({ audio : false , video : { mandatory : { chromeMediaSource : "desktop" , chromeMediaSourceId : sources[i].id , minWidth : 1280 , maxWidth : 1280 , minHeight : 800 , maxHeight : 800 , }, }, }) .then ((stream ) => handleStream (stream)) .catch ((error ) => handleError (error)); return ; } } } ); function handleStream (stream ) { const video = document .querySelector ("video" ); video.srcObject = stream; video.onloadedmetadata = (error ) => video.play (); } function handleError (error ) { console .log (error); }
dialog 模块
调用操作系统原生的对话框,工作在主线程,下面示例展示了一个用于选择多个文件和目录的对话框:
1 2 3 4 5 6 const { dialog } = require ("electron" );console .log ( dialog.showOpenDialog ({ properties : ["openFile" , "openDirectory" , "multiSelections" ], }) );
由于对话框工作在 Electron 的主线程上,如果需要在渲染器进程中使用,
那么可以通过remote
来获得:
1 2 const { dialog } = require ("electron" ).remote ;console .log (dialog);
contentTracing 模块
从 Chromium
收集跟踪数据,从而查找性能瓶颈。使用后需要在浏览器打开chrome://tracing/
页面,然后加载生成的文件查看结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const { app, contentTracing } = require ("electron" );app.on ("ready" , () => { const options = { categoryFilter : "*" , traceOptions : "record-until-full,enable-sampling" , }; contentTracing.startRecording (options, () => { console .log ("开始跟踪!" ); setTimeout (() => { contentTracing.stopRecording ("" , (path ) => { console .log ("跟踪数据已经记录至" + path); }); }, 8000 ); }); });
webview 标签
Electron 的<webview>
标签基于
Chromium,由于开发变动较大官方并不建议使用,而应考虑<iframe>
或者
Electron
的BrowserView
等选择,或者完全避免在页面进行内容嵌入。
<webview>
与<iframe>
最大不同是运行于不同的进程当中,Electron
应用程序与嵌入内容之间的所有交互都是异步进行的,这样可以保证应用程序与嵌入内容双方的安全。
1 2 3 4 <webview id ="uinika" src ="http://localhost:5000/web/server/electron.html" > </webview >
webContents 属性
webContents
是BrowserWindow
对象的一个属性,负责渲染和控制
Web 页面。
1 2 3 4 5 6 7 const { BrowserWindow } = require ("electron" );let window = new BrowserWindow ({ width : 600 , height : 500 });window .loadURL ("https://uinika.github.io/" );let contents = window .webContents ;console .log (contents);
window.open() 函数
该函数用于打开一个新窗口并加载指定url
,调用后将会为该url
创建一个BrowserWindow
实例,并返回一个BrowserWindowProxy
对象,但是该对象只能对打开的url
页面进行有限的控制。正常情况下,如果希望完全控制新窗口,可以直接创建一个新的BrowserWindow
。
1 2 window .open ("https://github.com" , "_blank" , "nodeIntegration=no" );
BrowserWindowProxy
对象拥有如下属性和方法:
win.closed
:子窗口关闭后设置为true
的布尔属性。
win.blur()
:将焦点从子窗口中移除。
win.close()
:强制关闭子窗口, 而不调用其卸载事件。
win.eval(code)
:code
字符串,需要在子窗口
Eval 的代码。
win.focus()
:聚焦子窗口(即将子窗口置顶 )。
win.print()
:调用子窗口的打印对话框。
win.postMessage(message, targetOrigin)
:向子窗口发送信息。
Electron 进程
Electron 的process
对象继承自 NodeJS
的process
对象,但是新增了一些有用的事件、属性、方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const { app, BrowserWindow } = require ("electron" );let mainWindow;app.on ("ready" , () => { mainWindow = new BrowserWindow ({ width : 800 , height : 500 , frame : false }); mainWindow.loadFile ("resource/index.html" ); console .log (process.type ); console .log (process.versions .node ); console .log (process.versions .chrome ); console .log (process.versions .electron ); console .log (process.resourcesPath ); });
沙箱机制
Chromium 通过将 Web
前端代码放置在一个与操作系统隔离的沙箱中运行,从而保证恶意代码不会侵犯到操作系统本身。但是
Electron 中渲染进程可以调用 NodeJS,而 NodeJS
又需要涉及大量操作系统调用,因而沙箱机制默认是禁用的。
某些应用场景下,需要运行一些不确定安全性的外部前端代码,为了保证操作系统安全,可能需要开启沙箱机制。此时首先在创建BrowserWindow
时传入sandbox
属性,然后在命令行添加--enable-sandbox
参数传递给
Electron 即可完成开启。
1 2 3 4 5 6 7 8 9 let win;app.on ("ready" , () => { window = new BrowserWindow ({ webPreferences : { sandbox : true , }, }); window .loadURL ("http://google.com" ); });
使用sandbox
选项之后,将会阻止 Electron
在渲染器中创建一个 NodeJS
运行时环境,此时新窗口中的window.open()
将按照浏览器原生的方式工作。
MacBook TouchBar 支持
针对 Mac 笔记本电脑上配置的 TouchBar 硬件,Electron
提供了一系列相关的类与操作接口:TouchBar
、TouchBarButton
、TouchBarColorPicker
、TouchBarGroup
、TouchBarLabel
、TouchBarPopover
、TouchBarScrubber
、TouchBarSegmentedControl
、TouchBarSlider
、TouchBarSpacer
。
创建应用图标
用于将 PNG 或 JPG 图片设置为托盘、Dock 和应用程序的图标。
1 2 3 4 5 const { BrowserWindow , Tray } = require ("electron" );const Icon = new Tray ("/images/icon.png" );let window = new BrowserWindow ({ icon : "/images/window.png" });
安全原则
由于 Electron 的发布通常落后最新版本 Chromium
几周甚至几个月,因此特别需要注意如下这些安全性问题:
使用安全的协议加载外部内容
外部资源尽量使用更安全的协议加载,比如HTTP
换成HTTPS
、WS
换成WSS
、FTP
换成FTPS
等。
1 2 3 4 5 6 7 8 9 10 11 12 <script crossorigin src ="http://cdn.com/react.js" > </script > <link rel ="stylesheet" href ="http://cdn.com/scss.css" /> <script crossorigin src ="https://cdn.com/react.js" > </script > <link rel ="stylesheet" href ="https://cdn.com/scss.css" /> <script > browserWindow.loadURL("http://uinika.github.io/); // 错误 browserWindow.loadURL("https://uinika.github.io/"); // 正确 </script >
加载外部内容时禁用 NodeJS
集成
使用BrowserWindow
、BrowserView
、<webview>
加载远程内容时,都需要通过禁用
NodeJS 集成去限制远程代码的执行权限,避免恶意代码跨站攻击。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <webview nodeIntegration src ="page.html" > </webview > <webview src ="page.html" > </webview > <script > const mainWindow = new BrowserWindow (); mainWindow.loadURL ("https://my-website.com" ); const mainWindow = new BrowserWindow ({ webPreferences : { nodeIntegration : false , preload : "./preload.js" , }, }); </script >
对于需要与远程代码共享的变量或函数,可以通过将其挂载至当前页面的window
全局对象来实现。
渲染进程中启用上下文隔离
上下文隔离是 Electron
提供的试验特性,通过为远程加载的代码创造一个全新上下文环境,避免与主进程中的代码出现冲突或者相互污染。
1 2 3 4 5 6 7 const mainWindow = new BrowserWindow ({ webPreferences : { contextIsolation : true , preload : "preload.js" , }, });
处理远程内容中的会话许可
当页面尝试使用某个特性时,会弹出通知让用户手动进行确认;而默认情况下,Electron
会自动批准所有的许可请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const { session } = require ("electron" );session .fromPartition ("some-partition" ) .setPermissionRequestHandler ((webContents, permission, callback ) => { const url = webContents.getURL (); if (permission === "notifications" ) { callback (true ); } if (!url.startsWith ("https://my-website.com" )) { return callback (false ); } });
不要禁用 webSecurity
在渲染进程禁用webSecurity
将导致许多重要的安全性功能被关闭,因此
Electron 默认开启。
1 2 3 4 5 const mainWindow = new BrowserWindow ({ webPreferences : { webSecurity : false , }, });
定义 CSP 安全策略
内容安全策略 CSP 允许 Electron 通过webRequest
对指定 URL
的访问进行约束,例如允许加载https://uinika.github.io/
这个源,那么https://hack.attacker.com
将不会被允许加载,CSP
是处理跨站脚本攻击、数据注入攻击的另外一层保护措施。
1 2 3 4 5 6 7 8 9 10 const { session } = require ("electron" );session.defaultSession .webRequest .onHeadersReceived ((details, callback ) => { callback ({ responseHeaders : { ...details.responseHeaders , "Content-Security-Policy" : ["default-src 'none'" ], }, }); });
使用file://
协议打开本地文件时,可以通过元数据标签<meta>
的属性来添加
CSP 约束。
1 <meta http-equiv ="Content-Security-Policy" content ="default-src 'none'" />
别设置
allowRunningInsecureContent 为 true
Electron 默认不允许在 HTTPS 页面中加载 HTTP
来源的代码,如果将allowRunningInsecureContent
属性设置为true
会禁用这种保护。
1 2 3 4 5 const mainWindow = new BrowserWindow ({ webPreferences : { allowRunningInsecureContent : true , }, });
不要开启实验性功能
开发人员可以通过experimentalFeatures
属性启用未经严格测试的
Chromium 实验性功能,不过 Electron
官方出于稳定性和安全性考虑并不建议这样做。
1 2 3 4 5 const mainWindow = new BrowserWindow ({ webPreferences : { experimentalFeatures : true , }, });
不要使用 enableBlinkFeatures
Blink 是 Chromium 内置的 HTML/CSS
渲染引擎,开发者可以通过enableBlinkFeatures
启用其某些默认是禁用的特性。
1 2 3 4 5 const mainWindow = new BrowserWindow ({ webPreferences : { enableBlinkFeatures : ["ExecCommandInJavaScript" ], }, });
开启allowpopups
属性将使window.open()
创建一个新的窗口和BrowserWindows
,若非必要状况,尽量不要使用此属性。
1 2 3 4 5 <webview allowpopups src ="page.html" > </webview > <webview src ="page.html" > </webview >
验证 webview 选项与参数
通过渲染进程创建的<WebView>
默认不集成
NodeJS,但是它可以通过webPreferences
属性创建出一个独立的渲染进程。在<WebView>
标签开始渲染之前,Electron
将会触发一个will-attach-webview
事件,可以通过该事件防止创建具有潜在不安全选项的
Web 视图。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 app.on ("web-contents-created" , (event, contents ) => { contents.on ("will-attach-webview" , (event, webPreferences, params ) => { delete webPreferences.preload ; delete webPreferences.preloadURL ; webPreferences.nodeIntegration = false ; if (!params.src .startsWith ("https://www.zhihu.com/people/uinika/columns" )) { event.preventDefault (); } }); });
禁用或限制网页跳转
如果 Electron
应用程序不需要导航或只需导航至特定页面,最佳实践是将导航限制在已知范围,并禁止其它类型的导航。可以通过在will- navigation
事件处理函数中调用event.preventDefault()
并添加额外的判断来实现这一点。
1 2 3 4 5 6 7 8 9 10 11 const URL = require ("url" ).URL ;app.on ("web-contents-created" , (event, contents ) => { contents.on ("will-navigate" , (event, navigationUrl ) => { const parsedUrl = new URL (navigationUrl); if (parsedUrl.origin !== "https://www.zhihu.com/people/uinika/posts" ) { event.preventDefault (); } }); });
禁用或限制新窗口创建
限制在 Electron
应用程序中创建额外窗口,并避免因此带来额外的安全隐患。webContents
创建新窗口时会触发一个web-contents-created
事件,该事件包含了将要打开的
URL
以及相关选项,可以在这个事件中检查窗口的创建,从而对其进行相应的限制。
1 2 3 4 5 6 7 8 9 const { shell } = require ("electron" );app.on ("web-contents-created" , (event, contents ) => { contents.on ("new-window" , (event, navigationUrl ) => { event.preventDefault (); shell.openExternal (navigationUrl); }); });
Electron 2.0 版本开始,会在可执行文件名为 Electron
时会为开发者在控制台显示安全相关的警告和建议,开发人员也可以在process.env
或window
对象上配置ELECTRON_ENABLE_SECURITY_WARNINGS
或ELECTRON_DISABLE_SECURITY_WARNINGS
手动开启或关闭这些警告。
应用发布
Electron
的发布有别于传统桌面应用程序编译打包的发部过程,需要首先下载已经预编译完成的二进制包 ,Linux
下二进制包结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ➜ electron-v3.0.7-linux-x64 tree -L 2 . ├── blink_image_resources_200_percent.pak ├── content_resources_200_percent.pak ├── content_shell.pak ├── electron ├── icudtl.dat ├── libffmpeg.so ├── libnode.so ├── LICENSE ├── LICENSES.chromium.html ├── locales ├── natives_blob.bin ├── ui_resources_200_percent.pak ├── v8_context_snapshot.bin ├── version └── views_resources_200_percent.pak └── resources ├── default_app.asar └── electron.asar
接下来,就可以部署前面编写的源代码,Electron
里主要有如下两种部署方式:
直接将代码放置到resources
下的子目录,比如app
目录:
1 2 3 4 5 6 7 ├── app │ ├── package.json │ └── resource │ ├── index.html │ └── main.js ├── default_app.asar └── electron.asar
将应用打包加密为asar
文件以后放置到resources
目录,比如app.asar
文件。
1 2 3 ├── app.asar ├── default_app.asar └── electron.asar
asar 打包源码
asar
是一种简单的文件扩展格式,可以将文件如同tar
格式一样前后连接在一起,支持随机读写,并使用
JSON 来保存文件信息,可以方便的读取与解析。Electron 通过它可以解决
Windows
文件路径长度的限制,提高require
语句的加载速度,并且避免源代码泄漏。
首先,需要安装asar
这个 npm
包,然后可以选择全局安装然通过命令行使用。
1 2 ➜ npm install asar ➜ asar pack electron-demo app.asar
当然,更加工程化的方式是通过代码来执行打包操作,就像下面这样:
1 2 3 4 5 6 7 8 9 10 11 let asar = require ("asar" );src = "../octopus" ; dest = "build/app.asar" ; callback = () => { console .info ("asar打包完成!" ); }; asar.createPackage (src, dest, callback);
Electron 在 Web 页面可以通过file:
协议读取 asar
包中的文件,即将 asar 文件视为一个虚拟的文件夹来进行操作。
1 2 3 4 const { BrowserWindow } = require ("electron" );const mainWindow = new BrowserWindow ();mainWindow.loadURL ("file:///path/to/example.asar/static/index.html" );
如果需要对 asar 文件进行 MD5 或者 SHA 完整性校验,可以对 asar
档案文件本身进行操作。
rcedit 编辑可执行文件
RcEdit 是一款通过编辑窗口管理器的
rc 文件来对其进行配置的工具,Nodejs 社区提供了node-rcedit 工具对
Windows
操作系统的.exe
文件进行配置,首先通过npm i rcedit --save-dev
为项目安装该依赖项。
1 2 3 var rcedit = require ("rcedit" );rcedit (exePath, options, callback);
rcedit()
函数包含有如下属性:
exePath
:需要进行修改的 Windows
可执行文件所在路径。
options
:一个拥有如下属性的配置对象。
version-string
:版本字符串;
file-version
:文件版本;
product-version
:产品版本;
icon
:图标文件.ico
的路径;
requested-execution-level
:需要修改的执行级别(asInvoker
、highestAvailable
、requireAdministrator
)。
application-manifest
:本地清单文件的路径。
callback
:函数执行完毕之后回调,完整的函数签名为function(error)
。
yarn 包管理器
Yarn 是一款由 Facebook 推出的 JavaScript 包管理器,与 NodeJS 提供的
Npm 一样使用package.json
作为包信息文件。
测试当前 Yarn 的安装版本:
初始化新项目:
添加依赖包:
1 2 3 yarn add [package] yarn add [package]@[version] yarn add [package]@[tag]
将依赖项添加到不同的依赖项类别:devDependencies、peerDependencies 和
optionalDependencies。
1 2 3 yarn add [package] --dev yarn add [package] --peer yarn add [package] --optional
升级依赖包:
1 2 3 yarn upgrade [package] yarn upgrade [package]@[version] yarn upgrade [package]@[tag]
移除依赖包:
可以直接使用yarn
命令安装项目的全部依赖:
windows-installer
windows-installer 是一个用于为
Electron 应用程序构建 Windows 安装程序的 Npn 包,底层基于Squirrel (一组用于管理C# 或C++ 开发的
Windows 应用程序的安装、更新的工具库 )进行实现。
1 npm install --save-dev electron-winstaller
1 2 3 4 5 6 7 8 9 10 11 12 13 var electronInstaller = require ("electron-winstaller" );resultPromise = electronInstaller.createWindowsInstaller ({ appDirectory : "/tmp/build/my-app-64" , outputDirectory : "/tmp/build/installer64" , authors : "My App Inc." , exe : "myapp.exe" , }); resultPromise.then ( () => console .log ("It worked!" ), (e ) => console .log (`No dice: ${e.message} ` ) );
electron-build
除了像上面这样通过预编译包手动打包应用程序,也可以采用electron-forge 、electron-packager 等第三方包来完成这项工作,在这里笔者选择electron-builder 来进行自动化打包任务。
Electron Userland 是一个维护 Electron
模块的第三方社区,electron-builder 是由其维护的一款能够同时处理
Windows、MacOS、Linux 多平台的打包编译工具。由于 electron-builder
工具包的文件体积较大,其社区强烈推荐使用更快速的yarn
来代替npm
作为包管理方案。
1 yarn add electron-builder --dev
electron-builder 能够以命令行 或者JavaScript
API 的方式进行使用:
(1)如果安装在项目目录下的node_modules
目录,可以直接通过
NodeJS 提供的npx
以命令行方式使用:
(2)也可以像使用其它 Npm 包那样直接调用 electron-builder 提供的
API。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 "use strict" ;const builder = require ("electron-builder" );const Platform = builder.Platform ;builder .build ({ targets : Platform .MAC .createTarget (), config : { "//" : "build options, see https://goo.gl/QQXmcV" , }, }) .then (() => { }) .catch ((error ) => { });
官方推荐使用electron-webpack-quick-start 作为
Electron 应用的项目模板。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { "build" : { "appId" : "your.id" , "mac" : { "category" : "your.app.category.type" } } , "scripts" : { "pack" : "electron-builder --dir" , "dist" : "electron-builder" } , "postinstall" : "electron-builder install-app-deps }
如果项目中存在原生依赖,还需要设置nodeGypRebuild 为true
。
如果需要调试electron-builder
的行为,那么需要设置DEBUG=electron-builder
环境变量。
1 2 set DEBUG=electron-builder // Cmder$env :DEBUG=electron-builder // PowerShell
electron-forge
electron-forge 同样是由
Electron Userland 维护的一款命令行工具,用于快速建立、打包、发布一个
Electron 应用程序。
1 2 3 4 λ npm install -g electron-forge λ electron-forge init my-new-project λ cd my-new-project λ electron-forge start
目前 Github 上 electron-builder 的 Star 远远超过
electron-forge,由于笔者项目需要使用 React,因而也就选用带有支持 React
项目模板的 electron-builder,需要尝试 electron-forge
的同学可以移步官网查看更多信息。