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

![index页面效果](https://i-blog.csdnimg.cn/blog_migrate/aa6ddf03ae5063457375a55e9298aca3.png#pic_center
很好!页面出现了!不过可以看到标题并没有应用上,因为这是使用的index.html 中的title,electron中以页面标题、图标为优先显示。现在我们修改html中的标题,使它正确显示

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>我的程序</title>
</head>

使用ctrl + s保存,页面并没有刷新,我们也不能在页面中使用F5刷新,需要手动在窗口首选项中点击reload或者使用ctrl + r刷新
r刷新
那么我们每次修改都需要手动去刷新,这样太麻烦了,能不能让它修改后自动刷新呢?那就是使用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程序就开发完成了

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐