vue3-ts-vite集成electron记录

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,命名冲突

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    /**
    * @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
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      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
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    /* 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
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    # 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

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      // 开发环境插件
      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插件并注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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里面引入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    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

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      {
      "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文件把下面的三行复制进去,这样打包才不会报错
1
2
3
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


vue3-ts-vite集成electron记录
https://junsen.online/2023/11/26/vue3-ts-vite集成electron记录/
作者
Junsen
发布于
2023年11月26日
许可协议