IPC
Webview内主动触发
异步消息通知
// preload.js - 在畫面轉譯處理序中 (網頁)。
const { ipcRenderer } = require('electron')
ipcRenderer.on('need-clean-reply', (event, arg) => {
console.log(arg) // 印出 "貓咪肚子餓"
})
ipcRenderer.send('take-cat-home-message', '帶小貓回家')
// main.js - 在主處理序裡。
const { ipcMain } = require('electron')
ipcMain.on('take-cat-home-message', (event, arg) => {
console.log(arg) // prints "帶小貓回家"
event.reply('need-clean-reply', '貓咪肚子餓')
})
// preload.js - 在畫面轉譯處理序中 (網頁)。
const { ipcRenderer } = require('electron')
ipcRenderer.invoke('take-cat-home-handle', '帶小貓回家')
// then 回傳前可以做其他事,例如打掃家裡
.then(msg => console.log(msg)) // prints "小貓肚子餓,喵喵叫"
// main.js - 在主處理序裡。
const { ipcMain } = require('electron')
ipcMain.handle('take-cat-home-handle', async (event, arg) => {
console.log(arg) // prints "帶小貓回家"
return '小貓肚子餓,喵喵叫'
})
同步消息通知
// preload.js - 在畫面轉譯處理序中 (網頁)。
const { ipcRenderer } = require('electron')
const message = ipcRenderer.sendSync('take-cat-home-message', '帶小貓回家')
console.log(message) // prints "小貓肚子餓"
// main.js - 在主處理序裡。
const { ipcMain } = require('electron')
ipcMain.on('take-cat-home-message', (event, arg) => {
console.log(arg) // prints "帶小貓回家"
// event 回傳前你一直關注著小貓
event.returnValue = '小貓肚子餓'
})
主线程通知
// main.js - 在主處理序裡。
mainWindow.webContents.send('switch-cat', number);
// preload.js - 在畫面轉譯處理序中 (網頁)。
const { ipcRenderer } = require('electron')
ipcRenderer.on('switch-cat', (event, args) => switchCat(args));
获取webContents的方式
主线程
ipcMain.on('notify:new-msg', (event, chat) => {
const mainWindow = BrowserWindow.fromWebContents(event.sender); // 利用 event.sender 取得 currentWindow
const isFocused = mainWindow.isFocused(); // 確認 mainWindow 是否在最上面
const myNoti = new Notification({
title: `${chat.name}有新的對話`,
subtitle: chat.msg
});
myNoti.on('click', () => mainWindow.show()); // 將 mainWindow 帶到最上面
myNoti.on('close', () => mainWindow.show()); // 將 mainWindow 帶到最上面
myNoti.show();
if (!isFocused) {
// 工作列按鈕閃爍
mainWindow.flashFrame(true);
}
});
const mainWindow = BrowserWindow.fromWebContents(event.sender); // 利用 event.sender 取得 currentWindow
BrowserWindow.fromId(this.winId).webContents.send('update-badge', this.badgeCount);
渲染线程
// preload.js https://www.npmjs.com/package/@electron/remote
import { remote } from "electron"
remote.getCurrentWindow()
remote.getCurrentWebContents()
Clipboard
// @/utils/clipboardUtils.js
const read = clipboard => {
const aFormats = clipboard.availableFormats();
const isImageFormat = aFormats.find(f => f.includes('image'));
const isHtmlFormat = aFormats.find(f => f.includes('text/html'));
const isTextFormat = aFormats.find(f => f.includes('text/plain'));
const isRtfFormat = aFormats.find(f => f.includes('text/rtf'));
if (isImageFormat) {
const nativeImage = clipboard.readImage(); // 取得 clipboard 中的圖片
return nativeImage.toDataURL(); // data:image/png;
}
// 取得 clipboard 中的文字
else if (isTextFormat) return clipboard.readText();
// 取得 clipboard 中的 html 文字
else if (isHtmlFormat) return clipboard.readHTML();
// 取得 clipboard 中的 rtf 文字
else if (isRtfFormat) return clipboard.readRTF();
else return null;
}
module.exports = {
read,
}
下载
Electron内置模块实现
在 electron 中的下载行为,都会触发 session 的 will-download 事件。在该事件里面可以获取到 downloadItem 对象,通过 downloadItem 对象实现一个简单的文件下载管理器
拿到 downloadItem 后,暂停、恢复和取消分别调用 pause
、resume
和 cancel
方法。当我们要删除列表中正在下载的项,需要先调用 cancel 方法取消下载。
在 downloadItem 中监听 updated 事件,可以实时获取到已下载的字节数据,来计算下载进度和每秒下载的速度。
// 计算下载进度
const progress = item.getReceivedBytes() / item.getTotalBytes()
// 下载速度:
已接收字节数 / 耗时
Notes:
- Electron自身的下载模块忽略headers信息,无法满足断点续下载,需要调研request模块自身实现下载。
Request模块实现
https://github.com/request/request#requestoptions-callback Request模块请求配置
import request, { Headers } from "request";
interface DownLoadFileConfiguration {
remoteUrl: string;
localDir: string;
name: string;
onStart: (headers: Headers) => void;
onProgress: (received: number, total: number) => void;
onSuccess: (filePath: string) => void;
onFailed: () => void;
}
function downloadFile(configuration: DownLoadFileConfiguration) {
let received_bytes = 0;
let total_bytes = 0;
const {
remoteUrl,
localDir,
name,
onStart = noop,
onFailed = noop,
onSuccess = noop,
onProgress = noop,
} = configuration;
const req = request({
method: "GET",
uri: remoteUrl,
headers: {
"Content-Type": "application/octet-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
Pragma: "no-cache",
Range: `bytes=${0}-`,
},
});
function abort(this: any, filepath: string) {
this.abort();
// 文件操作https://ourcodeworld.com/articles/read/106/how-to-choose-read-save-delete-or-create-a-file-with-electron-framework
removeFile(filepath);
}
const absolutePath = path.resolve(localDir, name);
const out = fs.createWriteStream(absolutePath);
req.pipe(out);
req.on("response", function (data) {
total_bytes = parseInt(data.headers["content-length"] || "");
const id = uuidv4(); // ⇨ '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'
onStart(id, data.headers);
});
if (Object.prototype.hasOwnProperty.call(configuration, "onProgress")) {
req.on("data", function (chunk) {
received_bytes += chunk.length;
onProgress(received_bytes, total_bytes);
});
} else {
req.on("data", function (chunk) {
received_bytes += chunk.length;
});
}
req.on("end", function () {
onSuccess(absolutePath);
});
req.on("error", function (err) {
onFailed();
});
return {
abort: abort.bind(req, absolutePath),
pause: req.pause.bind(req),
resume: req.resume.bind(req),
}
}
Notes:
- Mac环境下,针对大文件,该包默认只能下载8s左右,猜测是
timeout
超时时间问题,使用keep-alive: true
就没有问题 -
request
模块的pause
、resume
、abort
方法在文档中没有声明,分别对应暂停、继续、取消操作。
断点续下载
前置条件
- 需要服务端支持
Accept-Ranges:bytes
可标识当前资源是支持范围请求的。
下载的细节处理
- 请求
Headers
加入Range
fetch(
"http://nginx.org/download/nginx-1.15.8.zip",
{
method: "GET", //请求方式
mode: 'cors',
headers: { //请求头
"Cache-Control": "no-cache",
Connection: "keep-alive",
Pragma: "no-cache",
Range: "bytes=0-1" // Range设置https://www.cnblogs.com/nextl/p/13885225.html
}
}
)
- 文件合并
- fs.createWriteStream创建文件写入流
- flags:如果是追加文件而不是覆盖它,则
flags
模式需为a
模式而不是默认的w
模式。
- flags:如果是追加文件而不是覆盖它,则
const absolutePath = path.resolve(localDir, name);
const out = fs.createWriteStream(absolutePath, {
flags: resume ? "a" : "w",
});
req.pipe(out);
- 追加(fs.appendFileSync)的方式
https://www.jianshu.com/p/934d3e8d371e
分片并发下载
安装第三方应用
exe安装包不像msi安装包有安装规范,且安装后会自动启动应用程序,而不是安装、启动分为两步。
https://www.pdq.com/blog/install-silent-finding-silent-parameters/
"Notion Setup 2.0.34.exe" -s /S
Notes: -s /S
是防止安装完,自动启动应用程序。
获取第三方应用启动路径
通过查找注册表实现
function getExecutablePath(packageName: string): Promise<string> {
return new Promise((resolve, reject) => {
exec(
`REG QUERY HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run /s /f ${packageName}`,
function (err, stdout: string, _stderr) {
if (err) {
reject("failed");
} else {
const executablePath = stdout
.split(" ")
.filter((item) => item.trim())[3];
resolve(executablePath);
}
}
);
});
}
启动三方应用
本质是通过子进程,调用不同系统环境的命令行唤起应用。
// /src/classes/App.cls.ts
import { spawn } from "child_process";
class App {
process: any;
pid: number;
constructor(process: any) {
this.process = process;
this.pid = process.pid;
}
private killApp(pid: number) {
// 检测程序是否启动
// wmic process where caption=”XXXX.exe” get caption,commandline /value
// 根据进程名杀死进程
// taskkill /F /IM XXX.exe
spawn("cmd", ["/c", "taskkill", "/pid", "/IM", pid.toString()], {
shell: true, // 隐式调用cmd
stdio: "inherit",
});
}
on(eventType: "error" | "close" | "exit", listener: (...args: any) => void) {
this.process.on(eventType, (code: number) => {
console.log(`spawn error: ${code}`);
listener(code);
});
}
close() {
this.killApp(this.pid);
}
}
export default App;
// /src/preload/index.ts
import { spawn } from "child_process";
import App from "../classes/App.cls";
function openApp(path: string): any {
const childProcess = spawn("cmd", ["/c", "start", path], {
shell: true, // 隐式调用cmd
stdio: "inherit",
});
console.log("--------childProcess------", childProcess);
const app = new App(childProcess);
/**
* Notes: 这里没有返回app,而是重新组装了对象 —— preload中无法返回复杂类型
* https://stackoverflow.com/questions/69203193/i-passed-a-class-to-my-front-end-project-through-electrons-preload-but-i-could
* https://www.electronjs.org/docs/latest/api/context-bridge#parameter--error--return-type-support
*/
return {
on: app.on.bind(app),
close: app.close.bind(app)
};
}
contextBridge.exposeInMainWorld("JSBridge", {
openApp,
});
// index.vue
const app = window.JSBridge.openApp(absolutePath);
app.on("error", (code: number) => {
console.log("---------spawn error----------", code, absolutePath);
});
app.on("close", (code: number) => {
console.log("---------spawn close----------", code, absolutePath);
});
app.on("exit", (code: number) => {
console.log("---------spawn exit----------", code, absolutePath);
});
setTimeout(() => {
app.close();
}, 10000);
Window命令行安装软件 —— winget需要安装
打包
通用方案
electron打包工具有两个:electron-builder,electron-packager,官方还提到electron-forge,其实它不是一个打包工具,而是一个类似于cli的工具集,目的是简化开发到打包的一整套流程,内部打包工具依然是electron-packager。
electron-builder与electron-packager相比各有优劣,electron-builder配置项较多,更加灵活,打包体积相对较小,同时上手难度大;而electron-packger配置简单易上手,但是打出来的应用程序包体积相对较大。
打包优化分为打包时间优化和体积优化
双package.json结构
增加打包配置:安装路径选择,icon等信息
平台
在 Windows 平台上可以打包成 NSIS 應用與 Portal 應用兩種類型:
类型 | 功能特点 | 更新方式 |
---|---|---|
Portal | 绿色程序,启动exe即可使用 | 下載新執行檔案 , 關閉並刪除舊執行檔案 => 更新完成 |
NSIS | 安装程序,安装后才能使用 | 下載新安裝檔 , 安裝後重開應用程式 => 更新完成 |
Portal 版本
如果你是 Portal 版的程式 , 只要下載新的 exe , 並覆蓋掉舊的應用程式 , 就算完成更新了 !NSIS 版本
當你用 electron-builder 生成一個 windows 安裝檔時 , 那個安裝檔就是 NSIS 版本
使用 electron-builder 產出的 NSIS 安裝檔 , 他具體的安裝步驟如下
執行安裝檔
安裝檔比對 Windows 內有沒有 appId 相同的 Electron 程式
如果有 appId 相同的 Electron 程式 , 執行 old-uninstaller.exe 將舊版解安裝
安裝目前版本的 Electron 程式 , 並將其開啟
因此如果你要更新應用程式 , 你只要拿到新版的 NSIS 安裝檔並執行它 , 安裝完成後你就可以享用更新後的應用程式了 !
https://ithelp.ithome.com.tw/articles/10254919
Q&A
- Q:
dialog
模块undefined
- A:
import { remote } from "electron";
通过remote.dialog
调用
- A:
- Q:
An Object could not be cloned?
- A:检测IPC通信中是否包含响应式数据
- Q:下载过程中,定义对象,可以持续更新么?
- A:尝试过,可以。