从零开始的electron入门与避坑
electron开发入门教程,与避坑。从零开始一步一步编写代码直到打包生成electron安装程序electron 通信electron 打包websockit 通信
1.开始之前
你需要注意以下内容
1.需要掌握html+js+css ,这个不用多说,页面展示、页面交互必备
2.需要掌握node.js 这是js 的运行时环境,用于使JavaScript在服务器端运行,开发时会用到许多node.js的api
3.包管理工具的使用
4.本项目,以electron程序开发流程为主,不注重页面内容所以没有使用vue/react 等前端框架
5. 了解electron文档
2.搭建基本目录结构
2.1 初始化项目文件夹
我这里新建了一个 D:\Project\electron-learning 文件夹,并使用npm init -y 初始化项目,会得到一个package.json的包配置文件,他的内容如下
{
"name": "electron-learning",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}**加粗样式**
2.2 文件结构

nodeServer文件夹
是node服务,与主进程代码分开,这样看着不混乱,这有什么作用呢?其实是为了处理一些特殊业务,而nodeServer/server.js是node 的启动文件
src文件夹
这个是页面文件夹,用于存放html、js、css,建议有这么一个文件夹单独存放页面文件,如果你想要做热更新的话可以这里替换src中的文件,而非更新整个程序,毕竟electron打包的程序体量还是挺大的。
index.js文件
这个是electron的主程序文件,它需要放到文件夹的根目录下,如果你想改变他的名字和位置,可以到package.json中修改mian:"index.js"配置项
3 起步
3.1安装electron
cnpm i electron
如果你是用的是npm i electron命令,不出意外的话,你应该会出意外 诶嘿(〃‘▽’〃) 你的下载很有可能会卡住。
此时需要设置cnpm淘宝镜像 (注意 npm 和 cnpm 在同一个项目中下载包时不要混用)
先ctrl + C 两下将npm下载停止
可以清理下缓存npm cache clean --force
设置淘宝镜像 npm install -g cnpm --registry=https://registry.npm.taobao.org
删除node_modules文件夹,后执行cnpm i electron -D,出现版本号就是成功了,如果你想下载特定版本 可以使用 cnpm i electron@你要下载的版本号,(注意 一定要安装到开发依赖 -D)
PS D:\Project\electron-learning> cnpm i electron -D
√ Linked 4 latest versions fallback to D:\Project\electron-learning\node_modules\.store\node_modules
Recently updated (since 2024-02-13): 1 packages (detail see file D:\Project\electron-learning\node_modules\.recently_updates.txt)
Today:
→ electron@latest(29.0.0) (13:23:54)
√ Run 1 script(s) in 12s.
√ Installed 1 packages on D:\Project\electron-learning
√ All packages installed (1 packages installed from npm registry, used 15s(network 15s), speed 35.35KB/s, json 1(373.52KB), tarball 150.01KB, manifests cache hit 3, etag hit 3 /tag hit 3 / miss 1)
devDependencies:
+ electron ^29.0.0
3.2 从hello world开始
在src/index.html页面中编写一些内容,例如显示一个hello world
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1>Hello World</h1>
</body>
</html>
在index.js文件中 编写基本代码 点击查看electron API 文档地址
// 使用commonjs引入electron
// app为主程序,
// BrowserWindow浏览器窗口,顾名思义,桌面端是以窗口来显示页面的
// dialog 弹窗,用于弹出提示、选择器、错误等
const { app, BrowserWindow, dialog } = require("electron");
//定义主窗口实例的变量
let mainWindow = null;
//创建主窗口实例的方法
const createMainWindow = () => {
// 设置浏览器窗口基本信息,更多参数请看文档 https://www.electronjs.org/zh/docs/latest/api/browser-window
mainWindow = new BrowserWindow({
webPreferences: true,//自带的窗口上方调试工具,默认就是true,不过打包的时候,改成false
icon: "./applogo.ico",//窗口图标
title: "我的程序",//窗口标题
resizable: false,//是否允许托拽边框调整大小,默认true,如果设置为true的话,则需要你在html页面中作自适应布局
width: 1200,//宽度
height: 800//高度
});
// 加载需要展示的页面文件
mainWindow.loadFile('./src/index.html');
};
// 主程序事件监听
// 当程序准备就绪
app.whenReady().then(async () => {
createMainWindow()
}).catch(e => {
// 一定要多使用错误提示 dialog.showErrorBox(标题,内容),不然的话,当程序运行不起来时你根本不知道什么错误
dialog.showErrorBox('主程序错误', e.toString());
});
现在运行它,使用 electron .命令,或者更好的做法,在package.json中添加配置,使用我们熟知的npm run start启动
"scripts": {
"start": "electron .",
"test": "echo \"Error: no test specified\" && exit 1"
},
执行命令
PS D:\Project\electron-learning> npm run start
> electron-learning@1.0.0 start
> electron .
[1172:0205/094017.879:ERROR:network_change_notifier_win.cc(267)] WSALookupServiceBegin failed with: 0
[9852:0205/094018.472:ERROR:network_change_notifier_win.cc(267)] WSALookupServiceBegin failed with: 0

那么我们每次修改都需要手动去刷新,这样太麻烦了,能不能让它修改后自动刷新呢?那就是使用electron-reload包,使用cnpm i electron-reload -D
PS D:\Project\electron-learning> cnpm i electron-reload -D
√ Linked 15 latest versions fallback to D:\Project\electron-learning\node_modules\.store\node_modules
√ Installed 1 packages on D:\Project\electron-learning
√ All packages installed (15 packages installed from npm registry, used 519ms(network 506ms), speed 13.07KB/s, json 1(6.61KB), tarball 0B, manifests cache hit 15, etag hit 15 / miss 1)
devDependencies:
+ electron-reload ^2.0.0-alpha.1
PS D:\Project\electron-learning>
在index.js中添加配置应用它注意,一定要判断开发环境,不然打包后就会报错,因为打包后的程序文件类型、路径会改变
// 使用commonjs引入electron
// app为主程序,
// BrowserWindow浏览器窗口,顾名思义,桌面端是以窗口来显示页面的
// dialog 弹窗,用于弹出提示、选择器、错误等
const { app, BrowserWindow, dialog } = require("electron");
// 引入node 路径管理
const path =require("node:path");
//定义主窗口实例的变量
let mainWindow = null;
//创建主窗口实例的方法
const createMainWindow = () => {
// 设置浏览器窗口基本信息,更多参数请看文档 https://www.electronjs.org/zh/docs/latest/api/browser-window
mainWindow = new BrowserWindow({
webPreferences: true,//自带的窗口上方调试工具,默认就是true,不过打包的时候,改成false
icon: "./applogo.ico",//窗口图标
title: "我的程序",//窗口标题
resizable: false,//是否允许托拽边框调整大小,默认true,如果设置为true的话,则需要你在html页面中作自适应布局
width: 1200,//宽度
height: 800//高度
});
// 加载需要展示的页面文件
mainWindow.loadFile('./src/index.html');
};
// app.isPackaged用于判断是否是开发环境
if (!app.isPackaged) {
// 引入热重载
const electronReload = require('electron-reload');
// 监视渲染进程代码文件,当文件发生变化时,自动重新加载应用程序
// __dirname 用来动态获取当前文件模块所属目录的绝对路径
electronReload(path.join(__dirname, 'src'), {
electron: path.join(__dirname, 'node_modules', '.bin', 'electron'),
});
};
// 主程序事件监听
// 当程序准备就绪
app.whenReady().then(async () => {
createMainWindow()
}).catch(e => {
// 一定要多使用错误提示 dialog.showErrorBox(标题,内容),不然的话,当程序运行不起来时你根本不知道什么错误
dialog.showErrorBox('主程序错误', e.toString());
});
现在修改src 中的内容就可以自动刷新了,不过,当你修改主进程内容时,程序并不会刷新,仍然需要手动重新启动项目
4.通信
4.1 配置预加载脚本
要想页面和主程序通信,则需要使用electron提供的方法,而要想在html页面中使用electron,则需要使用预加载脚本,那么什么是预加载脚本?来看看官方文档中是怎么说的!
什么是预加载脚本?
Electron 的主进程是一个拥有着完全操作系统访问权限的 Node.js 环境。 除了 Electron 模组 之外,您也可以访问 Node.js 内置模块 和所有通过 npm 安装的包。 另一方面,出于安全原因,渲染进程默认跑在网页页面上,而并非 Node.js里。
为了将 Electron 的不同类型的进程桥接在一起,我们需要使用被称为 预加载 的特殊脚本。
使用预加载脚本来增强渲染器
BrowserWindow 的预加载脚本运行在具有 HTML DOM 和 Node.js、Electron API 的有限子集访问权限的环境中。
::: info 预加载脚本沙盒化
从 Electron 20 开始,预加载脚本默认 沙盒化 ,不再拥有完整 Node.js 环境的访问权。 实际上,这意味着你只拥有一个 polyfilled 的 require 函数,这个函数只能访问一组有限的 API。
也就是说要想在html中使用electron、node中的一些方法,则需要配置一下,让我们跟着官方文档做一下!
在根目录下创建preload.js 预加载文件
const { contextBridge } = require('electron')
// 暴露方法给渲染器
contextBridge.exposeInMainWorld('versions', {
node: () => process.versions.node,
chrome: () => process.versions.chrome,
electron: () => process.versions.electron
// 除函数之外,我们也可以暴露变量
})
在index.js中的创建浏览器窗口代码中应用预加载脚本
//创建主窗口实例的方法
const createMainWindow = () => {
// 设置浏览器窗口基本信息,更多参数请看文档 https://www.electronjs.org/zh/docs/latest/api/browser-window
mainWindow = new BrowserWindow({
webPreferences: true,//自带的窗口上方调试工具,默认就是true,不过打包的时候,改成false
icon: "./applogo.ico",//窗口图标
title: "我的程序",//窗口表演
resizable: false,//是否允许托拽边框调整大小,默认true,如果设置为true的话,则需要你在html页面中做自适应布局
webPreferences: {//应用预加载脚本
preload: path.join(__dirname, 'preload.js')
},
width: 1200,//宽度
height: 800//高度
});
// 加载需要展示的页面文件
mainWindow.loadFile('./src/index.html');
};
在src/index.html 编写代码,页面正常显示
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我的程序</title>
</head>
<body>
<h1>Hello World</h1>
<p id="info"></p>
</body>
<script>
const information = document.getElementById('info')
information.innerText = `This app is using Chrome (v${window.versions.chrome()}), Node.js (v${window.versions.node()}), and Electron (v${window.versions.electron()})`
</script>
</html>
4.2 ipc通信
首先看文档描述
进程间通信 (IPC) 是在 Electron 中构建功能丰富的桌面应用程序的关键部分之一。
由于主进程和渲染器进程在 Electron 的进程模型具有不同的职责,因此 IPC 是执行许多常见任务的唯一方法,例如从 UI 调用原生 API 或从原生菜单触发 Web 内容的更改。
在 Electron 中,进程使用 ipcMain 和 ipcRenderer 模块,通过开发人员定义的“通道”传递消息来进行通信。 这些通道是 任意 (您可以随意命名它们)和 双向 (您可以在两个模块中使用相同的通道名称)的。
在本项目中,index.js中就是主进程。html页面就是就是由渲染器进程渲染出来的
4.2.1 渲染进程向主进程发送消息(单向)
在index.js中我在头部引入了ipcMain 并在app.whenReady()方法内部里监听了logMsg消息
// 使用commonjs引入electron
// app为主程序,
// BrowserWindow浏览器窗口,顾名思义,桌面端是以窗口来显示页面的
// dialog 弹窗,用于弹出提示、选择器、错误等
const { app, BrowserWindow, dialog, ipcMain } = require("electron");
// 引入node 路径管理
const path = require("node:path");
//定义主窗口实例的变量
let mainWindow = null;
//创建主窗口实例的方法
const createMainWindow = () => {
// 设置浏览器窗口基本信息,更多参数请看文档 https://www.electronjs.org/zh/docs/latest/api/browser-window
mainWindow = new BrowserWindow({
webPreferences: true,//自带的窗口上方调试工具,默认就是true,不过打包的时候,改成false
icon: "./applogo.ico",//窗口图标
title: "我的程序",//窗口标题
resizable: false,//是否允许托拽边框调整大小,默认true,如果设置为true的话,则需要你在html页面中做自适应布局
webPreferences: {//应用预加载脚本
preload: path.join(__dirname, 'preload.js')
},
width: 1200,//宽度
height: 800//高度
});
// 加载需要展示的页面文件
mainWindow.loadFile('./src/index.html');
// mainWindow.webContents.openDevTools()
};
// app.isPackaged用于判断是否是开发环境
if (!app.isPackaged) {
// 引入热重载
const electronReload = require('electron-reload');
// 监视渲染进程代码文件,当文件发生变化时,自动重新加载应用程序
// __dirname 用来动态获取当前文件模块所属目录的绝对路径
electronReload(path.join(__dirname, "src"), {
electron: path.join(__dirname, 'node_modules', '.bin', 'electron'),
});
};
// 主程序事件监听
// 当程序准备就绪
app.whenReady().then(async () => {
createMainWindow();
// 接收logMsg类型消息
ipcMain.on('logMsg', (event, msg) => {
console.log(msg)
});
}).catch(e => {
// 一定要多使用错误提示 dialog.showErrorBox(标题,内容),不然的话,当程序运行不起来时你根本不知道什么错误
dialog.showErrorBox('主程序错误', e.toString());
});
// 当所有页面关闭程序退出
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
在preload.js中
const { contextBridge, ipcRenderer } = require('electron/renderer')
contextBridge.exposeInMainWorld('electronAPI', {
logMsg: (msg) => ipcRenderer.send('logMsg', msg)
})
在src/index.html中
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我的程序</title>
</head>
<body>
<h1>Hello World</h1>
<button onclick="logMsg('我是从页面发送过来的消息')">向主进程发送消息</button>
<p id="info"></p>
</body>
<script>
const logMsg = (msg) => {
console.log( window.electronAPI)
window.electronAPI.logMsg(msg)
}
</script>
</html>
我在页面中使用了console.log,那么如何像浏览器那样打开开发者工具呢
或者在index.js中在创建窗口后调用openDevTool()方法打开开发者工具
// 加载需要展示的页面文件
mainWindow.loadFile('./src/index.html');
// mainWindow.webContents.openDevTools()
现在点击按钮,你应该在项目终端中看到一串打印
PS D:\Project\electron-learning> npm run start
> electron-learning@1.0.0 start
> electron .
[14172:0205/114825.098:ERROR:network_change_notifier_win.cc(267)] WSALookupServiceBegin failed with: 0
[21824:0205/114825.633:ERROR:network_change_notifier_win.cc(267)] WSALookupServiceBegin failed with: 0
鎴戞槸浠庨〉闈㈠彂閫佽繃鏉ョ殑娑堟伅
可以看到在electron项目中终端显示的中文乱码,现在在package.json文件中修改启动脚本设置字符编码为utf-8
"scripts": {
"start": "chcp 65001 && electron .",
"test": "echo \"Error: no test specified\" && exit 1"
},
现在重新启动并点击页面上的按钮,可以看到正确显示
PS D:\Project\electron-learning> npm run start
> electron-learning@1.0.0 start
> chcp 65001 && electron .
Active code page: 65001
[18916:0205/120144.451:ERROR:network_change_notifier_win.cc(267)] WSALookupServiceBegin failed with: 0
[15864:0205/120144.996:ERROR:network_change_notifier_win.cc(267)] WSALookupServiceBegin failed with: 0
我是从页面发送过来的消息
4.2.2 渲染进程向主进程发送消息(双向)
也就是说当渲染进程向主进程发送消息,主进程也会向渲染进程返回消息,让我们继续跟着文档做
在主进程index.js文件中添加监听ipc消息事件
// 选择文件方法
async function handleFileOpen () {
const { canceled, filePaths } = await dialog.showOpenDialog()
if (!canceled) {
return filePaths[0]
}
}
// 主程序事件监听
// 当程序准备就绪
app.whenReady().then(async () => {
createMainWindow();
// 当程序进入活跃状态时,如果没有窗口存在则自动创建一个主窗口
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createMainWindow();
})
// 接收logMsg类型消息
ipcMain.on('logMsg', (event, msg) => {
console.log(msg)
});
// 在主进程监听名字为dialog:openFile事件,返回了选择文件的函数
ipcMain.handle('dialog:openFile', handleFileOpen);
}).catch(e => {
// 一定要多使用错误提示 dialog.showErrorBox(标题,内容),不然的话,当程序运行不起来时你根本不知道什么错误
dialog.showErrorBox('主程序错误', e.toString());
});
// 当所有页面关闭程序退出
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
修改预加载脚本 preload.js
const { contextBridge, ipcRenderer } = require('electron/renderer')
contextBridge.exposeInMainWorld('electronAPI', {
// 暴露logMsg方法给渲染进程
logMsg: (msg) => ipcRenderer.send('logMsg', msg),
// 暴露openFile方法给渲染进程,当渲染进程调用时,向主进程发名为 dialog:openFile 的消息
openFile:() => ipcRenderer.invoke('dialog:openFile')
})
在src/index.html中尝试调用选择文件方法
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我的程序</title>
</head>
<body>
<h1>Hello World</h1>
<button onclick="logMsg('我是从页面发送过来的消息')">向主进程发送消息</button>
<button onclick="selectFile()">选择文件<strong id="fileBox"></strong></button>
<p id="info"></p>
</body>
<script>
const fileBoxEl=document.querySelector("#fileBox");
const logMsg = (msg) => {
console.log(window.electronAPI)
window.electronAPI.logMsg(msg)
}
const selectFile = async () => {
const filePath = await window.electronAPI.openFile()
fileBoxEl.innerText = filePath
}
</script>
</html>
4.2.3从主进程向渲染进程发送消息
文档最后使用了窗口菜单做例子,我们也这样做,不过既然加了菜单按钮那就完善一下菜单功能
在index.js文件顶部引入Menu、os、clipboard,用于查看当前系统版本和剪切板工具,其实是用于显示一个关于弹窗
const { app, BrowserWindow, dialog,clipboard, ipcMain,Menu } = require("electron");
// 引入node 路径管理
const path = require("node:path");
// 引入os,这个可以看到系统版本
const os = require('node:os');
在index.js文件创建主窗口实例的方法中配置菜单项,这里我使用了菜单分组
//创建主窗口实例的方法
const createMainWindow = () => {
// 设置浏览器窗口基本信息,更多参数请看文档 https://www.electronjs.org/zh/docs/latest/api/browser-window
mainWindow = new BrowserWindow({
webPreferences: false,//自带的窗口上方调试工具,默认就是true,不过打包的时候,改成false
icon: "./applogo.ico",//窗口图标
title: "我的程序",//窗口标题
resizable: false,//是否允许托拽边框调整大小,默认true,如果设置为true的话,则需要你在html页面中做自适应布局
webPreferences: {//应用预加载脚本
preload: path.join(__dirname, 'preload.js')
},
width: 1200,//宽度
height: 800//高度
});
// 加载需要展示的页面文件
mainWindow.loadFile('./src/index.html');
// mainWindow.webContents.openDevTools()
// 创建菜单项
const menuTemplate = [
{
label: '选项',
submenu: [
{
label: '测试发送消息',
click: () => {
mainWindow.webContents.send('mainMsg', "我是主程序发过来的消息");
}
},
{
label: '关于',
click: () => {
// 处理关于菜单项的点击事件
let text = `运行环境:${app.isPackaged ? "生产环境" : "开发环境"}\n版本号:${app.getVersion()}\nNODE:${process.versions.node}\nElectron:${process.versions.electron}\n系统:${os.type()},${os.arch()},${os.release()}`;
dialog.showMessageBox(mainWindow, {
title: "关于",//标题
type: 'info',//弹窗类型
noLink: true, // 按钮并列显示在右下角
message: app.getName(),//内容,这里使用项目名,app.getName()可以获取程序名,即package.json 中的name
detail: text,
buttons: ["确定", "复制"],//按钮数组
defaultId: 0//默认选择,注意,弹窗自带的右上角关闭按钮返回也是0,所以button第一项可以放个确定按钮
}).then(result => {
// result.response 返回的buttons按钮数组的下标
if (result.response === 1) {// 复制
clipboard.writeText(text);
}
})
}
},
{
label: '开发者工具',
submenu: [
{
label: '主窗口',
click: () => {
mainWindow.webContents.openDevTools()
}
}
// 其他窗口
]
}
]
}
];
// 创建菜单
const menu = Menu.buildFromTemplate(menuTemplate);
// 设置菜单栏
mainWindow.setMenu(menu);
};
你应该会看到这样的页面
在src/index.html中
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我的程序</title>
</head>
<body>
<h1>Hello World</h1>
<button onclick="logMsg('我是从页面发送过来的消息')">向主进程发送消息</button>
<button onclick="selectFile()">选择文件<strong id="fileBox"></strong></button>
<p id="msg"></p>
<p id="info"></p>
</body>
<script>
// 监听onMainMsg消息
window.electronAPI.onMainMsg(value=>{
const msgEl = document.getElementById('msg')
console.log(value)
msgEl.innerText = value
});
const fileBoxEl = document.querySelector("#fileBox");
const logMsg = (msg) => {
console.log(window.electronAPI)
window.electronAPI.logMsg(msg)
}
const selectFile = async () => {
const filePath = await window.electronAPI.openFile()
fileBoxEl.innerText = filePath
}
</script>
</html>
点击菜单看看
4.3使用websocket通信
ipc通信,基本就是那样,至于渲染进程之间的通信,则需要通过主进程做中介、路由,发送给指定页面,这边不在赘述了。相对于使用ipc通信,我更加喜欢使用ws,使用ws不仅可以在程序内部传递消息,还可以通过与程序外部的其他程序、网页进行通信,并且使用websockit还能避免跨域问题,同时由于于websockit服务器是在程序中启动的,也就是说运行在用户本地电脑上,也不用担心什么性能问题,现在我们来编写ws代码。
首先安装ws模快cnpm i ws然后在index.js顶部引入
// 引入websocket模快
const WS = require("ws");
// 引入url路径处理包,用于快捷读取url路径参数
const urlib = require("url");
在index.js编写WS服务端代码
// global为node存储全局变量的关键字
global.mainData = {
WS: null,//我们将websocket对象存储在global.mainData 中,这样就可以在其它地方一同使用
WSList: [],//我们将每个连接对象存储在WSList 中,以便管理多个连接
};
const open_WS_server = () => {
try {
// 20241是服务端口号,因为是要在本地启动服务所以不要使用一些如3000、5000、8080这些常用的端口号,防止端口号冲突
// (注册端口号范围是从1024开始,一直到49151)
global.mainData.WS = new WS.Server({ port: 20241 });
// 当有对象连接时,处理消息
global.mainData.WS.on("connection", function (ws, req) {
const urlQuery = urlib.parse(req.url, true).query
// 在连接对象添加user属性,用于存储连接对象的信息,用于区分每一个连接对象
ws.user = { keyName: urlQuery.keyName }
// 将新的连接对象放入WSList中
global.mainData.WSList.push(ws)
// 监听当有连接关闭时,关闭并删除列表中的连接
ws.on("close", function (e) {
const index = global.mainData.WSList.findIndex(d => d.user.keyName === urlQuery.keyName);
if (index !== -1) {
// 关闭列表中存储的相应连接对象,并删除它
global.mainData.WSList[index].close();
global.mainData.WSList.splice(index, 1);
}
});
// 监听连接的消息
ws.on("message", async function (msg) {
msg=JSON.parse(msg)
const msgData = {
fromKeyName: ws.user.keyName,//将连接名放到msgData对象里,用于区分消息来源
...msg
};
// 使用Switch 区分消息类型
switch (msgData.type) {
case "ping"://心跳类消息
{
// 将心跳消息原封不动返回
global.mainData.WSList.find(fd => fd.user.keyName == msgData.fromKeyName).send(JSON.stringify(msg));
break;
}
// 其他消息
default:
break;
}
});
})
} catch (error) {
// 捕获错误
dialog.showErrorBox('socket错误', error.toString());
}
}
在程序准备好时调用
app.whenReady().then(async () => {
createMainWindow();
open_WS_server();
编写客户端连接socket代码
因为使用的是websockit,当程序运行时你可以在任意网页、app等场景连接服务,这里我仍然使用了src/index.html页面
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我的程序</title>
</head>
<body>
<h1>Hello World</h1>
<button onclick="logMsg('我是从页面发送过来的消息')">向主进程发送消息</button>
<button onclick="selectFile()">选择文件<strong id="fileBox"></strong></button>
<p id="msg"></p>
<p id="info"></p>
<p id="ping"></p>
</body>
<script>
// 监听onMainMsg消息
window.electronAPI.onMainMsg(value => {
const msgEl = document.getElementById('msg')
console.log(value)
msgEl.innerText = value
});
const fileBoxEl = document.querySelector("#fileBox");
const logMsg = (msg) => {
console.log(window.electronAPI)
window.electronAPI.logMsg(msg)
}
const selectFile = async () => {
const filePath = await window.electronAPI.openFile()
fileBoxEl.innerText = filePath
}
// 定义连接对象
let WS = null;
// 定义重连定时器,用于当连接断开时重连
let reconnectTimer = null;
// 定义心跳定时器,用于保持连接,因为当一段时间内没有任何消息,会自动断开
let pingTimer = null;
const lineWS = async () => {
if (WS) return
// 使用路径传参,传一个用于区分连接的参数,我这里使用了keyName,的变量值用于区分
WS = await new WebSocket(`ws://127.0.0.1:20241?keyName=homePage`);
// 连接成功会触发
WS.onopen = (e) => {
clearTimeout(reconnectTimer);
// 心跳消息
pingTimer = setInterval(() => {
if (!WS) return clearInterval(pingTimer)
// 每搁1秒发送一个心跳消息,这里我传的是一个json字符串,type用于业务区分消息类型,data为消息内容
WS.send(JSON.stringify({
type: "ping",
data: {
pingTimer: new Date().getTime(),//这里传了一个当前时间戳,这样可以显示延迟
},
}));
}, 1000);
};
// 当连接断开时
WS.onclose = (e) => {
WS = null;
clearInterval(pingTimer);
if (e.code == 1006) { // 非手动断开
// 3秒重连一次
reconnectTimer = setTimeout(() => {
lineWS();
}, 3000);
}
};
WS.onerror = () => {
console.log("连接失败了");
WS = null;
};
WS.onmessage = (e) => {
const msgData = JSON.parse(e.data);
console.log(msgData)
switch (msgData.type) {
case "ping": //心跳类消息
{
const ping = new Date().getTime() - msgData.data.pingTimer;
document.getElementById('ping').innerText = "服务延迟:" + ping + "ms"
break;
}
default:
break;
}
};
}
document.addEventListener('DOMContentLoaded', function () {
//dom加载完成时调用
lineWS()
});
</script>
</html>
现在运行看看

在src/index.html页面发送时间戳main.js的websockit服务,mian.js又将时间戳返回给对应的页面,在页面中WS.onmessage 监听到了来自main.js中的消息,并拿到了之前发送过去的时间戳。通过与当前最新时间戳计算,就得到了延迟!这里只是使用延迟举个实时通信例子,你可以尝试多个页面开启多个连接进行互相通信。
5.打包
在打包之前我们需要配置一下程序图标,当然不配置也可以,那就会使用electro的logo
图标的要求为png,256*256像素,使用png类型在mac和window都能显示
在src目录下放置applogo.png图片。(路径并不固定,可以自己选位置)
在package.json中
{
"name": "electron-learning",
"version": "1.0.0",
"description": "从零开始的electron",
"main": "./index.js",
"scripts": {
"start": "chcp 65001 && electron .",
"build": "electron-builder",
"build-win64": "electron-builder --win --x64",
"build-win32": "electron-builder --win --ia32",
"build-mac": "electron-builder --mac"
},
"author": "蛋炒贩炒蛋",
"license": "ISC",
"dependencies": {
"ws": "^8.16.0"
},
"build": {
"productName": "myElectron",
"appId": "com.myapp",
"directories": {
"output": "dist"
},
"nsis": {
"perMachine": true,
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"createDesktopShortcut": true,
"shortcutName": "我的electron",
"uninstallDisplayName": "我的electron",
"artifactName": "我的electron ${os}-${arch} Setup ${version}.exe"
},
"mac": {
"target": [
"dmg"
],
"icon": "./src/applogo.png"
},
"win": {
"target": [
{
"target": "nsis"
}
],
"icon": "./src/applogo.png"
}
},
"devDependencies": {
"electron": "^29.0.0",
"electron-builder": "^24.12.0",
"electron-reload": "^2.0.0-alpha.1"
}
}
现在解释配置项,注意json文件不能添加注释
{
"name": "electron-learning",//项目名
"version": "1.0.0",//版本号
"description": "从零开始的electron",//项目描述
"main": "./index.js",//启动文件
"scripts": {
"start": "chcp 65001 && electron .",//开发环境启动命令
"build": "electron-builder",//打包命令,根据开发者系统打包相应系统的程序,可以使用下面命令打包对应版本
"build-win32": "electron-builder --win --x64", //打包win64
"build-win64": "electron-builder --win --ia32",//打包win32
"build-mac": "electron-builder --mac"//打包mac
},
"author": "蛋炒贩炒蛋",//作者,会在程序详细信息显示版权所有
"license": "ISC",//开源协议
"dependencies": {//生产依赖
"ws": "^8.16.0"//node websocket依赖
},
"build": {
"productName":"myElectron",//打包后的程序名称,注意,如果程序需要写入注册表等操作,不可使用中文,不添加此项配置则会使用name配置项(项目名)
"appId": "com.myapp",//程序id
"directories": {
"output": "dist"//打包后输出文件夹路径
},
"nsis": {//nsis安装程序配置项
"perMachine": true,//为所有人安装
"oneClick": false,//是否一键安装
"allowToChangeInstallationDirectory": true,//是否允许修改安装目录
"createDesktopShortcut": true//创建桌面快捷方式
"shortcutName": "我的electron",//快捷方式名称
"uninstallDisplayName": "我的electron",//卸载程序标题
"artifactName": "我的electron ${os}-${arch} Setup ${version}.exe"//安装程序名,这里自动获取了程序版本,与系统版本
},
"mac": {//mac打包配置项
"target": [
"dmg"
],
"icon": "./src/applogo.png"//程序图标
},
"win": {//windows打包配置项
"target": [
{
"target": "nsis"//打包成nsis安装程序
}
],
"icon": "./src/applogo.png"//程序图标
}
},
"devDependencies": {//开发依赖
"electron": "^29.0.0",//electron模快
"electron-builder": "^24.9.1",//打包模快
"electron-reload": "^2.0.0-alpha.1"//内容热刷新模快
}
}
执行打包cnpm run build-win32
> electron-learning@1.0.0 build-win32
> electron-builder --win --ia32
• electron-builder version=24.12.0 os=10.0.22631
• loaded configuration file=package.json ("build" field)
• writing effective config file=dist\builder-effective-config.yaml
• packaging platform=win32 arch=ia32 electron=29.0.1 appOutDir=dist\win-ia32-unpacked
⨯ Get "https://npm.taobao.org/mirrors/electron/29.0.1/electron-v29.0.1-win32-ia32.zip": x509: certificate has expired or is not yet valid:
github.com/develar/app-builder/pkg/download.(*Downloader).follow.func1
/Volumes/data/Documents/app-builder/pkg/download/downloader.go:206
github.com/develar/app-builder/pkg/download.(*Downloader).follow
/Volumes/data/Documents/app-builder/pkg/download/downloader.go:234
github.com/develar/app-builder/pkg/download.(*Downloader).DownloadNoRetry
/Volumes/data/Documents/app-builder/pkg/download/downloader.go:128
github.com/develar/app-builder/pkg/download.(*Downloader).Download
/Volumes/data/Documents/app-builder/pkg/download/downloader.go:112
github.com/develar/app-builder/pkg/electron.(*ElectronDownloader).doDownload
/Volumes/data/Documents/app-builder/pkg/electron/electronDownloader.go:192
github.com/develar/app-builder/pkg/electron.(*ElectronDownloader).Download
/Volumes/data/Documents/app-builder/pkg/electron/electronDownloader.go:177
github.com/develar/app-builder/pkg/electron.downloadElectron.func1.1
/Volumes/data/Documents/app-builder/pkg/electron/electronDownloader.go:73
github.com/develar/app-builder/pkg/util.MapAsyncConcurrency.func2
/Volumes/data/Documents/app-builder/pkg/util/async.go:68
runtime.goexit
/usr/local/Cellar/go/1.17/libexec/src/runtime/asm_amd64.s:1581
⨯ D:\Project\electron-learning\node_modules\.store\app-builder-bin@4.0.0\node_modules\app-builder-bin\win\x64\app-builder.exe process failed ERR_ELECTRON_BUILDER_CANNOT_EXECUTE
Exit code:
1 failedTask=build stackTrace=Error: D:\Project\electron-learning\node_modules\.store\app-builder-bin@4.0.0\node_modules\app-builder-bin\win\x64\app-builder.exe process failed ERR_ELECTRON_BUILDER_CANNOT_EXECUTE
Exit code:
1
at ChildProcess.<anonymous> (D:\Project\electron-learning\node_modules\.store\builder-util@24.9.4\node_modules\builder-util\src\util.ts:252:14)
at Object.onceWrapper (node:events:629:26)
at ChildProcess.emit (node:events:514:28)
at ChildProcess.cp.emit (D:\Project\electron-learning\node_modules\.store\cross-spawn@7.0.3\node_modules\cross-spawn\lib\enoent.js:34:29)
at maybeClose (node:internal/child_process:1105:16)
at Process.ChildProcess._handle.onexit (node:internal/child_process:305:5)
可以看到报错了,原因是Get "https://npm.taobao.org/mirrors/electron/29.0.1/electron-v29.0.1-win32-ia32.zip": x509: certificate has expired or is not yet valid: 这个下载链接证书过期了,下面我们更换electron下载源cnpm config ls查看配置文件位置,user配置项,就是配置文件
cnpm config list
; "user" config from C:\Users\16206\.cnpmrc
electron_mirror = "https://npm.taobao.org/mirrors/electron/"
; "cli" config from command line options
disturl = "https://cdn.npmmirror.com/binaries/node"
registry = "https://registry.npmmirror.com/"
userconfig = "C:\\Users\\16206\\.cnpmrc"
; node bin location = D:\node\node.exe
; node version = v20.10.0
; npm local prefix = D:\Project\electron-learning
; npm version = 9.9.2
; cwd = D:\Project\electron-learning
; HOME = C:\Users\16206
; Run `npm config ls -l` to show all defaults.
在你的.cnpmrc文件修改配置
electron_mirror=https://npmmirror.com/mirrors/electron/
重新执行cnpm run build-win32
PS D:\Project\electron-learning> cnpm run build-win32
> electron-learning@1.0.0 build-win32
> electron-builder --win --ia32
• electron-builder version=24.12.0 os=10.0.22631
• loaded configuration file=package.json ("build" field)
• writing effective config file=dist\builder-effective-config.yaml
• packaging platform=win32 arch=ia32 electron=29.0.1 appOutDir=dist\win-ia32-unpacked
• building target=nsis file=dist\我的electron win-ia32 Setup 1.0.0.exe archs=ia32 oneClick=false perMachine=true
• building block map blockMapFile=dist\我的electron win-ia32 Setup 1.0.0.exe.blockmap
在你的项目根目录下的dist文件夹下会生成安装程序
这样一个简单的electron程序就开发完成了
更多推荐


所有评论(0)