This repository has been archived on 2025-01-09. You can view files and clone it, but cannot push or open issues or pull requests.
electron-study/README.md
2025-01-09 18:30:49 +08:00

1042 lines
23 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Electron 入门
> WARNING: 文章部分内容已经过时,仅适用于 Electron ^11.2.1
## 技术架构
Electron = Chromium + Node.js + Native APIs
## 工作流程
启动APP -> 主进程创建window -> win加载界面 -> xxx行为
### 主进程
- 只有主进程才可以进行GUI的API操作
- 只有一个
### 渲染进程
- 可以有多个
- 和主进程通信完成原生API操作
## 环境搭建
1. 使用官方 Quick Start
```bash
$ git clone https://github.com/electron/electron-quick-start
$ cd electron-quick-start
$ npm install
```
```javascript
// 01 创建一个窗口
// 02 让窗口加载了一个界面这个界面就是用Web技术实现 运行在渲染进程
function createWindow () {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
// and load the index.html of the app.
mainWindow.loadFile('index.html')
// Open the DevTools.
// mainWindow.webContents.openDevTools()
// ...
}
```
2. 手动创建项目
```javascript
// main.js
const {app, BrowserWindow} = require('electron') // 导包
// 当 app 启动之后执行窗口创建等操作
app.whenReady().then(() => {
const mainWin = new BrowserWindow({
width: 600,
height: 400
})
// 在当前窗口中加载指定界面让它显示具体的内容
mainWin.loadFile('index.html')
mainWin.on('close', () => {
// ...
})
})
app.on('window-all-closed', () => {
// ...
app.quit()
})
```
```html
<!-- index.html -->
<!doctype html>
<head>
<title>这个窗口标题</title>
</head>
<body>
<h1>嘿!这是一段文字!</h1>
</body>
```
```json
// package.json
{
"name": "xxxx", // 项目的名字
"version": "x.x.x", // 项目的版本
"description": "", // 描述
"main": "main.js", // 入口文件
"scripts": {
"start": "electron ." // npm start
},
"keywords": [], // 关键词?
"author": "", // 作者
"license": "ISC", // 协议
"devDependencies": {
"electron": "^11.2.1" // 开发依赖
}
}
```
## 生命周期
ready: app初始化完成
dom-ready: 一个窗口中的文本加载完成
did-finsh-load: 导航完成时出发
window-all-closed: 所有窗口都被关闭时触发
before-quit: 在关闭窗口之前触发
will-quit: 在窗口关闭并且应用退出时触发
quit: 当所有窗口被关闭时触发
closed: 当窗口关闭时触发,此时删除窗口引用
```javascript
const { app, BrowserWindow } = require('electron')
function createWindow() {
let mainWin = new BrowserWindow({
width: 600,
height: 400
})
mainWin.loadFile('index.html')
mainWin.webContents.on('did-finish-load', () => {
console.log('3 - did finish load')
})
mainWin.webContents.on('dom-ready', () => {
console.log('2 - dom ready')
})
mainWin.on('close', () => {
console.log('8 - win closed') // 多窗口时最后触发
mainWin = null // 删除引用,释放空间
})
}
app.on('ready', () => {
console.log('1 - ready')
createWindow()
})
app.on('window-all-closed', () => {
console.log('4 - window all closed')
app.quit()
})
app.on('before-quit', () => {
console.log('5 - before quit')
})
app.on('will-quit', () => {
console.log('6 - will-quit')
})
app.on('quit', () => {
console.log('7 - quit')
})
```
## 窗口大小
```javascript
function createWindow() {
let mainWin = new BrowserWindow({
x: 100, // 相对于屏幕左上角
y: 100,
show: false, // 防止首页白屏
width: 800,
height: 400,
maxHeight: 600, // 最大值
maxWidth: 1000,
minHeight: 200, // 最小值
minWidth: 300,
resizable: false // 固定大小,禁止缩放
})
// 防止首页白屏,否则不显示
mainWin.on('ready-to-show', () => {
mainWin.show()
})
// ...
}
```
## 窗口标题及环境
```javascript
// main.js
function createWindow() {
let mainWin = new BrowserWindow({
width: 800,
height: 600,
show: false,
title: "标题", // 要在网页里的标题去除
icon: "w.png", // 设置图标
frame: true, // 是否显示标题栏
transparent: false, // 透明窗体
autoHideMenuBar: true, // 隐藏菜单
webPreferences: {
nodeIntegration: true, // Node集成环境
enableRemoteModule: true, // 开启远程模块 remote
}
})
// ...
}
```
```javascript
// index.js
const { remote } = require('electron')
window.addEventListener('DOMContentLoaded', () => {
// 点击按钮打开新窗口
const oBtn = document.getElementById('btn')
oBtn.addEventListener('click', () => {
// 创建窗口
let indexMain = new remote.BrowserWindow({
width: 200,
height: 200
})
indexMain.loadFile('list.html')
indexMain.on('close', () => {
indexMain = null
})
})
})
```
## 自定义窗口的实现
```html
<!-- index.html -->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
/* ... */
</style>
</head>
<body>
<div class="menu-container">
<div class="left">
<div class="logo"></div>
<div class="title">Electron 自定义标题栏</div>
</div>
<div class="right">
<div class="function min">
<!-- ... -->
</div>
<div class="function max">
<!-- ... -->
</div>
<div class="function close">
<!-- ... -->
</div>
</div>
</div>
<div class="app">
<h2>主体内容</h2>
</div>
<script src="index.js"></script>
</body>
</html>
```
```javascript
// index.js
const { remote } = require('electron')
window.addEventListener('DOMContentLoaded', () => {
// 利用 remote 获取当前窗口对象
let mainWin = remote.getCurrentWindow()
// 获取元素添加点击操作的监听
let aBtns = document.getElementsByClassName('function')
aBtns[0].addEventListener('click', () => {
// 最小化
mainWin.minimize()
})
aBtns[1].addEventListener('click', () => {
// 最大化
mainWin.isMaximized() ? mainWin.restore() : mainWin.maximize()
})
aBtns[2].addEventListener('click', () => {
// 关闭窗口操作
mainWin.close()
})
})
```
## 阻止窗口关闭
```html
<!-- index.html -->
<html lang="en">
<head>
<!-- ... -->
</head>
<body>
<!-- ... -->
<div class="dialog">
<div class="message">
是否关闭?
</div>
<span class="confirm yes"></span>
<span class="confirm no"></span>
</div>
<script src="index.js"></script>
</body>
</html>
```
```javascript
// index.js
const { remote } = require('electron')
window.addEventListener('DOMContentLoaded', () => {
window.onbeforeunload = function () {
let oBox = document.getElementsByClassName('dialog')[0]
oBox.style.display = 'block'
let yBtn = oBox.getElementsByClassName('confirm')[0]
let nBtn = oBox.getElementsByClassName('confirm')[1]
yBtn.addEventListener('click', () => {
mainWin.destroy() // 不用 close 防止死循环
})
nBtn.addEventListener('click', () => {
oBox.style.display = 'none'
})
return false
}
// ...
}
```
## 父子及模态窗口
```javascript
// index.js
window.addEventListener('DOMContentLoaded', () => {
let oBtn = document.getElementById('btn')
oBtn.addEventListener('click', () => {
let subWin = new remote.BrowserWindow({
// ...
parent: remote.getCurrentWindow(), // 设置父窗口
modal: true, // 设置模态窗口
})
// ...
})
})
```
## 自定义菜单
[Electorn文档 Menu部分](https://www.electronjs.org/zh/docs/latest/api/menu)
```javascript
// ...
console.log(process.platform) // 平台判断
// ...
const createWindow = () => {
let mainWin = new BrowserWindow({
// ...
});
// 定义自己需要的菜单项
let menuTemp = [
{
label: "文件",
submenu: [
{
label: "打开文件",
click() {
console.log("打开文件夹")
}
},
{
type: 'separator' // 分隔符
},
{
label: "关闭文件夹"
},
{
label: "关于",
role: 'about'
},
],
},
{ label: "编辑" },
];
// 利用上述模板生成一个菜单项
let menu = Menu.buildFromTemplate(menuTemp);
// 添加到应用里
Menu.setApplicationMenu(menu);
// ...
};
// ...
```
## 菜单角色及类型
```javascript
function createWindow() {
let mainWin = new BrowserWindow({
// ...
})
// 01 自定义菜单的内容
let menuTemp = [
{
label: '角色',
submenu: [
{ label: '复制', role: 'copy' },
{ label: '剪切', role: 'cut' },
{ label: '粘贴', role: 'paste' },
{ label: '最小化', role: 'minimize' }
]
},
{
label: '类型',
submenu: [
{ label: '选项1', type: 'checkbox' },
{ label: '选项2', type: 'checkbox' },
{ label: '选项3', type: 'checkbox' },
{ type: 'separator' },
{ label: 'item1', type: 'radio' },
{ label: 'item2', type: 'radio' },
{ type: 'separator' },
{ label: 'windows', type: 'submenu', role: 'windowMenu'}
]
},
{
label: '其他',
submenu: [
{
label: '打开',
icon: 'open.png', // 自定义图标
accelerator: 'ctrl + o', // 快捷键
click() {
console.log('open 执行了')
}
}
]
}
]
// 02 依据上述数据创建一个 menu
let menu = Menu.buildFromTemplate(menuTemp)
// 03 将上述的菜单添加至 app 中
Menu.setApplicationMenu(menu)
// ...
```
## 动态创建菜单
```html
<!-- index.html -->
<html lang="en">
<head>
<!-- ... -->
</head>
<body>
<button id="addMenu">创建自定义菜单</button>
<input type="text" placeholder="输入自定义菜单项内容" id="menuCon">
<button id="addItem">添加菜单项</button>
</body>
</html>
```
```javascript
// index.js
const { remote } = require('electron')
const Menu = remote.Menu
const MenuItem = remote.MenuItem
window.addEventListener('DOMContentLoaded', () => {
// 获取相应的元素
let addMenu = document.getElementById('addMenu')
let menuCon = document.getElementById('menuCon')
let addItem = document.getElementById('addItem')
// 自定义全局变量存放菜单项
let menuItem = new Menu()
// 生成自定义的菜单
addMenu.addEventListener('click', () => {
// 创建菜单
let menuFile = new MenuItem({ label: '文件', type: 'normal' })
let menuEdit = new MenuItem({ label: '编辑', type: 'normal' })
let customMenu = new MenuItem({ label: '自定义菜单项', submenu: menuItem })
// 将创建好的自定义菜单添加至 menu
let menu = new Menu()
menu.append(menuFile)
menu.append(menuEdit)
menu.append(customMenu)
// 将 menu 添加至 app 中显示
Menu.setApplicationMenu(menu)
})
// 动态添加菜单项
addItem.addEventListener('click', () => {
// 获取当前 input 输入框中的元素
let con = menuCon.vaule.trim()
if (con) {
menuItem.append(new MenuItem({ label: con, type: 'normal' }))
menuCon.vaule = ''
}
})
})
```
## 右键菜单的创建
```javascript
// index.js
const { remote } = require('electron')
const Menu = remote.Menu
let contextTemp = [
{lable: 'Run Code'},
{lable: '转到定义'},
{type: 'separator'},
{
lable: '其他功能',
click() {
alert('其他功能选项');
}
},
]
let menu = Menu.buildFromTemplate(contextTemp)
// 给鼠标添加监听
window.addEventListener('DOMContentLoaded', () => {
window.addEventListener('contextmenu', (ev) => {
ev.preventDefault()
menu.popup({window: remote.getCurrentWindow()})
}, false)
})
/**
* 01 创建一个自定义的菜单内容
* 02 在鼠标右击行为发生后显示出来
*/
```
## 主进程与渲染进程通信
```javascript
// main.js
const createWindow = () => {
let mainWin = new BrowserWindow({
// ...
})
let temp = [
{
label: 'send',
click() {
// 主进程主动与渲染进程发送消息
BrowserWindow.getFocusedWindow().webContents.send('mtp', '来自于主进程的消息')
}
}
]
let menu = Menu.buildFromTemplate(temp)
// ...
}
// ...
// 主进程接收消息操作
ipcMain.on('msg1', (ev, data) => {
console.log(data)
ev.sender.send('msg1Re', '来自于主进程的异步消息')
})
ipcMain.on('msg2', (ev, data) => {
console.log(data)
ev.returnValue = '来自于主进程的同步消息'
})
```
```javascript
// index.js
const { ipcRenderer } = require('electron')
window.onload = () => {
// 获取元素
let aBtn = document.getElementsByTagName('button')
// 01 采用异步的 API 在渲染进程中给主进程发送信息
aBtn[0].addEventListener('click', () => {
ipcRenderer.send('msg1', '来自于渲染进程的一条异步消息')
})
// 02 采用同步的 API 在渲染进程中给主进程发送信息
aBtn[1].addEventListener('click', () => {
let val = ipcRenderer.sendSync('msg2', '来自于渲染进程的一条同步消息')
alert(val)
})
// 接收消息的区域
ipcRenderer.on('msg1Re', (ev, data) => {
alert(data)
})
ipcRenderer.on('mtp', (ev, data) => {
alert(data)
})
}
```
## 渲染进程间通信
### 通过本地储存存储
```javascript
// main.js
// ...
// 定义全局变量存放主窗口 Id
let mainWinId = null
const createWindow = () => {
let mainWin = new BrowserWindow({
// ...
})
mainWinId = mainWin.id
// ...
}
// 接受其他进程发送的数据,完成后续的逻辑
ipcMain.on('openWin2', () => {
// 接收到渲染进程中按钮点击信息之后完成窗口2的打开
let subWin1 = new BrowserWindow({
// ...
parent: BrowserWindow.fromId(mainWinId), // 或定义全局变量
// ...
})
// ...
})
// ...
```
```javascript
// index.js
window.onload = () => {
// 先获取元素
let oBtn = document.getElementById('btn')
oBtn.addEventListener('click', () => {
ipcRenderer.send('openWin2')
// 打开窗口2之后保存数据至本地储存
localStorage.setItem('name', '啦啦啦啦')
})
}
```
```javascript
// subWin1.js
window.onload = () => {
let oInput = document.getElementById('txt')
let val = localStorage.getItem('name')
oInput.value = val
}
```
### 通过主进程
```javascript
// main.js
// 定义全局变量存放主窗口 Id
let mainWinId = null
const createWindow = () => {
let mainWin = new BrowserWindow({
// ...
})
mainWinId = mainWin.id
// ...
}
// 接受其他进程发送的数据,完成后续的逻辑
ipcMain.on('openWin2', (_, data) => {
// 接收到渲染进程中按钮点击信息之后完成窗口2的打开
let subWin1 = new BrowserWindow({
// ...
})
// ...
// 由 index -> main* -> subWin
// 转交给进程
subWin1.webContents.on('did-finish-load', () => {
subWin1.webContents.send('its', data)
})
})
// 由 subWin -> main* -> index
ipcMain.on('stm', (_, data) => {
// 转交给进程
let mainWin = BrowserWindow.fromId(mainWinId)
mainWin.webContents.send('mti', data)
})
// ...
```
```javascript
// index.js
window.onload = () => {
// 先获取元素
let oBtn = document.getElementById('btn')
// 由 index* -> main -> subWin1
oBtn.addEventListener('click', () => {
ipcRenderer.send('openWin2', '来自于 index 进程')
})
// 由 subWin1 -> main -> index*
// 接受消息
ipcRenderer.on('mti', (_, data) => {
alert(data)
})
}
```
```javascript
// subWin1.js
window.onload = () => {
// ...
// 在 sub 中发送数据给 index.js
let oBtn = document.getElementById('btn')
// subWin1* -> main -> index
oBtn.addEventListener('click', () => {
ipcRenderer.send('stm', '来自于sub进程')
})
// index -> main -> subWin1*
// 接收数据
ipcRenderer.on('its', (_, data) => {
alert(data)
})
}
```
## Dialog模块
[Electron官网 Dialog文档](https://www.electronjs.org/zh/docs/latest/api/dialog)
```javascript
// index.js
window.onload = () => {
let oBtn = document.getElementById('btn')
let oBtnErr = document.getElementById('btnErr')
oBtn.addEventListener('click', () => {
remote.dialog.showOpenDialog({
defaultPath: __dirname, // 默认打开目录
buttonLabel: '请选择', // 按钮上的文字
title: '啦啦啦啦', // 对话框标题
properties: ['openFiles', 'multiSelections'], // 文件类型
filters: [ // 文件类型过滤
{'name': '代码文件', extensions: ['js', 'json', 'html']},
{'name': '图片文件', extensions: ['ico', 'jpeg', 'png']},
{'name': '媒体类型', extensions: ['avi', 'mp4', 'mp3']},
],
}).then(ret => {
alert(ret)
})
})
oBtnErr.addEventListener('click', () => {
remote.dialog.showErrorBox('自定义标题', '当前错误内容')
})
}
```
## shell 与 iframe
### shell
```html
<!-- index.html -->
<html lang="en">
<head>
<!-- ... -->
</head>
<body>
<h2>shell and iframe</h2>
<!-- 默认行为是在当前页面打开网页 -->
<a id="openUrl" href="https://cantyonion.site">打开url</a>
<br><br>
<button id="openFolder">打开目录</button>
<script src="index.js"></script>
</body>
</html>
```
```javascript
// index.js
window.onload = () => {
// 1 获取元素
let oBtn1 = document.getElementById('openUrl')
let oBtn2 = document.getElementById('openFolder')
oBtn1.addEventListener('click', (ev) => {
ev.preventDefault() // 阻止默认行为
let urlPath = oBtn1.getAttribute('href')
// 打开外部浏览器
shell.openExternal(urlPath)
})
oBtn2.addEventListener('click', () => {
// 打开当前目录
shell.showItemInFolder(path.resolve(__filename))
})
}
```
### iframe
```html
<!-- index.html -->
<html lang="en">
<head>
<!-- ... -->
</head>
<body>
<iframe src="https://cantyonion.site" frameborder="0" id="webview"></iframe>
<script src="index.js"></script>
</body>
</html>
```
```javascript
// index.js
ipcRenderer.on('open', () => {
let iframe = document.getElementById('webview')
iframe.src = 'https://cantyonion.site/git/'
})
```
``` javascript
// main.js
const createWindow = () => {
let mainWin = new BrowserWindow({
// ...
})
let tmp = [
{
label: '菜单',
submenu: [
{
label: '关于',
click() {
shell.openExternal('https://cantyonion.site')
}
},
{
label: '打开',
click() {
BrowserWindow.getFocusedWindow().webContents.send('open')
}
},
]
}
]
let menu = Menu.buildFromTemplate(tmp)
Menu.setApplicationMenu(menu)
// ...
}
// ...
```
## 消息通知
前往[MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/Notification)查看详细内容
```javascript
window.onload = () => {
let oBtn = document.getElementById('btn')
oBtn.addEventListener('click', () => {
let option = {
title: '啦啦啦', // 通知标题
body: 'ok,哈哈哈哈', // 信息体
icon: 'c.png' // 图标
}
let mayNotification = new window.Notification(option.title, option)
mayNotification.onclick = () => {
alert("点击了消息页卡")
}
})
}
```
## 全局快捷键
```javascript
// main.js
// ...
app.on('ready', () => {
// 注册
let ret = globalShortcut.register('ctrl + q', () => {
console.log("快捷键注册成功")
})
if (!ret) {
console.log('注册失败')
}
// 判断某个快捷键是否已经注销
console.log(globalShortcut.isRegistered('ctrl + q'))
})
app.on('will-quit', () => {
globalShortcut.unregister('ctrl + q')
// 自动注销使用快捷键
globalShortcut.unregisterAll()
})
```
## 剪切板模块
```html
<!-- index.html -->
<html lang="en">
<head>
<!-- ... -->
</head>
<body>
<h2>剪切板</h2>
<input type="text" placeholder="输入需要复制的内容">
<button>复制</button>
<br><br>
<input type="text" placeholder="将内容粘贴至此处">
<button>粘贴</button>
<br><br>
<button id="clipImg">将图片拷贝至剪切板再粘贴至界面</button>
<script src="index.js"></script>
</body>
</html>
```
```javascript
// index.js
// ...
window.onload = () => {
// 获取元素
let aBtn = document.getElementsByTagName('button')
let aInput = document.getElementsByTagName('input')
let oBtn = document.getElementById('clipImg')
let ret = null
aBtn[0].onclick = () => {
// 复制内容
ret = clipboard.writeText(aInput[0].value)
}
aBtn[1].onclick = () => {
// 粘贴内容
aInput[1].value = clipboard.readText(ret)
}
oBtn.onclick = () => {
// 将图片放置于剪切板的时候要求图片类型属于 nativeImage 类型
let oImage = nativeImage.createFromPath('c.png')
clipboard.writeImage(oImage)
// 将剪切板中的图片作为 DOM 元素显示在界面上
let oImg = clipboard.readImage()
let oImgDom = new Image()
oImgDom.src = oImg.toDataURL() // 转换为base64
document.body.appendChild(oImgDom)
}
}
```