大家好,我是徐徐。今天我们要讲的是如何用 Vite 构建一个 Electron 项目。
前言
最近准备重写 Electron 相关的实战教程,一年前在掘金上写过相关的文章,现在看来都太过粗糙,大部分东西都是为了记录写下的,并没有系统整理过相关的内容,自己一直在工作中做 Electron 相关的开发,最近又有了一些新的感悟以及相关的实践总结,所以决定从各个方面去写一下相关的实战教程,打算每一篇文章都把自己的思路讲明白,然后附上相应的源码。这一篇文章是讲如何用使用 Vite 搭建 Electron 项目开发环境。
初始化项目
我们这里初始化项目直接从 npm init
开发,不用任何脚手架,这样构建出来的项目才是最干净的最自由的项目,你可以随心所欲配置任何内容。另外通过从 0 去搭建项目会让你对项目架构有更加清晰的认知,对项目的启动入口和方式有更加深入的认识,知道这个程序的入口程序在哪里,也了解如何输出的,编程最重要的点就是正确地识别输入和输出。下面我们可以正式开始了!
首先在项目根目录执行命令行 npm init
。
这样我们就开启了一个项目。
更多关于 npm init
的内容可参考:docs.npmjs.com/cli/v10/com...
规划项目结构
在一个 Electron 项目中,如果按照核心功能来划分的话大概会有这几个大块:主进程,渲染进程,preload。主进程和渲染进程公用的模块,主进程主要是一些和底层以及 electron 一些主进程的 api 内容构成的;渲染进程主要是跟常规 Web 应用相关的内容;preload 是一个特殊的模块,主要是为了将 Electron 的不同类型的进程桥接在一起;主进程和渲染进程公用的模块,这个就是主进程和渲染进程公用的一些源码。
上面说的只是按照功能层面去划分的核心目录,当然在正常的开发中肯定还必不可少一些基础的文件目录,比如一些脚本文件,环境配置,静态资源,以及常规的配置文件。下面是我们整个项目的结构目录。
basic
electron-proplay
├─ resource(需要打包到安装包内的资源,这些资源不会被编译)
├─ scripts(工程调试、编译过程中需要使用的脚本文件)
├─ node_modules(依赖包目录)
├─ vite(vite 相关的配置)
├─ src(源码)
│ ├─ common(主进程与渲染进程公用的源码)
│ ├─ main(主进程源码)
│ └─└─ index.ts(主进程入口文件)
│ ├─ renderer(渲染进程源码)
│ │ ├─ assets(一起编译打包的静态资源)
│ │ ├─ components(全局公共组件)
│ │ ├─ pages(整个应用的所有页面,包含子页面或子控件则以页面名设置子目录)
│ │ ├─ store(放置公共模块)
│ │ ├─ utils (工具类:toast、alert、i18n等)
│ └──└─ index.ts(渲染进程入口文件)
│ ├─ preload(前置脚本)
│ └──└─ index.ts
├─ .vscode(vscode配置文件)
├─ .prettier(beautify的配置文件,用于团队源码风格一致)
├─ .npmrc(项目环境变量,主要是一些镜像源地址的配置)
├─ .gitignore(git排除文件)
├─ .nvmrc(nvm版本配置文件)
└─ package.json
对于上面的目录结构,常规的 electron 应用应该大致都是这些东西,当然各自会有差别,这个完全看自己的习惯。梳理好了项目目录结构,后面久只管填充内容就行。
安装依赖
现在我们需要安装依赖来满足我们构建应用的需求,我们先分一个类,这样的话好区分相关的依赖作用。
工程类:eslint
、typescript
、prettier
、vite
核心业务:electron
、react
ts 初始化
basic
yarn add typescript -D
npx tsc --init
初始化成功之后会有 tsconfig.json
文件。
更多可参考:www.typescriptlang.org/download/
eslint 初始化
basic
npm init @eslint/config@latest
初始化成功之后会有 eslint.config.mjs
文件。
更多可参考:eslint.org/docs/latest...
prettier 初始化
basic
yarn add --dev --exact prettier
node --eval "fs.writeFileSync('.prettierrc','{}\n')"
初始化成功之后会有 .prettierrc
文件。
更多可参考:prettier.io/docs/en/ins...
安装vite,electron,react
basic
yarn add vite electron react react-dom -D
设置项目配置
通过上面的依赖安装我们基本上就可以开启一个最基础的 electron
项目了,但是在这之前我们需要做一些项目的常规配置,比如 ts
的设置,prettier
规则的设置,git
忽略文件,nvm
配置等。
TS 配置
对于 TS 配置,我们可以先用他默认生成的配置,如果后期有需求,可以对相应的配置做改动或者放开相应的注释
ESlint 配置
ESlint 的配置我们也先用默认生成的配置。
prettier 设置
这里先用个常用的配置,如果后期有需求再改动。
basic
{
printWidth: 80, //单行长度
tabWidth: 2, //缩进长度
useTabs: false, //使用空格代替tab缩进
semi: true, //句末使用分号
singleQuote: true, //使用单引号
quoteProps: 'as-needed', //仅在必需时为对象的key添加引号
jsxSingleQuote: true, // jsx中使用单引号
trailingComma: 'all', //多行时尽可能打印尾随逗号
bracketSpacing: true, //在对象前后添加空格-eg: { foo: bar }
jsxBracketSameLine: true, //多属性html标签的'>'折行放置
arrowParens: 'always', //单参数箭头函数参数周围使用圆括号-eg: (x) => x
requirePragma: false, //无需顶部注释即可格式化
insertPragma: false, //在已被preitter格式化的文件顶部加上标注
proseWrap: 'preserve',
htmlWhitespaceSensitivity: 'ignore', //对HTML全局空白不敏感
vueIndentScriptAndStyle: false, //不对vue中的script及style标签缩进
endOfLine: 'lf', //结束行形式
embeddedLanguageFormatting: 'auto', //对引用代码进行格式化
};
git 忽略配置
.gitignore
文件
basic
node_modules
*.log
*.log*
build
dist
.DS_Store
.idea
nvm 配置
.nvmrc
文件,方便进去项目直接切换对应的 Node 版本。
basic
v20.16.0
这个可以使用 VSCODE 的插件 vsc-nvm
,具体可参考 marketplace.visualstudio.com/items?itemN...
npm 源配置
主要设置下载源,为后期更加快速的添加其他的依赖以及加快 Electron 的下载。
basic
registry=https://registry.npmmirror.com
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/
构建 Vite 服务
把上面的配置好之后我们需要通过构建 Vite 服务来完成我们整个项目最核心的搭建。这里需要做三件事情,第一个是对各个进程编写最简单的代码,第二个是对各个进程进行 Vite 服务的配置,第三是编写脚本去启动各个进程的服务。
进行 Vite 服务配置
创建 vite
目录,然后再在下面创建 main.js
,render.js
,preload.js
,service.js
四个文件,分别用于主进程的配置,渲染进程的配置,前置脚本的配置以及构建 Vite 服务的方法。
vite/main.js
javascript
import { cwd } from "process";
import path from "path";
import { builtinModules } from "module";
import { fileURLToPath } from "url";
import { defineConfig } from "vite";
const __dirname = fileURLToPath(new URL(".", import.meta.url));
const sharedResolve = {
alias: {
"@": path.resolve(__dirname, "../src"),
},
};
export default defineConfig({
root: path.resolve(__dirname, "../src/main"),
envDir: cwd(),
resolve: sharedResolve,
build: {
outDir: path.resolve(__dirname, "../dist/main"),
minify: false,
lib: {
entry: path.resolve(__dirname, "../src/main/index.ts"),
formats: ["cjs"],
},
rollupOptions: {
external: [
"electron",
...builtinModules,
],
output: {
entryFileNames: "[name].cjs",
},
},
emptyOutDir: true,
chunkSizeWarningLimit: 2048,
},
})
这个是主进程的配置,配置好入口文件和输出文件即可,注意 external
选项。
vite/render.js
javascript
import path from "path";
import { builtinModules } from "module";
import { fileURLToPath } from "url";
import react from "@vitejs/plugin-react-swc";
import { defineConfig } from "vite";
const __dirname = fileURLToPath(new URL(".", import.meta.url));
const sharedResolve = {
alias: {
"@": path.resolve(__dirname, "../src"),
},
};
export default defineConfig({
root: path.resolve(__dirname, "../"),
base: "./",
resolve: sharedResolve,
build: {
watch: {},
outDir: path.resolve(__dirname, "../dist/render"),
minify: true,
assetsInlineLimit: 1048576,
emptyOutDir: true,
chunkSizeWarningLimit: 2048,
rollupOptions: {
onwarn(warning, warn) {
if (
warning.code === "MODULE_LEVEL_DIRECTIVE" &&
warning.message.includes(`"use client"`)
) {
return;
}
warn(warning);
},
external: [...builtinModules],
},
commonjsOptions: {
include: [/node_modules/],
},
},
plugins: [react()],
optimizeDeps: {
include: ["@ant-design/icons-svg", "antd"],
},
});
这个是渲染进程的配置,跟常规的 React + Vite 前端项目基本没啥差别,注意多了三个依赖,分别是 @vitejs/plugin-react-swc、@ant-design/icons-svg、 antd
。我们需要去添加一下
basic
yarn add @vitejs/plugin-react-swc @ant-design/icons-svg antd -D
vite/preload.js
javascript
import { cwd } from "process";
import path from "path";
import { builtinModules } from "module";
import { fileURLToPath } from "url";
import { defineConfig } from "vite";
const __dirname = fileURLToPath(new URL(".", import.meta.url));
const sharedResolve = {
alias: {
"@": path.resolve(__dirname, "../src"),
},
};
export default defineConfig({
root: path.resolve(__dirname, "../src/preload"),
envDir: cwd(),
resolve: sharedResolve,
build: {
outDir: path.resolve(__dirname, "../dist/preload"),
minify: false,
lib: {
entry: path.resolve(__dirname, "../src/preload/index.ts"),
formats: ["cjs"],
},
rollupOptions: {
external: ["electron", ...builtinModules],
output: {
entryFileNames: "[name].cjs",
}
},
emptyOutDir: true,
chunkSizeWarningLimit: 2048,
},
});
这个前置脚本的配置就是简单的借助 Vite 进行编译的操作。
vite/service.js
javascript
import { spawn } from 'child_process';
import { build, createServer } from 'vite';
let spawnProcess = null;
const renderDev = {
async createRenderServer(serverOptions) {
const { sharedOptions, config } = serverOptions;
process.env.VITE_CURRENT_RUN_MODE = 'render';
const options = {
configFile: false,
...sharedOptions,
...config,
};
const server = await createServer(options);
await server.listen();
server.printUrls();
return server;
},
};
const preloadDev = {
async createRenderServer(viteDevServer, serverOptions) {
const { sharedOptions, config } = serverOptions;
process.env.VITE_CURRENT_RUN_MODE = 'preload';
const options = {
configFile: false,
...sharedOptions,
...config,
};
return build({
...options,
plugins: [
{
name: 'reload-page-on-preload-package-change',
writeBundle() {
viteDevServer.ws.send({
type: 'full-reload',
});
},
},
],
});
},
};
const mainDev = {
async createMainServer(renderDevServer, serverOptions, electronPath) {
const { sharedOptions, config } = serverOptions;
const protocol = `http${renderDevServer.config.server.https ? 's' : ''}:`;
const host = renderDevServer.config.server.host || 'localhost';
const port = renderDevServer.config.server.port;
process.env.VITE_DEV_SERVER_URL = `${protocol}//${host}:${port}/`;
process.env.VITE_CURRENT_RUN_MODE = 'main';
const options = {
configFile: false,
...sharedOptions,
...config,
};
return build({
...options,
plugins: [
{
name: 'reload-app-on-main-package-change',
writeBundle() {
if (spawnProcess != null) {
spawnProcess.kill('SIGINT');
spawnProcess = null;
}
spawnProcess = spawn(String(electronPath), ['.']);
if (spawnProcess) {
spawnProcess.stdout.on('data', (d) => {
const data = d.toString().trim();
console.log(data);
});
spawnProcess.stderr.on('data', (err) => {
console.error(`stderr: ${err}`);
});
}
process.on('SIGINT', () => {
if (spawnProcess) {
spawnProcess.kill();
spawnProcess = null;
}
process.exit();
});
},
},
],
});
},
};
const createViteElectronService = async (options) => {
const {
render,
preload,
main,
electronPath,
sharedOptions = {
mode: 'dev',
build: {
watch: {},
},
},
} = options;
try {
const renderDevServer = await renderDev.createRenderServer({ config: render, sharedOptions });
await preloadDev.createRenderServer(renderDevServer, { config: preload, sharedOptions });
await mainDev.createMainServer(renderDevServer, { config: main, sharedOptions }, electronPath);
} catch (err) {
console.error(err);
}
};
export default createViteElectronService;
这个文件实现了一个用于开发 Vite 和 Electron 应用的服务,主要功能包括:
- 启动开发服务器: 使用 Vite 启动一个开发服务器,用于渲染进程的开发。
- 构建预加载脚本: 使用 Vite 构建预加载脚本,并在脚本变化时重新加载页面。
- 构建主进程: 使用 Vite 构建主进程代码,并在代码变化时重启 Electron 应用。
有了这个文件,我们在日常开发中就可以热更新整个应用了。在这里我们需要注意的是 spawnProcess = spawn(String(electronPath), ['.'])
这里我们相当于通过 node 启动了一个 electron 的进程,模仿了官网例子中的electron .
命令,这样可以更加连贯的实现一键启动各个开发进程的目的,融合渲染进程和主进程的启动。具体可参考:www.electronjs.org/zh/docs/lat...
编写最简单的代码
通过上面的步骤,我们现在需要去补充一些入口文件,创建 src
目录,然后再在下面创建 main
,render
,preload
三个目录。
src/main/index.ts
主进程代码编写,实现一个打开窗口的方法。
typescript
import { BrowserWindow,app } from "electron";
import { resolve } from "path";
const createWindow = () => {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: join(__dirname, "../preload/index.cjs"),
},
});
if (import.meta.env.MODE === "dev") {
if (import.meta.env.VITE_DEV_SERVER_URL) {
mainWindow.loadURL(import.meta.env.VITE_DEV_SERVER_URL);
}
} else {
mainWindow.loadFile(resolve(__dirname, "../render/index.html"));
}
};
const main = () => {
createWindow();
}
app.whenReady().then(() => {
main();
})
到这里我们就需要改动一些 tsconfig.json
的配置了,以满足 import.meta
以及其他的适配,主要改动如下:
json
{
"include":["src"],
"compilerOptions":{
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Node",
"baseUrl": "./",
"paths": {
"@/*": ["./src/*"]
},
}
}
src/render/index.tsx & app.tsx
实现最简单的 React 应用。
app.tsx
tsx
import React from "react"
const App = () => {
return <div>Hello Vite + Electron</div>
}
export default App
index.tsx
tsx
import React from 'react'
import { createRoot } from 'react-dom/client';
import App from './app';
const container = document.getElementById('root');
const root = createRoot(container!);
root.render(<App />);
index.html
根目录添加 index.html
html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Electron Proplay</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./src/render/index.tsx"></script>
</body>
</html>
src/preload/index.ts
暴露一个 log
方法。
typescript
import { contextBridge } from "electron";
contextBridge.exposeInMainWorld("EP", {
log: (data:string) => {console.log(data)}
})
编写启动脚本
我们把上面的代码内容填充好了之后就可以开始编写启动脚本,然后在开发模式下启动整个应用了。
scripts/dev.js
javascript
#!/usr/bin/env node
import electronPath from "electron";
import main from "../vite/main.js";
import render from "../vite/render.js";
import preload from "../vite/preload.js";
import createViteElectronService from '../vite/service.js'
createViteElectronService({
render,
preload,
main,
electronPath
});
package.json
scripts 中添加 dev
执行命令,入口换成 dist/main/index.cjs
,这样才能在启动electron进程的时候以它为入口文件, type 为 module
,全部是 es
模式处理。
json
"main": "dist/main/index.cjs",
"type": "module",
"scripts": {
"dev": "node ./scripts/dev.js"
},
经过上面的步骤一个最简单的开发环境就搭建起来了,不出意外会出现如下情况:
配置调试环境
VSCODE 调试配置
Electron 项目相当于一个 node.js 项目,我们可以在 VSCode 里面做断点调试,方便我们开发。具体做法如下:
在工程根目录下创建 .vscode
子目录,并在这个子目录下新建一个名为 launch.json
的文件,配置如下:
json
{
"version": "0.1.0",
"configurations": [
{
"type":"node",
"request": "launch",
"name": "Start",
"program": "${workspaceFolder}/scripts/dev.js",
"cwd": "${workspaceFolder}"
}
]
}
在这个配置文件中,我们通过 type 属性指定了需要启动什么类型的程序,通过 program 属性指定了需要启动的脚本文件的路径,通过 cwd 属性指定了启动后进程的工作目录。在 VSCode 环境下 workspaceFolder 就相当于当前工作目录。然后我们还需要把 vite 的 sourcemap 配置打开,不然无法调试 TS 代码,所以需要对 vite 目录下的各个配置文件中的 build 选项添加 sourcemap: true
的配置。这样在 VSCode 打上断点就可以调试了,试图如下:
渲染进程调试
渲染进程的调试其实跟我们平常开发前端页面的调试是一样的,只需要在创建窗口的时候对<font style="color:rgb(28, 30, 33);">devTools</font>
这个选项进行设置。如果设置为 <font style="color:rgb(28, 30, 33);">false</font>
, 则无法使用 <font style="color:rgb(28, 30, 33);">BrowserWindow.webContents.openDevTools ()</font>
打开 DevTools,默认值为 <font style="color:rgb(28, 30, 33);">true</font>
。具体可参考:www.electronjs.org/docs/latest...
typescript
mainWindow.webContents.openDevTools({ mode: "detach", activate: true, });
主进程调试
虽然 VSCode 给我了我们调试代码的能力,但是有的时候我们还是需要像使用 Chrome 内置的调试工具调试Electron 主进程的代码,然后去看一些内存的情况以及性能指标,这个时候我们就需要进行额外的一个小小的配置啦,在 vite\service.js 中的 createMainServer
的方法中
javascript
spawnProcess = spawn(String(electronPath), ['--inspect','.']);
加上 --inspect
后执行 npm run dev
即可开始调试,然后在 chrome 中进入 chrome://inspect/#devices 这个地址,点击 Open dedicated DevTools for Node 即可看到如下界面,这样就可以开始愉快的调试主进程啦!
结语
通过上面一系列的操作,我们终于搭建出了一个完整的 Electron 开发环境,核心思路是通过 Vite 构建几个开发服务,然后再联动起来,实现了高效的开发流程和自动重载功能,这为我们在开发和调试过程中提供了极大的便利和效率提升。
源码
开源项目
一站式Electron开发解决方案 Electron-Prokit:github.com/Xutaotaotao...