Skip to content

electron-vite 快速开始

安装 VSCode

官网:https://code.visualstudio.com/download

1. 选择 system installer 版本下载

alt text

2. 安装勾选 VSCode 环境变量

alt text

3. 安装插件

  • Vue - Official
  • Prettier - Code formatter
  • ESLint
  • GitLens — Git supercharged
  • Error Lens
  • Auto Import
  • Auto Rename Tag

环境配置

1. 安装 Node.js

点击下载 https://nodejs.org/dist/20.18.1/node-v20.18.1-x64.msi

powershell
PS D:\project\pc.hifi> node -v
v20.18.1

经验提示

建议直接覆盖安装的方式更新 Node.js 版本而不是使用 nvm 管理工具切换 Node.js 版本

alt text

2. 检查电脑策略是否为 RemoteSigned

打开 PowerShell,输入 Get-ExecutionPolicy 看是否输出 RemoteSigned,如果是 Restricted 则需要继续往下看,需要设置为 RemoteSigned

powershell
Get-ExecutionPolicy

超级管理员方式打开 PowerShell,执行策略设置为 RemoteSigned

powershell
Set-ExecutionPolicy RemoteSigned

3. 全局配置 npm 镜像加速

powershell
npm config set registry https://registry.npmmirror.com
powershell
PS C:\Users\Administrator\Desktop>  npm config get registry
https://registry.npmmirror.com
PS C:\Users\Administrator\Desktop>

经验提示

electron 项目建议使用 npm 而不是 yarn 或者 pnpm。

electron-vite 初始化项目

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

快速开始:https://cn.electron-vite.org/guide/#搭建第一个-electron-vite-项目

1. npm create 创建项目

bash
npm create @quick-start/electron@latest

2. 安装项目依赖

bash
npm i
npm 查看可以升级的包

1. 查看可以升级的包

要查看可以升级的包,您可以使用以下命令:

bash
npm outdated

该命令会列出当前项目中所有已安装的包,以及它们的当前版本、所需的版本(根据 package.json 中的 dependenciesdevDependencies)和最新版本。如果某个包有可用的更新,它会显示出来。

输出示例:

bash
Package        Current  Wanted  Latest  Location
express         4.17.1   4.18.0  5.0.0  my-project
lodash          4.17.19  4.17.21 4.17.21 my-project
  • Current: 当前安装的版本。
  • Wanted: 根据 package.json 文件中规定的版本范围,npm 认为的期望版本。
  • Latest: 当前 npm 仓库中该包的最新版本。

2. 升级所有可升级的包

要将所有的包更新到最新版本,可以使用:

bash
npm update

这条命令会根据 package.json 文件中的版本范围(如 ^~)来更新包。如果你希望强制升级到最新的版本,不管 package.json 中的版本范围,下面有几个方法。

如果你想升级某个包,可以使用:

bash
npm update <package-name>
2.1. 手动升级 package.json

如果你希望升级到最新的版本,确保你的 package.json 中的版本范围是开放的(例如使用 ^latest),然后运行:

bash
npm install

这会按照 package.json 的版本要求安装所有依赖项的最新版本。

2.2. 使用 npx npm-check-updates (ncu)

如果你希望强制升级所有包并自动更新 package.json 中的版本,可以使用 npm-check-updates(简称 ncu)工具:

  1. 安装 npm-check-updates 工具:

    bash
    npm install -g npm-check-updates
  2. 运行 ncu 来查看可以升级的包:

    bash
    ncu
  3. 使用 ncu -u 来自动更新 package.json 中的版本号:

    bash
    ncu -u
  4. 最后,重新安装所有依赖:

    bash
    npm install

这样,所有依赖包将被升级到最新版本,package.json 会自动更新版本号,并安装这些更新的依赖。

总结:

  • npm outdated 查看可以升级的包。
  • npm update 升级符合版本范围的依赖。
  • npx npm-check-updates 强制升级所有包并更新 package.json

3. 启动项目

bash
npm run dev

4. 修改 electron-builder 配置文件

electron-builder.yml 改为 electron-builder.config.js

js
module.exports = {
  appId: "com.electron.app",
  productName: "electron-vite-start",
  directories: {
    buildResources: "build",
  },
  files: [
    "!**/.vscode/*",
    "!src/*",
    "!electron.vite.config.{js,ts,mjs,cjs}",
    "!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}",
    "!{.env,.env.*,.npmrc,pnpm-lock.yaml}",
  ],
  asarUnpack: ["resources/**"],
  win: {
    executableName: "electron-vite-start",
  },
  nsis: {
    artifactName: "${name}-${version}-setup.${ext}",
    shortcutName: "${productName}",
    uninstallDisplayName: "${productName}",
    createDesktopShortcut: "always",
  },
  mac: {
    entitlementsInherit: "build/entitlements.mac.plist",
    extendInfo: {
      NSCameraUsageDescription:
        "Application requests access to the device's camera.",
      NSMicrophoneUsageDescription:
        "Application requests access to the device's microphone.",
      NSDocumentsFolderUsageDescription:
        "Application requests access to the user's Documents folder.",
      NSDownloadsFolderUsageDescription:
        "Application requests access to the user's Downloads folder.",
    },
    notarize: false,
  },
  dmg: {
    artifactName: "${name}-${version}.${ext}",
  },
  linux: {
    target: ["AppImage", "snap", "deb"],
    maintainer: "electronjs.org",
    category: "Utility",
  },
  appImage: {
    artifactName: "${name}-${version}.${ext}",
  },
  npmRebuild: false,
  publish: {
    provider: "generic",
    url: "https://example.com/auto-updates",
  },
  electronDownload: {
    mirror: "https://npmmirror.com/mirrors/electron/",
  },
};

package.json 指定 electron-builder --config=electron-builder.config.js 文件打包

json
{
  "scripts": {
    "format": "prettier --write .",
    "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
    "start": "electron-vite preview",
    "dev": "electron-vite dev",
    "build": "electron-vite build",
    "postinstall": "electron-builder install-app-deps",
    "build:unpack": "npm run build && electron-builder --dir", 
    "build:win": "npm run build && electron-builder --win", 
    "build:mac": "npm run build && electron-builder --mac", 
    "build:linux": "npm run build && electron-builder --linux"
    "build:unpack": "npm run build && electron-builder --config=electron-builder.config.js --dir", 
    "build:win": "npm run build && electron-builder --config=electron-builder.config.js --win", 
    "build:mac": "npm run build && electron-builder --config=electron-builder.config.js --mac", 
    "build:linux": "npm run build && electron-builder --config=electron-builder.config.js --linux"
  },
}

INFO

指定配置文件 https://www.electron.build/cli

在使用 electron-builder 时,你可以通过指定配置文件的方式来使用不同的配置。默认情况下,electron-builder 会寻找项目根目录下的 electron-builder.yml, electron-builder.json, 或 electron-builder.config.js 文件作为配置文件。

如果你希望显式指定配置文件,你可以在命令行中使用 --config 选项。例如,假设你将配置文件保存为 build-config.js,你可以在命令行中这样指定:

bash
electron-builder --config build-config.js

这种方式允许你在构建项目时灵活选择和使用不同的配置文件。你可以将配置文件以 YAML、JSON 或 JavaScript 的格式保存,只需确保通过 --config 选项指定正确的路径和文件名即可。

打包构建

powershell
npm run build:win

如何解压 electron 中的 asar 文件

powershell
asar extract app.asar app_extracted

electron-builder.config.js 修改后:

js
const isWin32 = process.platform === "win32";

module.exports = {
  appId: "com.electron.app",
  productName: "electron-vite-start",
  directories: {
    buildResources: "build",
  },
  files: [
    "!**/.vscode/*",
    "!src/*",
    "!electron.vite.config.{js,ts,mjs,cjs}",
    "!{electron.vite.config.mjs,electron-builder.config.js}",
    "!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}",
    "!{.env,.env.*,.npmrc,pnpm-lock.yaml}",
  ],
  asarUnpack: ["resources/**"],
  electronLanguages: isWin32
    ? ["en", "zh-TW", "zh-CN", "en-US", "en-GB"]
    : ["en", "zh_TW", "zh_CN", "en_US", "en_GB"],
  win: {
    icon: "build/icon.ico",
    target: [{ target: "nsis", arch: ["x64"] }],
    executableName: "electron-vite-start",
  },
  nsis: {
    artifactName: "${name}-${version}-setup.${ext}",
    artifactName: "${name}-${os}-${arch}-${version}-setup.${ext}",
    shortcutName: "${productName}",
    uninstallDisplayName: "${productName}",
    createDesktopShortcut: "always",
    oneClick: false, // 设置为 false 以提供安装类型选择界面,允许用户选择是否创建桌面图标,允许用户选择安装路径
    perMachine: true, // 设置为 true 将使安装程序默认为所有用户安装应用,这需要管理员权限
    allowToChangeInstallationDirectory: true, // 如果设置为 true,安装程序将允许用户更改安装目录
    allowElevation: true, // 一般情况下,此字段不会被直接使用,权限提升主要依赖于 perMachine 的设定。当perMachine为true,安装程序会请求管理员权限
    deleteAppDataOnUninstall: true, // 如果设置为 true,卸载程序将删除AppData中的所有程序数据
    createStartMenuShortcut: true, // 如果设置为 true,安装程序将在开始菜单中创建程序快捷方式
  },
  mac: {
    type: "development",
    icon: "build/icon.icns",
    identity: null,
    hardenedRuntime: false,
    target: [
      {
        target: "dmg", // dmg、pkg、mas、mas-dev
        arch: ["universal"], // 'x64', 'arm64', 'universal'
      },
    ],
    // // #region
    // // fix: that's the same in both x64 and arm64 builds and not covered by the x64ArchFiles rule: "undefined"
    // mergeASARs: false,
    // singleArchFiles: "*",
    // x64ArchFiles: "*",
    // // #endregion

    entitlementsInherit: "build/entitlements.mac.plist",
    extendInfo: {
      NSCameraUsageDescription:
        "Application requests access to the device's camera.",
      NSMicrophoneUsageDescription:
        "Application requests access to the device's microphone.",
      NSDocumentsFolderUsageDescription:
        "Application requests access to the user's Documents folder.",
      NSDownloadsFolderUsageDescription:
        "Application requests access to the user's Downloads folder.",
    },
    notarize: false,
  },
  dmg: {
    artifactName: "${name}-${version}.${ext}",
    artifactName: "${name}-${os}-${arch}-${version}.${ext}",
  },
  linux: {
    target: ["AppImage", "snap", "deb"],
    maintainer: "electronjs.org",
    maintainer: "",
    category: "Utility",
  },
  appImage: {
    artifactName: "${name}-${version}.${ext}",
    artifactName: "${name}-${os}-${arch}-${version}.${ext}",
  },
  npmRebuild: false,
  publish: {
    provider: "generic",
    url: "https://example.com/auto-updates",
    url: "",
  },
  electronDownload: {
    mirror: "https://npmmirror.com/mirrors/electron/",
  },
};

打包构建

powershell
npm run build:unpack
powershell
asar extract app.asar app_extracted

5. .prettierrc.yaml 改成 .prettierrc.js

.prettierrc.js 内容如下

js
module.exports = {
  // 使用单引号代替双引号
  singleQuote: true,
  // 在每行代码末尾添加分号
  semi: true,
  // 设置代码的最大打印宽度为140个字符
  printWidth: 140,
  // 在多行对象或数组的最后一项后面添加逗号
  trailingComma: "all",
  // 在对象大括号内不添加空格
  bracketSpacing: false,
};

electron-builder.config.js

js
module.exports = {
  appId: "com.electron.app",
  productName: "electron-vite-start",
  directories: {
    buildResources: "build",
  },
  files: [
    "!**/.vscode/*",
    "!src/*",
    "!{electron.vite.config.mjs,electron-builder.config.js}",
    "!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}", 
    "!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.js,dev-app-update.yml,CHANGELOG.md,README.md}", 
    "!{.env,.env.*,.npmrc,pnpm-lock.yaml}",
  ],
  // ...
};

6. .eslintrc.cjs 修改成如下:

要让 ESLint 支持 Prettier 的配置,你需要将 Prettier 的规则集集成到 ESLint 中。以下是更新后的 ESLint 配置,包含对 Prettier 配置的支持:

配合 VSCode 的 prettier 插件,yyds

javascript
module.exports = {
  extends: [
    "eslint:recommended", // 使用 ESLint 推荐的规则
    "plugin:vue/vue3-recommended", // 使用 Vue 3 推荐的规则
    "@electron-toolkit", // 使用 Electron 项目推荐的 ESLint 配置
    "@vue/eslint-config-prettier", // 使用 Vue 项目与 Prettier 兼容的 ESLint 配置
    "prettier", // 使用 Prettier 的 ESLint 配置来保证代码格式一致
  ],
  plugins: ["prettier"], // 启用 Prettier 插件来处理代码格式问题
  rules: {
    "vue/require-default-prop": "off", // 关闭 Vue 对于 props 默认值的要求
    "vue/multi-word-component-names": "off", // 关闭 Vue 对组件命名必须包含多个单词的规则
    "vue/attribute-hyphenation": "off", // 允许在模板中使用大写字母作为属性名,而不需要强制转换成小写字母
    "vue/v-on-event-hyphenation": "off", // 允许在模板中使用大写字母作为事件绑定的属性名
    // 配置 Prettier 的相关选项,确保代码格式符合预期
    "prettier/prettier": [
      "error", // 如果格式不符合 Prettier 规则则报错
      {
        singleQuote: true, // 使用单引号代替双引号
        semi: true, // 语句末尾加分号
        printWidth: 120, // 每行代码最大长度为 120 个字符
        trailingComma: "all", // 在可能的地方(对象、数组等)添加尾逗号
        bracketSpacing: false, // 对象字面量中的括号不加空格(例如:{a: 1})
      },
    ],
  },
};

主要更改包括:

  • extends 中添加 'prettier' 以确保 ESLint 配置支持并遵循 Prettier 的规则。
  • prettier 配置中添加自定义规则以确保代码风格一致。

这个配置文件确保 ESLint 和 Prettier 的规则不会冲突,并且可以保持代码风格的一致性。

7. 格式化代码

powershell
npm run format
powershell
PS C:\Users\Administrator\Desktop\pc-app> npm run format

> pc-app@1.0.0 format
> prettier --write .

.eslintrc.cjs 30ms (unchanged)
.prettierrc.js 1ms (unchanged)
.vscode/extensions.json 1ms (unchanged)
.vscode/launch.json 3ms (unchanged)
.vscode/settings.json 1ms (unchanged)
electron-builder.config.js 3ms (unchanged)
electron.vite.config.mjs 3ms (unchanged)
package-lock.json 53ms (unchanged)
package.json 1ms (unchanged)
README.md 16ms (unchanged)
src/main/index.js 10ms (unchanged)
src/preload/index.js 2ms (unchanged)
src/renderer/index.html 14ms (unchanged)
src/renderer/src/App.vue 7ms (unchanged)
src/renderer/src/assets/base.css 18ms (unchanged)
src/renderer/src/assets/main.css 8ms (unchanged)
src/renderer/src/components/Versions.vue 4ms (unchanged)
src/renderer/src/main.js 1ms (unchanged)
PS C:\Users\Administrator\Desktop\pc-app>

8. package.json 增加 "type": "commonjs"

json
{
  "name": "electron-vite-start",
  "version": "1.0.0",
  "description": "An Electron application with Vue",
  "main": "./out/main/index.js",
  "author": "example.com",
  "homepage": "https://electron-vite.org",
  "type": "commonjs", 
  "scripts": {
    // ...
  }
}

9. 打包 Windows 安装包

powershell
npm run build:win
powershell
PS C:\Users\Administrator\Desktop\pc-app> npm run build:win

> pc-app@1.0.0 build:win
> npm run build && electron-builder --win


> pc-app@1.0.0 build
> electron-vite build

vite v5.4.1 building SSR bundle for production...
2 modules transformed.
out/main/index.js  1.48 kB
✓ built in 42ms
vite v5.4.1 building SSR bundle for production...
1 modules transformed.
out/preload/index.js  0.42 kB
✓ built in 5ms
vite v5.4.1 building for production...
11 modules transformed.
../../out/renderer/index.html                      0.55 kB
../../out/renderer/assets/electron-DtwWEc_u.svg    5.82 kB
../../out/renderer/assets/index-D5G5Cj71.css       6.55 kB
../../out/renderer/assets/index-Be6tFnq3.js      162.31 kB
✓ built in 288ms
  • electron-builder  version=24.13.3 os=10.0.22631
  • writing effective config  file=dist\builder-effective-config.yaml
  • packaging       platform=win32 arch=x64 electron=31.4.0 appOutDir=dist\win-unpacked
  • downloading     url=https://npmmirror.com/mirrors/electron/31.4.0/electron-v31.4.0-win32-x64.zip size=111 MB parts=8
  • downloaded      url=https://npmmirror.com/mirrors/electron/31.4.0/electron-v31.4.0-win32-x64.zip duration=9.824s
  • building        target=nsis file=dist\pc-app Setup 1.0.0.exe archs=x64 oneClick=true perMachine=false
  • building block map  blockMapFile=dist\pc-app Setup 1.0.0.exe.blockmap

electron-vite 多窗口

实现效果如下:

alt text

1. 修改 electron.vite.config.mjs

electron.vite.config.mjs 文件修改点如下

js
import { resolve } from "path";
import { defineConfig, externalizeDepsPlugin } from "electron-vite";
import vue from "@vitejs/plugin-vue";

export default defineConfig({
  main: {
    plugins: [externalizeDepsPlugin()],
  },
  preload: {
    build: {
      rollupOptions: {
        input: {
          index: resolve(__dirname, "src/preload/index.js"),
          about: resolve(__dirname, "src/preload/about.js"),
        },
      },
    },
    plugins: [externalizeDepsPlugin()],
  },
  renderer: {
    build: {
      rollupOptions: {
        input: {
          index: resolve(__dirname, "src/renderer/index.html"),
          about: resolve(__dirname, "src/renderer/about.html"),
        },
      },
    },
    resolve: {
      alias: {
        "@renderer": resolve("src/renderer/src"),
      },
    },
    plugins: [vue()],
  },
});

2. src/renderer/修改如下

1、增加 about.html 文件

src/renderer/about.html
src/renderer/about/main.js
src/renderer/about/App.vue
html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>About</title>
    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
    <meta
      http-equiv="Content-Security-Policy"
      content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
    />
  </head>

  <body>
    <div id="app"></div>
    <script type="module" src="./about/main.js"></script>
  </body>
</html>

2、修改 src/renderer/index.html 文件

src/renderer/src/ 重命名为 src/renderer/index/

src/renderer/index.html
src/renderer/index/main.js
src/renderer/index/App.vue
html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Electron</title>
    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
    <meta
      http-equiv="Content-Security-Policy"
      content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
    />
  </head>

  <body>
    <div id="app"></div>
    <script type="module" src="./index/main.js"></script>
  </body>
</html>

3. 增加 src/preload/about.js

js
import { contextBridge } from "electron";
import { electronAPI } from "@electron-toolkit/preload";

// Custom APIs for renderer
const api = {};

// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
// just add to the DOM global.
if (process.contextIsolated) {
  try {
    contextBridge.exposeInMainWorld("electron", electronAPI);
    contextBridge.exposeInMainWorld("api", api);
  } catch (error) {
    console.error(error);
  }
} else {
  window.electron = electronAPI;
  window.api = api;
}

4. src/main/index.js 增加 createAboutWindow 函数

Details
js
import { app, shell, BrowserWindow, ipcMain } from "electron";
import { join } from "path";
import { electronApp, optimizer, is } from "@electron-toolkit/utils";
import icon from "../../resources/icon.png?asset";

function createWindow() {
  // Create the browser window.
  const mainWindow = new BrowserWindow({
    width: 900,
    height: 670,
    show: false,
    autoHideMenuBar: true,
    ...(process.platform === "linux" ? { icon } : {}),
    webPreferences: {
      preload: join(__dirname, "../preload/index.js"),
      sandbox: false,
    },
  });

  mainWindow.on("ready-to-show", () => {
    mainWindow.show();
  });

  mainWindow.webContents.setWindowOpenHandler((details) => {
    shell.openExternal(details.url);
    return { action: "deny" };
  });

  // HMR for renderer base on electron-vite cli.
  // Load the remote URL for development or the local html file for production.
  if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
    console.log(
      "process.env['ELECTRON_RENDERER_URL']",
      process.env["ELECTRON_RENDERER_URL"] + "/index.html"
    );
    mainWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]);
  } else {
    mainWindow.loadFile(join(__dirname, "../renderer/index.html"));
  }
}
function createAboutWindow() {
  // Create the browser window.
  const mainWindow = new BrowserWindow({
    width: 312,
    height: 422,
    show: false,
    autoHideMenuBar: true,
    ...(process.platform === "linux" ? { icon } : {}),
    webPreferences: {
      preload: join(__dirname, "../preload/about.js"),
      sandbox: false,
    },
  });

  mainWindow.on("ready-to-show", () => {
    mainWindow.show();
  });

  mainWindow.webContents.setWindowOpenHandler((details) => {
    shell.openExternal(details.url);
    return { action: "deny" };
  });

  // HMR for renderer base on electron-vite cli.
  // Load the remote URL for development or the local html file for production.
  if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
    console.log(
      "process.env['ELECTRON_RENDERER_URL']",
      process.env["ELECTRON_RENDERER_URL"] + "/about.html"
    );
    mainWindow.loadURL(process.env["ELECTRON_RENDERER_URL"] + "/about.html");
  } else {
    mainWindow.loadFile(join(__dirname, "../renderer/about.html"));
  }
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
  // Set app user model id for windows
  electronApp.setAppUserModelId("com.electron");

  // Default open or close DevTools by F12 in development
  // and ignore CommandOrControl + R in production.
  // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
  app.on("browser-window-created", (_, window) => {
    optimizer.watchWindowShortcuts(window);
  });

  // IPC test
  ipcMain.on("ping", () => console.log("pong"));

  createWindow();

  createAboutWindow();

  app.on("activate", function () {
    // On macOS it's common to re-create a window in the app when the
    // dock icon is clicked and there are no other windows open.
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
});

// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on("window-all-closed", () => {
  if (process.platform !== "darwin") {
    app.quit();
  }
});

// In this file you can include the rest of your app"s specific main process
// code. You can also put them in separate files and require them here.

electron-vite 自定义 titlebar

1. 修改窗口为无边框

src/index/main.js

js
function createWindow() {
  // Create the browser window.
  const mainWindow = new BrowserWindow({
    width: 900,
    height: 670,
    show: false,
    frame: false,
    autoHideMenuBar: true,
    ...(process.platform === "linux" ? { icon } : {}),
    webPreferences: {
      preload: join(__dirname, "../preload/index.js"),
      sandbox: false,
    },
  });
  // ...
}

2. 创建 titlebar 处理

提示:

特别注意这里:https://www.electronjs.org/zh/docs/latest/tutorial/window-customization#window-controls-overlay

VSCode 的最小化、缩放、最大化按钮是通过 titleBarStyletitleBarOverlay 设置的

js
const { BrowserWindow } = require("electron");
const win = new BrowserWindow({
  titleBarStyle: "hidden",
  titleBarOverlay: {
    color: "#2f3241",
    symbolColor: "#74b1be",
    height: 60,
  },
});

titleBarOverlay 被设置后,只显示 最小化, 最大化/还原,关闭窗口的按钮

(1)、src/main/titlebar.js

Details
js
import { app, ipcMain, BrowserWindow } from "electron";
app.whenReady().then(() => {
  ipcMain.on("minimize", (event) => {
    const window = BrowserWindow.fromWebContents(event.sender);
    window.minimize();
  });

  ipcMain.on("maximize", (event) => {
    const window = BrowserWindow.fromWebContents(event.sender);
    if (window.isMaximized()) {
      window.unmaximize();
    } else {
      window.maximize();
    }
  });

  ipcMain.on("close", (event) => {
    const window = BrowserWindow.fromWebContents(event.sender);
    window.close();

    // 检查如果所有窗口都关闭了,退出应用
    //   if (BrowserWindow.getAllWindows().length === 0) {
    //     app.quit();
    //   }
  });
});

(2)、src/main/index.js 引入

js
import { app, shell, BrowserWindow, ipcMain } from "electron";
import { join } from "path";
import { electronApp, optimizer, is } from "@electron-toolkit/utils";
import icon from "../../resources/icon.png?asset";
import "./titlebar";

3. src/preload/index.js

js
import { ipcRenderer, contextBridge } from "electron";
import { electronAPI } from "@electron-toolkit/preload";

// Custom APIs for renderer
const api = {
  minimize: () => ipcRenderer.send("minimize"),
  maximize: () => ipcRenderer.send("maximize"),
  close: () => ipcRenderer.send("close"),
};

// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
// just add to the DOM global.
if (process.contextIsolated) {
  try {
    contextBridge.exposeInMainWorld("electron", electronAPI);
    contextBridge.exposeInMainWorld("api", api);
  } catch (error) {
    console.error(error);
  }
} else {
  window.electron = electronAPI;
  window.api = api;
}

4.编写 src/renderer/index/components/Titlebar.vue 组件

Details
vue
<template>
  <div class="titlebar">
    <div class="title"></div>
    <div class="controls">
      <button @click="minimize">-</button>
      <button @click="maximize">□</button>
      <button @click="close" class="close-btn">×</button>
    </div>
  </div>
</template>

<script setup>
function minimize() {
  window.api.minimize();
}

function maximize() {
  window.api.maximize();
}

function close() {
  window.api.close();
}
</script>

<style scoped>
.titlebar {
  -webkit-app-region: drag;
  display: flex;
  justify-content: space-between;
  align-items: center;
  background-color: #eff4f9;
  height: 30px;
}

.title {
  font-size: 14px;
  font-weight: bold;
}

.controls {
  -webkit-app-region: no-drag;
  display: flex;
}

.controls button {
  width: 50px;
  box-sizing: border-box;
  height: 30px;
  border: none;
  background: transparent;
  font-size: 20px;
  cursor: pointer;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all ease 0.1s;
  outline: none;
}

.controls button:hover {
  background-color: #e1e1e1;
}
.controls button.close-btn:hover {
  background-color: #c42b1c;
  color: #fff;
}
</style>

5. src/renderer/index/App.vue 引入 Titlebar.vue

Details
vue
<template>
  <div>
    <Titlebar />
  </div>
</template>

<script setup>
import Titlebar from "./components/Titlebar.vue";
</script>

<style scoped></style>

electron-vite 添加 @electron/remote 模块

⚠️ 警告!

此模块有许多微妙的陷阱。几乎总有比使用此模块更好的方法来完成你的任务。例如,ipcRenderer.invoke 可以满足许多常见的用例。

具体 @electron/remote 模块的使用条件和方法请查阅 https://www.npmjs.com/package/@electron/remote

以下仅是 electron-vite 中的使用方法

1. 安装@electron/remote模块

bash
npm i @electron/remote --registry=https://registry.npmmirror.com

2. src/main/index.js 添加以下高亮行代码修改

js
import { app, shell, BrowserWindow, ipcMain, session } from "electron";
import { join } from "path";
import { electronApp, optimizer, is } from "@electron-toolkit/utils";
import icon from "../../resources/icon.png?asset";
import "./titlebar";

require("@electron/remote/main").initialize();

function createWindow() {
  // Create the browser window.
  const mainWindow = new BrowserWindow({
    width: 900,
    height: 670,
    show: false,
    frame: false,
    autoHideMenuBar: true,
    ...(process.platform === "linux" ? { icon } : {}),
    webPreferences: {
      preload: join(__dirname, "../preload/index.js"),
      sandbox: false,
      webviewTag: true,
      webSecurity: false,

      nodeIntegration: true,
      contextIsolation: false,
    },
  });

  require("@electron/remote/main").enable(mainWindow.webContents);

  mainWindow.on("ready-to-show", () => {
    mainWindow.show();
  });

  mainWindow.webContents.setWindowOpenHandler((details) => {
    shell.openExternal(details.url);
    return { action: "deny" };
  });

  // HMR for renderer base on electron-vite cli.
  // Load the remote URL for development or the local html file for production.
  if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
    console.log(
      "process.env['ELECTRON_RENDERER_URL']",
      process.env["ELECTRON_RENDERER_URL"] + "/index.html"
    );
    mainWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]);
  } else {
    mainWindow.loadFile(join(__dirname, "../renderer/index.html"));
  }

  if (is.dev) {
    mainWindow.webContents.openDevTools();
  }
}

3. 渲染进程 @electron/remote 引入

src/main/preload/index.js 添加 require('@electron/remote');代码引入

js
import {ipcRenderer, contextBridge} from 'electron';
import {electronAPI} from '@electron-toolkit/preload';

require('@electron/remote');

// Custom APIs for renderer
const api = {
  minimize: () => ipcRenderer.send('minimize'),
  maximize: () => ipcRenderer.send('maximize'),
  close: () => ipcRenderer.send('close'),
  loadPlugin: () => ipcRenderer.invoke('load-plugin'),
  getPreloadJsPath: () => ipcRenderer.invoke('get-preload-js-path'),
  isDev: () => ipcRenderer.invoke('isDev'),
};

// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
// just add to the DOM global.
if (process.contextIsolated) {
  try {
    contextBridge.exposeInMainWorld('electron', electronAPI);
    contextBridge.exposeInMainWorld('api', api);
  } catch (error) {
    console.error(error);
  }
} else {
  window.electron = electronAPI;
  window.api = api;
}

electron-vite 生成图标 electron-icon-builder

https://www.npmjs.com/package/electron-icon-builder

安装图标转换模块

bash
npm install electron-icon-builder -D --registry=https://registry.npmmirror.com

pcakge.json 添加命令 electron-icon-builder

json
{
  "scripts": {
    "logo": "electron-icon-builder --input=./public/logo.png --output=./public/"
  }
}

转换生成图标

powershell
npm run logo

electron-vite 加载本地资源

electron 加载本地资源例子

electron-vite 源码保护

官方文档:https://cn.electron-vite.org/guide/source-code-protection

1. 如何配置

js
import { defineConfig, bytecodePlugin } from "electron-vite";

export default defineConfig({
  main: {
    plugins: [bytecodePlugin()],
  },
  preload: {
    plugins: [bytecodePlugin()],
  },
  renderer: {
    // ...
  },
});

提示

bytecodePlugin 仅适用于生产阶段构建并且只支持主进程和预加载脚本。 需要注意的是,预加载脚本需要禁用 sandbox 才能支持字节码,因为字节码是基于 Node 的 vm 模块实现。从 Electron 20 开始,渲染器默认会被沙箱化,所以如果你想使用字节码来保护预加载脚本,你需要设置 sandbox: false

2. 判断源码被保护?

1. 打包一个免安装包的

powershell
npm run build:unpack

alt text

2. 进入 dist\win-unpacked\resources\app.asar 所在的目录

alt text

3. 执行 asar extract app.asar app_extracted 解压 app.asar 文件

如何解压 electron 中的 asar 文件

powershell
asar extract app.asar app_extracted

alt text

4. 对比前后的代码-被保护前后的代码

(1)被保护的源码,是二进制的,一般无法被破解,看不到源码:

alt text

(2)未开启保护源码,相当于裸奔:

alt text

electron-vite 配置别名

1. electron-vite 配置别名

js
import { resolve } from "path"; 
import {
  defineConfig,
  bytecodePlugin,
  externalizeDepsPlugin,
} from "electron-vite";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";
import VueDevTools from "vite-plugin-vue-devtools";
import Components from "unplugin-vue-components/vite";
import { AntDesignVueResolver } from "unplugin-vue-components/resolvers";

export default defineConfig({
  main: {
    plugins: [externalizeDepsPlugin()],
  },
  preload: {
    build: {
      rollupOptions: {
        input: {
          index: resolve(__dirname, "src/preload/index.js"),
          about: resolve(__dirname, "src/preload/about.js"),
        },
      },
    },
    plugins: [externalizeDepsPlugin()],
  },
  renderer: {
    build: {
      sourcemap: true, // 启用 source map
      rollupOptions: {
        input: {
          index: resolve(__dirname, "src/renderer/index.html"),
          about: resolve(__dirname, "src/renderer/about.html"),
        },
      },
    },
    resolve: { 
      alias: { 
        "@": resolve("src"), 
        "@renderer": resolve("src/renderer"), 
        "@index": resolve("src/renderer/index"), 
      }, 
    }, 
    plugins: [
      vue(),
      vueJsx(),
      VueDevTools(),
      Components({
        resolvers: [
          AntDesignVueResolver({
            importStyle: false, // css in js
          }),
        ],
      }),
    ],
    // server: {
    //   proxy: {
    //     '/api/': {
    //       target: 'http://172.20.5.37', // 目标服务器地址
    //       changeOrigin: true,
    //     },
    //   },
    // },
  },
});

2. VSCode 按住 ctrl 键不放+点击鼠标左键 跳转到指定的文件

默认情况下,有如下配置别名的路径,如果想要快速访问 utils.js,一般会 ctrl+我们会鼠标左键点击会直接进入文件内部,但是如果没有任何配置是不会生效的

js
import { getNextId } from "@/renderer/index/utils/utils";

想要在 ctrl+鼠标左键进入文件,需要项目根目录增加 jsconfig.json 文件,内容如下:

json
{
  "compilerOptions": {
    "target": "es5",
    "module": "esnext",
    "baseUrl": "./",
    "moduleResolution": "node",
    "paths": {
      "@/*": ["src/*"],
      "@renderer/*": ["src/renderer/*"],
      "@index/*": ["src/renderer/index/*"]
    },
    "lib": ["esnext", "dom", "dom.iterable", "scripthost"],
    "allowJs": true
  }
}

electron-vite 减少包的体积

关键点,移除非必要的代码或者文件

  1. 移除不需要的语言包
  2. 移除多余的模块,非必要的模块不要
  3. 查看打包后的是否有多余的代码被打包进去了

electron-vite 如何引入 .node 插件

1. 静态引入

引入已存在的文件,文件不存在则会报错

2. 动态引入

可以条件引入对应的文件,文件可以不存在也不会报错,适用于 Windows/Mac/Linux 等多平台开发,有的有对应的安装包,有的没有对应的安装包

electron-vite js 代码压缩、js 代码混淆、html 代码压缩

1. 安装依赖

  1. Terser(压缩 JS 代码)
  2. rollup-plugin-javascript-obfuscator(对 JS 代码进行进一步混淆)
  3. vite-plugin-html(压缩 HTML 代码)
bash
npm install terser rollup-plugin-javascript-obfuscator vite-plugin-html -D

2. electron.vite.config.mjs 配置示例

!!!注意这些配置都是希望在打包后生效,然而直接配置会导致开发和生产环境都生效,故需要通过一定的条件判断是否为打包命令,才进行添加对应的配置

js
import {
  defineConfig,
  externalizeDepsPlugin,
  bytecodePlugin,
} from "electron-vite";
import vue from "@vitejs/plugin-vue";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";

// 1) 代码混淆插件(JS Obfuscation)
import obfuscator from "rollup-plugin-javascript-obfuscator";

// 2) HTML 压缩插件
import { createHtmlPlugin } from "vite-plugin-html";

import { resolve } from "path";

export default defineConfig(({ mode }) => {
  const isProd = mode === "production";
  let terserObj = {};
  if (isProd) {
    // 为什么这么配置?
    // 开发环境无需进行压缩等处理
    terserObj = {
      sourcemap: false, // 启用 source map
      minify: "terser", // 使用 terser 进行压缩
      terserOptions: {
        compress: {
          drop_console: true, // 去掉 console
          drop_debugger: true, // 去掉 debugger
        },
      },
    };
  }
  return {
    // 主进程配置
    main: {
      build: {
        ...terserObj,
        rollupOptions: {
          // 在 rollupOptions 下添加代码混淆插件
          plugins: [
            // 这里对主进程生成的 js 进一步混淆
            isProd &&
              obfuscator({
                compact: true,
                controlFlowFlattening: true,
                controlFlowFlatteningThreshold: 1,
                deadCodeInjection: true,
                deadCodeInjectionThreshold: 1,
              }),
          ].filter(Boolean),
        },
      },
      plugins: [
        isProd && bytecodePlugin(), // 将主进程代码编译成 V8 字节码(可选)
        externalizeDepsPlugin(), // electron-vite 内置插件
        nodeResolve({ preferBuiltins: true }),
        commonjs(),
      ].filter(Boolean),
    },

    // 预加载脚本配置 (preload)
    preload: {
      build: {
        ...terserObj,
        rollupOptions: {
          input: {
            index: resolve(__dirname, "src/preload/index.js"),
            about: resolve(__dirname, "src/preload/about.js"),
          },
          plugins: [
            isProd &&
              obfuscator({
                compact: true,
                controlFlowFlattening: true,
                controlFlowFlatteningThreshold: 1,
                deadCodeInjection: true,
                deadCodeInjectionThreshold: 1,
              }),
          ].filter(Boolean),
        },
      },
      plugins: [externalizeDepsPlugin(), isProd && bytecodePlugin()],
    },
    // 渲染进程配置 (renderer)
    renderer: {
      build: {
        ...terserObj,
        rollupOptions: {
          input: {
            index: resolve(__dirname, "src/renderer/index.html"),
            about: resolve(__dirname, "src/renderer/about.html"),
          },
          // 如果需要对渲染进程的 JS 进一步混淆,可同样添加
          plugins: [
            isProd &&
              obfuscator({
                compact: true,
                controlFlowFlattening: true,
                controlFlowFlatteningThreshold: 1,
                deadCodeInjection: true,
                deadCodeInjectionThreshold: 1,
              }),
          ].filter(Boolean),
        },
      },
      resolve: {
        alias: {
          "@renderer": resolve("src/renderer/src"),
        },
      },
      plugins: [
        vue(),
        /**
         * 使用 vite-plugin-html 对 HTML 进行压缩
         * 注意:只在生产环境下启用,不要在 dev 启用
         */
        isProd &&
          createHtmlPlugin({
            removeComments: true,
            collapseWhitespace: true,
            collapseBooleanAttributes: true,
            removeEmptyAttributes: true,
            minifyCSS: true, // 内联 CSS 也进行压缩
            minifyJS: true, // 内联 JS 也进行压缩
          }),
      ].filter(Boolean),
    },
  };
});

3. 功能说明 & 注意事项

  1. Terser(JS 压缩)

    • 使用 build.minify = 'terser' 可以让最终生成的 JS 被 Terser 压缩,配合 terserOptions 去除 console.logdebugger 等。
    • 如果你的项目更倾向于快速打包和较低强度的压缩,也可用内置的 esbuild,但想要更多可控的 JS 压缩/混淆选项,还是 terser 更灵活。
  2. rollup-plugin-javascript-obfuscator(JS 混淆)

    • rollupOptions.plugins 中使用,可以对打包出的 JS 代码进行进一步混淆,比如变量名、逻辑流扁平化等。
    • 优点是混淆程度高,反编译难度更大;缺点是打包后的体积会显著增大,也会拖慢代码运行速度。
    • 混淆常见于对源码安全性要求高,但也要权衡调试和运行效率的影响。
  3. vite-plugin-html(HTML 压缩)

    • 可以把打包输出的 HTML 进行压缩,比如移除注释、空格、内联 CSS/JS 压缩等。
    • 如果你有多入口 HTML,都能通过 rollupOptions.input 进行多入口打包,并统一由 vite-plugin-html 压缩。
  4. bytecodePlugin()(V8 字节码编译)

    • electron-vite 自带的 bytecodePlugin() 可以编译源代码为 V8 字节码,进一步增加反编译难度;注意它会让体积增大,并可能影响启动速度。
    • 你可以只对主进程、预加载脚本做字节码,也可以都做;使用时只需在对应 plugins 中加入即可。
  5. 在开发环境中调试

    • 通常只在生产build)环境下启用这些压缩/混淆插件。
    • 在开发模式下(npm run dev),保留源码的可读性和热重载效率通常更重要,不必开启所有的混淆和压缩。
  6. 安全性

    • 混淆并不是绝对安全。如果攻击者可以直接访问你的打包文件,可以通过逆向工程获取逻辑。
    • Electron 应用如果对安全有强需求,最好结合系统的代码签名(Code Signing)、完整性校验(Integrity Checks) 等。

4. 小结

  • Terser:负责基础的 JS 压缩、去除调试语句、变量名简化等。
  • rollup-plugin-javascript-obfuscator:在 Terser 之上进一步做逻辑流扁平化、字符串加密等深度混淆。
  • vite-plugin-html:对 HTML 进行压缩(空格、注释、内联 CSS/JS),保证打包产物的体积更小、更不易阅读。

只要在 electron.vite.config.mjs 中合理组合上述插件,就可以在 electron-vite 中同时实现JS 压缩 + JS 混淆 + HTML 压缩,从而在一定程度上保护源码并提升产物性能。

electron-vite 环境变量配置

希望根据打包命令的不同,打包出不同的包

defineConfig 可以接受一个函数作为回调,第一个参数中,会包含 mode、command,一般会根据这个 mode 和 command 命令来区分执行的命令,从而做出返回不同的配置的目录

electron.vite.config.mjs

  1. 默认的配置
js
export default defineConfig({
  main: {
    plugins: [externalizeDepsPlugin(), bytecodePlugin()],
    resolve: {
      alias: {
        "@": resolve("src"),
      },
    },
  },
  // ...
});
  1. 根据环境变量以及 mode 进行的配置
js
export default defineConfig(({ mode, command }) => {
  return {
    main: {
      plugins: [externalizeDepsPlugin(), bytecodePlugin()],
      resolve: {
        alias: {
          "@": resolve("src"),
        },
      },
    },
    // ...
  };
});

electron-builder.config.js

这里可以在执行命令的时候添加环境变量,来区分执行了哪一个命令,比如 cross-env BUILD_MODE=production,然后在代码中通过 process.env.BUILD_MODE 读取,从而实现根据不同的变量来打包不同的结果,如果要读取.env.x 相关的配置文件,则还可以直接通过 dotenv 来解析对应的文件实现

package.json

json
{
  "scripts": {
    "build:prod": "electron-vite build --mode=production && cross-env BUILD_MODE=production electron-builder --config=electron-builder.config.js"
  }
}

electron-builder.config.js

js
module.exports = {
  appId: "com.your_app.name",
  productName: "your_app_name",
  directories: {
    buildResources: "build",
  },
  files: [
    "!**/.vscode/*",
    "!src/*",
    "!electron.vite.config.{js,ts,mjs,cjs}",
    "!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.js,dev-app-update.yml,CHANGELOG.md,README.md}",
    "!{jsconfig.json,*.md,email_upload_file.png,electron-builder.config.js}",
    "!{.env,.env.*,.npmrc,pnpm-lock.yaml}",
  ],
  artifactName: `your_app_name-\${os}-\${arch}-${process.env.BUILD_MODE}-\${version}.\${ext}`,
};