# 介绍
Electron 是由 Github 开发,用 HTML,CSS 和 Javascript 来构建跨平台桌面应用程序的一个开源库。
Electron 通过将 Chromium 和 Node.js 合并到同一个运行时环境中,并将其打包为 Mac,Windows 和 Linux 系统下的应用来实现这一目的。
# 代码
https://gitee.com/huwanlong/electron-quick-start.git (opens new window)
# 官网
https://www.electronjs.org/ (opens new window)
# 特点
- 上手简单:只要会前端的知识就可以做桌面应用
- 跨平台:Mac,Linux,Windows
- 自动更新
# 安装
git clone https://github.com/electron/electron-quick-start.git
cd electorn-quick-start
npm install
# 如果下载慢 npm config set ELECTRON_MIRROR https://npm.taobao.org/mirrors/electron/
npm start
2
3
4
5
# 调试
Electron 分为渲染进程和主进程。
# 渲染进程
用户所看到的 web 界面就是由渲染进程描绘出来的。包括 HTML、CSS、JS。
# 调试方法
Ctrl+Shirt+i
# 主进程
Electron 运行 package.json 的 main 脚本的进程被称为主进程。在主进程中运行的脚本通过创建 web 页面来展示用户界面。一个 Electron 应用总是有且只有一个主进程。
# 浏览器调试方法
- 修改 package.json 中的"electron ."为"electorn --inspect=5858 ."
- 打开浏览器输入:chrome://inspect
- 点击 Configure,添加 localhost:5858
- 重启程序,在 chrome://inspect 页面的 Remote Target 中点击 inspect
- 在 Sources 中 Ctrl+p 找到 main.js,可以点击行标进行设置断点
# VS Code 调试方法
- 点击 VS Code 的 Debug 按钮,创建 launch.json
- 写入如下配置,然后 main.js 文件断点 Debug
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Main Process",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
"windows": {
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
},
"args": ["."],
"outputCapture": "std"
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 常用 api
# 事件
# app 常用事件
文档地址:https://www.electronjs.org/docs/all (opens new window)
- ready: 当 Electron 完成初始化时被触发
- window-all-closed: 所有窗口被关闭
- before-quit: 在应用程序开始关闭窗口之前触发
- will-quit: 当所有窗口都已关闭并且应用程序将退出时发出
- quit: 在应用程序退出时发出
# webContents 常用事件
文档地址: https://www.electronjs.org/docs/api/web-contents (opens new window)
- did-finish-load: 导航完成时触发,即选项卡的旋转器将停止旋转,并指派 onload 事件后。
- dom-ready: 一个框架中的文本加载完成后触发该事件。
# 进程对象
文档地址: https://www.electronjs.org/docs/api/process (opens new window)
可以获得系统参数,比如内存,系统版本,进程 ID 等等
在 renderer.js 中写页面的 JS 代码,为了使用 process 对象和 require,需要在 main.js 的创建窗口时,在 webPreferences 中配置 nodeIntegration: true
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello World!</title>
</head>
<body>
<div>
<h2>process</h2>
<button onclick="getProcessInfo()">查看process信息</button>
</div>
<!-- You can also require other files to run in this process -->
<script src="./renderer.js"></script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function getProcessInfo() {
console.log('getCPUUsage', process.getCPUUsage())
console.log('env', process.env)
console.log('arch', process.arch)
}
2
3
4
5
# File 对象
文档地址: https://www.electronjs.org/docs/api/file-object (opens new window)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello World!</title>
<link rel="stylesheet" href="./css/styles.css" />
</head>
<body>
<div class="for_file_drag" id="drag_test">
<h2>File对象</h2>
<span>往这里拖文件</span>
</div>
<!-- You can also require other files to run in this process -->
<script src="./renderer.js"></script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const fs = require('fs')
const dragWarpper = document.getElementById('drag_test')
dragWarpper.addEventListener('drop', e => {
e.preventDefault()
const files = e.dataTransfer.files
if (files && files.length > 0) {
const path = files[0].path
console.log('path:', path)
const content = fs.readFileSync(path)
console.log(content.toString())
}
})
dragWarpper.addEventListener('dragover', e => {
e.preventDefault()
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# webview
文档地址: https://www.electronjs.org/docs/api/webview-tag (opens new window)
需要在 main.js 的创建窗口时,在 webPreferences 中配置 webviewTag: true
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello World!</title>
<link rel="stylesheet" href="./css/styles.css" />
</head>
<body>
<div>
<h2>Webview</h2>
<span id="loading"></span>
<!-- preload 在网页里插入JS -->
<webview id="wb" src="http://baidu.com/" preload="./webview_test/preload.js"></webview>
</div>
<!-- You can also require other files to run in this process -->
<script src="./renderer.js"></script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// webview 实例
const wb = document.querySelector('#wb')
const loading = document.querySelector('#loading')
wb.addEventListener('did-start-loading', () => {
console.log('loading...')
loading.innerHTML = 'loading...'
})
wb.addEventListener('did-stop-loading', () => {
console.log('OK.')
loading.innerHTML = 'OK.'
// 插入CSS样式
wb.insertCSS(`
#su {
background: red !important;
}
`)
// 插入JS脚本
wb.executeJavaScript(`
alert(document.getElementById('su').value)
`)
wb.openDevTools() // 打开webview里面网页的控制台
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# window-open
通过 window-open 打开一个子窗口,通过 browser-window-proxy 对子窗口进行操作
文档地址: https://www.electronjs.org/docs/api/window-open (opens new window)
https://www.electronjs.org/docs/api/browser-window-proxy (opens new window)
<!-- 创建一个文件popup_page.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>子窗口</title>
</head>
<body>
<h2>这是弹出的子窗口</h2>
<button onclick="sendMessageToParent()">向父窗口传递信息</button>
</body>
<script>
function sendMessageToParent() {
window.opener.postMessage('这是来自子窗口的问候')
window.opener.postMessage({
type: 1,
message: '这是来自子窗口的问候',
})
}
</script>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div>
<h2>弹出子窗口</h2>
<button onclick="openNewWindow()">弹出</button>
<button onclick="closeWindow()">关闭</button>
</div>
2
3
4
5
// window-open
let subWin
function openNewWindow() {
// window.open('https://www.baidu.com','baidu')
subWin = window.open('popup_page.html', 'popup')
}
function closeWindow() {
subWin.close()
}
window.addEventListener('message', msg => {
console.log('接收到的消息', msg)
})
2
3
4
5
6
7
8
9
10
11
12
# browser-window
文档地址: https://www.electronjs.org/docs/api/browser-window (opens new window)
// Modules to control application life and create native browser window
const { app, BrowserWindow } = require('electron')
const path = require('path')
function createWindow() {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
// frame: false, // 不显示菜单
show: false, // 先不显示,加载完成了之后再显示
// backgroundColor: '#ff0000', // 背景颜色
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
webviewTag: true,
nodeIntegration: true,
},
})
mainWindow.once('ready-to-show', () => {
mainWindow.show()
})
// 子窗口
childWin = new BrowserWindow({
parent: mainWindow,
x: 0, // 坐标
y: 0,
})
// 模态窗口,只能操作这个窗口,可能用来做确认框
// childWin = new BrowserWindow({
// parent: mainWindow,
// modal: true
// })
}
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
# BrowserView
和 webview 类似,推荐使用 webview
文档地址: https://www.electronjs.org/docs/api/browser-view (opens new window)
const view = new BrowserView()
view.setBounds({
x: 10,
y: 10,
width: 300,
height: 200,
})
view.webContents.loadURL('https://www.baidu.com')
mainWindow.setBrowserView(view)
setTimeout(() => {
view.destroy()
}, 5000)
2
3
4
5
6
7
8
9
10
11
12
13
14
# Dialog
需要在 main.js 的创建窗口时,在 webPreferences 中配置 nodeIntegration: true,enableRemoteModule: true
文档地址: https://www.electronjs.org/docs/api/dialog (opens new window)
// 主线程使用方法
const { dialog } = require('electron')
// 渲染线程使用方法
const { dialog } = require('electron').remote
2
3
4
<div>
<h2>打开文件选择框</h2>
<button onclick="openDialog()">打开</button>
<button onclick="saveDialog()">保存文件</button>
<button onclick="messageDialog()">弹出消息提示</button>
</div>
2
3
4
5
6
const { dialog } = require('electron').remote
function openDialog() {
dialog
.showOpenDialog({
title: '请选择你喜欢的文件',
buttonLabel: '走你',
filters: [{ name: 'Custom File Type', extensions: ['js', 'html', 'json'] }],
})
.then(result => {
console.log(result)
})
.catch(err => {
console.log(err)
})
}
function saveDialog() {
dialog
.showSaveDialog({
title: '请选择要保存的文件名',
buttonLabel: '保存',
filters: [{ name: 'Custom File Type', extensions: ['js', 'html', 'json'] }],
})
.then(result => {
console.log(result)
fs.writeFileSync(result.filePath, '保存文件测试!!!')
})
.catch(err => {
console.log(err)
})
}
function messageDialog() {
dialog
.showMessageBox({
type: 'warning',
title: '您确定吗?',
message: '您真的想要删除这条数据么?',
buttons: ['OK', 'Cancel'],
})
.then(result => {
console.log(result)
})
.catch(err => {
console.log(err)
})
}
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
40
41
42
43
44
45
46
47
# 系统快捷键
文档地址: https://www.electronjs.org/docs/api/global-shortcut (opens new window)
// 主进程
app.whenReady().then(() => {
console.log('####ready')
createWindow()
app.on('activate', function() {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
// 注册一个'CommandOrControl+X'的全局快捷键
const ret = globalShortcut.register('CommandOrControl+X', () => {
console.log('CommandOrControl+X is pressed')
})
if (!ret) {
console.log('registration failed')
}
// 检查快捷键是否注册成功
console.log(globalShortcut.isRegistered('CommandOrControl+X'))
})
app.on('will-quit', () => {
// 注销快捷键
globalShortcut.unregister('CommandOrControl+X')
// 注销所有快捷键
globalShortcut.unregisterAll()
})
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 { remote } = require('electron')
remote.globalShortcut.register('CommandOrControl+G', () => {
console.log('您按下了Ctrl + G')
})
2
3
4
5
# ipcMain ipcRenderer
ipcMain 从主进程到渲染进程的异步通信
文档地址: https://www.electronjs.org/docs/api/ipc-main (opens new window)
ipcRenderer 从渲染进程到主进程的异步通信
文档地址: https://www.electronjs.org/docs/api/ipc-renderer (opens new window)
<div>
<h2>进程之间通信</h2>
<button onclick="sendMessageToMain()">异步发送信息给主进程</button>
<button onclick="sendMessageToMainAync()">同步发送信息给主进程</button>
</div>
2
3
4
5
const { ipcRenderer } = require('electron')
function sendMessageToMain() {
ipcRenderer.send('send-message-to-main-test', '这是来自于渲染进程的数据666')
}
ipcRenderer.on('send-message-to-renderer-test', (event, args) => {
console.log('渲染进程接收到的数据', args)
})
function sendMessageToMainAync() {
console.log(ipcRenderer.sendSync('sync-message', 'ping'))
}
2
3
4
5
6
7
8
9
10
// 主进程主动和渲染进程通信
setTimeout(() => {
mainWindow.webContents.send("send-message-to-renderer-test", "我是主进程,我主动和你搭讪")
}, 5000)
....
ipcMain.on("send-message-to-main-test", (event, args) => {
console.log("主进程接收到的数据是:", args)
event.reply("send-message-to-renderer-test", "这是来自于主进程的问候")
})
ipcMain.on("sync-message", (event, args) =>{
console.log("data: ", args)
event.returnValue = 'pong'
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Menu
原生应用菜单
文档地址: https://www.electronjs.org/docs/api/menu (opens new window)
https://www.electronjs.org/docs/api/menu-item (opens new window)
// 主进程中弹菜单,主进程很少有需求需要弹出菜单
setTimeout(() => {
const template = [{ label: '第一个菜单' }, { label: '第二个菜单' }, { role: 'undo' }, { type: 'separator' }, { label: '第三个菜单' }, { label: '第四个菜单' }]
const menu = Menu.buildFromTemplate(template)
// Menu.setApplicationMenu(menu) 这段是改变上面的横栏的菜单
menu.popup()
}, 2000)
2
3
4
5
6
7
<div>
<h2>弹出菜单</h2>
<input type="text" />
<button onclick="openMenu()">弹出菜单</button>
</div>
2
3
4
5
// 弹出菜单
const { remote } = require('electron')
const { Menu, MenuItem } = remote
function openMenu() {
const template = [
{ label: '第一个菜单' },
{
label: '点击测试',
click: () => {
console.log('点击事件OK')
},
},
{ role: 'undo' },
{ role: 'redo' },
{ label: '旅游', type: 'checkbox', checked: true },
{ label: '吃', type: 'checkbox', checked: true },
{ label: '逛街', type: 'checkbox', checked: false },
new MenuItem({
label: '我是menuItem生成的菜单',
click: () => {
console.log('您点击了menuItem的菜单')
},
}),
{
label: '子菜单测试',
submenu: [{ label: '子菜单-1' }, { label: '子菜单-2' }, { label: '子菜单-3' }],
},
]
const menu = Menu.buildFromTemplate(template)
// Menu.setApplicationMenu(menu) 这段是改变上面的横栏的菜单
menu.popup()
}
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
# net
文档地址: https://www.electronjs.org/docs/api/net (opens new window)
<div>
<h2>网络</h2>
<button onclick="accessBaidu()">访问百度</button>
</div>
2
3
4
// 网络
const { net } = require('electron').remote
function accessBaidu() {
const request = net.request('https://www.baidu.com')
request.on('response', response => {
console.log(`**statusCode:${response.statusCode}`)
console.log(`**headers:${JSON.stringify(response.headers)}`)
response.on('data', chunk => {
console.log('接收到的数据:', chunk.toString())
})
response.on('end', () => {
console.log('数据接收完成')
})
})
request.end()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 与高级框架结合
# react
git clone --depth 1 --single-branch --branch master https://github.com/electron-react-boilerplate/electron-react-boilerplate.git electron-react-start
cd electron-react-start
yarn
yarn dev
2
3
4
// 切换yarn淘宝源
npm install yrm -g
yrm ls
yrm use taobao
2
3
4
// 打包命令
yarn package
// 如果有需要升级的就按 yarn add electron-builder@latest 格式升级
2
3
# vue
npm install -g vue-cli
vue init simulatedgreg/electron-vue electron-vue-start
cd electron-vue-start
yarn
yarn dev
// 如果报process相关的错:参考 https://github.com/SimulatedGREG/electron-vue/issues/871#issuecomment-564302194 把 src/index.ejs 中 <% if (!process.browser) { %> 改成 <% if (!require('process').browser) { %>
2
3
4
5
6
// windows 无法编辑 node-sass
npm install -g node-gyp
npm install --global --production windows-build-tools
// 可以自动安装跨平台的编译器:gym
2
3
4
// 打包
yarn build
2