当前位置: 首页 > 工具软件 > When.js > 使用案例 >

Electron14以上版本中main.js、renderer.js及preload.js关系和消息调用

丁文轩
2023-12-01

Electron14以上版本中main.js、renderer.js及preload.js关系和消息调用

最近在了解跨平台桌面编程语言,发现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);}   
    }

)
 类似资料: