使用 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...

相关推荐
学不会•2 小时前
css数据不固定情况下,循环加不同背景颜色
前端·javascript·html
活宝小娜4 小时前
vue不刷新浏览器更新页面的方法
前端·javascript·vue.js
程序视点4 小时前
【Vue3新工具】Pinia.js:提升开发效率,更轻量、更高效的状态管理方案!
前端·javascript·vue.js·typescript·vue·ecmascript
coldriversnow4 小时前
在Vue中,vue document.onkeydown 无效
前端·javascript·vue.js
我开心就好o4 小时前
uniapp点左上角返回键, 重复来回跳转的问题 解决方案
前端·javascript·uni-app
开心工作室_kaic5 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
刚刚好ā5 小时前
js作用域超全介绍--全局作用域、局部作用、块级作用域
前端·javascript·vue.js·vue
沉默璇年6 小时前
react中useMemo的使用场景
前端·react.js·前端框架
yqcoder7 小时前
reactflow 中 useNodesState 模块作用
开发语言·前端·javascript
2401_882727577 小时前
BY组态-低代码web可视化组件
前端·后端·物联网·低代码·数学建模·前端框架