🖥️ 从 Web 项目迁移到 Electron 的路径与方案

把一个 HTML/Vue 前端变成桌面应用,核心不是「加个壳」,而是把运行环境从浏览器换成 Node + Chromium,同时把浏览器里不能用的东西逐个换掉。

🎯 先判断迁移成本

现有形态迁移难度说明
纯静态 HTML + CSS + JS⭐ 很低直接套壳,<script> 里不能有 require,改法最少
Vue/React 单页(已有构建)⭐⭐ 低把 dist/ 当静态资源加载即可,渲染层几乎不用改
依赖 window.open / alert / localStorage⭐⭐ 低这些在 Electron 渲染进程里都可用
依赖 DOM API 操作浏览器(history.pushState 打不开新窗口等)⭐⭐⭐ 中需要改用 BrowserWindow / webContents 替代
依赖 fetch 跨域请求⭐⭐⭐ 中要么配 CORS,要么走主进程代理
大量使用 Node 库(fs / path / crypto)⭐⭐⭐ 中必须挪到主进程,通过 IPC 调用
需要访问本地文件系统⭐⭐⭐⭐ 高引入文件读写、权限、路径解析整套逻辑
已有后端服务依赖⭐ 低沿用 HTTP/RPC 即可,不用改

判断标准一句话:这个项目有多少逻辑依赖「浏览器沙箱」这个前提?依赖越多,迁移越难。


📦 方案选型

方案 A:Electron + Vite(推荐,最主流)

vue-vite 项目
├── vite.config.ts
├── src/                    # 渲染进程代码,原样保留
├── electron/
│   ├── main.ts              # 主进程:窗口、菜单、托盘
│   ├── preload.ts           # 预加载:安全暴露 API
│   └── ipc.ts               # IPC 通道定义
└── package.json

优点:生态成熟、electron-vite 插件开箱即用、HMR 体验好
缺点:主/渲染进程边界要自己划

构建产物:vite build 出渲染层静态资源 → electron-builder 打包成安装包

方案 B:Electron 纯手写(无框架)

零构建工具,HTML 直接进 loadFile。适合纯静态小项目,或想完全控制构建产物。

方案 C:Tauri(Rust 后端,非 Electron)

同样的 Web 技术 + 极小安装包(几 MB vs Electron 的 100MB+)。
但:IPC 要写 Rust,前端调 Rust 的学习成本高。除非对体积有硬需求,否则迁移路径不推荐——从 Web 项目过来,Tauri 的增量成本比 Electron 高。

给从 Vue/HTML 过来的人的建议:先走 Electron,路径最短、资料最多。等真的被安装包体积卡住了再考虑 Tauri。


🛠️ 迁移步骤(以 Vue3 + Vite 为例)

1. 初始化 Electron 骨架

npm install -D electron electron-vite electron-builder

2. 配置 package.json 的构建脚本

{
  "main": "dist-electron/main.js",
  "scripts": {
    "dev": "electron-vite dev",
    "build": "electron-vite build",
    "dist": "npm run build && electron-builder"
  },
  "build": {
    "appId": "com.example.myapp",
    "productName": "MyApp",
    "files": ["dist/**/*", "dist-electron/**/*"],
    "mac": { "target": ["dmg"], "category": "public.app-category.developer-tools" },
    "win": { "target": ["nsis"] },
    "linux": { "target": ["AppImage"] }
  }
}

3. 写主进程(窗口 + 生命周期)

// electron/main.ts
import { app, BrowserWindow } from 'electron'
import path from 'node:path'
 
const createWindow = () => {
  const win = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,   // 必须 true
      nodeIntegration: false,   // 必须 false
    },
  })
 
  // 开发环境加载 dev server,生产加载打包后的静态文件
  if (process.env.NODE_ENV === 'development') {
    win.loadURL('http://localhost:5173')
  } else {
    win.loadFile(path.join(__dirname, '../dist/index.html'))
  }
}
 
app.whenReady().then(() => {
  createWindow()
  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })
})
 
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit()
})

4. 写 preload(安全桥梁)

这是整个迁移最关键的设计决策。 不要在渲染进程里直接开 nodeIntegration,而是用 preload 精确暴露需要的能力:

// electron/preload.ts
import { contextBridge, ipcRenderer } from 'electron'
 
contextBridge.exposeInMainWorld('api', {
  // 文件操作
  readFile: (path: string) => ipcRenderer.invoke('fs:read', path),
  writeFile: (path: string, content: string) =>
    ipcRenderer.invoke('fs:write', path, content),
 
  // 系统
  getVersion: () => ipcRenderer.invoke('app:version'),
  openExternal: (url: string) => ipcRenderer.invoke('shell:open', url),
 
  // 事件订阅
  onUpdate: (cb: (info: any) => void) =>
    ipcRenderer.on('app:update', (_e, info) => cb(info)),
})

5. 写主进程 IPC 处理

// electron/ipc.ts
import { ipcMain, app, shell } from 'electron'
import fs from 'node:fs/promises'
 
export function registerIpc() {
  ipcMain.handle('fs:read', async (_e, path: string) => {
    return await fs.readFile(path, 'utf-8')
  })
 
  ipcMain.handle('fs:write', async (_e, path: string, content: string) => {
    await fs.writeFile(path, content, 'utf-8')
    return true
  })
 
  ipcMain.handle('app:version', () => app.getVersion())
 
  // 安全:只允许 http/https,避免打开任意协议
  ipcMain.handle('shell:open', async (_e, url: string) => {
    if (!/^https?:\/\//.test(url)) throw new Error('只允许 http/https')
    await shell.openExternal(url)
  })
}

6. 渲染进程里替换浏览器 API

// 原来(纯 Web)
window.open('https://example.com')
localStorage.setItem('key', JSON.stringify(data))
fetch('https://api.example.com/data')
 
// 迁移后
window.api.openExternal('https://example.com')  // 走 IPC 到主进程
// localStorage 可用,不用改(但 Electron 的 localStorage 和浏览器的不共享)
await window.api.readFile('/path/to/file')     // 文件操作走 IPC

⚠️ 迁移中最容易踩的坑

1. file:// 协议下的跨域限制

用 loadFile 加载本地 HTML 时,fetch('/api/xxx') 会被当成本地文件请求,全部失败。

解决:

  • 开发环境用 loadURL('http://localhost:5173'),保留 dev server 代理
  • 生产环境要么把 API 地址改成绝对 URL(https://api.example.com),要么配 webSecurity: false(不推荐)

2. 路由 history 模式在 file:// 下 404

Vue Router 的 createWebHistory() 在 file:// 下刷新页面会 404(因为 /some/route 被当成文件路径找)。

解决:

// main.ts —— 注册协议拦截,把所有路径都指回 index.html
protocol.handle('app', (request) => {
  return net.fetch('file://' + path.join(__dirname, '../dist/index.html'))
})
// 然后 loadURL('app://local/index.html')

或改用 createWebHashMode()(#/route),最省事但 URL 难看。

3. 静态资源路径

file:// 下绝对路径 /assets/xxx.js 会指向磁盘根目录。Vite 配置里要改:

// vite.config.ts
export default defineConfig({
  base: './',   // 关键:从 /assets/ 改成 ./assets/
})

4. 跨域请求被 CORS 拦

渲染进程的 fetch 仍然受浏览器 CORS 限制。绕法:

  • 走主进程发请求(推荐,绕过 CORS)
  • 或者主进程起个本地代理服务

5. localStorage 数据不迁移

Electron 有独立的 userData 目录,浏览器里的 localStorage 不会自动带过来。如果用户从浏览器版迁移过来,需要自己做数据导入导出。

6. 安全配置别偷懒

webPreferences: {
  contextIsolation: true,    // 隔离渲染进程和 Node 环境
  nodeIntegration: false,    // 禁止渲染进程直接用 Node API
  sandbox: true,             // 沙箱(默认开启)
}

三个都别关。关掉 contextIsolation 等于把 Node 的全部能力暴露给页面,页面里任何 XSS 都能读你的文件系统。

7. macOS 打包签名与公证

不打签名(signing)和公证(notarization)的 macOS 应用,用户第一次打开会看到「无法验证开发者」警告,需要右键打开或 xattr -cr 绕过。要正常分发必须配 Apple Developer 证书。


📋 迁移检查清单

  • 确认现有项目用了几处浏览器特有 API(window.open、CORS、路由刷新)
  • 选 Electron(或评估 Tauri)
  • 搭好 main / preload / renderer 三段结构
  • contextIsolation: true、nodeIntegration: false 写死
  • 文件/网络/系统操作全部走 IPC,不在渲染进程直接碰 Node
  • Vite base 改成 './'
  • 路由模式处理(hash 或自定义协议)
  • 写 preload 的 TS 类型声明(window.api)
  • 配 electron-builder 的各平台 target
  • macOS 签名 + 公证
  • 自动更新(electron-updater 或自建)
  • 代码签名失败时的用户引导提示

🔗 常用命令

# 开发(带热更新)
npm run dev
 
# 只构建
npm run build
 
# 打包成安装包
npm run dist
 
# 指定平台
npm run dist -- --mac
npm run dist -- --win
 
# 清除构建缓存
rm -rf dist dist-electron

🔗 相关


最后更新:2026-10-05