最近在开发基于 WebRTC 做数据传输的文件管理工具,P-Pass File 需要对本地文件做读写操作,大量的操作干脆就直接以本地服务的方式去处理了。

之前为了快速开发客户端,使用 electron-forge vite ts 模板快速创建项目。可不曾想在主进程中使用 fork、child_process 启动 node 服务,vite 构建出来的产物会循环的启动 app,直接导致设备无法使用( issue 地址 )。找不出解决的办法,无奈只能使用老一套的方案处理了。

幸运的是,当时模块拆分对此影响比较小。

模块拆分方案

Electron 主进程作为启动器,fork 启动文件读写服务的子进程,ui 部分使用 hash 路由,构建之后作为资源随包分发。

与 electron-forge vite 等现有实现方式对比,初始直觉很容易就惯性的认为,electron 项目,不管是子进程还是渲染进程的内容,都应该归属于客户端大项目来管理[ electron, [ child_process , ui_process ] ]。因为基础的 electron 模板不提供现代的 UI 构建方式(Vue、 React等),开发过程又需要频繁的编辑、重启,所以在项目 package.json 引入相关依赖,vite.renderer.config.ts(在之前的模板中) 做相关的配置。
其实也是开发的时候启动了 electron 与 ui 相关的 dev server,在模板代码中也有体现。

if (MAIN_WINDOW_VITE_DEV_SERVER_URL) {
   mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL);
} else {
   mainWindow.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`));
}

在得到项目管理上的方便的同时,也相应的需要更多初始化配置、依赖的兼容。

所以在做权衡之后,将 electron 与渲染进程分离。electron 作为类服务端的一个启动器、进程管理器,更方便的隔离和选择技术栈。通过 preload 注入 apis;渲染进程当做 web 项目开发,通过 preload 环境参数区分接口调用,前期还能通过直接 loadURL 访问远端资源,达到更快速的项目迭代。 chat1

项目搭建

初始化项目 首先,创建一个新的项目目录并初始化 package.json。

mkdir my-electron-app
cd my-electron-app
npm init -y

安装依赖

安装 Electron、Webpack、TypeScript 和其他必要的依赖。

npm install electron --save-dev
npm install webpack webpack-cli webpack-dev-server ts-loader typescript --save-dev
npm install electron-builder --save-dev

初步依赖如下

"devDependencies": {
    "copy-webpack-plugin": "^12.0.2",
    "electron": "^34.0.1",
    "electron-builder": "^25.1.8",
    "ts-loader": "^9.5.2",
    "typescript": "^5.7.3",
    "webpack": "^5.97.1",
    "webpack-cli": "^6.0.1",
    "webpack-dev-server": "^5.2.0"
  },
  "dependencies": {
    "electron-log": "^5.2.4",
    "esm": "^3.2.25"
  }

配置 TypeScript

在项目根目录下创建 tsconfig.json 文件,配置 TypeScript 编译选项。

{
  "compilerOptions": {
    "target": "ES6",
    "module": "ESNext", // 使用 ESM 模块
    "moduleResolution": "node",
    "outDir": "./dist", // 输出目录
    "rootDir": "./src", // 源代码目录
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"]
}

配置 Webpack

在项目根目录下创建 webpack.config.js 文件,配置 Webpack。 如果不需要处理本地资源,CopyPlugin 不用配置。

import webpack from 'webpack';
import path from 'path';
import { fileURLToPath } from 'url';
import CopyPlugin from 'copy-webpack-plugin';

// 解决 __dirname 在 ESM 下的问题
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

export default (env) => {
  return {
    entry: {
      main: './src/main.ts',
      preload: './src/preload.ts',
    },
    target: 'electron-main',
    module: {
      rules: [
        {
          test: /\.ts$/,
          use: 'ts-loader',
          exclude: /node_modules/,
        },
      ],
    },
    resolve: {
      extensions: ['.ts', '.js'],
    },
    output: {
      filename: '[name].js',
      path: path.resolve(__dirname, 'dist'),
    },
    plugins: [
      new CopyPlugin({
        patterns: [
          { from: './src/server', to: 'server' },
          { from: './src/ui', to: 'ui' },
        ],
      }),
    ],
  };
};

创建 Electron 主进程文件

在 src 目录下创建 main.ts 文件,这是 Electron 的主进程入口文件。 没有渲染进程,也就不需要 index.html 和 renderer.ts。 创建 preload apis 注入文件。 在 src 目录下创建 preload.ts 文件。例子如下:

// See the Electron documentation for details on how to use preload scripts:
// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
import { contextBridge, ipcRenderer } from 'electron';

contextBridge.exposeInMainWorld('electron', {
  // 唤起文件选择器,返回所选择的目录或者文件地址
  openFileSelector: (options: any) => ipcRenderer.invoke('open-resource-selector', {
    properties: ['openFile', 'openDirectory', 'multiSelections'], // 允许选择文件和目录
    ...options,
  }),
  showItemInFolder: (path: string) => ipcRenderer.send('show-item-in-folder', path),
  systemInfo: {
    platform: process.platform,
    arch: process.arch,
    electron: process.versions.electron,
    node: process.versions.node,
    appVersion: process.env.APP_VERSION,
  }
});

配置 package.json 运行指令

"scripts": {
    "compile": "webpack --config webpack.config.js",
    "start": "npm run compile && electron .",
    "pack": "npm run compile && electron-builder --dir",
    "dist": "npm run compile && electron-builder"
  },

这里构建的逻辑就是使用 Webpack 将 ts 打包输出到 dist,后续再启动或者构建 Release。主进程代码量较少,就不使用文件监听的方式。

项目结构

├── build
│   └── icons
├── dist
│   ├── main.js
│   ├── preload.js
│   ├── server
│   └── ui
├── electron-builder.json
├── package-lock.json
├── package.json
├── release
├── src
│   ├── ipcListeners.ts
│   ├── main.ts
│   ├── preload.ts
│   ├── server
│   └── ui
├── tsconfig.json
└── webpack.config.js

项目构建与分发

将所需的 Node 服务构建输出到 Webpack 配置的 { from: './src/server', to: 'server' } 中,配置 icons 后在本地打包 dmg。将其上传至 Github Release,即可进行下载分发。

环境初始化、定义构建的平台、Node 版本等

name: Build and Release Electron App

on:
  push:
    tags:
      - 'v*'

jobs:
  build:
    runs-on: ${{ matrix.os }}
    
    strategy:
      matrix:
        os: [macos-latest, windows-latest]
        
    steps:
      - name: Checkout Code
        uses: actions/checkout@v4
        
      - name: Setup Node.js and npm
        uses: actions/setup-node@v4
        with:
          node-version: '18.19.0'
          npm-version: '10.2.3'

构建依赖的服务

前面说了,这里依赖了另外的服务资源,需要优先打包,不直接使用产物的原因是避免构建Node版本不一致等问题。 使用同样的授权 secrets.AUTH_TOKEN token,克隆仓库,安装依赖,构建服务。
PS:Window 平台下的 Path 处理格式。

# 构建服务端依赖
      - name: Build Server Dependency
        shell: bash
        run: |
          echo "Cloning server repository..."
          git clone https://x-access-token:${{ secrets.AUTH_TOKEN }}@github.com/hawkeye-xb/p-pass-file-server.git temp-server
          cd temp-server
          
          echo "Installing dependencies..."
          npm install --no-audit --no-fund --include=dev
          
          echo "Rebuilding native modules..."
          npm rebuild
          
          echo "Building server..."
          npx cross-env NODE_ENV=production node esbuild.config.js && node copy-node-files.js
          
          echo "Copying build artifacts..."
          cd ..
          mkdir -p src/server
          if [ "$RUNNER_OS" == "Windows" ]; then
            cp -r temp-server/dist/* src/server/ 2>/dev/null || powershell -Command "Copy-Item 'temp-server/dist/*' -Destination 'src/server' -Recurse -Force"
          else
            cp -r temp-server/dist/* src/server/
          fi
          
          rm -rf temp-server          
        env:
          GIT_TERMINAL_PROMPT: 0
          CI: true
          NODE_ENV: production

之后就是常规的 Electron 构建任务了。

上传

这里多给出上传的代码,会将 latest.yml.blockmap 自动更新需要的资源也上传到 Release 中。

      - name: Upload Release Assets
        uses: softprops/action-gh-release@v1
        if: startsWith(github.ref, 'refs/tags/')
        with:
          files: |
            release/*-win-x64.exe
            release/*-mac-x64.dmg
            release/*-mac-arm64.dmg            
          draft: false
          prerelease: ${{ contains(github.ref, '-beta') || contains(github.ref, '-alpha') }}
          name: Release ${{ github.ref_name }}
          tag_name: ${{ github.ref_name }}
        env:
          GITHUB_TOKEN: ${{ secrets.AUTH_TOKEN }}

完整的 CICD 文件查看