electron-vite 快速入门
安装 VSCode
官网:https://code.visualstudio.com/download
1. 选择 system installer
版本下载
2. 安装勾选 VSCode 环境变量
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
PS C:\Users\Administrator\Desktop> node -v
v20.18.1
经验提示
建议直接覆盖安装的方式更新 Node.js 版本而不是使用 nvm 管理工具切换 Node.js 版本
2. 检查电脑策略是否为 RemoteSigned
打开 PowerShell
,输入 Get-ExecutionPolicy
看是否输出 RemoteSigned
,如果是 Restricted
则需要继续往下看,需要设置为 RemoteSigned
Get-ExecutionPolicy
超级管理员方式打开 PowerShell
,执行策略设置为 RemoteSigned
Set-ExecutionPolicy RemoteSigned
3. 全局配置 npm 镜像加速
npm config set registry https://registry.npmmirror.com
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 创建项目
npm create @quick-start/electron@latest
2. 安装项目依赖
npm i
npm 查看可以升级的包
1. 查看可以升级的包
要查看可以升级的包,您可以使用以下命令:
npm outdated
该命令会列出当前项目中所有已安装的包,以及它们的当前版本、所需的版本(根据 package.json
中的 dependencies
或 devDependencies
)和最新版本。如果某个包有可用的更新,它会显示出来。
输出示例:
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. 升级所有可升级的包
要将所有的包更新到最新版本,可以使用:
npm update
这条命令会根据 package.json
文件中的版本范围(如 ^
或 ~
)来更新包。如果你希望强制升级到最新的版本,不管 package.json
中的版本范围,下面有几个方法。
如果你想升级某个包,可以使用:
npm update <package-name>
2.1. 手动升级 package.json
如果你希望升级到最新的版本,确保你的 package.json
中的版本范围是开放的(例如使用 ^
或 latest
),然后运行:
npm install
这会按照 package.json
的版本要求安装所有依赖项的最新版本。
2.2. 使用 npx npm-check-updates
(ncu)
如果你希望强制升级所有包并自动更新 package.json
中的版本,可以使用 npm-check-updates
(简称 ncu
)工具:
安装
npm-check-updates
工具:bashnpm install -g npm-check-updates
运行
ncu
来查看可以升级的包:bashncu
使用
ncu -u
来自动更新package.json
中的版本号:bashncu -u
最后,重新安装所有依赖:
bashnpm install
这样,所有依赖包将被升级到最新版本,package.json
会自动更新版本号,并安装这些更新的依赖。
总结:
npm outdated
查看可以升级的包。npm update
升级符合版本范围的依赖。npx npm-check-updates
强制升级所有包并更新package.json
。
3. 启动项目
npm run dev
4. 修改 electron-builder 配置文件
electron-builder.yml 改为 electron-builder.config.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 文件打包
{
"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
,你可以在命令行中这样指定:
electron-builder --config build-config.js
这种方式允许你在构建项目时灵活选择和使用不同的配置文件。你可以将配置文件以 YAML、JSON 或 JavaScript 的格式保存,只需确保通过 --config
选项指定正确的路径和文件名即可。
打包构建
npm run build:win
asar extract app.asar app_extracted

electron-builder.config.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/",
},
};
打包构建
npm run build:unpack
asar extract app.asar app_extracted

5. .prettierrc.yaml 改成 .prettierrc.js
.prettierrc.js 内容如下
module.exports = {
// 使用单引号代替双引号
singleQuote: true,
// 在每行代码末尾添加分号
semi: true,
// 设置代码的最大打印宽度为140个字符
printWidth: 140,
// 在多行对象或数组的最后一项后面添加逗号
trailingComma: "all",
// 在对象大括号内不添加空格
bracketSpacing: false,
};
electron-builder.config.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
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. 格式化代码
npm run format
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"
{
"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 安装包
npm run build:win
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 多窗口
实现效果如下:
1. 修改 electron.vite.config.mjs
electron.vite.config.mjs 文件修改点如下
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
<!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
<!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
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
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
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 的最小化、缩放、最大化按钮是通过 titleBarStyle
和 titleBarOverlay
设置的
const { BrowserWindow } = require("electron");
const win = new BrowserWindow({
titleBarStyle: "hidden",
titleBarOverlay: {
color: "#2f3241",
symbolColor: "#74b1be",
height: 60,
},
});
titleBarOverlay
被设置后,只显示 最小化, 最大化/还原,关闭窗口的按钮
(1)、src/main/titlebar.js
Details
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 引入
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
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
<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
<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
模块
npm i @electron/remote --registry=https://registry.npmmirror.com
2. src/main/index.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');
代码引入
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
安装图标转换模块
npm install electron-icon-builder -D --registry=https://registry.npmmirror.com
pcakge.json 添加命令 electron-icon-builder
{
"scripts": {
"logo": "electron-icon-builder --input=./public/logo.png --output=./public/"
}
}
转换生成图标
npm run logo
electron-vite 加载本地资源
electron-vite js 代码压缩、js 代码混淆、html 代码压缩
警告
官方文档:https://cn.electron-vite.org/guide/source-code-protection#v8-字节码的局限性
当启用最小化混淆(build.minify)时,字符串保护不起作用。这是因为字符串保护是基于字符代码实现的。然而,现代的压缩混淆工具(例如 esbuild 或 terser)会恢复转换后的字符代码,导致保护失败。 electron-vite 会发出警告。事实上,最小化混淆对于减小字节码大小的作用不大,因此建议在保护字符串时不要启用最小化混淆。
1. 安装依赖
- Terser(压缩 JS 代码)
- vite-plugin-html(压缩 HTML 代码)
npm install terser vite-plugin-html -D
2. electron.vite.config.mjs 配置示例
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. 如何配置
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. 打包一个免安装包的
npm run build:unpack
2. 进入 dist\win-unpacked\resources\app.asar
所在的目录
3. 执行 asar extract app.asar app_extracted
解压 app.asar
文件
asar extract app.asar app_extracted
4. 对比前后的代码-被保护前后的代码
(1)被保护的源码,是二进制的,一般无法被破解,看不到源码:
(2)未开启保护源码,相当于裸奔:
electron-vite 配置别名
1. electron-vite 配置别名
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+我们会鼠标左键点击会直接进入文件内部,但是如果没有任何配置是不会生效的
import { getNextId } from "@/renderer/index/utils/utils";
想要在 ctrl+鼠标左键进入文件,需要项目根目录增加 jsconfig.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 减少包的体积
提示
!!!关键点,移除非必要的代码或者文件
- 移除不需要的语言包(electron-builder electronLanguages) https://www.electron.build/app-builder-lib.interface.platformspecificbuildoptions#electronlanguages
- 渲染进程所有的包放到 devDependencies(收益较大),文档:https://cn.electron-vite.org/guide/build#外部依赖
- electron-builder 配置 files 移除必须包含的 node_modules 中多余的资源(典型例子:better-sqlite3) https://www.electron.build/app-builder-lib.interface.platformspecificbuildoptions#files
- 通过对 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
- 默认的配置
export default defineConfig({
main: {
plugins: [externalizeDepsPlugin(), bytecodePlugin()],
resolve: {
alias: {
"@": resolve("src"),
},
},
},
// ...
});
- 根据环境变量以及 mode 进行的配置
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
{
"scripts": {
"build:prod": "electron-vite build --mode=production && cross-env BUILD_MODE=production electron-builder --config=electron-builder.config.js"
}
}
electron-builder.config.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
npm i better-sqlite3 -S
2. 修改 package.json,增加 npx electron-rebuild -f
,当安装的时候重新构建原生的包,注意这里better-sqlite3
是放在dependencies
中的
{
"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
,用于减少包的体积,过滤掉未使用的代码
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
配置,具体看高亮部分
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
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
进行数据库操作,包含了数据库初始化、表创建、数据插入、查询、更新和删除等功能。
// 导入必要的模块
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:文件系统模块,用于检测和创建目录。
// 初始化数据库的函数
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:检查并创建版本表,确保数据库有版本记录。
// 在 Electron 的主进程中注册一个 IPC 事件处理器
ipcMain.handle("db_query", async (_, query, params) => {
const stmt = db.prepare(query); // 准备 SQL 查询
return stmt.all(...params); // 执行查询并返回结果
});
- ipcMain.handle:监听渲染进程发出的
db_query
请求,执行 SQL 查询并返回查询结果。
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 数据,恢复成对象。
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
请求,插入多个任务数据。捕获可能的错误并返回插入结果。
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。
ipcMain.handle("db_tasks_insert", (_, tasks) => {
return insertTasks(tasks);
});
- 处理插入多条任务:通过 IPC 接口调用
insertTasks
函数进行批量任务插入。
// 同步更新多条数据
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);
});
- 异步更新单条任务数据:通过
db_task_update
请求,异步更新指定任务。
// 同步更新单条数据
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;
}
});
- 同步删除单个任务:根据任务 ID 或 task_id 删除任务。
// 异步删除单条数据
ipcMain.on("db_task_delete", (event, task) => {
try {
deleteTaskByIdOrTaskId(task);
return true;
} catch (error) {
console.error(error);
return false;
}
});
- 异步删除单个任务:通过
db_task_delete
请求,删除指定任务。
// 删除多条数据
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(); // 关闭数据库连接
});
}
- 退出时关闭数据库连接:当应用退出时,关闭数据库连接,确保数据被正确保存。
数据库表和版本管理
- createVersionTable:创建一个版本表来跟踪数据库版本。
- getCurrentDatabaseVersion:获取当前的数据库版本号。
- updateDatabase:如果数据库版本不匹配,则执行更新操作。
- createTable:创建任务表,表结构包括多个字段(如
user_id_role
,task_id
,folderName
, 等)。
数据插入、更新、删除
- insertTask:插入单个任务。
- insertTasks:批量插入任务数据。
- updateTask:更新单个任务数据。
- updateTasks:批量更新任务数据。
- deleteTaskByIdOrTaskId:根据 ID 或 task_id 删除任务。
- deleteTasksByIdOrTaskId:批量删除任务。
整体来说,这段代码实现了一个完整的数据库管理系统,支持任务的增删改查,并且能够处理数据库版本更新。
7. preload.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;"
),
});
代码解释:
contextBridge.exposeInMainWorld
:- 这个方法用于将 Node.js 的功能暴露给渲染进程。我们把一些需要与数据库交互的函数暴露给渲染进程使用(这些函数会在主进程中通过 IPC 进行处理)。
数据库操作暴露的 API:
getTodoList
: 查询数据库中所有的待办事项。getTasksByUserRole
: 根据用户角色userIdRole
获取任务列表。insertTask
和insertTasks
: 插入单个或多个任务到数据库。updateTask
和updateTasks
: 更新单个或多个任务。deleteTask
和deleteTasks
: 删除单个或多个任务。getTaskById
: 根据任务 ID 查询特定任务。updateTaskStatus
: 更新任务的状态。getDatabaseVersion
: 获取当前数据库的版本。
如何使用:
- 在渲染进程中,你可以通过
window.electron
来访问这些功能。例如,查询所有任务:
window.electron.getTodoList().then((tasks) => {
console.log(tasks); // 打印所有任务
});
- 在渲染进程中你也可以使用其他操作来插入、更新、删除任务,所有的数据库交互都会通过 IPC 调用相应的主进程功能。