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 C:\Users\Administrator\Desktop> 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\electron-vite-start> 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\electron-vite-start>

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\electron-vite-start> 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 js 代码压缩、js 代码混淆、html 代码压缩

警告

官方文档:https://cn.electron-vite.org/guide/source-code-protection#v8-字节码的局限性

当启用最小化混淆(build.minify)时,字符串保护不起作用。这是因为字符串保护是基于字符代码实现的。然而,现代的压缩混淆工具(例如 esbuild 或 terser)会恢复转换后的字符代码,导致保护失败。 electron-vite 会发出警告。事实上,最小化混淆对于减小字节码大小的作用不大,因此建议在保护字符串时不要启用最小化混淆。

1. 安装依赖

  1. Terser(压缩 JS 代码)
  2. vite-plugin-html(压缩 HTML 代码)
bash
npm install terser vite-plugin-html -D

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

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";
import { createHtmlPlugin } from "vite-plugin-html";

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 语句
          // 更多的压缩选项可以加入,例如:
          reduce_vars: true, // 压缩变量
        },
        mangle: {
          toplevel: true, // 启用全局变量和函数名的混淆
          // 对所有变量和函数名进行混淆,包括局部作用域
          // 默认是混淆局部作用域的变量,如果你希望更多控制,额外可以配置:
          properties: false, // 是否混淆对象的属性名
        },
        output: {
          beautify: false, // 关闭美化输出
          comments: false, // 移除所有注释
        },
      },
    };
  }

  return {
    main: {
      build: {
        ...terserObj,
      },
      plugins: [externalizeDepsPlugin(), isProd && bytecodePlugin()].filter(
        Boolean
      ),
      resolve: {
        alias: {
          "@": resolve("src"),
        },
      },
    },
    preload: {
      build: {
        ...terserObj,
        rollupOptions: {
          input: {
            index: resolve(__dirname, "src/preload/index.js"),
            // about: resolve(__dirname, 'src/preload/about.js'),
          },
        },
      },
      plugins: [externalizeDepsPlugin(), isProd && bytecodePlugin()].filter(
        Boolean
      ),
      resolve: {
        alias: {
          "@": resolve("src"),
        },
      },
    },
    renderer: {
      build: {
        ...terserObj,
        rollupOptions: {
          input: {
            index: resolve(__dirname, "src/renderer/index.html"),
            // about: resolve(__dirname, 'src/renderer/about.html'),
          },
          output: !isProd
            ? {}
            : {
                // manualChunks(id) {
                //   // 文件路径 id
                //   // console.log(id);
                //   const chunkArray = ['dayjs', '@ant-design', 'vue', 'vue-router', 'echarts'];
                //   if (chunkArray.find((chunk) => id.includes(`node_modules/${chunk}/`))) {
                //     return id.toString().split('node_modules/')[1].split('/')[0].toString();
                //   }
                // },
                // chunkFileNames: (chunkInfo) => {
                //   const facadeModuleId = chunkInfo.facadeModuleId ? chunkInfo.facadeModuleId.split('/') : [];
                //   const fileName = facadeModuleId[facadeModuleId.length - 2] || '[name]';
                //   return `js/${fileName}/[name].[hash].js`;
                // },
              },
        },
      },
      resolve: {
        alias: {
          "@": resolve("src"),
          "@renderer": resolve("src/renderer"),
          "@index": resolve("src/renderer/index"),
        },
      },
      css: {
        // https://stackoverflow.com/questions/78997907/the-legacy-js-api-is-deprecated-and-will-be-removed-in-dart-sass-2-0-0
        preprocessorOptions: {
          scss: {
            api: "modern-compiler", // or "modern"
          },
        },
      },
      plugins: [
        vue(),
        vueJsx(),
        VueDevTools(),

        Components({
          resolvers: [
            AntDesignVueResolver({
              importStyle: false, // css in js
            }),
          ],
        }),
        isProd &&
          createHtmlPlugin({
            removeComments: true, // 删除 HTML 文件中的注释
            collapseWhitespace: true, // 折叠 HTML 中的空白字符(去除多余的换行和空格)
            collapseBooleanAttributes: true, // 折叠布尔属性(例如将 disabled="disabled" 转换为 disabled)
            removeEmptyAttributes: true, // 移除空属性(例如 class="" 或 style="")
            minifyCSS: true, // 压缩内联的 CSS 内容,减小 CSS 文件的大小
            minifyJS: true, // 压缩内联的 JavaScript 内容,减小 JS 文件的大小
          }),
      ].filter(Boolean),
    },
  };
});

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. 移除不需要的语言包(electron-builder electronLanguages) https://www.electron.build/app-builder-lib.interface.platformspecificbuildoptions#electronlanguages
  2. 渲染进程所有的包放到 devDependencies(收益较大),文档:https://cn.electron-vite.org/guide/build#外部依赖
  3. electron-builder 配置 files 移除必须包含的 node_modules 中多余的资源(典型例子:better-sqlite3) https://www.electron.build/app-builder-lib.interface.platformspecificbuildoptions#files
  4. 通过对 app.asar 解包查看是否又多余的 node_modules 模块以及文件 如何解压 electron 中的 asar 文件

electron-vite 如何引入 .node 插件

1. 静态引入

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

2. 动态引入

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

electron-vite 环境变量配置

官方文档:https://cn.electron-vite.org/config/#环境变量

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

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}`,
};

electron-vite 使用 better-sqlite

数据库可视化 SQLiteStudio 下载地址 https://github.com/pawelsalawa/sqlitestudio/releases

better-sqlite3 https://www.npmjs.com/package/better-sqlite3

1. 安装 better-sqlite3

powershell
npm i better-sqlite3 -S

2. 修改 package.json,增加 npx electron-rebuild -f,当安装的时候重新构建原生的包,注意这里better-sqlite3是放在dependencies中的

json
{
  "scripts": {
    "postinstall": "electron-builder install-app-deps", 
    "postinstall": "npx electron-rebuild -f", 
    "postinstall_backup": "electron-builder install-app-deps", 
    "rebuild-sqlite": "electron-rebuild -f -w better-sqlite3"
    // ...
  },
  "dependencies": {
    "@electron-toolkit/preload": "^3.0.1",
    "@electron-toolkit/utils": "^3.0.0",
    "better-sqlite3": "^11.8.1"
  }
  // ....
}

3. 修改 electron-builder.config.js,用于减少包的体积,过滤掉未使用的代码

js
module.exports = {
  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,.gitattributes}",
    "!{.env,.env.*,.npmrc,pnpm-lock.yaml}",
    "!**/better-sqlite3/{deps/**/*,src/**/*}", 
  ],
  // ...
};

4. electron.vite.config.mjs 添加 externalizeDepsPlugin() 插件(如果没有添加的话加上)

externalizeDepsPlugin 介绍:https://cn.electron-vite.org/guide/dev#dependencies-vs-devdependencies

完整的 electron.vite.config.mjs 配置,具体看高亮部分
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";
import { createHtmlPlugin } from "vite-plugin-html";

export default defineConfig(({ mode }) => {
  console.log("mode", 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 语句
          // 更多的压缩选项可以加入,例如:
          reduce_vars: true, // 压缩变量
        },
        mangle: {
          toplevel: true, // 启用全局变量和函数名的混淆
          // 对所有变量和函数名进行混淆,包括局部作用域
          // 默认是混淆局部作用域的变量,如果你希望更多控制,额外可以配置:
          properties: false, // 是否混淆对象的属性名
        },
        output: {
          beautify: false, // 关闭美化输出
          comments: false, // 移除所有注释
        },
      },
    };
  }

  return {
    main: {
      build: {
        ...terserObj,
      },
      plugins: [
        externalizeDepsPlugin(), 
        isProd && bytecodePlugin(),
      ].filter(Boolean),
      resolve: {
        alias: {
          "@": resolve("src"),
        },
      },
    },
    preload: {
      build: {
        ...terserObj,
        rollupOptions: {
          input: {
            index: resolve(__dirname, "src/preload/index.js"),
            // about: resolve(__dirname, 'src/preload/about.js'),
          },
        },
      },
      plugins: [
        externalizeDepsPlugin(), 
        isProd && bytecodePlugin(),
      ].filter(Boolean),
      resolve: {
        alias: {
          "@": resolve("src"),
        },
      },
    },
    renderer: {
      build: {
        ...terserObj,
        rollupOptions: {
          input: {
            index: resolve(__dirname, "src/renderer/index.html"),
            // about: resolve(__dirname, 'src/renderer/about.html'),
          },
          output: !isProd
            ? {}
            : {
                // manualChunks(id) {
                //   // 文件路径 id
                //   // console.log(id);
                //   const chunkArray = ['dayjs', '@ant-design', 'vue', 'vue-router', 'echarts'];
                //   if (chunkArray.find((chunk) => id.includes(`node_modules/${chunk}/`))) {
                //     return id.toString().split('node_modules/')[1].split('/')[0].toString();
                //   }
                // },
                // chunkFileNames: (chunkInfo) => {
                //   const facadeModuleId = chunkInfo.facadeModuleId ? chunkInfo.facadeModuleId.split('/') : [];
                //   const fileName = facadeModuleId[facadeModuleId.length - 2] || '[name]';
                //   return `js/${fileName}/[name].[hash].js`;
                // },
              },
        },
      },
      resolve: {
        alias: {
          "@": resolve("src"),
          "@renderer": resolve("src/renderer"),
          "@index": resolve("src/renderer/index"),
        },
      },
      css: {
        // https://stackoverflow.com/questions/78997907/the-legacy-js-api-is-deprecated-and-will-be-removed-in-dart-sass-2-0-0
        preprocessorOptions: {
          scss: {
            api: "modern-compiler", // or "modern"
          },
        },
      },
      plugins: [
        vue(),
        vueJsx(),
        VueDevTools(),

        Components({
          resolvers: [
            AntDesignVueResolver({
              importStyle: false, // css in js
            }),
          ],
        }),
        isProd &&
          createHtmlPlugin({
            removeComments: true, // 删除 HTML 文件中的注释
            collapseWhitespace: true, // 折叠 HTML 中的空白字符(去除多余的换行和空格)
            collapseBooleanAttributes: true, // 折叠布尔属性(例如将 disabled="disabled" 转换为 disabled)
            removeEmptyAttributes: true, // 移除空属性(例如 class="" 或 style="")
            minifyCSS: true, // 压缩内联的 CSS 内容,减小 CSS 文件的大小
            minifyJS: true, // 压缩内联的 JavaScript 内容,减小 JS 文件的大小
          }),
      ].filter(Boolean),
      // server: {
      //   proxy: {
      //     '/api/': {
      //       target: 'http://localhost:8080', // 目标服务器地址
      //       changeOrigin: true,
      //     },
      //   },
      // },
    },
  };
});

5. 数据库相关代码

database/index.js
js
import Database from "better-sqlite3"; // 用于操作 SQLite 数据库的库
import { app, ipcMain } from "electron"; // 用于 Electron 应用的全局功能
import path from "path"; // 用于处理和操作文件路径的模块
import fs from "fs";
let db; // 声明一个变量用来存储数据库实例

// 数据库版本
const DB_VERSION = 1; // 当前数据库版本

// 初始化数据库的函数
export function initDatabase() {
  // 判断当前环境是否是开发环境
  let databasePath = path.join(app.getPath("userData"), "database");

  // 确保数据库文件夹存在,如果不存在则创建它
  if (!fs.existsSync(databasePath)) {
    fs.mkdirSync(databasePath, { recursive: true });
  }

  // 初始化数据库并创建或打开指定路径的 SQLite 数据库文件
  db = new Database(path.join(databasePath, "uploadfile.db"), {
    verbose: console.log,
  });

  // 设置数据库的日志模式为 WAL(写时日志)模式,提高性能
  db.pragma("journal_mode = WAL");

  // 创建版本表
  createVersionTable();

  // 获取当前数据库版本
  const currentVersion = getCurrentDatabaseVersion();

  // 如果数据库版本不匹配,执行数据库更新
  if (currentVersion !== DB_VERSION) {
    updateDatabase(currentVersion);
  }

  // 创建表,如果表不存在则创建
  createTable();

  // 在 Electron 的主进程中注册一个 IPC 事件处理器
  ipcMain.handle("db_query", async (_, query, params) => {
    const stmt = db.prepare(query); // 准备 SQL 查询
    return stmt.all(...params); // 执行查询并返回结果
  });

  ipcMain.on("db_tasks_sync_get_by_user_role", (event, userIdRole) => {
    let result = getTasksByUserRole(userIdRole);
    result = result.map((task) => {
      const newItem = { ...task };
      newItem.formattedUploaded = newItem.formattedUploaded
        ? JSON.parse(newItem.formattedUploaded)
        : null;
      newItem.formattedTotal = newItem.formattedTotal
        ? JSON.parse(newItem.formattedTotal)
        : null;
      newItem.controller = newItem.controller
        ? JSON.parse(newItem.controller)
        : null;
      newItem.formattedSpeed = newItem.formattedSpeed
        ? JSON.parse(newItem.formattedSpeed)
        : null;
      newItem.total = newItem.priority;
      return newItem;
    });

    event.returnValue = result;
  });

  ipcMain.on("db_tasks_sync_insert", (event, tasks) => {
    try {
      insertTasks(tasks);
      event.returnValue = true;
    } catch (error) {
      console.error("db_tasks_sync_insert", error);
      event.returnValue = false;
    }
  });

  ipcMain.on("db_task_sync_insert", (event, task) => {
    try {
      const taskRowId = insertTask(task);
      console.log("db_task_sync_insert taskRowId:", taskRowId);
      event.returnValue = taskRowId;
    } catch (error) {
      console.error(error);
      event.returnValue = null;
    }
  });

  ipcMain.handle("db_tasks_insert", (_, tasks) => {
    return insertTasks(tasks);
  });

  // 同步更新多条数据
  ipcMain.on("db_tasks_sync_update", (event, tasks) => {
    try {
      updateTasks(tasks);
      event.returnValue = true;
    } catch (error) {
      console.error(error);
      event.returnValue = false;
    }
  });
  // 异步更新单条数据
  ipcMain.handle("db_task_update", (_, task) => {
    return updateTask(task);
  });
  // 同步更新单条数据
  ipcMain.on("db_task_sync_update", (event, task) => {
    try {
      updateTask(task);
      event.returnValue = true;
    } catch (error) {
      event.returnValue = false;
      console.error(error);
    }
  });
  // 异步更新多条数据
  ipcMain.handle("db_tasks_update", (_, tasks) => {
    return updateTasks(tasks);
  });

  // 同步删除单条数据
  ipcMain.on("db_task_sync_delete", (event, task) => {
    try {
      deleteTaskByIdOrTaskId(task);
      event.returnValue = true;
    } catch (error) {
      console.error(error);
      event.returnValue = false;
    }
  });
  // 异步删除单条数据
  ipcMain.on("db_task_delete", (event, task) => {
    try {
      deleteTaskByIdOrTaskId(task);
      return true;
    } catch (error) {
      console.error(error);
      return false;
    }
  });
  // 删除多条数据
  ipcMain.on("db_tasks_sync_delete", (event, tasks) => {
    try {
      deleteTasksByIdOrTaskId(tasks);
      event.returnValue = true;
    } catch (error) {
      console.error(error);
      event.returnValue = false;
    }
  });

  // 在应用退出时关闭数据库连接
  app.on("quit", () => {
    db.close(); // 关闭数据库连接
  });
}

// 创建版本表
function createVersionTable() {
  const createVersionTableQuery = `
    CREATE TABLE IF NOT EXISTS version (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      version INTEGER NOT NULL
    );
  `;
  db.prepare(createVersionTableQuery).run();

  // 检查是否有版本记录,若没有,则插入默认版本 1
  const currentVersion = getCurrentDatabaseVersion();
  if (!currentVersion) {
    const insertVersionQuery = `INSERT INTO version (version) VALUES (?);`;
    const stmt = db.prepare(insertVersionQuery);
    stmt.run(1); // 默认插入版本 1
  }
}

// 获取当前数据库版本
function getCurrentDatabaseVersion() {
  const selectVersionQuery = `SELECT version FROM version ORDER BY id DESC LIMIT 1;`;
  const stmt = db.prepare(selectVersionQuery);
  const result = stmt.get();
  return result ? result.version : null; // 默认返回旧版本(1)
}

// 更新数据库
function updateDatabase(currentVersion) {
  console.log(
    `Updating database from version ${currentVersion} to ${DB_VERSION}`
  );

  if (currentVersion === 1) {
    // 执行 1 -> 2 的更新操作
    updateToVersion2();
  }

  // 更新数据库版本记录
  const updateVersionQuery = `
    INSERT INTO version (version) VALUES (?);
  `;
  const stmt = db.prepare(updateVersionQuery);
  stmt.run(DB_VERSION);

  console.log(`Database updated to version ${DB_VERSION}`);
}

// 版本 1 -> 2 更新操作
function updateToVersion2() {
  // 示例:添加新的字段
  // const alterTableQuery = `ALTER TABLE tasks ADD COLUMN new_column TEXT;`;
  // db.prepare(alterTableQuery).run();
}

// 创建任务列表表
function createTable() {
  const createTableQuery = `
    CREATE TABLE IF NOT EXISTS todo_list (
      user_id_role TEXT,
      todo_id TEXT UNIQUE,
      task_title TEXT,
      task_description TEXT,
      priority INTEGER,
      due_date TEXT,
      status TEXT,
      created_at INTEGER,
      updated_at INTEGER,
      id INTEGER PRIMARY KEY AUTOINCREMENT
    );
  `;

  // 执行创建表的 SQL 语句
  db.prepare(createTableQuery).run();
}

// 插入任务数据
export function insertTask(task) {
  const insertQuery = `
    INSERT INTO todo_list (
      user_id_role, todo_id, task_title, task_description, priority, due_date, status, created_at, updated_at
    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
  `;

  const params = [
    task.user_id_role,
    task.todo_id,
    task.task_title,
    task.task_description,
    task.priority,
    task.due_date,
    task.status,
    task.created_at,
    task.updated_at,
  ];

  // 执行插入任务数据的 SQL 语句
  const stmt = db.prepare(insertQuery);
  const result = stmt.run(...params);
  // 返回插入的任务ID
  return result.lastInsertRowid;
}

// 批量插入任务数据
export function insertTasks(tasks) {
  const insertQuery = `
    INSERT INTO todo_list (
      user_id_role, todo_id, task_title, task_description, priority, due_date, status, created_at, updated_at
    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
  `;

  const stmt = db.prepare(insertQuery); // 准备 SQL 插入语句

  // 批量插入任务数据
  const transaction = db.transaction((tasks) => {
    for (const task of tasks) {
      const params = [
        task.user_id_role,
        task.todo_id,
        task.task_title,
        task.task_description,
        task.priority,
        task.due_date,
        task.status,
        task.created_at,
        task.updated_at,
      ];
      stmt.run(...params); // 执行每一条任务的插入
    }
  });

  transaction(tasks); // 开启事务并执行批量插入
}

// 示例:查询任务数据
export function getTasks() {
  const selectQuery = `SELECT * FROM todo_list;`;

  const stmt = db.prepare(selectQuery);
  return stmt.all();
}

// 根据 user_id_role 查询所有任务数据
export function getTasksByUserRole(user_id_role) {
  const selectQuery = `SELECT * FROM todo_list WHERE user_id_role = ?;`;

  const stmt = db.prepare(selectQuery);
  return stmt.all(user_id_role); // 执行查询并返回结果
}

// 示例:根据任务ID查询任务数据
export function getTaskById(todoId) {
  const selectQuery = `SELECT * FROM todo_list WHERE todo_id = ?;`;

  const stmt = db.prepare(selectQuery);
  return stmt.get(todoId);
}

// 根据传入对象的 id 或 todo_id 删除任务数据
export function deleteTaskByIdOrTaskId(task) {
  let deleteQuery;
  let identifier;

  // 检查传入对象中是否有 id 或 todo_id
  if (task.id) {
    // 如果传入对象中有 id,则按 id 删除
    deleteQuery = `DELETE FROM todo_list WHERE id = ?`;
    identifier = task.id;
  } else if (task.todo_id) {
    // 如果传入对象中有 todo_id,则按 todo_id 删除
    deleteQuery = `DELETE FROM todo_list WHERE todo_id = ?`;
    identifier = task.todo_id;
  } else {
    throw new Error("Object must have either id or todo_id");
  }

  const stmt = db.prepare(deleteQuery); // 准备 SQL 删除语句
  stmt.run(identifier); // 执行删除操作
}

// 批量根据 todo_id 或 id 删除任务数据
export function deleteTasksByIdOrTaskId(tasks) {
  // 生成批量删除的 SQL 语句
  const deleteQuery = `DELETE FROM todo_list WHERE id = ? OR todo_id = ?`;

  const stmt = db.prepare(deleteQuery); // 准备 SQL 删除语句

  const transaction = db.transaction((tasks) => {
    tasks.forEach((task) => {
      if (task.id || task.todo_id) {
        // 如果任务对象有 id 或 todo_id,则执行删除操作
        stmt.run(task.id, task.todo_id); // 执行删除操作,按照 id 或 todo_id 删除
      } else {
        console.warn("Task must have either id or todo_id");
      }
    });
  });

  transaction(tasks); // 执行事务,批量删除任务
}

// 更新单个任务数据(根据 todo_id 或 id)
export function updateTask(task) {
  let updateQuery;
  let params = [];

  // 判断更新的是 id 还是 todo_id
  if (task.id) {
    updateQuery = `
      UPDATE todo_list SET 
        user_id_role = ?, todo_id = ?, task_title = ?, task_description = ?, 
        priority = ?, due_date = ?, status = ?, created_at = ?, updated_at = ? 
      WHERE id = ?
    `;
    params = [
      task.user_id_role,
      task.todo_id,
      task.task_title,
      task.task_description,
      task.priority,
      task.due_date,
      task.status,
      task.created_at,
      task.updated_at,
      task.id,
    ];
  } else if (task.todo_id) {
    updateQuery = `
      UPDATE todo_list SET 
        user_id_role = ?, task_title = ?, task_description = ?, 
        priority = ?, due_date = ?, status = ?, created_at = ?, updated_at = ? 
      WHERE todo_id = ?
    `;
    params = [
      task.user_id_role,
      task.task_title,
      task.task_description,
      task.priority,
      task.due_date,
      task.status,
      task.created_at,
      task.updated_at,
      task.todo_id,
    ];
  } else {
    throw new Error("Task must have either id or todo_id");
  }

  const stmt = db.prepare(updateQuery); // 准备 SQL 更新语句
  stmt.run(...params); // 执行更新操作
}

// 批量更新任务数据(根据 todo_id 或 id)
export function updateTasks(tasks) {
  const updateQuery = `
    UPDATE todo_list SET 
      user_id_role = ?, task_title = ?, task_description = ?, 
      priority = ?, due_date = ?, status = ?, created_at = ?, updated_at = ? 
    WHERE todo_id = ? OR id = ?
  `;

  const stmt = db.prepare(updateQuery); // 准备 SQL 更新语句

  const transaction = db.transaction((tasks) => {
    tasks.forEach((task) => {
      if (task.id || task.todo_id) {
        const params = [
          task.user_id_role,
          task.task_title,
          task.task_description,
          task.priority,
          task.due_date,
          task.status,
          task.created_at,
          task.updated_at,
          task.todo_id,
          task.id,
        ];
        stmt.run(...params); // 执行批量更新操作
      } else {
        console.warn("Task must have either id or todo_id");
      }
    });
  });

  transaction(tasks); // 执行批量更新事务
}

6. 数据库代码解释

这段代码主要涉及了如何在 Electron 中使用 better-sqlite3 进行数据库操作,包含了数据库初始化、表创建、数据插入、查询、更新和删除等功能。

javascript
// 导入必要的模块
import Database from "better-sqlite3"; // 用于操作 SQLite 数据库的库
import { app, ipcMain } from "electron"; // 用于 Electron 应用的全局功能
import path from "path"; // 用于处理和操作文件路径的模块
import fs from "fs"; // 用于文件系统操作的模块
let db; // 声明一个变量用来存储数据库实例

// 数据库版本
const DB_VERSION = 1; // 当前数据库版本,用于后续的数据库升级管理
  • Database:使用 better-sqlite3 库进行 SQLite 数据库操作。
  • ipcMain:Electron 的主进程 IPC(进程间通信)API,用于接收渲染进程发出的消息。
  • path:用于处理文件和目录路径。
  • fs:文件系统模块,用于检测和创建目录。
javascript
// 初始化数据库的函数
export function initDatabase() {
  // 判断当前环境是否是开发环境
  let databasePath = path.join(app.getPath('userData'), 'database'); // 获取存储数据库文件的路径

  // 确保数据库文件夹存在,如果不存在则创建它
  if (!fs.existsSync(databasePath)) {
    fs.mkdirSync(databasePath, {recursive: true});
  }

  // 初始化数据库并创建或打开指定路径的 SQLite 数据库文件
  db = new Database(path.join(databasePath, 'uploadfile.db'), {verbose: console.log});

  // 设置数据库的日志模式为 WAL(写时日志)模式,提高性能
  db.pragma('journal_mode = WAL');

  // 创建版本表
  createVersionTable();

  // 获取当前数据库版本
  const currentVersion = getCurrentDatabaseVersion();

  // 如果数据库版本不匹配,执行数据库更新
  if (currentVersion !== DB_VERSION) {
    updateDatabase(currentVersion);
  }

  // 创建表,如果表不存在则创建
  createTable();
  • databasePath:构建数据库文件路径。app.getPath('userData') 返回 Electron 应用的用户数据目录。
  • fs.existsSync:检查文件夹是否存在。如果不存在,使用 fs.mkdirSync 创建该目录。
  • Database:初始化 SQLite 数据库,如果不存在数据库文件,better-sqlite3 会创建。
  • WAL:设置数据库为写时日志模式(WAL),有助于提高写入性能。
  • createVersionTable:检查并创建版本表,确保数据库有版本记录。
javascript
// 在 Electron 的主进程中注册一个 IPC 事件处理器
ipcMain.handle("db_query", async (_, query, params) => {
  const stmt = db.prepare(query); // 准备 SQL 查询
  return stmt.all(...params); // 执行查询并返回结果
});
  • ipcMain.handle:监听渲染进程发出的 db_query 请求,执行 SQL 查询并返回查询结果。
javascript
ipcMain.on("db_tasks_sync_get_by_user_role", (event, userIdRole) => {
  let result = getTasksByUserRole(userIdRole);
  result = result.map((task) => {
    const newItem = { ...task };
    newItem.formattedUploaded = newItem.formattedUploaded
      ? JSON.parse(newItem.formattedUploaded)
      : null;
    newItem.formattedTotal = newItem.formattedTotal
      ? JSON.parse(newItem.formattedTotal)
      : null;
    newItem.controller = newItem.controller
      ? JSON.parse(newItem.controller)
      : null;
    newItem.formattedSpeed = newItem.formattedSpeed
      ? JSON.parse(newItem.formattedSpeed)
      : null;
    newItem.total = newItem.totalSize;
    return newItem;
  });

  event.returnValue = result;
});
  • ipcMain.on:监听来自渲染进程的请求 db_tasks_sync_get_by_user_role,获取指定用户角色的任务列表并对其数据进行处理。
  • JSON.parse:解析存储为字符串的 JSON 数据,恢复成对象。
javascript
ipcMain.on("db_tasks_sync_insert", (event, tasks) => {
  try {
    insertTasks(tasks);
    event.returnValue = true;
  } catch (error) {
    console.error("db_tasks_sync_insert", error);
    event.returnValue = false;
  }
});
  • ipcMain.on:监听 db_tasks_sync_insert 请求,插入多个任务数据。捕获可能的错误并返回插入结果。
javascript
ipcMain.on("db_task_sync_insert", (event, task) => {
  try {
    const taskRowId = insertTask(task);
    console.log("db_task_sync_insert taskRowId:", taskRowId);
    event.returnValue = taskRowId;
  } catch (error) {
    console.error(error);
    event.returnValue = null;
  }
});
  • 插入单条任务数据:在数据库中插入单个任务,并返回插入的任务 ID。
javascript
ipcMain.handle("db_tasks_insert", (_, tasks) => {
  return insertTasks(tasks);
});
  • 处理插入多条任务:通过 IPC 接口调用 insertTasks 函数进行批量任务插入。
javascript
// 同步更新多条数据
ipcMain.on("db_tasks_sync_update", (event, tasks) => {
  try {
    updateTasks(tasks);
    event.returnValue = true;
  } catch (error) {
    console.error(error);
    event.returnValue = false;
  }
});
  • 同步更新多条任务数据:处理更新多个任务数据的请求,并捕获错误。
javascript
// 异步更新单条数据
ipcMain.handle("db_task_update", (_, task) => {
  return updateTask(task);
});
  • 异步更新单条任务数据:通过 db_task_update 请求,异步更新指定任务。
javascript
// 同步更新单条数据
ipcMain.on("db_task_sync_update", (event, task) => {
  try {
    updateTask(task);
    event.returnValue = true;
  } catch (error) {
    event.returnValue = false;
    console.error(error);
  }
});
  • 同步更新单个任务数据:同步更新单个任务的请求。
javascript
// 异步更新多条数据
ipcMain.handle("db_tasks_update", (_, tasks) => {
  return updateTasks(tasks);
});
  • 异步更新多条任务数据:批量更新任务数据。
javascript
// 同步删除单条数据
ipcMain.on("db_task_sync_delete", (event, task) => {
  try {
    deleteTaskByIdOrTaskId(task);
    event.returnValue = true;
  } catch (error) {
    console.error(error);
    event.returnValue = false;
  }
});
  • 同步删除单个任务:根据任务 ID 或 task_id 删除任务。
javascript
// 异步删除单条数据
ipcMain.on("db_task_delete", (event, task) => {
  try {
    deleteTaskByIdOrTaskId(task);
    return true;
  } catch (error) {
    console.error(error);
    return false;
  }
});
  • 异步删除单个任务:通过 db_task_delete 请求,删除指定任务。
javascript
// 删除多条数据
ipcMain.on("db_tasks_sync_delete", (event, tasks) => {
  try {
    deleteTasksByIdOrTaskId(tasks);
    event.returnValue = true;
  } catch (error) {
    console.error(error);
    event.returnValue = false;
  }
});
  • 同步删除多个任务:批量删除任务数据。
javascript
  // 在应用退出时关闭数据库连接
  app.on('quit', () => {
    db.close(); // 关闭数据库连接
  });
}
  • 退出时关闭数据库连接:当应用退出时,关闭数据库连接,确保数据被正确保存。

数据库表和版本管理

  • createVersionTable:创建一个版本表来跟踪数据库版本。
  • getCurrentDatabaseVersion:获取当前的数据库版本号。
  • updateDatabase:如果数据库版本不匹配,则执行更新操作。
  • createTable:创建任务表,表结构包括多个字段(如 user_id_role, task_id, folderName, 等)。

数据插入、更新、删除

  • insertTask:插入单个任务。
  • insertTasks:批量插入任务数据。
  • updateTask:更新单个任务数据。
  • updateTasks:批量更新任务数据。
  • deleteTaskByIdOrTaskId:根据 ID 或 task_id 删除任务。
  • deleteTasksByIdOrTaskId:批量删除任务。

整体来说,这段代码实现了一个完整的数据库管理系统,支持任务的增删改查,并且能够处理数据库版本更新。

7. preload.js

js
// preload.js
const { contextBridge, ipcRenderer } = require("electron");

// 通过 contextBridge 暴露安全的 API 给渲染进程
contextBridge.exposeInMainWorld("electron", {
  // 获取数据库中的所有待办事项
  getTodoList: () => ipcRenderer.invoke("db_query", "SELECT * FROM todo_list;"),

  // 根据 user_id_role 获取任务列表
  getTasksByUserRole: (userIdRole) =>
    ipcRenderer.invoke("db_tasks_sync_get_by_user_role", userIdRole),

  // 插入单个任务
  insertTask: (task) => ipcRenderer.invoke("db_task_sync_insert", task),

  // 插入多个任务
  insertTasks: (tasks) => ipcRenderer.invoke("db_tasks_insert", tasks),

  // 更新单个任务
  updateTask: (task) => ipcRenderer.invoke("db_task_update", task),

  // 更新多个任务
  updateTasks: (tasks) => ipcRenderer.invoke("db_tasks_update", tasks),

  // 删除单个任务
  deleteTask: (task) => ipcRenderer.invoke("db_task_delete", task),

  // 删除多个任务
  deleteTasks: (tasks) => ipcRenderer.invoke("db_tasks_sync_delete", tasks),

  // 查询任务数据,根据 ID 或 todo_id
  getTaskById: (todoId) =>
    ipcRenderer.invoke(
      "db_query",
      "SELECT * FROM todo_list WHERE todo_id = ?",
      [todoId]
    ),

  // 异步更新数据库中的任务状态
  updateTaskStatus: (taskId, status) =>
    ipcRenderer.invoke("db_task_sync_update", {
      todo_id: taskId,
      status: status,
    }),

  // 获取数据库版本
  getDatabaseVersion: () =>
    ipcRenderer.invoke(
      "db_query",
      "SELECT version FROM version ORDER BY id DESC LIMIT 1;"
    ),
});

代码解释:

  1. contextBridge.exposeInMainWorld:

    • 这个方法用于将 Node.js 的功能暴露给渲染进程。我们把一些需要与数据库交互的函数暴露给渲染进程使用(这些函数会在主进程中通过 IPC 进行处理)。
  2. 数据库操作暴露的 API:

    • getTodoList: 查询数据库中所有的待办事项。
    • getTasksByUserRole: 根据用户角色 userIdRole 获取任务列表。
    • insertTaskinsertTasks: 插入单个或多个任务到数据库。
    • updateTaskupdateTasks: 更新单个或多个任务。
    • deleteTaskdeleteTasks: 删除单个或多个任务。
    • getTaskById: 根据任务 ID 查询特定任务。
    • updateTaskStatus: 更新任务的状态。
    • getDatabaseVersion: 获取当前数据库的版本。

如何使用:

  • 在渲染进程中,你可以通过 window.electron 来访问这些功能。例如,查询所有任务:
javascript
window.electron.getTodoList().then((tasks) => {
  console.log(tasks); // 打印所有任务
});
  • 在渲染进程中你也可以使用其他操作来插入、更新、删除任务,所有的数据库交互都会通过 IPC 调用相应的主进程功能。