使用 Vite 搭建 Electron 项目开发环境

大家好,我是徐徐。今天我们要讲的是如何用 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 应用应该大致都是这些东西,当然各自会有差别,这个完全看自己的习惯。梳理好了项目目录结构,后面久只管填充内容就行。

安装依赖

现在我们需要安装依赖来满足我们构建应用的需求,我们先分一个类,这样的话好区分相关的依赖作用。

工程类:eslinttypescriptprettiervite

核心业务:electronreact

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.jsrender.jspreload.jsservice.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 目录,然后再在下面创建 mainrenderpreload 三个目录。

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 构建几个开发服务,然后再联动起来,实现了高效的开发流程和自动重载功能,这为我们在开发和调试过程中提供了极大的便利和效率提升。

源码

github.com/Xutaotaotao...

开源项目

一站式Electron开发解决方案 Electron-Prokitgithub.com/Xutaotaotao...

相关推荐
C语言魔术师15 分钟前
【小游戏篇】三子棋游戏
前端·算法·游戏
匹马夕阳1 小时前
Vue 3中导航守卫(Navigation Guard)结合Axios实现token认证机制
前端·javascript·vue.js
你熬夜了吗?1 小时前
日历热力图,月度数据可视化图表(日活跃图、格子图)vue组件
前端·vue.js·信息可视化
桂月二二8 小时前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
hunter2062069 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb9 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角9 小时前
CSS 颜色
前端·css
九酒9 小时前
从UI稿到代码优化,看Trae AI 编辑器如何帮助开发者提效
前端·trae
浪浪山小白兔10 小时前
HTML5 新表单属性详解
前端·html·html5
lee57610 小时前
npm run dev 时直接打开Chrome浏览器
前端·chrome·npm