主题
Electron 基础使用
安装依赖
shell
npm i axios
npm i pinia
npm i pinia-plugin-persistedstate
npm i vue@3 vue-router@4
npm i vite @vitejs/plugin-vue -D
npm i electron electron-icon-builder @electron-forge/cli -D
npm i less less-loader -D
npm i @types/node -D
npm i cross-env npm-run-all -D目录结构
shell
electron-template-app/
├── build # 应用图标资源
├── electron/
│ ├─ api/ # 暴露渲染进程使用的api
│ ├─ window/ # 窗口管理相关代码
│ ├─ store/ # 本地存储
│ ├─ main.js # 主进程
│ ├─ preload.js # 预脚本
├── forge.config.js
├── index.html
├── out/ # 分发的应用
├── package.json
├── src/ # 前端源码
└── vite.config.jspackage.json
json
{
"name": "electron-template-app",
"version": "1.0.0",
"main": "electron/main.js",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"electron:dev": "cross-env NODE_ENV=development electron .",
"electron:pro": "npm run build && cross-env NODE_ENV=production electron .",
"dev:app": "npm-run-all -p dev electron:dev",
"start": "npm run build && electron-forge start",
"package": "electron-forge package",
"make": "npm run build && electron-forge make",
"generate:icons": "npx electron-icon-builder --input=build/icon.png --output=build --flatten"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"@electron-forge/cli": "^7.10.2",
"@electron-forge/maker-deb": "^7.10.2",
"@electron-forge/maker-rpm": "^7.10.2",
"@electron-forge/maker-squirrel": "^7.10.2",
"@electron-forge/maker-zip": "^7.10.2",
"@electron-forge/plugin-auto-unpack-natives": "^7.10.2",
"@electron-forge/plugin-fuses": "^7.10.2",
"@electron/fuses": "^1.8.0",
"@types/node": "^24.10.0",
"@vitejs/plugin-vue": "^6.0.1",
"cross-env": "^10.1.0",
"electron": "^39.0.0",
"electron-icon-builder": "^2.0.1",
"npm-run-all": "^4.1.5",
"less": "^4.2.0",
"less-loader": "^12.0.0",
"vite": "^7.2.1"
},
"dependencies": {
"axios": "^1.13.2",
"dayjs": "^1.11.19",
"element-plus": "^2.11.5",
"pinia": "^3.0.3",
"pinia-plugin-persistedstate": "^4.5.0",
"vue": "^3.5.23",
"vue-router": "^4.6.3"
}
}vite.config.js
js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path, { resolve } from 'path';
import { fileURLToPath } from 'url';
import { PORT } from './electron/settings.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export default defineConfig({
base: './',
plugins: [vue()],
server: {
port: PORT,
},
build: {
sourcemap: false, // 禁止 map 文件
minify: 'esbuild',
},
resolve: {
//设置别名
alias: {
'@': path.join(__dirname, './src'),
'@views': path.join(__dirname, './src/views'),
},
// 导入时想要省略的扩展名列表
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
},
css: {
// css预处理器
preprocessorOptions: {
less: {
additionalData: `@import "${resolve(__dirname, 'src/assets/style/global.less')}";`,
javascriptEnabled: true,
},
},
},
});forge.config.js
js
import path from 'path';
import { FusesPlugin } from '@electron-forge/plugin-fuses';
import { FuseV1Options, FuseVersion } from '@electron/fuses';
import { TITLE } from './electron/settings.js';
export default {
packagerConfig: {
name: TITLE,
executableName: TITLE,
asar: true,
prune: true,
compression: 'maximum',
osxSign: false,
osxNotarize: false,
icon: path.resolve('./build/icons/icon'), // ESM 推荐使用 path.resolve,不带扩展名
extraResource: ['./dist', './build'],
ignore: [
/^\/src/,
/^\/test/,
/\.map$/,
/\.ts$/,
/\.md$/,
/README/,
/yarn\.lock/,
/package-lock\.json/,
/node_modules\/\.cache/,
/node_modules\/@types/,
/node_modules\/electron/,
/node_modules\/electron-devtools-installer/,
/node_modules\/webpack/,
/node_modules\/vite/,
/node_modules\/eslint/,
/node_modules\/rollup/,
/node_modules\/@vitejs/,
/node_modules\/@babel/,
],
},
rebuildConfig: {},
makers: [
{
name: '@electron-forge/maker-squirrel',
config: {
setupIcon: path.resolve('./build/icons/icon.ico'), // ESM + path.resolve
},
},
{
name: '@electron-forge/maker-zip',
platforms: ['darwin', 'win32'],
},
{
name: '@electron-forge/maker-deb',
config: {},
},
{
name: '@electron-forge/maker-rpm',
config: {},
},
],
plugins: [
{
name: '@electron-forge/plugin-auto-unpack-natives',
config: {},
},
new FusesPlugin({
version: FuseVersion.V1,
[FuseV1Options.RunAsNode]: false,
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
[FuseV1Options.OnlyLoadAppFromAsar]: true,
}),
],
};index.html
html
<!doctype html>
<html lang="en">
<head>
<title></title>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, viewport-fit=cover" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>Electron 目录相关代码
main.js
js
import { app, BrowserWindow } from 'electron';
import { createWindow } from './window/index.js';
import { registerIpc } from './window/register-ipc.js';
import store from './store/index.js';
if (!app.requestSingleInstanceLock()) {
app.quit();
process.exit(0);
}
store.init();
registerIpc();
app.whenReady().then(async () => {
let win = await createWindow();
win.webContents.send('onMessage', { name: 'zs' });
app.on('activate', async () => {
if (BrowserWindow.getAllWindows().length === 0) {
await createWindow();
}
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});preload.js
js
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electronAPI', {
apiExecute: (op, ...args) => ipcRenderer.invoke('apiExecute', op, ...args),
onMessage: (callback) => {
const listener = (_event, data) => callback(data);
ipcRenderer.on('onMessage', listener);
return () => ipcRenderer.removeListener('onMessage', listener);
},
});settings.js
js
// 窗口标题
export const TITLE = 'My App';
// 窗口宽高
export const WIDTH = 1147;
export const HEIGHT = 776;
// 静态服务器端口
export const PORT = 5573;
export const BASE_URL = 'http://localhost:8000';env.d.ts
ts
export {};
declare global {
interface Window {
electronAPI: any;
}
}window/
js
import { app, BrowserWindow } from 'electron';
import path from 'path';
import { fileURLToPath } from 'url';
import { TITLE, WIDTH, HEIGHT, PORT } from '../settings.js';
let mainWindow = null;
const isDev = !app.isPackaged;
const __dirname = path.dirname(fileURLToPath(import.meta.url));
export const createWindow = async () => {
mainWindow = new BrowserWindow({
title: TITLE,
width: WIDTH,
height: HEIGHT,
autoHideMenuBar: true,
frame: false, // 禁用默认标题栏
titleBarStyle: 'hiddenInset', // 隐藏标题栏
trafficLightPosition: { x: 5, y: 5 },
backgroundColor: '#ffffff',
icon: path.resolve(__dirname, '../../build/icon.png'),
webPreferences: {
preload: path.resolve(__dirname, '../preload.js'),
contextIsolation: true,
nodeIntegration: false,
},
});
if (isDev) {
mainWindow.webContents.openDevTools();
await mainWindow.loadURL(`http://localhost:${PORT}`);
} else {
await mainWindow.loadFile(path.resolve(__dirname, '../../dist/index.html'));
}
return mainWindow;
};
export const getMainWindow = () => {
return mainWindow;
};js
import { ipcMain } from 'electron';
import handleExecute from '../api/index.js';
export const registerIpc = () => {
ipcMain.handle('apiExecute', handleExecute);
};api/
自动注册api/modules下的所有导出方法
js
import fs from 'fs';
import path from 'path';
import { fileURLToPath, pathToFileURL } from 'url';
import Result from '../utils/result.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// 模块目录
const modulesDir = path.join(__dirname, 'modules');
// 创建一个空对象来收集所有导出方法
const api = {};
// 动态导入所有模块
for (const file of fs.readdirSync(modulesDir)) {
if (file.endsWith('.js')) {
const modulePath = pathToFileURL(path.join(modulesDir, file)).href;
const mod = await import(modulePath);
Object.assign(api, mod.default || mod);
}
}
// 默认方法兜底
api.default = (op) => {
const msg = `暂无方法 function ${op}()`;
console.log(msg);
return Result.fail(msg);
};
// 执行入口
const handleExecute = (_e, op, ...args) => {
console.log(`正在执行 function ${op}()`);
return api[op] ? api[op](...args) : api.default(op);
};
export default handleExecute;js
export const test = (data) => {
console.log(data);
return data;
};request/request.js
js
import axios from 'axios';
import store from '../store/index.js';
const request = axios.create({
baseURL: '',
timeout: 20000, // 请求超时 20s
headers: {
'content-type': 'application/json',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36',
},
});
// 前置拦截器(发起请求之前的拦截)
request.interceptors.request.use(
(config) => {
const headers = store.get('headers');
if (JSON.stringify(headers) !== '{}') {
config.headers = {
...config.headers,
...headers,
};
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 后置拦截器(获取到响应时的拦截)
request.interceptors.response.use(
async (response) => {
// 二进制数据则直接返回
if (response.request.responseType === 'blob' || response.request.responseType === 'arraybuffer') {
return response.data;
}
if (response.data.code && Number(response.data.code) === 401) {
console.log('登录过期');
store.clear();
}
return response.data;
},
(error) => {
return Promise.reject(error);
}
);
export default request;store/index.js
自动对存储进行文件写入
js
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { app } from 'electron';
const isDev = !app.isPackaged;
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const STORE_PATH = isDev ? path.resolve(__dirname, './store.json') : path.join(app.getPath('userData'), 'store.json');
let store = {};
// 保存数据
function save() {
fs.writeFileSync(STORE_PATH, JSON.stringify(store, null, 2));
}
export default {
init() {
console.log('缓存路径=>', STORE_PATH);
// 初始化
if (fs.existsSync(STORE_PATH)) {
try {
store = JSON.parse(fs.readFileSync(STORE_PATH, 'utf8'));
} catch (e) {
store = {};
}
} else {
try {
fs.writeFileSync(STORE_PATH, JSON.stringify({}, null, 2));
console.log('已创建空的 store.json');
} catch (e) {
console.error('创建 store.json 失败:', e);
}
}
},
set(key, value) {
store[key] = value;
save();
},
get(key) {
return store[key];
},
all() {
return store;
},
clear() {
store = {};
save();
},
};utils/result.js
js
/**
* 通用返回结构
*/
class Result {
constructor(success, message, data) {
this.success = success;
this.message = message;
if (data !== undefined) {
this.data = data;
}
}
/** 创建成功结果 */
static ok(data, message = '操作成功') {
return new Result(true, message, data);
}
/** 创建失败结果 */
static fail(message = '操作失败', data) {
return new Result(false, message, data);
}
}
export default Result;前端相关
js
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import { createPinia } from 'pinia';
import piniaPluginPersistedState from 'pinia-plugin-persistedstate';
import ElementPlus from 'element-plus';
import zhCn from 'element-plus/es/locale/lang/zh-cn';
import 'element-plus/dist/index.css';
const app = createApp(App);
const pinia = createPinia();
pinia.use(piniaPluginPersistedState);
app.use(ElementPlus, {
locale: zhCn,
});
app.use(router);
app.use(pinia);
app.mount('#app');vue
<template>
<div class="mac-drag-region"></div>
<div class="app-content">
<router-view v-slot="{ Component }">
<component :is="Component"></component>
</router-view>
</div>
</template>
<script setup>
// 接收主线程参数
import { onMounted } from 'vue';
onMounted(() => {
window.electronAPI.onMessage((res) => {
console.log('来源主线程', res);
});
});
</script>
<style lang="less">
/* 只让左上角三色按钮区域能拖动 */
.mac-drag-region {
width: 100%;
height: @mac-status-bar-h;
border-radius: 6px; /* 让区域柔和一点 */
-webkit-app-region: drag;
}
/* 其他内容禁止拖动 */
.app-content,
button,
input,
textarea {
-webkit-app-region: no-drag;
}
</style>js
import { defineStore } from 'pinia';
import { ref } from 'vue';
export const useTestStore = defineStore(
'test',
() => {
const userInfo = ref({});
function setUserInfo(value) {
userInfo.value = value;
}
return {
userInfo,
setUserInfo,
};
},
{
// 持久化
persist: true,
}
);js
import { createRouter, createWebHashHistory } from 'vue-router';
const routes = [{ path: '/', component: () => import('../views/Home.vue') }];
export default createRouter({
history: createWebHashHistory(),
routes,
});vue
<template>
<div>Hello World.</div>
</template>
<script setup>
import { onMounted } from 'vue';
const init = async () => {
// 通信
let res = await window.electronAPI.apiExecute('test', '这是参数');
let res2 = await window.electronAPI.apiExecute('12312', '这是参数');
console.log('res返回数据', res);
console.log('res2返回数据', res2);
};
onMounted(init);
</script>
<style lang="less" scoped></style>icon生成
把icon.png放入
build/中后执行命令
shell
npm run generate:icons打包
执行命令后生成
out/目录
shell
npm run make