Electron 快速上手

0x01 概述

(1)基本介绍

  • Electron 官网:https://www.electronjs.org
  • Electron 是一个使用 HTML、CSS 和 JavaScript 构建跨平台桌面应用程序的框架
  • Electron = Chromium + NodeJS + Native API
    • Chromium:负责页面渲染处理
    • Node.js:负责后台逻辑处理
    • Native API:修改系统注册表的项
  • Electron 可以结合 React、Vue、Less 等可以转换为 HTML、CSS、JS 的框架与编程语言

(2)流程模型

graph TB M(Main<br />主进程<br />NodeJS) M--"进程通信<br />IPC"-->R1(渲染进程1) & R2(渲染进程2) & R(...) & Rn(渲染进程n) M--"原生 API<br />Native API"-->S("Windows | Linux | Mac")
  • 核心是主进程 Main,是 JS 文件
    • 其中是 NodeJS 环境,可以调用所有 NodeJS 提供的 API
    • 主进程主要目的是管理渲染进程
      • 可以管理多个渲染进程
    • 可以调用 Native API
  • 渲染进程继承了 Chromium,负责根据 HTML、CSS、JS 渲染窗口
    • 可以与主进程双向通信
  • Native API 可以根据系统的不同自动使用合适的系统接口,从而实现跨平台

(3)开发环境

  • Node:v20.11.1
  • npm:v9.8.1
  • 编辑工具:Visual Studio Code

0x02 第一个工程

(1)创建工程

  1. 新建文件夹 project,在新目录下开启终端

  2. 使用命令 npm init -y 创建 NodeJS 开发环境,在 package.json 中:

    1. 需要保证 author 项和 description 不能为空字符串
    2. scripts 中添加新命令:"start": "electron ."

    建议采用默认的 CommonJS 模块

    {
      "name": "project",
      "version": "1.0.0",
      "description": "Electron Application",
      "main": "main.js",
      "scripts": {
        "start": "electron ."
      },
      "keywords": [
        "Electron"
      ],
      "author": "SRIGT",
      "license": "MIT",
      "devDependencies": {
        "electron": "^31.1.0"
      }
    }
    
    
  3. 使用命令 npm install -D electron 安装开发依赖 Electron

  4. (可选)配置热更新

    1. 使用命令 npm install -D nodemon 安装开发依赖 nodemon

    2. 修改 package.json

      // ...
      "scripts": {
        "start": "nodemon --exec electron ."
      },
      // ...
      
    3. 创建 nodemon.json

      {
        "ignore": [
          "node_modules",
          "dist"
        ],
        "restartable": "r",
        "watch": ["*.*"],
        "ext": "html,css,js"
      }
      
      
  5. 在工程目录下新建 main.js

    // 1. 引入应用与浏览器窗口
    const { app, BrowserWindow } = require("electron");
    
    // 2. 当 App 准备好后执行回调函数
    app.on("ready", () => {
      // 3. 创建浏览器窗口对象并使用变量存储
      const window = new BrowserWindow({
        width: 800, // 窗口宽度
        height: 600, // 窗口高度
        autoHideMenuBar: true, // 隐藏默认菜单栏
      });
    
      // 4. 窗口加载页面
      window.loadURL("https://www.cnblogs.com/SRIGT");
    });
    
    
  6. 使用命令 npm run start 启动工程

(2)本地页面

  1. 在工程根目录创建 pages 目录,其中包括本地页面的 HTML 和 CSS

  2. 在 pages 中创建 index.html、index.css

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>本地页面</title>
      <link rel="stylesheet" href="./index.css">
    </head>
    
    <body>
      <h1>这是一个在 Electron 中展示的本地页面</h1>
    </body>
    
    </html>
    
    body {
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      height: 100vh;
      background-color: #f0f8ff;
    }
    
    
  3. 修改 main.js

    const { app, BrowserWindow } = require("electron");
    
    app.on("ready", () => {
      const window = new BrowserWindow({
        width: 800,
        height: 600,
        autoHideMenuBar: true,
      });
    
      // 加载本地(页面)文件
      window.loadFile("./pages/index.html");
    });
    
    
  4. 使用命令 npm run start 启动工程

  5. 使用快捷键 Ctrl+Shift+i 可以在 Electron 应用中开启控制台

    • 之后可以通过代码禁用控制台
  6. 在 Console 页中,控制台发出关于 CSP(内容安全策略)的警告,需要在页面的头部配置以下内容解决:

    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src https://*; child-src 'none';" />
    

(3)适应系统

  • 主要在于 Windows 系统与 Mac 系统对窗口与应用的关系处理

    • 在 Windows 系统中,最后一个窗口关闭后,应用也自动关闭
    • 在 Mac 系统中,最后一个窗口关闭后,应用不会自动关闭
  • 修改 main.js

    const { app, BrowserWindow } = require("electron");
    
    function createWindow() {
      const window = new BrowserWindow({
        width: 800,
        height: 600,
        autoHideMenuBar: true,
      });
      window.loadFile("./pages/index.html");
    }
    
    app.on("ready", () => {
      createWindow();
    
      // 当应用被激活时,如果没有打开窗口,则创建窗口
      app.on("activate", () => {
        if (BrowserWindow.getAllWindows().length === 0) {
          createWindow();
        }
      });
    });
    
    // 当所有窗口关闭时,如果不是 Mac 系统,则退出应用
    app.on("window-all-closed", () => {
      if (process.platform !== "darwin") {
        app.quit();
      }
    });
    
    

0x03 进程

(1)渲染进程

  • 每个 Electron 应用的主进程的控制程序是唯一的(如 main.js)

  • 每个 BrowserWindow 对象实例都对应着一个或多个独立的渲染进程控制程序

    1. 在 pages 目录下创建 render.js

      const button = document.querySelector("button");
      button.addEventListener("click", () => {
        alert("触发点击事件");
      });
      
      
    2. 修改 pages\index.html

      <!-- ... -->
      
      <body>
        <h1>这是一个在 Electron 中展示的本地页面</h1>
        <button>点击按钮</button>
        <script type="text/javascript" src="./render.js"></script>
      </body>
      
      <!-- ... -->
      
    3. 此时 render.js 就是渲染进程的控制程序

(2)预加载脚本

预加载脚本在渲染进程上运行,但可以访问一部分 NodeJS 的 API

  1. 在工程根目录下创建 preload.js 作为预加载脚本

    console.log("执行预加载脚本...");
    
  2. 修改 main.js

    const { app, BrowserWindow } = require("electron");
    const path = require("path");
    
    function createWindow() {
      const window = new BrowserWindow({
        width: 800,
        height: 600,
        autoHideMenuBar: true,
        webPreferences: {
          preload: path.resolve(__dirname, "./preload.js"),
        },
      });
      window.loadFile("./pages/index.html");
    }
    
    // ...
    
    • 如果报错 ReferenceError: __dirname is not defined,则将 __dirname 改为 process.cwd()
  3. 修改 render.js

    console.log("渲染进程进行中...");
    
    // ...
    
  4. 使用命令 npm run start 启动工程

    • 此时,根据 Visual Studio Code 的终端输出以及窗口的控制台输出可以发现,脚本的执行顺序为:

      graph LR 主进程-->预加载脚本-->渲染进程
    • 此时仅实现了主进程与预加载脚本的关联,还需要完成渲染进程与预加载脚本的关联

  5. 修改 preload.js

    const { contextBridge } = require("electron");
    contextBridge.exposeInMainWorld("key", {
      value: "Hello World",
    });
    
    
  6. 修改 render.js

    const button = document.querySelector("button");
    button.addEventListener("click", () => {
      alert(window.key.value);
    });
    
    
  7. 使用命令 npm run start 启动工程

(3)进程通信

  • 进程通信主要指主进程渲染进程之间通信,通过“中间人”——预加载脚本实现进程通信

a. 渲染进程向主进程单向通信

  • 方法

    graph LR 渲染进程--"ipcRender.send"-->d((数据))--"ipcMain.on"-->主进程
  • 常用于在 Web 中调用主进程的 API

  • 举例:

    1. 修改 pages\index.html

      <!-- ... -->
      
      <body>
        <input type="text" />
        <button>提交</button>
        <script type="text/javascript" src="./render.js"></script>
      </body>
      
      <!-- ... -->
      
    2. 修改 pages\render.js

      const input = document.querySelector("input");
      const button = document.querySelector("button");
      button.addEventListener("click", () => {
        // 调用 api.save 方法
        api.save(input.value);
      });
      
      
    3. 修改 preload.js

      const { contextBridge, ipcRenderer } = require("electron");
      contextBridge.exposeInMainWorld("api", {
        save: (text) => {
          // 发送数据到主进程
          ipcRenderer.send("save", text);
        },
      });
      
      
    4. 修改 main.js

      const { app, BrowserWindow, ipcMain } = require("electron");
      const path = require("path");
      
      function createWindow() {
        const window = new BrowserWindow({
          width: 800,
          height: 600,
          autoHideMenuBar: true,
          webPreferences: {
            preload: path.resolve(process.cwd(), "./preload.js"),
          },
        });
        window.loadFile("./pages/index.html");
      
        // 接收来自 preload.js 的数据
        ipcMain.on("save", (event, data) => {
          console.log(data);
        });
      }
      
      

b. 渲染进程向主进程双向通信

  • 方法

    flowchart LR 渲染进程<--"ipcRender.invoke"-->d((数据))<--"ipcMain.handle"-->主进程
  • 常用于:从渲染进程调用主进程方法并等待结果

  • 举例:

    1. 修改 pages\index.html

      <!-- ... -->
      
      <body>
        <button>获取</button>
        <script type="text/javascript" src="./render.js"></script>
      </body>
      
      <!-- ... -->
      
    2. 修改 pages\render.js

      const button = document.querySelector("button");
      button.addEventListener("click", async () => {
        let data = await api.read();
        document.body.innerHTML += `<p>${data}</p>`;
      });
      
      
    3. 修改 preload.js

      const { contextBridge, ipcRenderer } = require("electron");
      contextBridge.exposeInMainWorld("api", {
        read: () => {
          return ipcRenderer.invoke("read", "hello");
        },
      });
      
      
    4. 修改 main.js

      const { app, BrowserWindow, ipcMain } = require("electron");
      const path = require("path");
      
      function createWindow() {
        const window = new BrowserWindow({
          width: 800,
          height: 600,
          autoHideMenuBar: true,
          webPreferences: {
            preload: path.resolve(process.cwd(), "./preload.js"),
          },
        });
        window.loadFile("./pages/index.html");
      
        ipcMain.handle("read", (event, data) => {
          console.log(data);
          return "Hello World!";
        })
      }
      
      

c. 主进程向渲染进程通信

  • 方法

    graph LR 主进程--"BrowserWindow().webContents.send"-->d((数据))--"ipcRender.on"-->渲染进程
  • 常用于:主进程主动向渲染进程发送数据

  • 举例:

    1. 修改 pages\index.html

      <!-- ... -->
      
      <body>
        <script type="text/javascript" src="./render.js"></script>
      </body>
      
      <!-- ... -->
      
    2. 修改 pages\render.js

      window.onload = () => {
        api.getMsg((event, msg) => {
          alert(msg);
        });
      };
      
      
    3. 修改 preload.js

      const { contextBridge, ipcRenderer } = require("electron");
      contextBridge.exposeInMainWorld("api", {
        getMsg: (callback) => {
          return ipcRenderer.on("msg", callback);
        },
      });
      
      
    4. 修改 main.js

      const { app, BrowserWindow } = require("electron");
      const path = require("path");
      
      function createWindow() {
        const window = new BrowserWindow({
          width: 800,
          height: 600,
          autoHideMenuBar: true,
          webPreferences: {
            preload: path.resolve(process.cwd(), "./preload.js"),
          },
        });
        window.loadFile("./pages/index.html");
      
        setTimeout(() => {
          window.webContents.send("msg", "Hello World!");
        }, 2000);
      }
      
      

d. 渲染进程之间通信

  • 方法

    graph LR A[渲染进程 A]--ipcRender.send-->d1((数据))--ipcMain.on-->主进程--"BrowserWindow().webContents.send"-->d2((数据))--ipcRender.on-->B[渲染进程 B]

0x04 打包

  1. 使用命令 npm install -D electron-builder 安装开发依赖 electron-builder

  2. 修改 package.json

    {
      "name": "project",
      "version": "1.0.0",
      "description": "Electron Application",
      "main": "main.js",
      "scripts": {
        "start": "nodemon --exec electron .",
        "build": "electron-builder"
      },
      "build": {
        "appId": "com.example.demo",
        "win": {
          "icon": "./icon.ico",
          "target": [
            {
              "target": "nsis",
              "arch": ["x64"]
            }
          ]
        },
        "nsis": {
          "oneClick": false,
          "perMachine": true,
          "allowToChangeInstallationDirectory": true
        }
      },
      "keywords": [
        "Electron"
      ],
      "author": "SRIGT",
      "license": "MIT",
      "devDependencies": {
        "electron": "^31.1.0",
        "electron-builder": "^25.0.0-alpha.9",
        "nodemon": "^3.1.4"
      }
    }
    
    
    • 添加打包命令:"scripts": { "build": "electron-builder" }
    • 添加打包配置:"build": {}
      • 应用 id:"appId": "com.example.demo"
      • 适应 Windows 系统:"win": {}
        • 图标:"icon": "./icon.svg"
        • 安装程序格式与位数:"target": [{ "target": "nsis", "arch": ["x64"] }]
      • 配置 nsis 格式:
        • 禁用一键安装:"oneClick": false
        • 每台机器安装一次:"perMachine": true
        • 允许自定义安装目录:"allowToChangeInstallationDirectory": true
  3. 使用命令 npm run build 执行打包

    • 如果因为下载依赖包报错,则可以根据报错提供的 URL 下载对应的包,并解压到 C:\Users[username]\AppData\Local\electron-builder\Cache 目录下
posted @ 2024-06-27 10:29  SRIGT  阅读(48)  评论(0编辑  收藏  举报