1
头图

官网:https://www.electronjs.org/zh/

安装依赖

初始化package.json

pnpm init

安装依赖

pnpm add -D electron

安装报错解决方案:https://blog.csdn.net/qq_38463737/article/details/140277803
1、打开npm的配置文件

# cmd 运行打开配置文件
npm config edit

2、在空白地方添加淘宝镜像,下面三个(缺什么补什么,但要是同一个公司单位的镜像)

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

image.png
手动配置完重新安装即可

pnpm add -D electron

image.png

启动一个简单的项目

1、修改package.json文件中的mainscripts配置段

其中author和description为必填
main 主进程脚本,这指向跟目录的main.js
start 为启动命令:electron .
{
  "name": "Electron-补习所",
  "version": "1.0.0",
  "description": "electron-test",
  "main": "main.js",
  "scripts": {
    "start": "electron ."
  },
  "keywords": [],
  "author": "wangfan",
  "license": "ISC",
  "devDependencies": {
    "electron": "^32.1.2"
  }
}

2、根目录新建main.js

const { app, BrowserWindow } = require("electron");

// 监听app的ready事件
app.on("ready", () => {
  // 创建一个窗口
  const win = new BrowserWindow({
    width: 500, // 窗口宽度
    height: 500, // 窗口高度
    autoHideMenuBar: true, // 隐藏默认菜单
  });
  win.loadURL('https://www.electronjs.org/zh/'); // 加载线上url页面
  // BrowserWindow的更多配置可参考文档:https://www.electronjs.org/zh/docs/latest/api/browser-window
});

image.png
image.png
这样一个简单的项目就启动了。

加载本地页面

根目录新建pages文件夹,在里面写入index.htmlindex.css,正常的写一些内容
image.png
main.js文件修改,通过loadFile方法加载本地页面

const { app, BrowserWindow } = require("electron");

// 监听app的ready事件
app.on("ready", () => {
  const win = new BrowserWindow({
    width: 500,
    height: 500, 
    autoHideMenuBar: true,
    alwaysOnTop: true 
  });
  win.loadFile('./pages/index.html'); // 加载页面-指定路径
});

启动项目:pnpm run start
image.png
开发者工具,打开后会有一个内容安全策略警告
image.png
解决方法是配置CSP,具体配置可上MDN上查看:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CSP
image.png
image.png
配置完后重启项目,内容安全策略警告就会消失。

完善窗口行为

这里我将new BrowserWindow封装为一个函数createWindow,当应用ready(准备完毕)后,调用createWindow创建一个窗口。

const { app, BrowserWindow } = require("electron");

// 创建一个窗口-封装
function createWindow() {
  // 创建一个窗口
  const win = new BrowserWindow({
    width: 800, // 窗口宽度
    height: 500, // 窗口高度
    autoHideMenuBar: true, // 隐藏默认菜单
  });
  win.loadFile("./pages/index.html"); // 加载页面
  // BrowserWindow的更多配置可参考文档:https://www.electronjs.org/zh/docs/latest/api/browser-window
}

// 监听app的ready事件
app.on("ready", () => {
  createWindow();
  // window所有窗口关闭时,并且不是苹果系统,退出应用-管理窗口的生命周期
  app.on("window-all-closed", () => {
    if (process.platform !== "darwin") app.quit();
  });
});

// 应用被激活时,窗口数量为0,自动创建一个窗口-管理窗口的生命周期
app.on("activate", () => {
  if (BrowserWindow.getAllWindows().length === 0) createWindow();
});

窗口行为分两种,windows系统和mac系统,在windows系统中,所有窗口关闭后自动关闭应用,而mac系统则是自动缩小到任务栏不会关闭应用。
所以这里需要做窗口行为管理,也就是官网所说的管理窗口的生命周期

自动启动应用

项目每次主进程(main.js)修改后,应用都需要手动重启,非常麻烦,我们可以安装nodemon来自动重启项目

pnpm i nodemon -D

package.json中配置

{
  // 省略其它配置...... 
  
  "scripts": {
    "start": "nodemon --exec electron ." // 通过nodemon --exec来启动electron .
  }
}

这样每次修改主进程内容后应用会自动重启
另外你也可以在文件中具体配置nodemon,来实现主进程、页面内容修改后自动重启功能。
根目录新建:nodemon.json

{
  "ignore": ["node modules", "dist"],
  "restartable": "r",
  "watch": ["*.*"],
  "ext": "html,js,css"
}

主进程与渲染进程

electron中,主进程只有一个,渲染进程可以有n
main.js就是主进程,主进程是node环境运行的
html中运行的js就是渲染进程,渲染进程是在web环境中运行的
新建一个js文件,在index.html中引入它
image.png
修改js文件

// render.js
let btn = document.getElementById("btn1");

btn.addEventListener("click", () => {
  alert("弹窗");
});

image.png
主进程(node环境)与渲染进程(web环境)作用的环境不同,他们之间是隔离开的,web端不能使用nodeAPInode也无法使用webAPI

web端由于是沙箱环境,如果你想在web端获取一些系统、环境、硬件的信息,可能就需要借助node能力与系统交互,那么electron就为我们提供了主进程与渲染进程之间的通信。

主进程与渲染进程之间通过预加载脚本通信,预加载脚本在渲染端(web环境)运行,虽然预加载脚本在web端运行,但是它可以访问一部分的nodeAPI,预加载脚本就是主进程与渲染进程之间的桥梁。

根目录新建预加载脚本:preload.js
image.png
这样我们的目录结构就一目了然了,pages文件夹下管理渲染进程,preload.js管理预加载脚本,main.js管理主进程

我们随便在preload.js中写点东西打印

// preload.js
console.log('预加载脚本')

然后在主进程中引入预加载脚本,预加载脚本只能使用绝对路径,这里使用node模块path引入根目录下的preload.js

// main.js
const { app, BrowserWindow } = require("electron");
const path = require("path");

// 创建一个窗口-封装
function createWindow() {
  const win = new BrowserWindow({
    // 省略其它配置项...
    
    webPreferences: {
      preload: path.resolve(__dirname,'./preload.js'), // 加载预加载脚本,绝对路径
    }
  });
  win.loadFile("./pages/index.html"); // 加载页面
}

// 监听app的ready事 件
app.on("ready", () => {
  createWindow();
});

// 省略其它配置项...

启动项目后会发现窗口打印了预加载脚本的输出语句
image.png
应用的运行顺序是:主进程 -> 预加载脚本 -> 渲染进程

现在我们来看预加载脚本如何与渲染进程通信的
使用 contextBridge 来选择要从预加载脚本中暴露哪些 API,通过exposeInMainWorld向渲染器进程暴露一个全局的 window.abc变量。

// preload.js
const { contextBridge } = require("electron");

// 向渲染进程暴露全局myAPI变量
contextBridge.exposeInMainWorld("myAPI", {
  mytext: "这是暴露的变量,预加载进程可使用部分nodeAPI",
  version: process.version
});

然后在渲染进程中打印window
image.png
image.png
你会发现暴露的myAPI直接挂载在window身上
若是挂在在window身上,你可以直接获取myAPI变量

btn.addEventListener("click", () => {
  //   alert("弹窗");
  //   console.log(window);
  console.log(myAPI);
});

image.png
这样就做到了渲染进程访问预加载脚本的能力。

进程通讯(IPC)-重要

需求:点击按钮,在系统D盘创建一个hello.text文件,文件内容来自用户输入

渲染进程向主进程通信(单项)

先编写页面

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta
      http-equiv="Content-Security-Policy"
      content="default-src 'self'; img-src https://*; child-src 'none';"
    />
    <title>Document</title>
    <link rel="stylesheet" href="./index.css" />
  </head>
  <body>
    <h1>欢迎光临</h1>
    <button id="btn1">查看弹窗</button>
    <br>
    <br>
    <hr>
    <button id="setBtn">向D盘写入hello.txt</button>
    <input type="text" id="input">
  </body>
  <script src="./render.js"></script>
</html>
// render.js
let btn = document.getElementById("btn1");
let setBtn = document.getElementById("setBtn");
let input = document.getElementById("input");

btn.addEventListener("click", () => {
  console.log(myAPI);
});

// 设置写入按钮的点击事件
setBtn.onclick = () => {
  // 调用预加载脚本中定义的函数saveFile并传入input的输入内容
  myAPI.saveFile(input.value)
};

编写预加载脚本
在预加载脚本中,通过ipcRenderer将消息发送到主进程创建的监听器

// preload.js
const { contextBridge, ipcRenderer } = require("electron");

contextBridge.exposeInMainWorld("myAPI", {
  mytext: "这是暴露的变量",
  saveFile: (data) => {
    // 通过ipcRenderer.send向主进程发送消息
    ipcRenderer.send("file-save", data);
  },
});

编写主进程
在主进程中,通过ipcMain监听预加载脚本发送的消息,监听的时机在加载页面之前。
1、创建窗口
2、订阅预加载脚本
3、加载页面

// main.js
const { app, BrowserWindow, ipcMain } = require("electron");
const path = require("path");
const fs = require("fs"); // 引入node的fs模块操纵文件

// 自定义的函数,用于写入文件
// 回调函数有两个参数:一个IpcMainEvent结构和传入的变量
// 但是这里event用不上,所以用_占位
function writeFile(_, data) {
  // 在用户D盘下写入hello.txt文件,文件内容为data
  fs.writeFileSync("D:/hello.txt", data);
}

// 创建一个窗口-封装
function createWindow() {
  // 创建一个窗口
  const win = new BrowserWindow({
    width: 800, // 窗口宽度
    height: 500, // 窗口高度
    autoHideMenuBar: true, // 隐藏默认菜单
    webPreferences: {
      preload: path.resolve(__dirname, "./preload.js"), // 加载预加载脚本,绝对路径
    },
  });

  // 订阅预加载脚本,在加载页面之前,调用自定义函数writeFile
  ipcMain.on("file-save", writeFile);

  win.loadFile("./pages/index.html"); // 加载页面
}

// 监听app的ready事 件
app.on("ready", () => {
  createWindow();
  // 省略其它...
});

根据上面的步骤,就可以在系统D盘创建一个hello.txt文件,内容为输入框的内容。
image.png
image.png
image.png

渲染进程向主进程通信(双向)

读取我们之前写入的hello.txt文件内容
image.png
在页面中写好结构

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta
      http-equiv="Content-Security-Policy"
      content="default-src 'self'; img-src https://*; child-src 'none';"
    />
    <title>Document</title>
    <link rel="stylesheet" href="./index.css" />
  </head>
  <body>
    <h1>欢迎光临</h1>
    <button id="btn1">查看弹窗</button>
    <br>
    <br>
    <hr>
    <button id="setBtn">向D盘写入hello.txt</button>
    <input type="text" id="input">
    <br>
    <br>
    <hr>
    <button id="getBtn">读取D盘中hello.txt的内容</button>
  </body>
  <script src="./render.js"></script>
</html>

修改render.js的内容,同样的,调用预加载脚本中的自定义函数

// render.js
let btn = document.getElementById("btn1");
let setBtn = document.getElementById("setBtn");
let input = document.getElementById("input");
let getBtn = document.getElementById("getBtn");

btn.addEventListener("click", () => {
  console.log(myAPI);
});

setBtn.onclick = () => {
  console.log(input.value);
  myAPI.saveFile(input.value);
};

// 读取D盘中hello.txt的内容
// 实际上ipcRenderer.invoke返回的是一个Promise,所以使用async-await获取
getBtn.onclick = async () => {
  let txt = await myAPI.readFile();
  console.log(txt);
};

在预加载脚本中,通过invoke来向主进程暴露请求

// preload.js
const { contextBridge, ipcRenderer } = require("electron");
console.log("预加载脚本");

contextBridge.exposeInMainWorld("myAPI", {
  mytext: "这是暴露的变量",
  saveFile: (data) => {
    ipcRenderer.send("file-save", data);
  },
  // 自定义读取函数
  readFile: () => {
    // 通过ipcRenderer.invoke向主进程发送请求
    // 并将请求回来的值返回出去,ipcRenderer.invoke返回的是一个Promise
    return ipcRenderer.invoke("file-read");
  },
});

在主进程中监听invoke请求并执行自定义函数操作,最后将结果返回
通过ipcMain.handle监听invoke事件

const { app, BrowserWindow, ipcMain } = require("electron");
const path = require("path");
const fs = require("fs");

function writeFile(_, data) {
  fs.writeFileSync("D:/hello.txt", data);
}
// 自定义函数,读取D盘下的hello.txt文件内容,并返回
function readFile() {
  // 利用node的fs模块操作文件,readFileSync读取文件,toString将buffer转为字符串返回
  return fs.readFileSync("D:/hello.txt").toString();
}

// 创建一个窗口-封装
function createWindow() {
  const win = new BrowserWindow({
    width: 800, 
    height: 500, 
    autoHideMenuBar: true,
    webPreferences: {
      preload: path.resolve(__dirname, "./preload.js"), // 加载预加载脚本,绝对路径
    },
  });

  // 订阅预加载脚本,在加载页面之前
  // 监听预加载脚本send事件
  ipcMain.on("file-save", writeFile);
  // 监听预加载脚本invoke事件
  ipcMain.handle("file-read", readFile);

  win.loadFile("./pages/index.html"); // 加载页面
 
}

// 监听app的ready事 件
app.on("ready", () => {
  createWindow();
});

image.png

打包应用

pnpm install electron-builder -D

package.json中配置

{
  "name": "original-electron",
  "version": "1.0.0",
  "description": "original electron",
  "main": "main.js",
  "scripts": {
    "start": "nodemon --exec electron .",
    "build": "electron-builder" // 打包命令
  },
  "build": {
    "appId": "myelectron-app", // 应用程序唯一标识符
    "win": { 
      "icon": "./logo.ico", // 应用图标
      "target": [
        {
          "target": "nsis", // 指定使用 NSIS 作为安装程序格式
          "arch":  ["x64"] // 生成64位安装包
        }
      ]
    },
    "nsis": {
      "oneClick": false, // 设置为 false 使安装程序显示安装向导界面,而不是一键安装
      "perMachine": true, // 允许每台机器安装一次,而不是每个用户都安装
      "allowToChangeInstallationDirectory": true // 允许用户在安装过程中选择安装目录
    }
  },
}

运行打包命令

pnpm run build

目录中会生成dist文件夹,内部就打包的产物


参考文档:
安装依赖失败:https://blog.csdn.net/qq_38463737/article/details/140277803
官网:https://www.electronjs.org/zh/
electron-vite脚手架:https://cn.electron-vite.org/


兔子先森
405 声望17 粉丝

致力于新技术的推广与优秀技术的普及。