项目搭建

1. 创建项目

使用rsbuild创建react+typescript项目

pnpm create rsbuild@latest

2. 调整项目结构

  • src下创建ui文件夹存放前端代码
  • App.cssApp.tsxenv.d.tsindex.tsx移动到ui文件夹下
  • 修改rsbuild.config.ts中的配置
import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
import path from 'path';

export default defineConfig({
  plugins: [pluginReact()],
  server: {
    port: 5677,
  },
  html: {
    title: '奇怪的面板',
    tags: [
      {
        tag: 'html',
        attrs: { 
          // 给html添加lang=zh-CN属性
          lang: 'zh-CN',
        },
      },
    ],
  },
  source: {
    entry: {
      // 指定入口文件
      index: './src/ui/index.tsx',
    },
  },
  output: {
    distPath: {
      // 指定输出目录
      root: 'dist/web',
    },
    // 指定资源路径前缀,不设置的话通过electron访问页面时会因为找到不到资源文件而白屏
    assetPrefix: './',
  },
  resolve: {
    // 配置别名
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
});

3. 配置electron

pnpm install --save-dev electron

如果安装的很慢,可以在根目录创建一个.npmrc文件,添加以下内容,从淘宝镜像获取electron

electron_mirror=https://npmmirror.com/mirrors/electron/

安装完成后你可能会看到以下警告

╭ Warning ───────────────────────────────────────────────────────────────────────────────────╮
│                                                                                            │
│   Ignored build scripts: core-js, electron.                                                │
│   Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts.   │
│                                                                                            │
╰────────────────────────────────────────────────────────────────────────────────────────────╯

执行以下命令

pnpm approve-builds

勾选core-jselectron,然后回车,运行安装脚本

src下创建app文件夹存放electron代码

创建一个简单的electron应用

src/app/main.js
import { app, BrowserWindow } from 'electron';
import path from 'path';

let mainWindow;

// 在应用准备好后执行
app.on('ready', () => {
  // 创建一个浏览器窗口
  mainWindow = new BrowserWindow({});
  // 加载index.html文件
  // 指定的路径为项目运行pnpm build后的产物路径
  mainWindow.loadFile(path.join(app.getAppPath() + '/dist/web/index.html'));
});

修改一下package.json

package.json
{
  "name": "q_desktop",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  // 指定入口文件
  "main": "src/app/main.js",
  "scripts": {
    // 打包前端代码
    "build:web": "rsbuild build",
    // 启动前端应用
    "dev:web": "rsbuild dev --open",
    // 启动electron应用
    "dev:electron": "electron .",
    // 格式化代码
    "format": "prettier --write .",
    // 预览前端代码
    "preview:web": "rsbuild preview"
  },
  "dependencies": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  },
  "devDependencies": {
    "@rsbuild/core": "^1.2.16",
    "@rsbuild/plugin-react": "^1.1.1",
    "@types/node": "^22.13.11",
    "@types/react": "^19.0.10",
    "@types/react-dom": "^19.0.4",
    "electron": "^35.0.3",
    "prettier": "^3.5.2",
    "typescript": "^5.8.2"
  },
  "pnpm": {
    "onlyBuiltDependencies": [
      "core-js",
      "electron"
    ]
  }
}

运行命令看一下第一版效果

pnpm build:web
pnpm dev:electron
图1:运行效果
图1:运行效果

4. typescript+electron

使用typescript书写electron代码,获取更多的类型提示

  • 修改根目录下的tsconfig.json,将electron代码排除
tsconfig.json
{
  "compilerOptions": {
    "lib": ["DOM", "ES2020"],
    "jsx": "react-jsx",
    "target": "ES2020",
    "noEmit": true,
    "skipLibCheck": true,
    "useDefineForClassFields": true,

    /* modules */
    "module": "ESNext",
    "isolatedModules": true,
    "resolveJsonModule": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,

    /* type checking */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true
  },
  "include": ["src"],
  /* 排除electron代码 */
  "exclude": ["src/app"]
}
  • src/app下创建tsconfig.json文件
src/app/tsconfig.json
{
    "compilerOptions": {
        "strict": true,
        "target": "ESNext",
        "module": "NodeNext",
        /* 指定编译后的目录 */
        "outDir": "../../dist/app",
        "skipLibCheck": true
    }
}
  • 修改src/app/main.jssrc/app/main.ts
src/app/main.ts
import { app, BrowserWindow } from 'electron';
import path from 'path';

let mainWindow: BrowserWindow;

app.on('ready', () => {
  mainWindow = new BrowserWindow({});
  mainWindow.loadFile(path.join(app.getAppPath() + '/dist/web/index.html'));
});
  • 修改package.json,调整入口添加tsc命令将electron代码编译为js
package.json
{
  ...
  // 指定入口文件
  "main": "dist/app/main.js",
  "scripts": {
    ...
    // 编译electron代码
    "translate:electron":"tsc --project src/app/tsconfig.json"
  },
  ...
}

现在成功的使用typescript书写electron代码了,但是运行项目更加麻烦了

# 先编译前端代码
pnpm build:web
# 再编译electron代码
pnpm translate:electron
# 最后启动electron应用
pnpm dev:electron

5. 打包配置

  • 安装electron-builder
pnpm install --save-dev electron-builder
  • 在根目录创建electron-builder.json设置打包配置
electron-builder.json
{
    // 应用的唯一标识符
    "appId":"com.q.desktop.app",
    // 应用的图标
    "icon":"./icon.png",
    // 应用包含的文件
    "files":["dist/app","dist/web"],
    // mac打包配置
    "mac":{
        "target":"dmg"
    },
    // linux打包配置
    "linux":{
        "target":"AppImage",
        "category":"Utility"
    },
    // windows打包配置
    "win":{
         // 打包一个无需安装的版本和一个安装包
        "target":["portable","msi"]
    }
}
避雷

因为之前配置web编译路径和app编译路径都在dist下,所以我偷懒设置files:["dist"]
但是electron-builder打包时也会生成文件到dist下,导致了滚雪球,包的体积增大了一倍!!!

  • 修改package.json增加打包命令
package.json
{
  ...
  // 新增描述信息 没有也不影响打包
  "description": "electron study by liu7i",
  // 新增作者信息 没有也不影响打包
  "author": "liu7i",
  "scripts": {
    ...
    // mac打包
    "package:mac":"npm run translate:electron && npm run build:web && electron-builder --mac --arm64",
    // windows打包
    "package:win":"npm run translate:electron && npm run build:web && electron-builder --win --x64",
    // linux打包
    "package:linux":"npm run translate:electron && npm run build:web && electron-builder --linux --x64"
  },
  ...
}
  • 运行打包命令
# mac打包
pnpm package:win

你可能会遇上下面的报错信息:

...
ERROR: Cannot create symbolic link : 您的客户端没有所需的特权 : C:\Users\22458\AppData\Local\electron-builder\Cache\winCodeSign\548692403\darwin\10.12\lib\libcrypto.dylib
ERROR: Cannot create symbolic link : 您的客户端没有所需的特权 : C:\Users\22458\AppData\Local\electron-builder\Cache\winCodeSign\548692403\darwin\10.12\lib\libssl.dylib
...

说明终端的权限不足,需要以管理员身份运行终端

以管理员身份运行终端,重新打包,你可能会遇上下面的报错信息:

• updating asar integrity executable resource  executablePath=dist\win-unpacked\q_desktop.exe
  ⨯ Get "https://github.com/electron-userland/electron-builder-binaries/releases/download/winCodeSign-2.6.0/winCodeSign-2.6.0.7z": read tcp 192.168.1.4:59737->20.205.243.166:443: wsarecv: A connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed to respond.

大概率是网络问题,如果你有代理可以运行下面的命令设置代理

# 地址和端口看自身的代理配置,我的代理在本地的7890端口
pnpm config set proxy http://127.0.0.1:7890
pnpm config set https-proxy http://127.0.0.1:7890

再次打包,成功~

图2:打包产物
图2:打包产物

6. 开发体验优化

前面已经成功的完成了项目的搭建以及打包,但是每次修改react代码都需要打包一次才能看到效果,非常的麻烦,下面进行一些优化

  • 安装corss-env,在启动时设置环境变量,让electron在开发环境去加载react的开发服务器地址
pnpm install --save-dev cross-env
  • 修改package.json,在启动electron时设置环境变量
package.json
{
  ...
  "scripts": {
    ...
    // 先执行一下typescript编译,然后启动electron应用,设置环境变量为开发环境
    "dev:electron": "npm run translate:electron && cross-env NODE_ENV=development electron .", 
    ...
  },
  ...
}
  • 创建src/app/utils.ts文件,定义一个环境判断方法
src/app/utils.ts
/** @function 是否为开发环境 */
export function isDev(): boolean {
  return process.env.NODE_ENV === 'development';
}
  • 修改src/app/main.ts,在开发环境下加载react的开发服务器地址
src/app/main.ts
import { app, BrowserWindow } from 'electron';
import path from 'path';
import { isDev } from './utils.js';

let mainWindow: BrowserWindow;

app.on('ready', () => {
  mainWindow = new BrowserWindow({});

  if (isDev()) {
    // 端口号为react的开发服务器端口号
    mainWindow.loadURL('http://127.0.0.1:5677');
  } else {
    mainWindow.loadFile(path.join(app.getAppPath() + '/dist/web/index.html'));
  }
});

现在以及可以体验到react的热更新了

# 终端1
pnpm dev:web
# 终端2
pnpm dev:electron

项目启动后修改一下react的代码,可以看到electron窗口内容会同步更新
现在需要两个终端开启动开发环境,还可以继续优化

  • 安装npm-run-all在一个终端启动多个命令(可同时终端)
pnpm install --save-dev npm-run-all
  • 修改package.json,使用npm-run-all同时启动reactelectron
package.json
{
  ...
  "scripts": {
    ...
    // --parallel 并行执行模式
    "dev":"npm-run-all --parallel dev:web dev:electron",
    ...
  },
  ...
}

现在可以直接pnpm dev启动开发环境