{T}

electron-vite

官网:https://cn.electron-vite.org/

github:https://github.com/alex8088/electron-vite/issues

快速开始

electron-vite 是一个新型构建工具,旨在为 Electron 提供更快、更精简的开发体验。它主要由五部分组成:

  • 一套构建指令,它使用 Vite 打包你的代码,并且它能够处理 Electron 的独特环境,包括 Node.js 和浏览器环境

  • 集中配置主进程、渲染器和预加载脚本的 Vite 配置,并针对 Electron 的独特环境进行预配置

  • 为渲染器提供快速模块热替换(HMR)支持,为主进程和预加载脚本提供热重载支持,极大地提高了开发效率

  • 优化 Electron 主进程资源处理

  • 使用 V8 字节码保护源代码

electron-vite 快速、简单且功能强大,旨在开箱即用。

electron-vite 需要 Node.js 版本 14.18+ 和 Vite 版本 3.0+

code
npm  i  electron-vite  -D

在安装了 electron-vite 的项目中,你可以直接使用 npx electron-vite 运行,也可以在 package.json 文件中添加 npm scripts:

json

json
{
  "scripts": {
    "start": "electron-vite preview", // 开启 Electron 程序预览生产构建
    "dev": "electron-vite dev", // 开启开发服务和 Electron 程序
    "prebuild": "electron-vite build"  // 为生产构建代码
  }
}

你还可以指定其他 CLI 选项,例如 --outDir。 有关 CLI 选项的完整列表,可以在你的项目中运行 npx electron-vite -h。了解更多有关 命令行界面 的信息。

配置 electron-vite

当以命令行方式运行 electron-vite 时,electron-vite 将会自动尝试解析项目根目录下名为 electron.vite.config.js 的配置文件。最基本的配置文件如下所示:

js
// electron.vite.config.js

export default {
  main: {
    // vite config options
  },
  preload: {
    // vite config options
  },
  renderer: {
    // vite config options
  }
}

了解更多有关 配置 的信息。

Electron 入口

当使用 electron-vite 打包代码时,Electron 应用程序的入口点应更改为输出目录中的主进程入口文件。默认的输出目录 outDirout。你的 package.json 文件会是这样:

json
{
 "name": "electron-app",
 "version": "1.0.0",
 "main": "./out/main/index.js"
}

Electron 的工作目录将是输出目录,而不是你的源代码目录。因此在打包 Electron 应用程序时可以将源代码排除

了解更多有关 生产构建 的信息

搭建 electron-vite 项目

在命令行中运行以下命令:

bash
npm create @quick-start/electron
yarn create @quick-start/electron
pnpm create @quick-start/electron

然后按照提示操作即可!

code
✔ Project name: … <electron-app>
✔ Select a framework: › vue
✔ Add TypeScript? … No / Yes
✔ Add Electron updater plugin? … No / Yes
✔ Enable Electron download mirror proxy? … No / Yes
Scaffolding project in ./<electron-app>...
Done.

命令行界面

electron-vite

别名:electron-vite develectron-vite serve

该命令将构建主进程和预加载脚本源代码,并为渲染器启动一个开发服务器,最后启动 Electron 应用程序

electron-vite preview

该命令将构建主进程、渲染器和预加载脚本源代码,并启动 Electron 应用程序进行预览

electron-vite build

该命令将构建主进程、渲染器和预加载脚本源代码。通常在打包 Electron 应用程序之前,需要执行此命令

选项

通用选项

选项描述
-c, --config <file>定义配置文件路径
-l, --logLevel <level>设置日志级别 (optional: info, warn, error, silent)
-m, --mode <mode>设置环境模式
-w, --watch用于热重载的监视模式 (default: false)
--ignoreConfigWarning忽略配置缺失警告 (default: false)
--sourcemap输出 source maps 支持 debug (default: false)
--outDir <dir>设置输出目录 (default: out)
--entry <file>指定 Electron 入口文件
-v, --version显示版本号
-h, --help显示可用的 CLI 选项

--ignoreConfigWarning 选项允许你在配置缺失时忽略警告。例如,不需要使用预加载脚本

Dev 选项

选项描述
--inspect [port]指定端口启用 V8 inspector (default: 5858)
--inspectBrk [port]--inspect 一样,但会暂停运行
--remoteDebuggingPort远程调试端口
--rendererOnly仅为渲染器启动开发服务

提示

  • --inspect 选项允许你在指定的端口上启用 V8 Inspector。外部调试器可以连接到此端口。有关更多详细信息,请参阅 V8 Inspector

  • --inspectBrk 选项与 --inspect 选项类似,但会在 JavaScript 的第一行暂停执行。

  • --remoteDebuggingPort 选项用于 IDE 调试。

  • --rendererOnly 选项仅用于 dev 命令以跳过主进程和预加载脚本构建,并仅为渲染器启动开发服务。此选项将大大提高 dev 命令速度。

使用 --rendererOnly 选项时,electron-vite 命令必须至少运行过一次。此外,你需要在不更改主进程和预加载脚本源代码的情况下使用它。

Preview 选项

选项描述
--skipBuild跳过构建

--skipBuild 选项仅用于 preview 命令跳过构建并启动 Electron 应用程序进行预览

开发

项目结构

推荐使用如下项目结构:

code
.
├──src
│  ├──main
│  │  ├──index.ts
│  │  └──...
│  ├──preload
│  │  ├──index.ts
│  │  └──...
│  └──renderer    # with vue, react, etc.
│     ├──src
│     ├──index.html
│     └──...
├──electron.vite.config.ts
├──package.json
└──...

遵循此约定,electron-vite 可以用最少的配置进行工作

当运行 electron-vite 时,它会自动寻找主进程、渲染器和预加载脚本的入口文件。默认的入口配置:

  • 主进程: <root>/src/main/{index|main}.{js|ts|mjs|cjs}
  • 预加载脚本: <root>/src/preload/{index|preload}.{js|ts|mjs|cjs}
  • 渲染器: <root>/src/renderer/index.html

如果找不到入口点,它将抛出一个错误。你可以通过设置 build.rollupOptions.input 选项来修复它。

自定义

尽管我们强烈推荐上面的项目结构,但这不是必需的。你可以对其进行配置以满足你的使用场景。

假设你有下面这样的项目文件结构:

code
.
├──electron
│  ├──main
│  │  ├──index.ts
│  │  └──...
│  └──preload
│     ├──index.ts
│     └──...
├──src   # with vue, react, etc.
├──index.html
├──electron.vite.config.ts
├──package.json
└──...

你的 electron.vite.config.ts 文件应该是这样:

js
import { defineConfig } from 'electron-vite'
import { resolve } from 'path'

export default defineConfig({
  main: {
    build: {
      rollupOptions: {
        input: {
          index: resolve(__dirname, 'electron/main/index.ts')
        }
      }
    }
  },
  preload: {
    build: {
      rollupOptions: {
        input: {
          index: resolve(__dirname, 'electron/preload/index.ts')
        }
      }
    }
  },
  renderer: {
    root: '.',
    build: {
      rollupOptions: {
        input: {
          index: resolve(__dirname, 'index.html')
        }
      }
    }
  }
})

默认情况下,渲染器的工作目录位于 src/renderer 中。在此示例中,渲染器的 root 选项应设置为 “.”

使用预加载脚本

预加载脚本会在渲染器的网页加载之前注入。 如果你想向渲染器加入需要特殊权限的功能,你可以通过 contextBridge 接口定义 全局对象

预加载脚本的作用:

  • 增强渲染器:预加载脚本运行在具有 HTML DOM APIs 和 Node.js、Electron APIs 的有限子集访问权限的环境中
  • 在主进程和渲染进程之间通信:使用 Electron 的 ipcMainipcRenderer 模块进行进程间通信(IPC)

例子

创建一个预加载脚本并通过 contextBridge.exposeInMainWorld 将方法或变量暴露给渲染器

js
import { contextBridge, ipcRenderer } from  'electron'
contextBridge.exposeInMainWorld('electron', {
  ping: () => ipcRenderer.invoke('ping')
})
  1. 将脚本附在渲染进程上,在 BrowserWindow 构造器中使用 webPreferences.preload 传入脚本的路径。

js

code
import { app, BrowserWindow } from  'electron'
import path from  'path'
const  createWindow  = () => {
 const  win  =  new  BrowserWindow({
 webPreferences: {
 preload: path.join(__dirname, 'preload.js'),
 },
 })
 ipcMain.handle('ping', () =>  'pong')
 win.loadFile('index.html')
}
app.whenReady().then(() => {
 createWindow()
})
code
import { app, BrowserWindow } from  'electron'
import path from  'path'
const  createWindow  = () => {
 const  win  =  new  BrowserWindow({
 webPreferences: {
 preload: path.join(__dirname, 'preload.js'),
 },
 })
 ipcMain.handle('ping', () =>  'pong')
 win.loadFile('index.html')
}
app.whenReady().then(() => {
 createWindow()
})
  1. 在渲染器进程中使用暴露的函数和变量:

js

code
const  func  =  async () => {
 const  response  =  await window.electron.ping()
 console.log(response) // prints out 'pong'
}
func()
code
const  func  =  async () => {
 const  response  =  await window.electron.ping()
 console.log(response) // prints out 'pong'
}
func()

沙盒的限制

从 Electron 20 开始,预加载脚本默认沙盒化,不再拥有完整 Node.js 环境的访问权。实际上,这意味着你只拥有一个 polyfilled 的 require 函数(类似于 Node 的 require 模块),它只能访问一组有限的 API。

可用的 API详细信息
Electron 模块渲染进程模块
Node.js 模块events, timers, url
Polyfilled 的全局模块Buffer, process, clearImmediate, setImmediate

提示

因为 require 函数是一个功能有限的 polyfill,你无法把 preload 脚本拆成多个文件并作为 CommonJS 模块来加载,除非指定了 sandbox: false

在 Electron 中,可以使用 BrowserWindow 构造函数中的 sandbox: false 选项在每个进程的基础上禁用渲染器沙盒。

js

code
const  win  =  new  BrowserWindow({
 webPreferences: {
 sandbox: false
 }
})
code
const  win  =  new  BrowserWindow({
 webPreferences: {
 sandbox: false
 }
})

了解有关 Electron 进程沙盒 的更多信息。

高效

也许有些开发人员认为使用预加载脚本不方便且不灵活。但我们为什么要推荐:

  • 这是安全的做法,大多数流行的 Electron 应用程序(slack、visual studio code 等)都这样做。
  • 避免混合开发(nodejs 和浏览器),让渲染器成为一个常规的 web 应用程序,让 web 开发人员更容易上手。

基于效率考虑,推荐使用 @electron-toolkit/preload。非常容易将 Electron APIs(ipcRenderer、webFrame、process)暴露给渲染器。

首先,在启用上下文隔离的情况下,使用 contextBridge 将 Electron APIs 暴露给渲染器,否则将其添加到全局 DOM。

js

code
import { contextBridge } from  'electron'
import { electronAPI } from  '@electron-toolkit/preload'
if (process.contextIsolated) {
 try {
 contextBridge.exposeInMainWorld('electron', electronAPI)
 } catch (error) {
 console.error(error)
 }
} else {
 window.electron = electronAPI
}
code
import { contextBridge } from  'electron'
import { electronAPI } from  '@electron-toolkit/preload'
if (process.contextIsolated) {
 try {
 contextBridge.exposeInMainWorld('electron', electronAPI)
 } catch (error) {
 console.error(error)
 }
} else {
 window.electron = electronAPI
}

然后,在渲染进程中直接使用 Electron APIs:

js

code
// Send a message to the main process with no response
window.electron.ipcRenderer.send('electron:say', 'hello')
// Send a message to the main process with the response asynchronously
window.electron.ipcRenderer.invoke('electron:doAThing', '').then(re  => {
 console.log(re)
})
// Receive messages from the main process
window.electron.ipcRenderer.on('electron:reply', (_, args) => {
 console.log(args)
})
code
// Send a message to the main process with no response
window.electron.ipcRenderer.send('electron:say', 'hello')
// Send a message to the main process with the response asynchronously
window.electron.ipcRenderer.invoke('electron:doAThing', '').then(re  => {
 console.log(re)
})
// Receive messages from the main process
window.electron.ipcRenderer.on('electron:reply', (_, args) => {
 console.log(args)
})

了解更多有关 @electron-toolkit/preload

提示

@electron-toolkit/preload 需要禁用 sandbox

IPC 安全问题

最安全的方法是使用辅助函数来包装 ipcRenderer 调用,而不是直接通过 context bridge 暴露 ipcRenderer 模块。

Webview

将预加载脚本附加到 webview 的最简单方法是通过 webContents 的 will-attach-webview 事件处理。

js

code
mainWindow.webContents.on('will-attach-webview', (e, webPreferences) => {
 webPreferences.preload =  join(__dirname, '../preload/index.js')
})
code
mainWindow.webContents.on('will-attach-webview', (e, webPreferences) => {
 webPreferences.preload =  join(__dirname, '../preload/index.js')
})

nodeIntegration

目前,electorn-vite 不支持 nodeIntegration。其中一个重要的原因是 Vite 的 HMR 是基于原生 ESM 实现的。但是还有一种支持方式就是使用 require 导入 node 模块,不太优雅。或者你可以使用插件 vite-plugin-commonjs-externals 来处理。

也许将来会有更好的方法来支持。但需要注意的是,使用预加载脚本是一个更好、更安全的选择。

dependencies vs devDependencies

  • 对于主进程和预加载脚本,最佳实践是将依赖项外部化,只打包自己的代码。

    我们需要将应用程序需要的依赖安装到 package.jsondependencies 中。然后使用 externalizeDepsPlugin 将它们外部化而不打包它们。

    js

    code
    import { defineConfig, externalizeDepsPlugin } from  'electron-vite'
    export  default  defineConfig({
     main: {
     plugins: [externalizeDepsPlugin()]
     },
     preload: {
     plugins: [externalizeDepsPlugin()]
     },
     // ...
    })
    code
    import { defineConfig, externalizeDepsPlugin } from  'electron-vite'
    export  default  defineConfig({
     main: {
     plugins: [externalizeDepsPlugin()]
     },
     preload: {
     plugins: [externalizeDepsPlugin()]
     },
     // ...
    })

    在打包应用程序的时候,这些依赖也会一起打包,比如 electron-builder。不用担心他们会丢失。另一方面,devDependencies 则不会被打包。

    值得注意的是一些只支持 ESM 的模块(例如 lowdbexecanode-fetch),我们不应该将其外部化。我们应该让 electron-vite 把它打包成一个 CJS 标准模块来支持 Electron。

    js

    code
    import { defineConfig, externalizeDepsPlugin } from  'electron-vite'
    export  default  defineConfig({
     main: {
     plugins: [externalizeDepsPlugin({ exclude: ['lowdb'] })],
     build: {
     rollupOptions: {
     output: {
     manualChunks(id) {
     if (id.includes('lowdb')) {
     return  'lowdb'
     }
     }
     }
     }
     }
     },
     // ...
    })
    code
    import { defineConfig, externalizeDepsPlugin } from  'electron-vite'
    export  default  defineConfig({
     main: {
     plugins: [externalizeDepsPlugin({ exclude: ['lowdb'] })],
     build: {
     rollupOptions: {
     output: {
     manualChunks(id) {
     if (id.includes('lowdb')) {
     return  'lowdb'
     }
     }
     }
     }
     }
     },
     // ...
    })
  • 对于渲染器,它通常是完全打包的,所以依赖项最好安装在 devDependencies 中。这使得最终的分发包更小。

多窗口应用程序

当 Electron 应用程序具有多窗口时,这意味着可能有多个 html 页面和预加载脚本,你可以像下面一样修改你的配置文件:

js

code
// electron.vite.config.js
export  default {
 main: {},
 preload: {
 build: {
 rollupOptions: {
 input: {
 browser: resolve(__dirname, 'src/preload/browser.js'),
 webview: resolve(__dirname, 'src/preload/webview.js')
 }
 }
 }
 },
 renderer: {
 build: {
 rollupOptions: {
 input: {
 browser: resolve(__dirname, 'src/renderer/browser.html'),
 webview: resolve(__dirname, 'src/renderer/webview.html')
 }
 }
 }
 }
}
code
// electron.vite.config.js
export  default {
 main: {},
 preload: {
 build: {
 rollupOptions: {
 input: {
 browser: resolve(__dirname, 'src/preload/browser.js'),
 webview: resolve(__dirname, 'src/preload/webview.js')
 }
 }
 }
 },
 renderer: {
 build: {
 rollupOptions: {
 input: {
 browser: resolve(__dirname, 'src/renderer/browser.html'),
 webview: resolve(__dirname, 'src/renderer/webview.html')
 }
 }
 }
 }
}

传递 CLI 参数给 Electron 应用程序

建议通过环境变量和模式来处理命令行参数:

  • 对于 Electron CLI 命令:

js

code
import { app } from  'electron'
if (import.meta.env.MAIN_VITE_LOG  ===  'true') {
 app.commandLine.appendSwitch('enable-logging', 'electron_debug.log')
}
code
import { app } from  'electron'
if (import.meta.env.MAIN_VITE_LOG  ===  'true') {
 app.commandLine.appendSwitch('enable-logging', 'electron_debug.log')
}

在开发中,可以使用上面的方法来处理。分发后,你可以直接附加 Electron 支持的参数。例如 .\app.exe --enable-logging

提示

electron-vite 已经支持 inspectinspect-brkremote-debugging-port 命令,所以你不需要为这些命令做这样的处理。有关更多详细信息,请参阅命令行界面

  • 对于应用程序参数:

js

code
const  param  =  import.meta.env.MAIN_VITE_MY_PARAM  ===  'true'  ||  /--myparam/.test(process.argv[2])
code
const  param  =  import.meta.env.MAIN_VITE_MY_PARAM  ===  'true'  || /--myparam/.test(process.argv[2])
  1. 在开发中,使用 import.meta.envModes 来决定是否使用。
  2. 在生产中,使用 process.argv 来处理。