一、首先需要先有一个能正常运行的基于umi的项目
项目可以是antdPro的模板项目,也可以是通过umi创建的项目,创建过程不再赘述,可以参考官网创建
二、接入electron
首先根目录下创建app文件夹,用于管理electron文件
1.项目根目录下的配置
(1)安装electron依赖
yarn add electron -D
yarn add concurrently -D
yarn add cross-env -D
yarn add wait-on -D
(2)package.json下配置
- scripts 下新增electron启动命令,wait-on http://localhost:8000这个端口要跟前端运行的端口一致才能监听
"electron": "electron app/main.js",
"electron-dev": "concurrently \"cross-env BROWSER=none yarn start:dev\" \"wait-on http://localhost:8000 && cross-env NODE_ENV=development electron app/main.js\"",
"electron-start": "yarn build && cross-env NODE_ENV=production electron app/main.js"
- 配置主文件入口
"main": "app/main.js",
- 我的package.json,可以参考位置
{
"name": "project",
"version": "1.0.0",
"private": true,
"description": "goinfo V3版本",
"author": "jack",
"main": "app/main.js",
"workspaces": [
"packages/*"
],
"scripts": {
"analyze": "cross-env ANALYZE=1 max build",
"build": "max build",
"build:packages": "yarn workspace goinfo-core build && yarn workspace goinfo-components build && yarn workspace goinfo-common-view build",
"deploy": "npm run build && npm run gh-pages",
"dev": "npm run start:dev",
"electron": "electron app/main.js",
"electron-dev": "concurrently \"cross-env BROWSER=none yarn start:dev\" \"wait-on http://localhost:8000 && cross-env NODE_ENV=development electron app/main.js\"",
"electron-start": "yarn build && cross-env NODE_ENV=production electron app/main.js",
"gh-pages": "gh-pages -d dist",
"i18n-remove": "pro i18n-remove --locale=zh-CN --write",
"postinstall": "max setup",
"jest": "jest",
"lint": "npm run lint:js && npm run lint:prettier && npm run tsc",
"lint-staged": "lint-staged",
"lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx ",
"lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src ",
"lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src",
"lint:prettier": "prettier -c --write \"**/**.{js,jsx,tsx,ts,less,md,json}\" --end-of-line auto",
"openapi": "max openapi",
"prettier": "prettier -c --write \"**/**.{js,jsx,tsx,ts,less,md,json}\"",
"preview": "npm run build && max preview --port 8000",
"record": "cross-env NODE_ENV=development REACT_APP_ENV=test max record --scene=login",
"serve": "umi-serve",
"start": "cross-env UMI_ENV=dev max dev",
"start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev max dev",
"start:no-mock": "cross-env MOCK=none UMI_ENV=dev max dev",
"start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev max dev",
"start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev",
"test": "jest",
"test:coverage": "npm run jest -- --coverage",
"test:update": "npm run jest -- -u",
"tsc": "tsc --noEmit"
},
"lint-staged": {
"**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js",
"**/*.{js,jsx,tsx,ts,less,md,json}": [
"prettier --write"
]
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 10"
],
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@ant-design/pro-components": "^2.3.57",
"@ant-design/use-emotion-css": "1.0.4",
"@umijs/route-utils": "^2.2.2",
"antd": "^5.13.0",
"classnames": "^2.3.2",
"fs-extra": "^11.1.1",
"glob": "^7.1.4",
"goinfo-common-view": "^1.0.0",
"goinfo-components": "^1.0.0",
"goinfo-core": "^1.0.0",
"handsontable": "^13.1.0",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"omit.js": "^2.0.2",
"rc-menu": "^9.8.2",
"rc-util": "^5.27.2",
"react": "^18.2.0",
"react-dev-inspector": "^1.8.4",
"react-dom": "^18.2.0",
"react-helmet-async": "^1.3.0",
"sortablejs": "^1.15.0",
"umi-request": "^1.4.0"
},
"devDependencies": {
"@ant-design/pro-cli": "^2.1.5",
"@testing-library/react": "^13.4.0",
"@types/classnames": "^2.3.1",
"@types/express": "^4.17.17",
"@types/history": "^4.7.11",
"@types/jest": "^29.4.0",
"@types/lodash": "^4.14.191",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@types/react-helmet": "^6.1.6",
"@umijs/fabric": "^2.14.1",
"@umijs/lint": "^4.0.52",
"@umijs/max": "^4.0.52",
"concurrently": "^9.0.1",
"cross-env": "^7.0.3",
"electron": "^32.1.2",
"eslint": "^8.34.0",
"express": "^4.18.2",
"gh-pages": "^3.2.3",
"jest": "^29.4.3",
"jest-environment-jsdom": "^29.4.3",
"lint-staged": "^10.5.4",
"mockjs": "^1.1.0",
"prettier": "^2.8.4",
"swagger-ui-dist": "^4.15.5",
"ts-node": "^10.9.1",
"typescript": "^4.9.5",
"umi-presets-pro": "^2.0.2",
"wait-on": "^8.0.1"
},
"engines": {
"node": ">=12.0.0"
}
}
2.创建的app文件夹下的配置
(1)创建main.js主文件(electron主配置文件)
- mainWindow.loadURL('http://localhost:8000/');这边的端口要跟前端运行起来的端口一致
// electron打包配置
const {
app,
BrowserWindow,
globalShortcut,
// dialog
} = require('electron');
const path = require('path');
const isPro = process.env.NODE_ENV !== 'development';
const remote = require('@electron/remote/main');
remote.initialize();
// window对象的全局引用
let mainWindow;
function createWindow() {
mainWindow = new BrowserWindow({
minWidth: 800, // 最小宽度
minHeight: 600, // 最小高度
width: 1000,
heigth: 800,
title: '标题',
// autoHideMenuBar: true, // 关闭工具栏
// frame: false, // 设置为false后为无边框窗口,即无法拖拽,拉伸窗体大小,没有菜单项
icon: path.join(__dirname, './assets/logo.ico'),
webPreferences: {
preload: path.join(__dirname, 'preload.js'), // 预加载文件
// 是否启用Node integration
nodeIntegration: true, // Electron 5.0.0 版本之后它将被默认false
// 是否在独立 JavaScript 环境中运行 Electron API和指定的preload 脚本.默认为 true
contextIsolation: false, // Electron 12 版本之后它将被默认true
},
});
remote.enable(mainWindow.webContents);
// 注册快捷键
globalShortcut.register('CommandOrControl+M', () => {
mainWindow.maximize();
});
globalShortcut.register('CommandOrControl+T', () => {
mainWindow.unmaximize();
});
globalShortcut.register('CommandOrControl+H', () => {
mainWindow.close();
});
// 引入目录
require(path.join(__dirname, 'menu.js'));
if (isPro) {
// 生产环境
mainWindow.loadFile(`${__dirname}/build/index.html`);
// mainWindow.loadURL(`http://192.168.10.15:30102`);
} else {
// 这边要对应前端dev运行起来的端口
mainWindow.loadURL('http://localhost:8000/');
// 打开开发者工具
mainWindow.webContents.openDevTools();
}
// 解决应用启动白屏问题
mainWindow.on('ready-to-show', () => {
mainWindow.show();
mainWindow.focus();
});
// // 关闭窗口弹框确认
// mainWindow.on("close", (e) => {
// e.preventDefault();
// dialog.showMessageBox(mainWindow, {
// type: "warning",
// title: "关闭",
// message: "是否要关闭窗口",
// buttons: ["取消", "确定"],
// }).then((index) => {
// if (index.response === 1) {
// app.exit();
// }
// })
// })
// 关闭时触发下列事件
mainWindow.on('closed', () => {
mainWindow = null;
});
}
// 是否允许打开多个窗口
// const gotTheLock = app.requestSingleInstanceLock()
// if (!gotTheLock) {
// // 检测到本次未取得锁,即有已存在的实例在运行,则本次启动立即退出,不重复启动。
// app.quit()
// } else {
// app.on('second-instance', (event, commandLine, workingDirectory) => {
// // 监听到第二个实例被启动时,检测当前实例的主窗口,并显示出来取得焦点
// if (mainWindow) {
// if (mainWindow.isMinimized()) mainWindow.restore()
// mainWindow.focus()
// }
// })
// }
app.on('ready', createWindow);
// 热加载
try {
require('electron-reloader')(module, {});
} catch (_) {}
// 所有窗口关闭时退出应用
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if ((mainWindow = null)) {
createWindow();
}
});
// app.on('before-quit', (event) => {
// dialog.showOpenDialog({
// })
// // 询问用户是否退出
// event.preventDefault() // 阻止本次退出
// })
(2)创建目录文件menu.js(桌面客户端运行时顶部的菜单配置)
const { app, Menu } = require('electron');
const isMac = process.platform === 'darwin';
const template = [
// { role: 'appMenu' } // 如果是mac系统才有
...(isMac
? [
{
label: app.name,
submenu: [
{
role: 'about',
},
{
type: 'separator',
},
{
role: 'services',
},
{
type: 'separator',
},
{
role: 'hide',
},
{
role: 'hideOthers',
},
{
role: 'unhide',
},
{
type: 'separator',
},
{
role: 'quit',
},
],
},
]
: []),
// { role: 'fileMenu' }
{
label: '文件',
submenu: [
{
label: '新建',
accelerator: 'CmdOrCtrl+N',
click: () => {},
},
{
label: '打开',
accelerator: 'CmdOrCtrl+O',
click: () => {},
},
{
label: '保存',
accelerator: 'CmdOrCtrl+S',
click: () => {},
},
{
type: 'separator',
}, // 分割线
,
isMac
? {
role: 'close',
}
: {
role: 'quit',
label: '退出',
},
],
},
// { role: 'editMenu' }
{
label: '编辑',
submenu: [
{
role: 'undo',
label: '撤消',
},
{
role: 'redo',
label: '恢复',
},
{
type: 'separator',
},
{
role: 'cut',
label: '剪切',
},
{
role: 'copy',
label: '复制',
},
{
role: 'paste',
label: '粘贴',
},
...(isMac
? [
{
role: 'pasteAndMatchStyle',
},
{
role: 'delete',
},
{
role: 'selectAll',
},
{
type: 'separator',
},
{
label: 'Speech',
submenu: [
{
role: 'startSpeaking',
},
{
role: 'stopSpeaking',
},
],
},
]
: [
{
role: 'delete',
label: '删除',
},
{
type: 'separator',
},
{
role: 'selectAll',
label: '全选',
},
]),
],
},
// { role: 'viewMenu' }
{
label: '查看',
submenu: [
{
role: 'reload',
label: '重新加载',
},
{
role: 'forceReload',
label: '强制重新加载',
},
{
role: 'toggleDevTools',
label: '切换开发工具栏',
},
{
type: 'separator',
},
{
role: 'resetZoom',
label: '原始开发工具栏窗口大小',
},
{
role: 'zoomIn',
label: '放大开发工具栏窗口',
},
{
role: 'zoomOut',
label: '缩小开发工具栏窗口',
},
{
type: 'separator',
},
{
role: 'togglefullscreen',
label: '切换开发工具栏全屏',
},
],
},
// { role: 'windowMenu' }
{
label: '窗口',
submenu: [
{
role: 'minimize',
label: '最小化',
},
...(isMac
? [
{
type: 'separator',
},
{
role: 'front',
},
{
type: 'separator',
},
{
role: 'window',
},
]
: [
{
role: 'close',
label: '关闭',
},
]),
],
},
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
(3)创建preload.js文件(监听浏览器事件)
window.addEventListener('DOMContentLoaded', () => {
// const replaceText = (selector, text) => {
// const element = document.getElementById(selector)
// if (element) element.innerText = text
// }
// for (const dependency of ['chrome', 'node', 'electron']) {
// replaceText(`${dependency}-version`, process.versions[dependency])
// }
});
(4)app文件夹下创建package.json(electron项目打包所需相关依赖)
- 需要安装3个依赖
electron
electron-builder // 用于打包electron项目
electron-reloader // 用于编写electron时,代码保存可以热更新
- scripts下配置打包命令 dist-win32 dist-win64 dist-mac
- app文件夹下的package.json,直接复制到你的项目中,然后在app文件夹下执行yarn安装依赖
// package.json
{
"name": "electron_project",
"version": "0.0.1",
"description": "description",
"author": "jack",
"main": "main.js",
"scripts": {
"dist-win32": "electron-builder --win --ia32",
"dist-win64": "electron-builder --win --x64",
"dist-mac": " electron-builder --mac"
},
"dependencies": {
"@electron/remote": "^2.0.10"
},
"devDependencies": {
"electron": "^25.1.0",
"electron-builder": "^23.6.0", // 用于打包
"electron-reloader": "^1.2.3" // 用于编写electron时,代码保存可以热更新
},
"build": { // electron打包配置
"productName": "electron_project",
"appId": "com.example.app",
"directories": {
"output": "dist"
},
"files": [ // 打包时需要保存的文件
"build/**/*",
"main.js",
"menu.js",
"preload.js",
],
"extraMetadata": {
"main": "./main.js"
},
"win": {
"icon": "./assets/logo.ico"
},
"mac": {
"icon": "./assets/logo.ico"
},
"linux":{
"icon": "./assets/logo.ico"
}
}
}
三、运行&打包
- 打包正常的HTML静态页面还是在根目录下打包,这个不变
- 打包exe安装包需在app文件夹下打包,并且静态的HTML资源需要放在build文件夹中,才能打进去
1.运行
- 根目录下执行
yarn electron-dev
即可运行web端,web端启动后,会自动运行electron桌面端应用,并且支持热更新
2.打包流程
打包需要分两步:
(1)打包web端文件
在根目录执行
yarn build
(2)打包electron容器
把根目录打包后的web端文件放入app下的build文件夹中
再在app文件夹下运行
dist-win32/dist-win64
打包后的exe最终会输出到app/dist文件夹下
四、打包后使用的问题
- baseURL接口地址的配置
生产环境baseURL一般都是空,跟后端打包在一起使用的。但是打包成exe后,无法放在一起了,baseURL需要写成固定的 - publicPath静态资源地址的问题
正常是publicPath: '/' 也就是默认在根目录访问
如果项目部署在某个文件夹下,publicPath: '/project/' 代表在project文件夹中部署,那静态资源会去project文件夹中找
但是打包成exe后,前端的位置就变成了安装路径,所以要用相对路径。 publicPath: './'
五、修改图标
1.修改窗口图标
2.更换Electron应用程序的桌面图标
可以是PNG格式或者ICO格式,ICO格式直接改后缀是不行的,打包会报错,必须通过转换工具转一下,并且大小不能超过256*256