前言

这段时间在整理今年的资料。这篇文章记录于2023年初,当时的情况是要将网页端的内容移动到服务端,最省事省力的方案自然是用Electron解决(至于臃肿的问题,那就交给消费者自己去烦恼吧😂)

以下部分仅作记录,今天暂且就水一篇

安装

修改.yarnrc

1
2
3
registry "https://registry.npmmirror.com"
ELECTRON_MIRROR "https://npmmirror.com/mirrors/electron/"
ELECTRON_BUILDER_BINARIES_MIRROR "https://npmmirror.com/mirrors/electron-builder-binaries/"

Vue启动

1
2
3
4
npm create electron-vite
or
git clone https://github.com/electron-vite/electron-vite-vue
然后yarn

Quick start

1
2
3
mkdir my-electron-app && cd my-electron-app
yarn init
yarn add --dev electron

解决CSP问题

1
2
<!-- https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CSP -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">

预加载

你不能直接在主进程中编辑DOM,因为它无法访问渲染器 文档 上下文。 它们存在于完全不同的进程!

这是将 预加载 脚本连接到渲染器时派上用场的地方。 预加载脚本在渲染器进程加载之前加载,并有权访问两个 渲染器全局 (例如 windowdocument) 和 Node.js 环境。

创建一个名为 preload.js 的新脚本如下:

1
2
3
4
5
6
7
8
9
10
window.addEventListener('DOMContentLoaded', () => {
const replaceText = (selector, text) => {
const element = document.getElementById(selector)
if (element) element.innerText = text
}

for (const dependency of ['chrome', 'node', 'electron']) {
replaceText(`${dependency}-version`, process.versions[dependency])
}
})

要将此脚本附加到渲染器流程,请在你现有的 BrowserWindow 构造器中将路径中的预加载脚本传入 webPreferences.preload 选项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 需在当前文件内开头引入 Node.js 的 'path' 模块
const path = require('path')

// modify your existing createWindow() function
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})

win.loadFile('index.html')
}
// ...

进程通信

https://www.electronjs.org/zh/docs/latest/tutorial/ipc

主进程的主要目的是使用 BrowserWindow 模块创建和管理应用程序窗口

electron中的ipc通信(二)

大概是全网最详细的Electron ipc 讲解(一)——主进程与渲染进程的两情相悦

主进程与渲染进程通信

主进程

  • 一个electron只能有一个主进程
  • 主进程指的是,你在执行 electron . 命令后,对应的当前目录下面的package.json文件下面的main的指定文件将会被执行,这里指的是

渲染进程(Renderer Process)

  • 由主进程调用GUI接口创建的页面,都有自己的进程,叫做渲染进程。主进程通过实例化BrowserWindow,每个BrowserWindow实例都会渲染一个web页面,相互独立,当一个BrowserWindow被销毁,相应的渲染进程也会被终止。
  • 渲染进程被主进程管理,每个渲染进程相互独立,只管理他们自己的web页面

两者需要通信,就需要用ipc通信

Example

单向

主进程.ts(可以写NodeJS代码,访问本地存储)

1
2
3
4
5
6
7
8
9
10
11
ipcMain.on("async-message", (event, arg) => {
//异步消息回复
console.log(`async-message:我接收到了异步消息`, arg);
event.reply("async-reply", "哥们我收到消息了-来自异步");
});

ipcMain.on("sync-message", (event, arg) => {
//同步消息回复
console.log(`async-message:我接收到了同步消息`, arg);
event.returnValue = "哥们我收到消息了-来自同步";
});

渲染进程.ts

1
2
3
4
5
6
7
8
const { ipcRenderer } = require('electron');
ipcRenderer.on("async-reply", (event, arg) => {
console.log("异步消息:", arg);
data = JSON.parse(arg)
console.log(data);
});

ipcRenderer.send("async-message", "发个异步消息");

ipcRenderer.send进行触发,ipcRenderer.on对返回进行相应

双向

ipcRenderer.invokeipcMain.handle 搭配使用

1
2
3
4
5
6
7
//render.js
const { ipcRenderer } = require('electron');

async function invokeMessageToMain() {
const replyMessage = await ipcRenderer.invoke('render-invoke-to-main', '我是渲染进程通过 invoke 发送的消息');
console.log('replyMessage', replyMessage);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//main.js
ipcMain.handle('render-invoke-to-main', async (event, message) => {
console.log(`receive message from render: ${message}`)
const result = await asyncWork();
return result;
})

const asyncWork = async () => {
return new Promise(resolve => {
setTimeout(() => {
resolve('延迟 2 秒获取到主进程的返回结果')
}, 2000)
})
}

功能

读取文件

使用icp

Electron教程(五)读取本地文件内容, icpMain icpRenderer 之间的交互

双向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//main.ts
ipcMain.handle('render-invoke-to-main', async (event, message) => {
console.log(`receive message from render: ${message}`)
const result = await asyncWork();
return result;
})
async function asyncWork(){
return new Promise(resolve => {
let fs = require("fs");
fs.readFile('E:\\code\\test_node\\a.json', function (err, data) {
if (err) {
console.log(err)
resolve(err)
}
resolve(data.toString())
});
})
}
1
2
3
//render.js
const replyMessage = await ipcRenderer.invoke('render-invoke-to-main', 'render invoke');
data = JSON.parse(replyMessage)

单向

1
2
3
4
5
6
7
8
9
10
11
//main.js
ipcMain.on('asynchronous-message', function(event, arg) {
let fs = require("fs");
fs.readFile("E:\\code\\test_node\\a.json","utf8",(err,data)=>{
if(err){
event.sender.send('asynchronous-reply', "读取失败");
}else{
event.sender.send('asynchronous-reply', data);
}
})
});
1
2
3
4
5
6
7
8
9
//render.js
const { ipcRenderer } = require('electron');
ipcRenderer.on("asynchronous-reply", function(event, arg) {
// 这里的arg是从主线程请求的数据
console.log(arg)
data = JSON.parse(arg)
console.log(data);
});
ipcRenderer.send("asynchronous-message", "ping");

使用预加载

修改DOM树

1
2
3
4
5
6
7
8
9
10
11
12
13
window.addEventListener('DOMContentLoaded', () => {
let readAndDisplay = () => {
let fs = require("fs");
fs.readFile("E:\\code\\test_node\\input.txt","utf8",(err, data) => {
if (err) return console.error(err);
console.log(data.toString());
});
}

document.querySelector('#btn1').addEventListener("click", event => {
readAndDisplay();
})
})

全屏

1
2
3
4
5
6
mainWindow = new BrowserWindow({
//...
fullscreenable:true,
fullscreen: true,
simpleFullscreen:true,
});

取消菜单栏

1
2
import { Menu } from 'electron'
Menu.setApplicationMenu(null) // null值取消顶部菜单栏

使用系统摄像头

未解决的issue

Electron加载HTTP页面打开本地摄像头

纯HTML5打开

Electron 从入门到实践07之实战 相机应用(还是mediaDevices.getUserMedia,但是保存到了本地)

Esc退出全屏

https://www.electronjs.org/zh/docs/latest/api/global-shortcut

1
const { app, globalShortcut } = require('electron')

在CreateWindow()中添加相关

1
2
3
globalShortcut.register('ESC', function () {
win.close()
})

流程模型

https://www.electronjs.org/zh/docs/latest/tutorial/process-model

Electron 应用程序的结构非常相似。 作为应用开发者,你将控制两种类型的进程:主进程渲染器进程。 这类似于上文所述的 Chrome 的浏览器和渲染器进程。

窗口管理

主进程的主要目的是使用 BrowserWindow 模块创建和管理应用程序窗口。

BrowserWindow 类的每个实例创建一个应用程序窗口,且在单独的渲染器进程中加载一个网页。 您可从主进程用 window 的 webContent 对象与网页内容进行交互。

1
2
3
4
5
6
7
const { BrowserWindow } = require('electron')

const win = new BrowserWindow({ width: 800, height: 1500 })
win.loadURL('https://github.com')

const contents = win.webContents
console.log(contents)

预加载(Preload 脚本)

预加载(preload)脚本包含了那些执行于渲染器进程中,且先于网页内容开始加载的代码 。 这些脚本虽运行于渲染器的环境中,却因能访问 Node.js API 而拥有了更多的权限。

预加载脚本可以在 BrowserWindow 构造方法中的 webPreferences 选项里被附加到主进程。

main.js

1
2
3
4
5
6
7
8
const { BrowserWindow } = require('electron')
//...
const win = new BrowserWindow({
webPreferences: {
preload: 'path/to/preload.js',
},
})
//...

因为预加载脚本与浏览器共享同一个全局 Window 接口,并且可以访问 Node.js API,所以它通过在全局 window 中暴露任意 API 来增强渲染器,以便你的网页内容使用。

进程间通信

https://www.electronjs.org/zh/docs/latest/tutorial/ipc

打包

electron-forge

https://www.electronforge.io/

electron-builder

资料

官方

https://www.electronjs.org/zh/docs/latest/

https://www.electronjs.org/

electron/electron-quick-start

blogs

electron 应用开发优秀实践

Bugs

Electron+Vite+Vue3中

public中可能会有几张无法打包

在App.vue中引用,会无法显示

应急解决方案:将public的打进入assets

After build, assets can not resolved correct. / 打包后,assets下资源路径不正确 #287