跳转到内容

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

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

Will Try My Best.