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

文摘   2024-11-20 10:10   北京  

关注公众号 前端界,回复“加群

加入我们一起学习,天天进步


大家好,我是芝士。今天我们要讲的是如何用 Vite 构建一个 Electron 项目开发环境。

画板

前言

最近准备重写 Electron 相关的实战教程,一年前在掘金上写过相关的文章,现在看来都太过粗糙,大部分东西都是为了记录写下的,并没有系统整理过相关的内容,自己一直在工作中做 Electron 相关的开发,最近又有了一些新的感悟以及相关的实践总结,所以决定从各个方面去写一下相关的实战教程,打算每一篇文章都把自己的思路讲明白,然后附上相应的源码。这一篇文章是讲如何用使用 Vite 搭建 Electron 项目开发环境。

初始化项目

我们这里初始化项目直接从 npm init 开发,不用任何脚手架,这样构建出来的项目才是最干净的最自由的项目,你可以随心所欲配置任何内容。另外通过从 0 去搭建项目会让你对项目架构有更加清晰的认知,对项目的启动入口和方式有更加深入的认识,知道这个程序的入口程序在哪里,也了解如何输出的,编程最重要的点就是正确地识别输入和输出。下面我们可以正式开始了!

首先在项目根目录执行命令行 npm init

这样我们就开启了一个项目。

更多关于 npm init的内容可参考:https://docs.npmjs.com/cli/v10/commands/npm-init

规划项目结构

在一个 Electron 项目中,如果按照核心功能来划分的话大概会有这几个大块:主进程,渲染进程,preload。主进程和渲染进程公用的模块,主进程主要是一些和底层以及 electron 一些主进程的 api 内容构成的;渲染进程主要是跟常规 Web 应用相关的内容;preload 是一个特殊的模块,主要是为了将 Electron 的不同类型的进程桥接在一起;主进程和渲染进程公用的模块,这个就是主进程和渲染进程公用的一些源码。

上面说的只是按照功能层面去划分的核心目录,当然在正常的开发中肯定还必不可少一些基础的文件目录,比如一些脚本文件,环境配置,静态资源,以及常规的配置文件。下面是我们整个项目的结构目录。

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 初始化

yarn add typescript -D
npx tsc --init

初始化成功之后会有 tsconfig.json 文件。

更多可参考:https://www.typescriptlang.org/download/

eslint 初始化

npm init @eslint/config@latest

初始化成功之后会有 eslint.config.mjs 文件。

更多可参考:https://eslint.org/docs/latest/use/getting-started

prettier 初始化

yarn add --dev --exact prettier
node --eval "fs.writeFileSync('.prettierrc','{}\n')"

初始化成功之后会有 .prettierrc 文件。

更多可参考:https://prettier.io/docs/en/install.html

安装vite,electron,react

yarn add vite electron react react-dom -D

设置项目配置

通过上面的依赖安装我们基本上就可以开启一个最基础的 electron 项目了,但是在这之前我们需要做一些项目的常规配置,比如 ts 的设置,prettier 规则的设置,git 忽略文件,nvm 配置等。

TS 配置

对于 TS 配置,我们可以先用他默认生成的配置,如果后期有需求,可以对相应的配置做改动或者放开相应的注释

ESlint 配置

ESlint 的配置我们也先用默认生成的配置。

prettier 设置

这里先用个常用的配置,如果后期有需求再改动。

{
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文件

node_modules
*.log
*.log*
build
dist
.DS_Store
.idea

nvm 配置

.nvmrc 文件,方便进去项目直接切换对应的 Node 版本。

v20.16.0

这个可以使用 VSCODE 的插件 vsc-nvm,具体可参考 https://marketplace.visualstudio.com/items?itemName=henrynguyen5-vsc.vsc-nvm

npm 源配置

主要设置下载源,为后期更加快速地添加其他的依赖以及加快 Electron 的下载。

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

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"),
    minifyfalse,
    lib: {
      entry: path.resolve(__dirname, "../src/main/index.ts"),
      formats: ["cjs"],
    },
    rollupOptions: {
      external: [
        "electron",
        ...builtinModules,
      ],
      output: {
        entryFileNames"[name].cjs",
      },
    },
    emptyOutDirtrue,
    chunkSizeWarningLimit2048,
  },
})

这个是主进程的配置,配置好入口文件和输出文件即可,注意 external 选项。

vite/render.js

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"),
    minifytrue,
    assetsInlineLimit1048576,
    emptyOutDirtrue,
    chunkSizeWarningLimit2048,
    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。我们需要去添加一下

yarn add @vitejs/plugin-react-swc @ant-design/icons-svg antd -D

vite/preload.js

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"),
    minifyfalse,
    lib: {
      entry: path.resolve(__dirname, "../src/preload/index.ts"),
      formats: ["cjs"],
    },
    rollupOptions: {
      external: ["electron", ...builtinModules],
      output: {
        entryFileNames"[name].cjs",
      }
    },
    emptyOutDirtrue,
    chunkSizeWarningLimit2048,
  },
});


这个前置脚本的配置就是简单的借助 Vite 进行编译的操作。

vite/service.js

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 = {
      configFilefalse,
      ...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 = {
      configFilefalse,
      ...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 = {
      configFilefalse,
      ...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 .命令,这样可以更加连贯的实现一键启动各个开发进程的目的,融合渲染进程和主进程的启动。具体可参考:https://www.electronjs.org/zh/docs/latest/tutorial/quick-start#%E8%BF%90%E8%A1%8C%E4%B8%BB%E8%BF%9B%E7%A8%8B

编写最简单的代码

通过上面的步骤,我们现在需要去补充一些入口文件,创建 src 目录,然后再在下面创建 mainrenderpreload 三个目录。

src/main/index.ts

主进程代码编写,实现一个打开窗口的方法。

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 以及其他的适配,主要改动如下:

{
  "include":["src"],
  "compilerOptions":{
    "target""ESNext",
    "module""ESNext",                                
    "moduleResolution""Node",  
    "baseUrl""./",
    "paths": {
      "@/*": ["./src/*"]
    },  
  }
}

src/render/index.tsx & app.tsx

实现最简单的 React 应用。

app.tsx

import React from "react"

const App = () => {
return <div>Hello Vite + Electron</div>
}

export default App

index.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

<!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 方法。

import { contextBridge } from "electron";

contextBridge.exposeInMainWorld("EP", {
  log: (data:string) => {console.log(data)}
})

编写启动脚本

我们把上面的代码内容填充好了之后就可以开始编写启动脚本,然后在开发模式下启动整个应用了。

scripts/dev.js

#!/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 模式处理。

"main""dist/main/index.cjs",
"type""module",
"scripts": {
  "dev""node ./scripts/dev.js"
},

经过上面的步骤一个最简单的开发环境就搭建起来了,不出意外会出现如下情况:

配置调试环境

VSCODE 调试配置

Electron 项目相当于一个 node.js 项目,我们可以在 VSCode 里面做断点调试,方便我们开发。具体做法如下:

在工程根目录下创建 .vscode 子目录,并在这个子目录下新建一个名为 launch.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 打上断点就可以调试了,试图如下:

渲染进程调试

渲染进程的调试其实跟我们平常开发前端页面的调试是一样的,只需要在创建窗口的时候对devTools这个选项进行设置。如果设置为 false, 则无法使用 BrowserWindow.webContents.openDevTools () 打开 DevTools,默认值为 true。具体可参考:https://www.electronjs.org/docs/latest/api/web-contents#contentsopendevtoolsoptions

mainWindow.webContents.openDevTools({ mode: "detach", activate: true, });

主进程调试

虽然 VSCode  给我了我们调试代码的能力,但是有的时候我们还是需要像使用 Chrome 内置的调试工具调试Electron 主进程的代码,然后去看一些内存的情况以及性能指标,这个时候我们就需要进行额外的一个小小的配置啦,在 vite\service.js 中的 createMainServer 的方法中

spawnProcess = spawn(String(electronPath), ['--inspect','.']);

加上 --inspect 后执行 npm run dev 即可开始调试,然后在 chrome 中进入 chrome://inspect/#devices 这个地址,点击 Open dedicated DevTools for Node 即可看到如下界面,这样就可以开始愉快的调试主进程啦!

结语

通过上面一系列的操作,我们终于搭建出了一个完整的 Electron 开发环境,核心思路是通过 Vite 构建几个开发服务,然后再联动起来,实现了高效的开发流程和自动重载功能,这为我们在开发和调试过程中提供了极大的便利和效率提升。

源码

https://github.com/Xutaotaotao/electron-proplay/tree/feature-init

开源项目

一站式Electron开发解决方案Electron-Prokit

https://github.com/Xutaotaotao/electron-prokit

最后

大家好,我是芝士,最近创建了一个低代码/前端工程化/高级前端面试交流群,欢迎加我微信,拉你进群,互相监督学习进步等!



关注福利,关注公众号后,在首页:

  • 回复「简历获取精选的简历模板

  • 回复「思维图」获取完整 JavaScript 相关思维图

  • 回复「电子书」可下载我整理的大量前端资源,包含面试、Vue实战项目、CSS和JavaScript电子书等。

  • 回复「Node」获取简历制作建议

最后不要忘了点个赞再走噢

前端界
高质量文章分享、实践干货、技术前沿、学习资料, 你感兴趣的都在前端界
 最新文章