vue3-ts-vite集成electron记录


安装electron、electron-builder

npm install —save-dev electron electron-builder

准备工作

项目目录结构

│  .cz-config.js
│  .env
│  .env.development
│  .env.production
│  .eslintrc.cjs
│  .gitignore
│  .npmrc
│  .prettierrc.json
│  catalogTree.txt
│  commitlint.config.cjs
│  env.d.ts
│  index.html
│  package.json
│  pnpm-lock.yaml
│  README.md
│  tsconfig.app.json
│  tsconfig.json
│  tsconfig.node.json
│  uno.config.ts
│  vite.config.ts

├─.husky
│      commit-msg
│      pre-commit

├─.vscode
│      extensions.json

├─electron
│  │  background.ts
│  │  
│  ├─plugins
│  │      vite-electron-build.ts
│  │      vite-electron-dev.ts
│  │      
│  ├─preload
│  │      index.ts
│  │      
│  └─utils
│          build.ts
│          handle-files.ts

├─public
│      favicon.ico

└─src
    │  App.vue
    │  env.d.ts
    │  global.d.ts
    │  main.ts

    ├─assets
    │      base.css
    │      logo.svg
    │      main.css

    ├─request
    │      index.ts

    ├─router
    │      index.ts

    ├─stores
    │      counter.ts

    ├─utils
    │      indexed-db.ts

    └─views
            ThreeDemo.vue

根目录新建electron文件夹

安装我的项目结构在根目录新建electron文件夹以及文件夹内的所有文件夹与文件

  • background.ts 等同于electron文档中介绍的 main.js,因为项目已经存在main.ts,命名冲突

    /**
     * @description electron 主进程文件,因为项目已经有同名的main.ts了,所以使用background.ts
     */
    
    import path from 'path'
    import { app, BrowserWindow } from 'electron'
    
    // 禁用electron缓存
    app.commandLine.appendSwitch('disable-gpu-cache')
    
    function createWindow() {
        const win = new BrowserWindow({
            width: 800,
            height: 600,
            autoHideMenuBar: false,
            webPreferences: {
                preload: process.argv[2]
                    ? path.join(__dirname, '../preload/index.ts')
                    : path.join(__dirname, 'preload/index.js'),
                nodeIntegration: true, // 禁用 Node.js 整合
                contextIsolation: true, // 启用上下文隔离
                sandbox: true, // 启用沙盒模式
                webSecurity: true
            }
        })
    
        if (process.argv[2]) {
            win.webContents.openDevTools()
            win.loadURL(process.argv[2])
        } else {
            win.loadFile('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()
        }
    })
    
  • utils里面的build.ts、handle-files.ts,分别处理electron文件的热更新、与项目的dist打包文件同步

    • utils/build.ts
    export const buildBackground = (entryPoints: string, outfile: string) => {
        // entryPoints: ['electron/background.ts'],
        // outfile: 'electron/dist/background.js',
    
        // eslint-disable-next-line @typescript-eslint/no-var-requires
        require('esbuild').buildSync({
            entryPoints: [entryPoints],
            bundle: true,
            target: 'es2020',
            outfile,
            platform: 'node',
            external: ['electron']
        })
    }
    
    
    • electron/utils/handle-files.ts

      import fs from 'fs'
      import path from 'path'
      
      // 使用 fs 模块进行清空目标目录
      export function emptyDirectorySync(directory: string): void {
          if (fs.existsSync(directory)) {
              const files = fs.readdirSync(directory)
      
              files.forEach((file) => {
                  const filePath = path.join(directory, file)
      
                  if (fs.lstatSync(filePath).isDirectory()) {
                      emptyDirectorySync(filePath)
                  } else {
                      fs.unlinkSync(filePath)
                  }
              })
      
              fs.rmdirSync(directory)
          }
      }
      
      // 使用 fs 模块进行复制
      export function copyFolderSync(source: string, target: string) {
          if (!fs.existsSync(target)) {
              fs.mkdirSync(target)
          }
      
          const files = fs.readdirSync(source)
      
          files.forEach((file) => {
              const sourceFilePath = path.join(source, file)
              const targetFilePath = path.join(target, file)
      
              if (fs.lstatSync(sourceFilePath).isDirectory()) {
                  copyFolderSync(sourceFilePath, targetFilePath)
              } else {
                  fs.copyFileSync(sourceFilePath, targetFilePath)
              }
          })
      }
      
  • preload是electron的预加载文件,例如IPC通信、vue与electron通信桥梁都可以在这里面定义,以下是electron官网的介绍

    什么是预加载脚本,并且学会如何使用预加载脚本来安全地将特权 API 暴露至渲染进程中。 不仅如此,你还会学到如何使用 Electron 的进程间通信 (IPC) 模组来让主进程与渲染进程间进行通信。

    Electron 的主进程是一个拥有着完全操作系统访问权限的 Node.js 环境。 除了 Electron 模组 之外,您也可以访问 Node.js 内置模块 和所有通过 npm 安装的包。 另一方面,出于安全原因,渲染进程默认跑在网页页面上,而并非 Node.js里。

    为了将 Electron 的不同类型的进程桥接在一起,我们需要使用被称为 预加载 的特殊脚本。

    electron预加载文档

    • preload/index.ts
    /* eslint-disable eslint-comments/disable-enable-pair */
    /* eslint-disable @typescript-eslint/no-var-requires */
    
    const { contextBridge, ipcRenderer } = require('electron')
    
    contextBridge.exposeInMainWorld('versions', {
        node: () => process.versions.node,
        chrome: () => process.versions.chrome,
        electron: () => process.versions.electron
        // 除函数之外,我们也可以暴露变量
    })
    
    console.log(ipcRenderer)
    
  • plugins目录是vite-plugins,处理elctron开发环境与生产环境

    • electron/plugins/vite-electron-build.ts
    # electron/plugins/vite-electron-build.ts
    
    // 生产环境插件
    import fs from 'node:fs'
    import path from 'node:path'
    import type { Plugin } from 'vite'
    import * as electronBuilder from 'electron-builder'
    import { buildBackground } from '../utils/build'
    import { emptyDirectorySync, copyFolderSync } from '../utils/handle-files'
    
    // 源文件夹路径
    const sourcePath = path.resolve(process.cwd(), 'dist')
    
    // 目标文件夹路径
    const targetPath = path.resolve(process.cwd(), './electron/dist')
    
    // 导出 Vite 插件
    export const ElectronBuildPlugin = (): Plugin => {
        return {
            name: 'electron-build',
            closeBundle: () => {
                // 清空目标目录
                emptyDirectorySync(targetPath)
    
                // 执行复制操作
                copyFolderSync(sourcePath, targetPath)
    
                // 构建 Electron 后台脚本
                buildBackground('electron/background.ts', 'electron/dist/background.js')
                // 构建 preload预加载
                buildBackground('electron/preload/index.ts', 'electron/dist/preload/index.js')
    
                // 读取 package.json 文件并更新其中的 "main" 字段
                const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'))
                packageJson.main = './background.js'
    
                // 写入更新后的 package.json 到目标文件夹
                fs.writeFileSync('./electron/dist/package.json', JSON.stringify(packageJson, null, 4))
    
                // 配置 Electron Builder 并执行构建
                const outputDir = './electron/dist/node_modules'
    
                // 确保输出目录不存在时再创建
                if (!fs.existsSync(outputDir)) {
                    fs.mkdirSync(outputDir)
                }
    
                electronBuilder.build({
                    config: {
                        directories: {
                            output: path.resolve(process.cwd(), './electron/release'),
                            app: targetPath
                        },
                        asar: true,
                        appId: 'gzjstech.com',
                        productName: 'vite-electron',
                        nsis: {
                            oneClick: false,
                            perMachine: false,
                            allowToChangeInstallationDirectory: true
                        }
                    }
                })
            }
        }
    }
    
    • vite-electron-dev.ts

      // 开发环境插件
      import type { AddressInfo } from 'net'
      import fs from 'node:fs'
      import { spawn, type ChildProcessWithoutNullStreams } from 'child_process'
      import type { Plugin } from 'vite'
      import { buildBackground } from '../utils/build'
      
      // 定义 Electron 进程变量
      let ElectronProcess: ChildProcessWithoutNullStreams
      
      // 导出 Vite 插件
      export const ElectronDevPlugin = (): Plugin => {
          return {
              name: 'electron-dev',
              configureServer: (server) => {
                  // 构建 Electron 后台脚本
                  buildBackground('electron/background.ts', 'electron/dist/background.js')
      
                  // 在服务器监听事件时
                  server.httpServer?.on('listening', () => {
                      // 获取服务器地址信息
                      const addressInfo = server.httpServer?.address() as AddressInfo
                      const IP = `http://localhost:${addressInfo.port}`
      
                      // 启动 Electron 进程
                      // eslint-disable-next-line @typescript-eslint/no-var-requires
                      ElectronProcess = spawn(require('electron') as unknown as string, [
                          'electron/dist/background.js',
                          IP
                      ])
      
                      // 监听后台脚本文件的变化,重新启动 Electron 进程
                      fs.watchFile('electron/background.ts', () => {
                          ElectronProcess.kill() // 终止现有 Electron 进程
                          buildBackground('electron/background.ts', 'electron/dist/background.js') // 重新构建后台脚本
                          // eslint-disable-next-line @typescript-eslint/no-var-requires
                          ElectronProcess = spawn(require('electron') as unknown as string, [
                              'electron/dist/background.js',
                              IP
                          ]) // 启动新的 Electron 进程
                      })
      
                      // 监听 Electron 进程的错误输出
                      ElectronProcess.stderr.on('data', (data) => {
                          console.log(`electron process: ${data.toString()}`)
                      })
                  })
              }
          }
      }
      

      vite.config.ts

  • 引入我们编写的vite插件并注册

import { fileURLToPath, URL } from 'node:url'
import UnoCSS from 'unocss/vite'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { ElectronDevPlugin } from './electron/plugins/vite-electron-dev'
import { ElectronBuildPlugin } from './electron/plugins/vite-electron-build'

// https://vitejs.dev/config/
export default defineConfig({
    envPrefix: 'APPLET_',
    plugins: [vue(), UnoCSS(), ElectronDevPlugin(), ElectronBuildPlugin()],
    base: './',
    resolve: {
        alias: {
            '@': fileURLToPath(new URL('./src', import.meta.url))
        }
    },
    server: {
        proxy: {
            // 跨域代理
            '/apis': {
                target: 'http://' + env.VUE_APP_BASE_API,
                changeOrigin: true,
                rewrite: (path) => path.replace(/^\/apis/, '')
            }
            // 代理 WebSocket 或 socket
            // '/socket.io': {
            //   target: 'ws://localhost:3000',
            //   ws: true
            //  }
        }
    }
})

preload注入后无法在web层获取相关变量

  • 需要在scr下新建global.d.ts,并且在tsconfig.app.json里面引入

    import type { Method, ResponseType } from 'axios'
    
    export {}
    declare global {
        interface Window {
            // 这里新增preload里面的注入变量,防止window.versions报错
            electronAPI?: any //全局变量名
            versions?: any //
        }
        interface AxiosConfig {
            params?: any
            data?: any
            url?: string
            method?: Method
            headersType?: string
            responseType?: ResponseType
        }
    
        interface IResponse<T = any> {
            code: string
            data: T extends any ? T : T & any
        }
        type AxiosHeaders =
            | 'application/json'
            | 'application/x-www-form-urlencoded'
            | 'multipart/form-data'
    }
    declare const window: any
    
    • tsconfig.app.json

      {
        "extends": "@vue/tsconfig/tsconfig.dom.json",
        // "include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
        "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue", "electron/**/*.ts"],
        "exclude": ["src/**/__tests__/*"],
        "compilerOptions": {
          "composite": true,
          "baseUrl": "./",
          "paths": {
            "@/*": ["./src/*"]
          },
          "types": ["three"]
        }
      }
      

打包注意点

  • 需要在根目录新建 .npmrc文件把下面的三行复制进去,这样打包才不会报错
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
registry=https://registry.npm.taobao.org/
electron_builder_binaries_mirror=https://npm.taobao.org/mirrors/electron-builder-binaries/

集成方案来自b站up主小满zs,与借鉴了electron-vite框架,在他们的基础上完善了这一版,通过vite生命周期实现打包完成vite后再打包electron,这样开发模式启动时就能带起electron