最近在了解跨平台桌面编程语言,发现Electron很有意思,它将Node.js,Html和CSS整合,可以利用Html做出很好的用户界面,也解决了系统调用权限的问题。由于接触不多,难免会有理解不周的地方。
Electron框架分成三部分,第一个是主框架(相当于CEF frame),它将内嵌Html页面,第二个是BrowserWindow,由外框架生成,负责向用户呈现Html,第三个是内含的preload.js它负责沟通主框架和内嵌页面之间的通信。
在14版本之前,主框架会开放出一个remote模块,它负责向Html开放Node.js的All of the Node.js APIs are available,以及主框架中的对象,例如mainWindow,及一些函数。但是后来发现虽然编程方便了,但安全性和效率会变的很差。由其当程序变大后,性能会下降很多。后来干脆不许在renderer.js中自由调用Node.js的API了。取而代之的是在preload.js中通过contextBridge向html暴露出需要使用到的功能,这样会大大提高执行性能和安全性。
当使用大于14版本时,我发现不能在renderer.js中加载Node.js中的remote,以至于想各种方法来开放它,我试了不少办法,均不奏效,包括修改如下main.js中的各种属性。
webPreferences: {
nodeIntegration: true, // 是否集成 Nodejs
contextIsolation: false
}
我下面将在Electron18d+VS Code中用一个简单的例子分步说明如何在主框架和Html之间进行交换数据,共演示了三种ipc方法:
1.Html发送消息到main;
2.main发送消息到Html;
3.Html发送消息到main并执行回调。
所用的文件main.js,menu.js,index.html,renderer.js,preload.js分别如下:
menu.js
菜单模板,演示如何生成主菜单及Node.js中如何引入外部变量和函数
var mod = require('./server.js');
const template = [
{
label: '文件',
submenu: [
{
label: 'New',
click(){ mod.MenuClick('New')}
},
{
label: 'Open',
click(){ mod.MenuClick('Open')}
}
]
}
];
module.exports = template;
main.js
启动框架
const {app, BrowserWindow, ipcMain} = require('electron')
const {dialog, Menu, MenuItem} = require('electron')
const template = require('./menu.js');
const path = require('path')
const fs = require('fs')
function createWindow() {
// Create the browser window.
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()
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
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()
})
})
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.
const menu = Menu.buildFromTemplate(template); //从模板创建主菜单
Menu.setApplicationMenu(menu);
// ipc main接收html的消息并响应
ipcMain.on('myAction', function (event, arg)
{
console.log(arg); // 打印ping
event.returnValue = 'pong';
});
// ipc main接收html的消息并执行回调
ipcMain.handle('callback', (event, count) => {return ++count});
// 由menu.js调用
function MenuClick(menustring)
{
switch(menustring)
{
case 'New':
console.log('click New');
break;
case 'Open':
// 打开一个本地文件,并在HTML中显示文件内容
const files = dialog.showOpenDialogSync({
filters: [
{name: "Text Files", extensions: ['txt', 'js', 'json']},
properties: ['openFile']
});
if(files)
{
const txtRead = fs.readFileSync(files[0], 'utf8');
// ipc main发送消息到html
mainWindow.webContents.send('myAction', txtRead)
}
break;
}
}
module.exports.MenuClick = MenuClick;
index.html
包含一段文字,用于显示文件内容
<!--index.html-->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>Hello World!</title>
</head>
<body>
<h1 id="hText">Hello World 你好!</h1>
<button id="btn">发消息到main</button>
We are using Node.js <span id="node-version"></span>,
Chromium <span id="chrome-version"></span>,
and Electron <span id="electron-version"></span>.
<!-- You can also require other files to run in this process -->
<script src="./renderer.js"></script>
</body>
</html>
renderer.js
与index.html对应的页面js
/ This file is required by the index.html file and will
// be executed in the renderer process for that window.
// No Node.js APIs are available in this process because
// `nodeIntegration` is turned off. Use `preload.js` to
// selectively enable features needed in the rendering
// process.
// 发送消息到main
document.getElementById("btn").onclick = () => {
var result = window.ipcRenderer.send('myAction', 'ping')
console.log(result); // 打印pong
};
// 接收main消息,返回数据到Html页面
window.ipcRenderer.receive('myAction', (message) => {
document.getElementById("hText").innerText = message;
console.log(message);
// 运行一个计数器,验证回调函数
counter()
});
function counter() {window.ipcRenderer.invoke('callback', 888).then((result) => {
// 在main中加1后返回,打印889
console.log(result);});
}
preload.js
上下文件通信,向html暴露会用到的系统功能
// All of the Node.js APIs are available in the preload process.
// It has the same sandbox as a Chrome extension.
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])
}
})
// Preload (Isolated World)
// 三种方式在main与html之间通信
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld(
'ipcRenderer',
{
// From render to main.
send: (channel, args) => { return ipcRenderer.sendSync(channel, 'ping') },
// From main to render.
// listener为自定义的消息处理函数,原型在renderer中
receive: (channel, listener) => {
ipcRenderer.on(channel, (event, args) => listener(args));},
// From render to main and back again.
invoke: (channel, args) => {
return ipcRenderer.invoke(channel, args);}
}
)