1. 应用基础
入口main.js,在app中启动一个window加载preload.js和html文件,然后html中添加render.js用于渲染。
定义preload.js是用于渲染页面和主程序进行交互,使用contextBridge将nodejs的api暴露给渲染页面),主要代码如下:
app.whenReady().then(() =>{
const mainWindow = new BrowserWindow({webPreferences: {preload: path.join(__dirname, 'preload.js')}})
mainWindow.loadFile('index.html')
}
BrowserWindow中可以继续用BrowserView来加载外部网址:
const view = new BrowserView()
win.setBrowserView(view)
view.webContents.loadURL('https://electronjs.org')
2. main与render的交互
搞这么复杂的原因就是浏览器的js页面不允许加载js库,很多复杂的功能实现不了,必须得在main.js中实现。
需要preload.js的原因是,渲染进程中不允许直接使用ipcRender;
需要渲染js的原因是:preload.js无法操纵页面元素
真是麻了。总结一下:
- html中使用
<script src="render.js"></script>
加载页面事件监听和元素操纵的代码 - preload.js使用
contextBridge.exposeInMainWorld
暴露接口给render.js,在render.js中使用window.接口进行调用 - main.js相当于后端代码,除了初始化app、创建window等外,还可以创建一系列函数,提供给preload.js进行使用。
使用ipcMain & ipcRender。ipcRenderer提供方法从渲染进程 (web 页面) 发送同步或异步的消息到主进程。ipcMain则在主进程接受消息并进行处理,也可以将结果返回给渲染进程。
示例用的交互代码如下:
## preload.js
window.addEventListener('DOMContentLoaded', () => {
const replaceText = (selector, text) => {
const element = document.getElementById(selector)
if (element) element.innerText = text
}
for (const type of ['chrome', 'node', 'electron']) {
replaceText(`${type}-version`, process.versions[type])
}
})
渲染页面通过如下代码加载渲染用的js代码:
<script src="./renderer.js"></script>
官方的electron fiddle示例跑出来也有问题,这里总结一下能成功运行的方法。
2.1 同步:使用ipcRender.sendSync和ipcMain.on(event.returnValue)
示例如下:
// render.js
const { ipcRenderer } = require('electron');
function sendSyncMessageToMain() {
const replyMessage = ipcRenderer.sendSync('render-send-sync-to-main', '我是渲染进程通过 syncSend 发送给主进程的消息');
console.log('replyMessage', replyMessage); // '主进程回复的消息'
}
// main.js
const { ipcMain } = require('electron');
ipcMain.on('render-send-sync-to-main', (event, message) => {
console.log(`receive message from render: ${message}`)
event.returnValue = '主进程回复的消息';
})
2.2 异步:使用ipcRender.invoke和ipcMain.handle(return)
渲染进程异步等待主进程的回应, invoke 的返回值是一个 Promise 。我们可以不使用 await 来接收 invoke 的返回结果,打印的结果符合我们的预期,是一个 Promise。
// render.js
const { ipcRenderer } = require('electron');
async function sendAsyncMessageToMain() {
const replyMessage = await ipcRenderer.invoke('render-invoke-to-main', '我是渲染进程通过 invoke 发送的消息');
console.log('replyMessage', replyMessage);
}]
// main.js
const { ipcMain } = require('electron');
ipcMain.handle('render-invoke-to-main', async (event, message) => {
console.log(`receive message from render: ${message}`)
const result = await asyncWork();
return result;
})
3. 综合示例
下面是使用highs.js进行线性规划求解的一个例子,效果如图:
3.1 后台函数main.js
定义求解函数
const highs_promise = require("highs")();
const { app,BrowserWindow, ipcMain } = require('electron');
const path = require('node:path')
let win;
function createWindow() {
win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
preload: path.join(__dirname, 'preload.js')
}
});
win.loadFile('index.html');
win.on('closed', () => {win = null;});
}
app.whenReady().then(() => {
createWindow();
ipcMain.handle('solve-lp', async (event, data) => {
const highs = await highs_promise
return highs.solve(data)
})
})
3.2 接口封装preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
solve: async (data) => {return await ipcRenderer.invoke('solve-lp', data)}
})
3.3 前端函数render.js
添加按钮事件,将求解结果绑定到dom元素上
document.getElementById('solve-btn').addEventListener('click', (event) => {
event.preventDefault()
const promise = window.electronAPI.solve(document.getElementById('Problem').value)
promise.then(res => {document.getElementById('result').innerHTML = JSON.stringify(res, null, " ")})
})
3.4 网页文件index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Linear Programming Solver</title>
</head>
<body>
<h1>线性规划求解器</h1>
<h2>输入问题</h2>
<form id="linear-form">
<textarea id="Problem" name="Problem" rows="10" cols="80">
Maximize obj:x1 + 2 x2 + 4 x3 + x4
Subject To
c1: - x1 + x2 + x3 + 10 x4 <= 20
c2: x1 - 4 x2 + x3 <= 30
c3: x2 - 0.5 x4 = 0
Bounds
0 <= x1 <= 40
2 <= x4 <= 3
End</textarea><br><br>
<button id="solve-btn">求解</button>
</form>
<div id="result"></div>
<script src="linear-form.js"></script>
</body>
</html>
3.5 项目配置文件package.json
{
"name": "test",
"version": "1.0.0",
"description": "test",
"main": "main.js",
"scripts": {
"start": "electron .",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "test",
"license": "ISC",
"dependencies": {"highs": "^1.8.0"}
}
使用cnpm install highs --save
安装好highs,然后执行cnpm start
就可以运行程序啦。
4. 高级功能
autoUpdater
const { app, autoUpdater } = require('electron/main')
app.whenReady().then(() => {
const server = 'https://your-deployment-url.com'
const feed = `${server}/update/${process.platform}/${app.getVersion()}`
try {
autoUpdater.setFeedURL(feed)
} catch (error) {
console.log(error)
}
})
clipboard
main.js中的函数如下:
const { app, BrowserWindow, ipcMain, clipboard } = require('electron/main')
ipcMain.handle('clipboard:readText', () => {
return clipboard.readText()
})
ipcMain.handle('clipboard:writeText', (event, text) => {
clipboard.writeText(text)
})
通过preload.js提供给html文件:
const { contextBridge, ipcRenderer } = require('electron/renderer')
contextBridge.exposeInMainWorld('clipboard', {
readText: () => ipcRenderer.invoke('clipboard:readText'),
writeText: (text) => ipcRenderer.invoke('clipboard:writeText', text)
})
然后在render.js中定义html的页面渲染函数:
window.addEventListener('DOMContentLoaded', () => {
const copyButton = document.querySelector('#copy')
const pasteButton = document.querySelector('#paste')
const textarea = document.querySelector('textarea')
copyButton.onclick = () => {
window.clipboard.writeText('Hello from Electron!') # 向剪贴板中写入text
}
pasteButton.onclick = async () => {
textarea.value = await window.clipboard.readText()
}
})
文件对话框
const { filePaths, canceled } = await dialog.showOpenDialog(mainWindow, {properties: ['openFile']})
菜单栏
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
网络
const { app, net } = require('electron/main')
app.whenReady().then(() => {
const request = net.request('https://github.com')
request.on('response', (response) => {
console.log(`STATUS: ${response.statusCode}`)
console.log(`HEADERS: ${JSON.stringify(response.headers)}`)
response.on('data', (chunk) => {
console.log(`BODY: ${chunk}`)
})
response.on('end', () => {
console.log('No more data in the response.')
})
})
request.end()
})