问题: 通常的做法是先开一个终端启动vite项目,再开一个终端启动electron
期望: 在 npm run dev 时,先启动vite项目,再自动启动 electron
不同的热更新:普通浏览器端的开发,代码运行在chrome中。而 Electron 相当于一个壳子,代码运行在它提供的渲染进程中,所以主进程文件的热更新和.vue组件的热更新是两个独立是事情
思路: 通过编写 vite 插件,自动启动 electron。该插件,分为开发环境和生产环境两个版本
开发环境下的插件
准备工作
- 在tsconfig.node.json中添加我们的插件文件,让tsc可以扫描他们
- 在 vite.config.ts 中引入插件
正式编写
核心思想: 监听 configureServer 钩子,主要做3件事
- 监听开发服务器的启动(listening事件),生成 vite 项目运行的 http 地址,该地址用于 electron 启动(即 win.loadURL() 所需的地址)
- 调用 node 的 child_process 子进程的
.spawn()方法 。作用:将 http 地址传入主进程文件;以命令行的形式自动启动 electron。主进程中,怎么接收这个http地址?process.argv - 通过 fs.watch ,监听主进程文件的变化,通过先销毁当前主进程,再重启新主进程的方式,手动实现主进程文件的热更新
vite.electron.dev.ts
import type { Plugin } from 'vite'
import type { AddressInfo } from 'net'
import { spawn } from 'child_process'
import fs from 'node:fs'
import esbuild from 'esbuild'
// 将 mainElectron.ts 转换为 mainElectron.js
const buildMainElectron = () => {
esbuild.buildSync({
entryPoints: ['src/mainElectron.ts'], // 转换文件的路径
bundle: true,
outfile: 'dist/mainElectron.js', // 转换后的文件的路径
platform: 'node',
external: ['electron'],
target: 'node12'
})
}
export const ElectronDevPlugin = (): Plugin => {
return {
name: 'vite-plugin-electron-dev',
apply: 'serve', // 仅允许该插件在开发环境下执行
// 在 configureServer 钩子中,监听开发服务器的 listening 启动事件
configureServer(server) {
buildMainElectron() // 转换文件,因为Electron不认识.ts
server.httpServer?.once('listening', () => {
// 获取开发服务器的地址信息
const addressInfo = server.httpServer?.address() as AddressInfo
// 1. 提取地址信息中的端口号, 并生成一个地址, 给 Electron 启动服务用
const ipForElectron = `http://localhost:${addressInfo.port}`
// 2. 进程传参数,并启动 Electron
// child_process 子进程的 .spawn() 方法, 能直接执行传入的文件路径,
// 而 require('electron') 的返回值就是 electron 的所在路径。
// 所以这句话就是,将vite项目运行的http地址,传入 mainElectron.js ,然后启动 Electron
// ! 在 vite 中直接使用 CommonJS ,要删掉 package.json 中的 type:module
let ElectronProcess = spawn(require('electron'), ['dist/mainElectron.js', ipForElectron])
// 3. mainElectron.ts 实现热更新
fs.watch('src/mainElectron.ts', () => {
// a. 销毁上一个主进程
ElectronProcess.kill()
// b. 重新转换为 .js 文件
buildMainElectron()
// c. 重新启动一个 Electron
ElectronProcess = spawn(require('electron'), ['dist/mainElectron.js', ipForElectron])
})
})
}
}
}
mainElectron.ts
import { app, BrowserWindow } from 'electron'
app.whenReady().then(() => {
const win = new BrowserWindow({
width: 900,
height: 800,
webPreferences: {
nodeIntegration: true, // 允许在渲染进程中使用node的api
contextIsolation: false, // 关闭渲染进程的沙箱
webSecurity: false // 关闭跨域检测
}
})
// 根据不同环境,载入不同的内容
if (process.argv[2]) { // 开发环境
win.loadURL(process.argv[2])
} else { // 生产环境
win.loadFile('index.html')
}
})
生产环境下的插件
准备工作
- 在 vite.config.ts 中引入插件
正式编写
核心思想: 监听 closeBundle 钩子(要在 vite 打包完成后,才能进行 electron 的打包),主要做2件事
- 向 package.json 中添加 main 属性,值就是主进程文件的路径,因为主进程文件是 electron 打包的入口文件
- 调用 electron-builder 的 build 方法进行打包
vite.electron.build.ts
import type { Plugin } from 'vite'
import esbuild from 'esbuild'
import fs from 'node:fs'
import path from 'node:path'
import * as electronBuilder from 'electron-builder'
// 将 mainElectron.ts 转换为 mainElectron.js
const buildMainElectron = () => {
esbuild.buildSync({
entryPoints: ['src/mainElectron.ts'], // 转换文件的路径
bundle: true,
outfile: 'dist/mainElectron.js', // 转换后的文件的路径
platform: 'node',
external: ['electron'],
target: 'node12'
})
}
export const ElectronBuildPlugin = (): Plugin => {
return {
name: 'vite-plugin-electron-build',
apply: 'build', // 仅允许该插件在生产环境下执行
//! 要在 vite 打包完成后,才能进行 electron 的打包
closeBundle() {
buildMainElectron()
// electron 打包的第一件事就是在 package.json 中配置 main 属性,
// 因为这是 electron 打包的入口文件
// 1. 读取 package.json
const jsonObj = JSON.parse(fs.readFileSync('package.json').toString())
// 2. 添加 'main':'dist/mainElectron.js'
jsonObj['main'] = 'dist/mainElectron.js'
// 3. 新的 package.json 写入 dist 目录
console.log('打印jsonObj', jsonObj);
fs.writeFileSync('dist/package.json', JSON.stringify(jsonObj))
// 创建一个空的node_modules目录 不然会打包失败
fs.mkdirSync(path.join(process.cwd(), "dist/node_modules"));
// 使用 electron-builder 打包
electronBuilder.build({
config: {
appId: 'com.example.app',
productName: 'vite-electron',
directories: {
output: path.join(process.cwd(), "release"), //输出目录
app: path.join(process.cwd(), "dist"), //app目录
},
asar: true,
nsis: {
oneClick: false, //取消一键安装
}
}
})
}
}
}