需求推进技术探索,技术实现需求设计。刚开始本来是用 vue 做一个 web 项目,随着开发推进,产品要求做成桌面端应用。。。
想到了用 Electron 框架构建桌面应用程序,查了一下大多都是使用 Electron 一步步构建项目,把已有 web 项目打包成桌面应用文档比较少,不过也找到一篇不错的文档,下面记录一下踩坑过程。
Electron 文档
Electron API 文档
利用electron和electron-builder把前端web项目生成桌面程序
Node.js
,使用最新的LTS版本原镜像下载可能太慢或下载失败,可将 npm 和 electron 的镜像地址都切换为淘宝镜像,cmd 命令输入:
npm config edit
将下面的地址复制、粘贴、保存、关闭
registry=http://registry.npm.taobao.org/
electron_mirror=https://npm.taobao.org/mirrors/electron/
electron-builder-binaries_mirror=https://npm.taobao.org/mirrors/electron-builder-binaries/
.
├─icons # icon 图标目录
├─node_modules
├─h5 # vue 资源
├─gulpfile.js # gulp 配置文件
├─main.js # 应用程序的入口文件
├─package.json
├─README.md
# windows
mkdir my-electron-app && cd my-electron-app
复制的官方文档,运行居然报错,PowerShell 提示:标记 “&&” 不是此版本中的有效语句分隔符
将命令行语句中的 && 改为分号 ; 就好了
# windows
mkdir my-electron-app; cd my-electron-app
{
"name": "my-electron-app",
"version": "1.0.0",
"author": "freeman",
"description": "electron app",
"private": false,
"scripts": {
"view": "cross-env ELECTRON_ENV=dev electron .",
"start": "cross-env ELECTRON_ENV=dev .\\node_modules\\.bin\\gulp watch:electron",
"build": "cross-env ELECTRON_ENV=dep electron-builder"
},
"main": "main.js",
"build": {
"productName": "my electron app",
"appId": "com.wss.app",
"directories": {
"output": "builder"
},
"nsis": {
"oneClick": false,
"allowElevation": true,
"allowToChangeInstallationDirectory": true,
"installerIcon": "./h5/favicon.ico",
"uninstallerIcon": "./h5/favicon.ico",
"installerHeaderIcon": "./h5/favicon.ico",
"createDesktopShortcut": true,
"createStartMenuShortcut": true
},
"win": {
"target": [
"nsis",
"zip"
]
},
"files": [
"h5/**/*",
"main.js"
]
},
"devDependencies": {
"cross-env": "^7.0.3",
"electron": "^16.0.7",
"electron-builder": "^22.14.5",
"electron-connect": "^0.6.3",
"gulp": "^4.0.2"
}
}
cross-env
,运行开发环境和打包时方便指定环境变量,同时引入了gulp
配置热更新,编辑代码自动刷新应用"scripts": {
"view": "cross-env ELECTRON_ENV=dev electron .",
"start": "cross-env ELECTRON_ENV=dev .\\node_modules\\.bin\\gulp watch:electron",
"build": "cross-env ELECTRON_ENV=dep electron-builder"
},
设置入口文件为main.js
打包相关配置
"build": {
"productName": "my electron app", // 项目名 这也是生成的exe文件的前缀名
"appId": "com.wss.app", // 包名
"directories": { // 输出文件夹
"output": "builder"
},
"nsis": {
"oneClick": false, // 是否一键安装
"allowElevation": true, // 允许请求提升。 如果为false,则用户必须使用提升的权限重新启动安装程序。
"allowToChangeInstallationDirectory": true, // 允许修改安装目录
"installerIcon": "./h5/favicon.ico", // 安装图标
"uninstallerIcon": "./h5/favicon.ico", // 卸载图标
"installerHeaderIcon": "./h5/favicon.ico", // 安装时头部图标
"createDesktopShortcut": true, // 创建桌面图标
"createStartMenuShortcut": true // 创建开始菜单图标
},
"win": {
"target": [
"nsis",
"zip"
]
},
"files": [
"h5/**/*", // 要打包的目录
"main.js"
]
}
关于
Electron
项目出现白屏,控制台输出 Failed to load resource: net::ERR_FILE_NOT_FOUND 问题
npm run start
运行时出现:
执行npm run start
,窗口出现白屏,控制台输出找不到资源,可能是 vue 项目的publicPath
路径配置不对(vue 项目通过 npm run build 打包以后页面没有内容)npm run build
打包之后出现白屏:
files
里面要打包的目录可能指定错了,没有把我们要打包的目录指定对,electron-build 就不会将build文件夹打包进去 app.asar 文件里,这样安装完.exe
文件之后,控制台可能输出Not allowed to load local resource:
,桌面程序打开白屏。
参考:electron-builder 打包后 出现Not allowed to load local resource:
Node.js
,npm
依赖我都是安装在devDependencies
里,如果刚开始安装在dependencies
,打包的时候也会提示放到devDependencies
"devDependencies": {
"cross-env": "^7.0.3",
"electron": "^16.0.7",
"electron-builder": "^22.14.5",
"electron-connect": "^0.6.3",
"gulp": "^4.0.2"
}
因为想要实现热更新,安装了gulp
,还需要创建gulpfile.js
配置文件,这里只监听了main.js
const gulp = require('gulp');
const electron = require('electron-connect').server.create();
gulp.task('watch:electron', function () {
electron.start();
gulp.watch(['./main.js'], electron.restart);
});
vue 项目路由模式使用hash
,将打包后的 vue 资源放到 electron 项目根目录
const {
electron,
app,
BrowserWindow,
Menu,
screen
} = require('electron')
const path = require('path')
// app.isPackaged 如果应用已打包,则返回的属性,此属性可用于区分开发和生产环境
const isDevelopment = !app.isPackaged;
if(isDevelopment) {
try {
const client = require('electron-connect').client;
} catch (err) {
}
}
// 屏蔽安全告警在console控制台的显示
process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'
if (process.mas) app.setName('My Electron')
let mainWindow
function createWindow() {
const displayWorkAreaSize = screen.getAllDisplays()[0].workArea
const windowOptions = {
x: displayWorkAreaSize.x, // 窗口相对于屏幕的左偏移位置.默认居中
y: displayWorkAreaSize.y, // 窗口相对于屏幕的顶部偏移位置.默认居中
width: displayWorkAreaSize.width,
height: displayWorkAreaSize.height,
minWidth: 1220,
minHeight: 780,
show: false,
// movable: false, // 窗口是否可以拖动
// maximizable: false, // 窗口是否可以最大化
// titleBarStyle: 'hidden',
frame: true, // 指定 false 来创建一个 Frameless Window
title: app.getName(),
icon: path.join(__dirname, '/icons/icon.png'), // 托盘图标
hasShadow: false, // 窗口是否有阴影
autoHideMenuBar: true, // 自动隐藏菜单栏
webPreferences: {
devTools: process.env.ELECTRON_ENV == "dev",// 是否开启 DevTools
webSecurity: true, // //允许跨域
plugins: true, // 是否开启插件支持
experimentalFeatures: true, // 开启 Chromium 的 可测试 特性
experimentalCanvasFeatures: true, // 开启 Chromium 的 canvas 可测试特性
minimumFontSize: 10,
},
}
if (process.platform === 'linux') {
windowOptions.icon = path.join(__dirname, '/h5/favicon.ico')
}
mainWindow = new BrowserWindow(windowOptions)
mainWindow.loadURL(path.join('file://', __dirname, '/h5/index.html'))
// 设置窗口默认最大化
mainWindow.maximize()
// 设置用户是否可以调节窗口尺寸
// mainWindow.setResizable(false)
mainWindow.show()
mainWindow.on('closed', function () {
mainWindow = null
})
process.env.ELECTRON_ENV == "dev" && client.create(mainWindow);
}
app.on('ready', function () {
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu) // 设置菜单部分
createWindow()
})
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
})
app.on('activate', function () {
if (mainWindow === null) createWindow()
})
app.on('browser-window-created', function () {
let reopenMenuItem = findReopenMenuItem()
if (reopenMenuItem) reopenMenuItem.enabled = false
})
app.on('window-all-closed', function () {
let reopenMenuItem = findReopenMenuItem()
if (reopenMenuItem) reopenMenuItem.enabled = true
app.quit()
})
/**
* 注册键盘快捷键
*/
let template = [
{
label: '操作',
submenu: [
{
label: '回到系统入口',
accelerator: 'CmdOrCtrl+Q',
click: function (item, focusedWindow) {
if (focusedWindow.id === 1) {
BrowserWindow.getAllWindows().forEach(function (win) {
if (win.id > 1) {
win.close()
}
})
}
focusedWindow.loadURL(path.join('file://', __dirname, '/h5/index.html'))
}
}, {
label: '复制',
accelerator: 'CmdOrCtrl+C',
role: 'copy'
}, {
label: '粘贴',
accelerator: 'CmdOrCtrl+V',
role: 'paste'
}, {
label: '重新加载',
accelerator: 'CmdOrCtrl+R',
click: function (item, focusedWindow) {
if (focusedWindow) {
// on reload, start fresh and close any old
// open secondary windows
if (focusedWindow.id === 1) {
BrowserWindow.getAllWindows().forEach(function (win) {
if (win.id > 1) {
win.close()
}
})
}
focusedWindow.reload()
}
}
}
]
},
{
label: '窗口',
role: 'window',
submenu: [
{
label: '切换全屏',
accelerator: 'F11',
role: 'togglefullscreen',
// click: function (item, focusedWindow) { // 自定义 click 和使用 role 效果是一样的
// if (focusedWindow) {
// focusedWindow.setFullScreen(!focusedWindow.isFullScreen());
// }
// }
}, {
label: '最小化',
accelerator: 'CmdOrCtrl+M',
role: 'minimize'
}, {
label: '关闭',
accelerator: 'CmdOrCtrl+W',
role: 'close'
}
]
},
{
label: '帮助',
role: 'help',
submenu: [{
label: '意见反馈',
click: function () {
electron.shell.openExternal('https://www.electronjs.org/zh/docs/latest/api/app')
}
}]
}
]
function findReopenMenuItem() {
const menu = Menu.getApplicationMenu()
if (!menu) return
let reopenMenuItem
menu.items.forEach(function (item) {
if (item.submenu) {
item.submenu.items.forEach(function (item) {
if (item.key === 'reopenMenuItem') {
reopenMenuItem = item
}
})
}
})
return reopenMenuItem
}
// 设置是否显示 DevTools
if(process.env.ELECTRON_ENV == "dev") {
template[1].submenu.push({
label: '切换开发者工具',
role: 'toggleDevTools'
})
}
// 针对Mac端的一些配置
if (process.platform === 'darwin') {
const name = electron.app.getName()
template.unshift({
label: name,
submenu: [{
label: '退出',
accelerator: 'Command+Q',
click: function () {
app.quit()
}
}]
})
// Window menu.
template[3].submenu.push({
type: 'separator'
}, {
label: 'Bring All to Front',
role: 'front'
})
// addUpdateMenuItems(template[0].submenu, 1)
}
// 针对Windows端的一些配置
if (process.platform === 'win32') {
const helpMenu = template[template.length - 1].submenu
// addUpdateMenuItems(helpMenu, 0)
}
main.js
配置项可对照 Electron API 文档设置。本项目是把已有的 vue 项目打包,不是完全开发 Electron 应用,窗口菜单选项只能配置,不方便完全自定义。
// main.js
process.env.ELECTRON_ENV == "dev" && client.create(mainWindow);
function createWindow() {
const windowOptions = {
// ...
icon: path.join(__dirname, '/icons/icon.png'), // 托盘图标
// ...
}
}
配置图标这里需要注意,根据官方文档 nativeImage说明,eletron 可以设置大小为 32x32、64x64 。。。等一些尺寸的图标,但是同样指定一个 6464 的
png
,通过npm run build
打包发现报错 (必需为256256),同时还发现只添加一张照片也不行,会报错,于是指定了三张不同尺寸的 icon 图片。
build 报错截图:
icons 目录截图:
index.html
位置mainWindow.loadURL(path.join('file://', __dirname, '/h5/index.html'))
预览
npm run start
打包,打包输入目录在package.json
中配置
npm run build
打包之后,报错 A JavaScript error occurred in the main process
查了好久没有发现相关的问题,那就一定是项目中的配置存在问题,
通过图片发现是打包之后electron-connect
报错,报错原因是因为electron-connect
在开发环境安装的包,打包之后是不会安装的,生产引入未安装的包会报这种错,所以引入时要先判断下环境。
// main.js
// app.isPackaged 如果应用已打包,则返回的属性,此属性可用于区分开发和生产环境
const isDevelopment = !app.isPackaged;
if(isDevelopment) {
try {
const client = require('electron-connect').client;
} catch (err) {
}
}