Electron框架的使用

为什么研究Electron?

这个事想起来也是挺乌龙。
在2023年左右当时有个项目机会,要同时对应Windows和Mac,为了提高开发效率,就想采用些跨平台框架,但由于当时我们对Mac的了解有限,所以基于当时一段时间的调查,就选择了Electron。但最终这个项目我们没有承接到,而我误认为我们的另一个项目本身也是使用的Electron框架。
今年农历年前的时候,又有个项目机会,由于是存在Base的,所以指定要基于Electron来开发。我就认为我们已经存在相同架构的项目,可以直接复用,所以这个地方就没有考虑报价。结果之后和技术负责人确认的时候才发现我记错了,而更尴尬的事,当时的调研结果,没有存在SVN上,而他的硬盘前段时间坏了~,这个地方反而变成了一个空白,那就再拿点时间看一下吧。这回我写这上,记录一下。
这种跨平台开发的常见的需求,基本上就是使用Web来开发界面,实现一套界面对应不同平台。
框架做的好的,有些不同平台的底层存在封装,框架没有的功能,或者业务要求的功能,在底层自己实现。Electron的框架因为在NodeJS已经封装实现了一些不同平台的基本功能,所以看起来可以减少很多一部分工作量,这也是我们当时为什么选了这个框架的原因。但对于我们现在接触到的业务要求来看,基本都得自己实现底层功能,所以,当时的选择,也就那么回事吧,也对也不对~~

Electron框架的搭建

使用VS Code能方便些。

  1. 创建个目录,比如electron_test;
  2. 使用VS Code打开目录;
  3. 打开终端,npm init;
  4. 一路回车就行,之后可以再修改;
  5. 在electron_test下建两个文件:.env,.npmrc;这两个就是完整名称,没有前面的名字和扩展名;目的是提供环境变量和国内镜像路径,方便之后从国内镜像下载速度快些;具体内容放到后面;
  6. npm install --save-dev electron
  7. package.json中scripts中加一行start命令:
    “scripts”: {
    “start”: “electron .”
    }
  8. 在electron_test下创建main.js,这个文件就是Electron的主进程;在其中引入Electron,创建窗口,进行生命周期的回调处理;具体内容放到后面;
  9. 在electron_test下创建一个pages目录,用于存储展示UI的界面,在pages目录下创建index.html,也就是渲染进程;在main.js中打开窗口后加载这个html;
  10. 在pages 目录下创建render.js,控制渲染进程的JS;
  11. 在electron_test目录下创建preload.js,用于桥接主进程和渲染进程的消息;
  12. 通过npm start启动程序;
  13. 在main.js中处理Mac系统中的窗口问题。

主进程与渲染进程的通信

主进程中main.js与渲染进程render.js通过preload.js进行通信;
preload中使用contextBridge建立桥接;preload在渲染进程中,所以在浏览器中查看Log信息;
通过contextBridge.exposeInMainWorld向渲染进程暴漏属性和方法;
contextBridge.exposeInMainWorld(‘myAPI’, {
version: process.version,
saveFile: (data) => {
ipcRenderer.send(‘save-file’, data)
},
readFile(){
return ipcRenderer.invoke(‘read-file’)
}
})
渲染进程调用到主进程,使用send,在主进程中引入ipcMain,使用ipcMain.on来接收;
ipcMain.on(‘save-file’, saveFile)
渲染进程调用到主进程并接收主进程的返回,使用invoke,在主进程中使用handle来接收;
ipcMain.handle(‘read-file’, readFile)

对VUE等框架的使用

Electron也可以使用Vue、React等框架,通过Vite实现,叫做electron-vite。

调用本地Dll的方法

调用本地dll肯定是从主进程中进行,可以理解成从NodeJS中调用dll,可以通过使用NodeJS的插件。
记录下使用使用ffi-napi来调用dll,可以理解为让js能够使用loadlibrary和getprocessadress。
安装ffi-napi
npm install ffi-napi ref-napi
或者通过配置package.json,使用npm install来安装。
需要注意,这个插件需要使用python及VS来编译
npm install -g node-gyp

.env

Electron 镜像(给 electron-builder 用)

ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/

electron-builder 二进制包镜像

ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/

.npmrc

npm 镜像

registry=https://registry.npmmirror.com/

Electron 安装镜像

electron_mirror=https://npmmirror.com/mirrors/electron/

electron-builder 镜像

electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/

package.json

{
“name”: “electron_test”,
“version”: “1.0.0”,
“description”: “first test”,
“main”: “main.js”,
“scripts”: {
“start”: “electron .”,
“build”: “electron-builder”,
“test”: “echo “Error: no test specified” && exit 1”
},
“build”:{
“appId”: “com.electron.test”,
“win”:{
“icon”: “icon.ico”,
“target”: [
{
“target”: “nsis”,
“arch”: [“x64”]
}
]
},
“nsis”:{
“oneClick”: false,
“perMachine”: true,
“allowToChangeInstallationDirectory”: true
}
},
“author”: “linw”,
“license”: “ISC”,
“devDependencies”: {
“electron”: “^40.6.0”,
“electron-builder”: “^26.8.1”,
“nodemon”: “^3.1.13”
}
}

main.js

const {app, BrowserWindow, ipcMain} = require(‘electron’)
const path = require(‘path’)
const fs = require(‘fs’)
function saveFile(event, data) {
console.log(data)
fs.writeFileSync(‘./hello.txt’, data)
}
function readFile() {
const res = fs.readFileSync(‘./hello.txt’).toString()
console.log(‘###’, res)
return res
}
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload:path.resolve(__dirname, ‘./preload.js’),
nodeIntegration: true
}
})
ipcMain.on(‘save-file’, saveFile)
ipcMain.handle(‘read-file’, readFile)
win.loadFile(‘./pages/index.html’)
}

console.log(process.versions.electron)
console.log(process.versions.chrome)
console.log(__dirname )

app.on(‘ready’, () => {
createWindow()

app.on(‘activate’, () => {
if (BrowserWindow.getAllWindows().length === 0)
createWindow()
})
})

app.on(‘window-all-closed’, () => {
if (process.platform !== ‘darwin’)
app.quit()
})

index.html

Welcome Interface
<div class="welcome-container">
    <h1 class="welcome-title">Welcome!</h1>
    <p class="welcome-subtitle">Discover amazing features designed for you</p>
    <input id="input1" type="text" placeholder="Enter your name"></input>
    <button id="btn1" class="cta-button">Get Started</button>
     <button id="btn2" class="cta-button">Read content</button>
    <div class="features">
        <div class="feature">
            <div class="feature-icon">🚀</div>
            <div class="feature-text">Fast & Efficient</div>
        </div>
        <div class="feature">
            <div class="feature-icon">🔒</div>
            <div class="feature-text">Secure & Reliable</div>
        </div>
        <div class="feature">
            <div class="feature-icon">🎨</div>
            <div class="feature-text">Beautiful Design</div>
        </div>
    </div>
</div>
<script type="text/javascript" src="./render.js"></script>

render.js

const btn1 = document.getElementById(‘btn1’)
const btn2 = document.getElementById(‘btn2’)
console.log(‘render’)
btn1.onclick = function () {
console.log(‘btn1 click’)
alert(input1.value)
alert(myAPI.version)
myAPI.saveFile(input1.value)
}

btn2.onclick = async function () {
console.log(‘btn2 click’)
let data = await myAPI.readFile()
alert(data)
}

preload.js

console.log(‘preload’, process.version)
const { contextBridge, ipcRenderer } = require(‘electron’)
const { readFile } = require(‘original-fs’)

contextBridge.exposeInMainWorld(‘myAPI’, {
version: process.version,
saveFile: (data) => {
ipcRenderer.send(‘save-file’, data)
},
readFile(){
return ipcRenderer.invoke(‘read-file’)
}
})

使用package.json配置安装ffi-napi的配置

{
“name”: “electron_dll”,
“version”: “1.0.0”,
“description”: “”,
“main”: “main.js”,
“scripts”: {
“start”: “electron .”,
“rebuild”: “@electron/rebuild”,
“build”: “electron-builder --win”,
“test”: “echo “Error: no test specified” && exit 1”
},
“build”: {
“appId”: “com.electron.dll.demo”,
“productName”: “ElectronDLLDemo”,
“directories”: {
“output”: “dist”
},
“extraResources”: [
“./resources/**”
],
“win”: {
“target”: “nsis”,
“arch”: [“x64”]
}
},
“dependencies”: {
“ffi-napi”: “^4.0.3”,
“ref-napi”: “^3.0.3”
},
“author”: “”,
“license”: “ISC”,
“devDependencies”: {
“electron”: “^40.6.1”,
“electron-builder”: “^24.6.4”,
“electron-rebuild”: “^3.2.9”
}
}

使用DLL的JS

main.js
const { app, BrowserWindow } = require(‘electron’)
const ffi = require(‘ffi-napi’);
const path = require(‘path’);
const ref = require(‘ref-napi’);

function getDllAbsolutePath() {
// 判断是否为打包后的环境
if (app.isPackaged) {
// 打包后:resources目录在exe同级
return path.join(process.resourcesPath, ‘FunctionControl.dll’);
} else {
// 开发环境:指向项目中的resources目录
return path.join(__dirname, ‘resources’, ‘FunctionControl.dll’);
}
}

// 封装DLL调用函数
function initDllCalls() {
try {
const dllPath = getDllAbsolutePath();
console.log(‘DLL路径:’, dllPath);

// 定义DLL接口(根据你的实际接口类型选择对应配置)
const privacyDll = ffi.Library(dllPath, {
    "SumTest": [ref.types.int, [ref.types.int, ref.types.int]],
});

// ========== 调用示例 ==========
// 调用SumTest(int返回,两个int参数)

const sumResult = privacyDll.SumTest(10, 20); // 传入两个整数参数
console.log(‘SumTest 调用结果:’, sumResult);

} catch (error) {
console.error(‘DLL调用失败:’, error);
// 常见错误:DLL路径错误、接口名拼写错误、参数/返回值类型不匹配、ABI未重建
}
}

const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600
})

win.loadFile(‘./pages/index.html’)
}

app.whenReady().then(() => {
createWindow()

app.on(‘activate’, () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})

app.on(‘window-all-closed’, () => {
if (process.platform !== ‘darwin’) app.quit()
})

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐