【Electron实战】Vite插件实现Electron的串联启动

859 阅读3分钟

问题: 通常的做法是先开一个终端启动vite项目,再开一个终端启动electron

期望: 在 npm run dev 时,先启动vite项目,再自动启动 electron

不同的热更新:普通浏览器端的开发,代码运行在chrome中。而 Electron 相当于一个壳子,代码运行在它提供的渲染进程中,所以主进程文件的热更新和.vue组件的热更新是两个独立是事情

思路: 通过编写 vite 插件,自动启动 electron。该插件,分为开发环境和生产环境两个版本

image.png

开发环境下的插件

准备工作

  • 在tsconfig.node.json中添加我们的插件文件,让tsc可以扫描他们

image.png

  • 在 vite.config.ts 中引入插件

image.png

正式编写

核心思想: 监听 configureServer 钩子,主要做3件事

  1. 监听开发服务器的启动(listening事件),生成 vite 项目运行的 http 地址,该地址用于 electron 启动(即 win.loadURL() 所需的地址)
  2. 调用 node 的 child_process 子进程的.spawn()方法 。作用:将 http 地址传入主进程文件;以命令行的形式自动启动 electron。主进程中,怎么接收这个http地址?process.argv
  3. 通过 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 中引入插件

image.png

正式编写

核心思想: 监听 closeBundle 钩子(要在 vite 打包完成后,才能进行 electron 的打包),主要做2件事

  1. 向 package.json 中添加 main 属性,值就是主进程文件的路径,因为主进程文件是 electron 打包的入口文件
  2. 调用 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, //取消一键安装
          }
        }
      })
    }
  }
}