image-20190504104413739

Electron 是一个使用Web 前端技术创建原生程序的框架,可以同时在macOS、windows、linux上运行,并轻松实现原生程序的体验。

开发基础

安装

  1. 安装 node.js 和npm

  2. 全局安装electron

    1
    sudo npm install -g electron
  3. 安装打包工具 electron-packager

    1
    sudo npm install -g electron-packager

运行应用

1
2
# 安装依赖库
$ npm install
1
2
# 运行应用
npm start

主进程调试

主进程是基于Node.js的,所以调试和Node.js类似,可在VS Code中进行调试:

第一步,设置VS Code的tasks,用于启动electron。相关配置如下:

1
2
3
4
5
6
7
8
9
10
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "0.1.0",
"command": "electron",
"isShellCommand": true,
"showOutput": "always",
"suppressTaskName": true,
"args": ["--debug=5858", "."]
}

其中,--debug=5858是用于调试Node.js的端口。

第二步,设置VS Code项目的调试配置。可以生成launch.json

1
2
3
4
5
6
7
8
9
10
11
{
"version": "0.2.0",
"configurations": [ {
"name": "Attach",
"type": "node",
"address": "localhost",
"port": 5858,
"request": "attach"
}
]
}

其中的port:5858需要和上面的--debug=5858端口保持一致。

开始调试主进程。

首先启动electron项目。

按下快捷键shift + command + p可以启动命令,输入task electron命令运行electron的task进行项目的启动。

项目启动后,再开始设置主进程代码的断点。在VS Code的调试界面中设置断点,并点击运行。这个时候,操作启动的electron应用,当运行到断点所在代码时,就可以触发断点调试。

electron-vue的调试

由于要使用webpack和babel,以支持ES6,所以需要代码转换相关的配置,具体配置如下:

launch.js中配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"version": "0.2.0",
"configurations": [
{
"name": "调试",
"type": "node",
"request": "launch",
"program": "${workspaceRoot}/src/main/index.dev.js",
"env": {
"DEBUG_ENV": "debug"
},
"stopOnEntry": false,
"args": [],
"cwd": "${workspaceRoot}",
// this points to the electron task runner
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron",
"runtimeArgs": [
"--nolazy"
],
"console": "integratedTerminal",
"sourceMaps": true
}
]
}

index.dev.js配置调试环境和 ES6代码转换;

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
process.env.NODE_ENV = 'development'

// Install `electron-debug` with `devtron`
require('electron-debug')({ showDevTools: false })

// Install `vue-devtools`
require('electron').app.on('ready', () => {
let installExtension = require('electron-devtools-installer')
installExtension.default(installExtension.VUEJS_DEVTOOLS)
.then(() => {})
.catch(err => {
console.log('Unable to install `vue-devtools`: \n', err)
})
})

// Require `main` process to boot app
if (process.env.DEBUG_ENV === 'debug') {
require('babel-core/register')({ //es6 转换
'presets': [
['env', {
'targets': {
'node': true
}
}]
]
})
}

// Require `main` process to boot app
require('./index')

再实际使用中存在断点位置跳动的问题,暂无解决办法。

渲染进程调试

在main.js中,在createWindow中,通过添加以下接口打开DevTools,进行页面调试,调试方法和使用Chrome进行调试一致。

1
2
3
4
 //渲染进程调试 Open the DevTools.
function createWindow() {
mainWindow.webContents.openDevTools()
}

打包后调试

electron-vue在开发环境默认启用electron-debug插件开启调试。但打包后看不到,可以注册快捷键来打开调试工具。

1
2
3
4
globalShortcut.register('CommandOrControl+Shift+L', () => {
let focusWin = BrowserWindow.getFocusedWindow()
focusWin && focusWin.toggleDevTools()
})

REPL

读取(Read)-运算(Eval)-输出(Print)-循环(Loop) (REPL) 是简单、交互式的计算机编程环境

如果你的 electronelectron-prebuilt 已经安装为本地项目依赖项,在终端执行:

1
./node_modules/.bin/electron --interactive

构建打包

安装 electron-builder

1
yarn add electron-builder --dev

执行构建,更多命令

1
electron-builder --mac

配置package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 构建配置
"build": {
"productName": "markdown-tool", //文件名
"copyright": "yoodu", //版权
"appId": "com.yoodu.markdown-tool", //包名
"mac": {
"icon": "./icon.icns",
"target": [
"dir", // 只打包成.app 执行文件,方便测试
"dmg"
]
},
"dmg":{
"background":"res/background.png",//背景图片路径
"window":{
//窗口左上角起始坐标
"x":100,
"y":100,
//窗口大小
"width":500,
"height":300
}
}
},

devDependencies为开发环境依赖,dependencies生产环境依赖(用户使用)

--save 将依赖的名称、版本要求添加到 dependencies

--save-dev 将依赖的名称、版本要求添加到 devDependencies

开发环境用户目录

/Users/fwk/Library/Application Support/Electron

asar 解压

解压整个包

1
npx asar extract app.asar destfolder

解压某个文件

1
npx asar extract-file app.asar main.js

架构

Electron 由main进程和renderer进程构成,main进程主要控制原生系统APP相关的功能,主要包括App的整个生命周期和原生界面的调用,如窗口创建,菜单栏,任务栏托盘等等,renderer进程等同与浏览器中的web页面,一般前端能用的东西都能在这里使用,另外Electron还提供了node.js和原生系统功能的支持,详见下图(图片来自Molunerfinn 博客,有详细的介绍)

main & renderer process tree

要注意的是在renderer中不能require 引入 main.js,而各个renderer之间可以相互require;

进程间通讯

ipcMain、ipcRenderer

main和renderer进程之间通过消息通讯,并在收到消息时执行回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// main端 选择要替换的目录
ipcMain.on('open-file-dialog', function (event) {
dialog.showOpenDialog({
properties: ['openFile', 'openDirectory']
}, function (files) {
if (files) event.sender.send('selected-directory', files)
})
})

// renderer端
selectDirBtn.addEventListener('click', function (event) { // 触发打开对话框
ipcRenderer.send('open-file-dialog')
})
ipcRenderer.on('selected-directory', function (event, path) {
document.getElementById('selectDirInput').value = path // 赋值并显示所选路径
})
remote

可以调用主进程对象的方法,而无需显式地发送进程间消息

1
2
3
4
5
6
// 渲染进程,通过 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');
主进程发消息
1
mainWindow.webContents.send('reload-file')
1
2
3
4
5
//渲染进程的组件接收,electron挂在原型链上this可以正确指向??
this.$electron.ipcRenderer.on('reload-file', (event, data) => {
console.log(data)
this.currentChange(this.currentCtag)
})

全局变量

http://electron.rocks/sharing-between-main-and-renderer-process/

1
2
3
4
5
6
7
8
9
// main端
// 变量
global.mdPath = ''
mdPath = '/Users/fwk/Library/Mobile Documents/iCloud~com~coderforart~iOS~MWeb/Documents/·MD笔记'

// 对象
global.sharedObject = {
someProperty: 'default value'
}
1
2
3
// renderer端
const { remote } = require('electron')
console.log(remote.getGlobal('mdPath'))
1
2
3
4
5
6
// main端
global.something = value;

// renderer端
const remote = require('electron').remote;
var something = remote.getGlobal('something');

数据库的引用

开发中使用lowdb管理本地数据库,遇到了主进程和渲染进程中引入的同一个数据模块,却是两份独立的内存引用的问题,导致数据不统一。原因是主进程和渲染进程是两个独立的进程,即使引入同一个模块,也会变成两份独立的引用。因此考虑将数据库只在一个进程中管理。

由于产品只需要一个窗口(只有一个渲染进程)因此为了方便数据的调用,直接把数据库放在渲染进程的main.js文件中,并挂在Vue的原型链上,所有组件都可以直接使用this.$db访问

1
2
import db from '../datastore/datastore.js'
Vue.prototype.$db = db

常用API

剪贴板

1
2
clipboard.writeText('Example String') 
clipboard.readText()

通知

主进程通知
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const {Notification} = require('electron')

const notification = new Notification({
title: `已存在图片目录“${title}”`,
body: '描述',
silent: true,
icon: '/path/to/icon.png'
hasReply: true,
replyPlaceholder: 'Type Here!!'
})
notification.on('show', () => console.log('showed'));
notification.on('click', () => console.info('clicked!!'));
notification.on('reply', (e, reply) => console.log('Replied:', reply));

notification.show()
渲染进程通知

这是一个 HTML5 API,只能在渲染器进程中使用

1
2
3
4
5
6
7
let myNotification = new Notification('标题', {
body: '通知正文内容'
})

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

Dock 显示

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

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

shell

使用默认程序处理文件或链接

1
2
3
const {shell} = require('electron')
shell.openExternal('https://github.com')
shell.showItemInFolder(fullPath)

菜单 menu

右键菜单

渲染进程
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
//vue
<script>
const Electron = require('electron')
const remote = Electron.remote
const { Menu, MenuItem } = remote

export default {
methods: {
contextMenu(event, data, node) {
this.$refs.tree.setCurrentKey(data.id)
this.currentChange(data, node)
let that = this
const template = [
{
label: '重命名',
click() {
that.editName(node, data)
},
},
{
label: '删除',
click() {
that.delCtag(node, data)
},
},
{
label: '新建标签',
click() {
that.addCtag(node, data)
},
},
]
const menu = Menu.buildFromTemplate(template)
menu.popup(remote.getCurrentWindow())
},
}
}
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// html实现
const {remote} = require('electron');
const {Menu, MenuItem} = remote;

//右键餐单
const menu = new Menu();
menu.append(new MenuItem({
label: '放大',
click:function () {
console.log('item 1 clicked')
}
}));
menu.append(new MenuItem({type: 'separator'}));//分割线
menu.append(new MenuItem({label: '缩小', type: 'checkbox', checked: true}));//选中

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

托盘tray

托盘图标格式

图标格式mac支持png,试过gif、tiff、svg、icns等都报错。

使用2倍图

如果发现图标很模糊,是因为视网膜屏幕图标分辨率达不到导致的。需要使用2倍图来解决,图片名称要加后缀@2x,如:

1倍图(16x16)名称:tray.png

2倍图(32x32)名称:tray@2x.png

2个图片都要提供,并放在同一目录,代码中只需写1倍图的名称即可,会根据屏幕自动使用。

fs-extra

选择文件夹并获取文件

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
import fs from 'fs-extra'

// 方法一 回调地狱
let dirFile = []
dialog.showOpenDialog({
properties: ['openDirectory']
}, (files) => {
console.log(files[0])
fs.readdir(files[0], (err, files) => {
if (err) {
console.log(err)
return
}
dirFile = files.filter(item => !(/(^|\/)\.[^/.]/g).test(item))
console.log(dirFile)
})

// 方法二 Promise链式调用 (推荐)
let files = dialog.showOpenDialog({properties: ['openDirectory']}) //返回数组
fs.readdir(files[0]) //返回Promise对象
.then(files => {
console.log(files)
return files.filter(item => !(/(^|\/)\.[^/.]/g).test(item))
})
.then(value => console.log(value))
.catch(err => console.error(err))

调用applescript

使用node-osascript ,终端执行安装

1
npm install node-osascript

1.执行applescript语句:’ ‘内为applescript语句,换行用\n分隔,也可用直接换行写,后面接回调函数。

1
2
3
4
5
const osascript = require('node-osascript')
osascript.execute('display dialog theDialogText buttons {"按键1", "按键2", "按键3"}\nset DlogResult to result\n return result', { theDialogText : '要传递的变量内容'}, function (err, result, raw){
if (err) return console.error(err)
console.log(result, raw)
})

2.执行脚本文件

1
2
3
4
osascript.executeFile('path/to/script.scpt', { varName : 'value'}, function(err, result, raw){
if (err) return console.error(err)
console.log(result, raw)
});

注意:延时delay只对applescript有效,osascript在js中为异步执行。

获取活动窗口信息

active-win

Works on macOS, Linux, Windows.

Get metadata about the active window (title, id, bounds, owner, etc)

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 activeWin = require('active-win')

(async () => {
console.log(await activeWin())
/*
{
title: 'Unicorns - Google Search',
id: 5762,
bounds: {
x: 0,
y: 0,
height: 900,
width: 1440
},
owner: {
name: 'Google Chrome',
processId: 310,
bundleId: 'com.google.Chrome',
path: '/Applications/Google Chrome.app'
},
memoryUsage: 11015432
}
*/
})()
node-ffi

node-ffi provides a powerful set of tools for interfacing with dynamic libraries using pure JavaScript in the Node.js environment.

I like this approach the best because it’s the most direct access from JavaScript to the native APIs. If you were going for multiple platforms, a package like active-win above might be best, but for just a few small accesses, using node-ffi to hook into native functions seems the cleanest to me. (and more future proof, since there’s no library you need to understand in order to update it if it has an issue)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import ffi from "ffi";
import ref from "ref";

declare var Buffer;

var user32 = new ffi.Library("user32", {
"GetForegroundWindow": ["int32", []],
"GetWindowTextA": ["int32", ["int32", "string", "int32"]],
});

export function GetForegroundWindowText() {
var buffer = new Buffer(256);
buffer.type = ref.types.CString;
var handle = user32.GetForegroundWindow();
let length = user32.GetWindowTextA(handle, buffer, 256);
return buffer.toString().substr(0, length);
}

Active window title of other applications

electron-log

方便打包后,输出log信息。

安装

1
npm i electron-log --save

使用

1
2
3
4
5
6
7
var log = require('electron-log');
log.error("error");
log.warn("warn");
log.info("info");
log.verbose("verbose");
log.debug("debug");
log.silly("silly");

默认会输出到console和log.log文件,所在路经:

  • on Linux: ~/.config/<app name>/log.log
  • on macOS: ~/Library/Logs/<app name>/log.log
  • on Windows: %USERPROFILE%\AppData\Roaming\<app name>\log.log

使用

版本管理

从版本 2.0.0, Electron 遵循 semver 。版本号中的各位对应 Major(大版本号).Minor(小版本号).Patch(补丁号)

Major 版本增量 Minor 版本增量 Patch 版本增量
Electron 突破性 API 变更 Electron 无突破性 API 变更 Electron bug 修复
Node.js 重大版本更新 Node.js 次要版本更新 Node.js patch 版本更新
Chromium 版本更新 修复相关的 chromium 补丁

package.json 中用以下方式控制版本更新的范围:

  • 兼容新发布的补丁版本:~2.2.0、2.2.x、2.2
  • 兼容新发布的小版本、补丁版本:^2.2.0、2.x、2
  • 兼容新发布的大版本、小版本、补丁版本:*、x

更新electron

现有项目更新到最新的稳定版本:

1
npm install --save-dev electron@latest

查看本地版本

1
npm ls electron -g

查看服务器最新版本

1
npm view electron version

更新 node 版本

Electron 有它自己的 Node 克隆, 并对 V8 构建细节进行修改, 并用于暴露Electron所需的 API。

检查 Node 克隆 中是否有 node 更新:

5-0-x 4-0-x 3-0-x
Chromium v72.0.3626.52 v69.0.3497.106 v66.0.3359.181
Node v12.0.0-unreleased v10.11.0 v10.2.0
V8 7.2.502.19 v6.9.427.24 v6.6.346.23

安全性

链接:https://www.zhihu.com/question/266893700/answer/668418233

主要分为这么几点:

  1. Chromium 本身会带来安全问题需要及时升级,但还是不可能完全与官方同步

  2. Security Is Everyone’s Responsibility,选好良好的第三方库,把关好自己的代码

  3. 隔离好不信任的上下文

  4. 打开Electron 的控制台安全警告

  5. 开发安全建议:

    1. 只加载HTTPS、FTPS等安全的资源
    2. 禁止在所有渲染器中使用Node.js集成显示远程内容
    3. 做所有显示远程内容的渲染器中启用上下文隔离。
    4. 在所有加载远程内容的会话中使用 ses.setPermissionRequestHandler().
    5. 不要禁用 webSecurity
    6. 定义一个Content-Security-Policy并设置限制规则(如:script-src ‘self’)
    7. 不要设置 allowRunningInsecureContent 为 true.
    8. 不要开启实验性功能
    9. 不要使用enableBlinkFeatures
    10. :不要使用 allowpopups
    11. :验证选项与参数
    12. 禁用或限制网页跳转
    13. 禁用或限制新窗口创建
    14. 不要使用 openExternal 接口打开不信任的第三方

想深入了解一下的话,可以看一下下面的资料进行学习:

Electronのセキュリティは難しい? - Technology of DeNA

https://www.blackhat.com/docs/us-17/thursday/us-17-Carettoni-Electronegativity-A-Study-Of-Electron-Security-wp.pdf

代码保护

官方讨论

源代码保护

添加加密功能

代码混淆

webpack js 混淆

通过混淆隐藏源码,降低可读性

加密

对敏感数据进行加密——CryptoJS

lowdb支持定义序列化和反序列化函数进行加密

常见坑

复制粘贴用不了

mac需要设置应用菜单栏,才能映射编辑的相关快捷键,否则按键无效;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 配置应用菜单,映射常用快捷键
var appMenu = [
{ label: 'Application',
submenu: [
{ label: 'About Application', selector: 'orderFrontStandardAboutPanel:' },
{ type: 'separator' },
{ label: 'Quit', accelerator: 'Command+Q', click: function () { app.quit() } }
]},
{ label: 'Edit',
submenu: [
{ label: 'Undo', accelerator: 'CmdOrCtrl+Z', selector: 'undo:' },
{ label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', selector: 'redo:' },
{ type: 'separator' },
{ label: 'Cut', accelerator: 'CmdOrCtrl+X', selector: 'cut:' },
{ label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:' },
{ label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:' },
{ label: 'Select All', accelerator: 'CmdOrCtrl+A', selector: 'selectAll:' }
]}
]
Menu.setApplicationMenu(Menu.buildFromTemplate(appMenu))

解决 electron npm 包下载慢

~/.npmrc里添加如下设置,

1
electron_mirror="https://npm.taobao.org/mirrors/electron/"

ESLint 失效,提示找不到文件和目录

1
ESLint: ENOENT: no such file or directory, open '/Users/fwk/Documents/_work/_Electron/toolbox/node_modules/_eslint-plugin-prettier@3.0.1@eslint-plugin-prettier/eslint-plugin-prettier.js'. Please see the 'ESLint' output channel for details.

npm install安装无效,使用yarn重新安装后解决:

1
yarn add --dev prettier eslint-plugin-prettier