这里写自定义目录标题
- 安装electron
- 第一种方式:vue init electron-vue
- 第二种方式:vue add electron-builder
- 启动electron
- 调试功能:background操作和使用
- 1、覆盖窗口的菜单上下文、右键菜单
- 2、监听关闭事件、阻止默认行为
- 3、创建悬浮窗口
- 4、窗口置顶
- 5、绑定全局快捷键
- 6、托盘、打包后的应用名称和图标logo
- 7、多次双击桌面的应用图标,导致启动了多个相同的应用程序
- 完整代码background.js
- 打包问题
- 1、electron-v12.2.3-win32-x64.zip
- 2、winCodeSign-2.6.0.7z.zip
- 3、nsis 和 nsis-resources
官方文档:
https://www.electronjs.org/zh/docs/latest/api/menu
安装electron
安装electron有两种方式,两种方式创建的项目结构大不相同
以下内容主要讲解第二种方式,在现有的vue项目中使用electron
// 方式一:基于electron-vue 创建新项目
vue init simulatedgreg/electron-vue 项目名
// 方式二:在现有的vue项目中安装electron包
vue add electron-builder
第一种方式:vue init electron-vue
vue init simulatedgreg/electron-vue 项目名
整体结构如下:
package.json大致的结构如下:
{
"name": "name",
"version": "0.0.1",
"author": "jmyinjg <jmyinjg@163.com>",
"description": "An electron-vue project",
"license": null,
"main": "./dist/electron/main.js",
"scripts": {
"build": "node .electron-vue/build.js && electron-builder",
"build:dir": "node .electron-vue/build.js && electron-builder --dir",
"build:clean": "cross-env BUILD_TARGET=clean node .electron-vue/build.js",
"build:web": "cross-env BUILD_TARGET=web node .electron-vue/build.js",
"dev": "node .electron-vue/dev-runner.js",
"pack": "npm run pack:main && npm run pack:renderer",
"pack:main": "cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.main.config.js",
"pack:renderer": "cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.renderer.config.js",
"postinstall": ""
},
"build": {
"productName": "midtools",
"appId": "com.jthe.midtools",
"directories": {
"output": "build"
},
"files": [
"dist/electron/**/*"
],
"dmg": {
"contents": [
{
"x": 410,
"y": 150,
"type": "link",
"path": "/Applications"
},
{
"x": 130,
"y": 150,
"type": "file"
}
]
},
"mac": {
"icon": "build/icons/icon.icns"
},
"win": {
"icon": "build/icons/icon.ico"
},
"linux": {
"icon": "build/icons"
}
},
"dependencies": {
"@jmyinjg/topsdk": "^1.6.0",
"ali-oss": "^6.9.0",
"axios": "^0.18.0",
"cheerio": "^1.0.0-rc.3",
"iconv-lite": "^0.5.1",
"mysql": "^2.18.1",
"request": "^2.88.2",
"superagent": "^5.2.2",
"uuid": "^8.1.0",
"view-design": "^4.2.0",
"vue": "^2.5.16",
"vue-electron": "^1.0.6",
"vue-router": "^3.0.1",
"vuex": "^3.0.1",
"vuex-electron": "^1.0.0"
},
"devDependencies": {
"ajv": "^6.5.0",
"babel-core": "^6.26.3",
"babel-loader": "^7.1.4",
"babel-minify-webpack-plugin": "^0.3.1",
"babel-plugin-import": "^1.13.0",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.7.0",
"babel-preset-stage-0": "^6.24.1",
"babel-register": "^6.26.0",
"cfonts": "^2.1.2",
"chalk": "^2.4.1",
"copy-webpack-plugin": "^4.5.1",
"cross-env": "^5.1.6",
"css-loader": "^0.28.11",
"del": "^3.0.0",
"devtron": "^1.4.0",
"electron": "^2.0.4",
"electron-builder": "^20.19.2",
"electron-debug": "^1.5.0",
"electron-devtools-installer": "^2.2.4",
"file-loader": "^1.1.11",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "0.4.0",
"multispinner": "^0.2.1",
"node-loader": "^0.6.0",
"style-loader": "^0.21.0",
"url-loader": "^1.0.1",
"vue-html-loader": "^1.2.4",
"vue-loader": "^15.2.4",
"vue-style-loader": "^4.1.0",
"vue-template-compiler": "^2.5.16",
"webpack": "^4.15.1",
"webpack-cli": "^3.0.8",
"webpack-dev-server": "^3.1.4",
"webpack-hot-middleware": "^2.22.2",
"webpack-merge": "^4.1.3"
}
}
第二种方式:vue add electron-builder
vue add electron-builder
整体结构如下:
package.json大致的结构如下:
{
"name": "name",
"productName": "应用名称",
"version": "1.5.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"electron:build": "vue-cli-service electron:build",
"electron:serve": "vue-cli-service electron:serve",
"postinstall": "electron-builder install-app-deps",
"postuninstall": "electron-builder install-app-deps"
},
"main": "background.js",
"dependencies": {
"axios": "~0.21.1",
"core-js": "~3.15.2",
"vue": "~3.1.5",
"vue-axios": "~3.2.4",
"vue-router": "~4.0.10",
"vuex": "~4.0.2",
......
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.13",
"@vue/cli-plugin-eslint": "~4.5.13",
"@vue/cli-plugin-router": "~4.5.13",
"@vue/cli-plugin-vuex": "~4.5.13",
"@vue/cli-service": "~4.5.13",
"@vue/compiler-sfc": "~3.1.5",
"babel-eslint": "~10.1.0",
"compression-webpack-plugin": "~6.1.1",
"electron": "^12.0.0",
"electron-devtools-installer": "^3.1.0",
"eslint": "~6.8.0",
"eslint-plugin-vue": "~7.13.0",
"postcss": "~8.3.5",
"vue-cli-plugin-electron-builder": "~2.1.1"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
启动electron
安装完成后,直接启动一下,看看效果
npm run electron:serve
Reload Ctrl+R 刷新
Toggle Developer Tools Ctrl+Shift+I 打开开发者工具
调试功能:background操作和使用
'use strict'
import { app, protocol, BrowserWindow } from 'electron'
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'
const isDevelopment = process.env.NODE_ENV !== 'production'
protocol.registerSchemesAsPrivileged([
{ scheme: 'app', privileges: { secure: true, standard: true } }
])
// 创建主窗口
async function createWindow() {
const win = new BrowserWindow({
width: 800, // 窗口的默认宽度
height: 600, // 窗口的默认高度
title: '当前窗口的标题',
webPreferences: {
nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION
}
})
if (process.env.WEBPACK_DEV_SERVER_URL) {
await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL)
// 开发环境下,打开调试工具
if (!process.env.IS_TEST) win.webContents.openDevTools()
} else {
createProtocol('app')
// 主窗口创建完成后进入到
win.loadURL('app://./index.html')
}
}
// 监听整个应用的关闭操作
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
// 关闭应用
app.quit()
}
})
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
app.on('ready', async () => {
if (isDevelopment && !process.env.IS_TEST) {
try {
await installExtension(VUEJS_DEVTOOLS)
} catch (e) {
console.error('Vue Devtools failed to install:', e.toString())
}
}
createWindow()
})
if (isDevelopment) {
if (process.platform === 'win32') {
process.on('message', (data) => {
if (data === 'graceful-exit') {
app.quit()
}
})
} else {
process.on('SIGTERM', () => {
app.quit()
})
}
}
1、覆盖窗口的菜单上下文、右键菜单
import {BrowserWindow, Menu } from 'electron'
let mainWin
// 主窗口的顶栏菜单
const template = [
{
label: '后退',
click: () => {
// 判断 浏览器是否可以后退到前一页面。
if (mainWin.webContents.canGoBack()) {
// 使浏览器回退到上一个页面。
mainWin.webContents.goBack();
}
}
},
{
label: '前进',
click: () => {
// 判断 浏览器是否可以前进到下一页面。
if (mainWin.webContents.canGoForward()) {
// 使浏览器前进到下一个页面。
mainWin.webContents.goForward();
}
}
},
{
label: '刷新',
click: () => {
mainWin.webContents.reloadIgnoringCache();
}
},
];
// 右键菜单
const contextMenu = Menu.buildFromTemplate([
{ label: '复制', role: 'copy' },
{ label: '粘贴', role: 'paste' }
]);
async function createWindow() {
mainWin = new BrowserWindow({
...
})
// 部署顶栏菜单
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
// 部署右键菜单
mainWin.webContents.on('context-menu', () => {
contextMenu.popup({ window: mainWin }); // 在指定窗口上显示上下文菜单
});
}
2、监听关闭事件、阻止默认行为
dialog.showMessageBoxSync会返回buttons的索引值,
例如点击了“否”,则choice=0;点击了“是”,则choice=1
注意:如果点击了右上角的叉,则choice=0,所以,“是”只能写在“否”后面
不理解为什么要这么设计
import { BrowserWindow, dialog } from 'electron'
let mainWin
async function createWindow() {
mainWin = new BrowserWindow({
...
})
// 监听 主窗口 右上角的关闭事件
mainWin.on('close', (e) => {
e.preventDefault(); // 阻止窗口默认的关闭行为
/**
* 弹出 对话框询问用户是否真的想要退出
* 这里的choice 值是buttons的索引,
* 例如点击了“否”,则choice=0;点击了“是”,则choice=1
* 注意:如果点击了右上角的叉,则choice=0,所以,“是”只能写在“否”后面
*/
const choice = dialog.showMessageBoxSync(null, {
type: 'info',
buttons: ['否', '是'],
title: '退出',
message: '确定要退出吗?再次开启需重新登录',
});
// 如果用户选择“是”,则关闭窗口
if (choice === 1) {
// 关闭窗口并释放内存
mainWin.destroy(); // 关闭主窗口
app.quit()
}
})
}
3、创建悬浮窗口
创建第二个窗口——悬浮球,透明背景,点击悬浮球,促使主窗口进入到某个页面
包含的知识点:
1、多个窗口,悬浮球窗口
2、其他窗口促使主窗口路由跳转
3、区分当前环境是electron,还是浏览器
4、窗口的显示和隐藏
background.js
import { BrowserWindow, ipcMain, screen } from 'electron'
let mainWinRoutePath = '/'; // 主窗口的当前路由
let mainWin, ballWin
async function createSBallWindow() {
mainWin = new BrowserWindow({
...
})
...
}
// 创建 工具球窗口
async function createSBallWindow() {
ballWin = new BrowserWindow({
width: 65, // 悬浮窗口的宽度 比实际DIV的宽度要多5px,用于处理阴影
height: 65, // 悬浮窗口的高度 比实际DIV的高度要多5px,用于处理阴影
type: 'toolbar', //创建的窗口类型为工具栏窗口
frame: false, // 是否创建有边框的窗口(含最大化/最小化/关闭功能),false创建无边框窗口
resizable: false, // 是否允许窗口大小缩放
movable: true, // 是否可以移动
show: true, // 是否在创建完成后,让窗口显示,如果希望在用户登录后再显示工具球,则设置成false,再通过消息来打开窗口
transparent: true, // 设置透明,只有在frame为false时才能生效
hasShadow: false, // 是否显示阴影
alwaysOnTop: true, // 窗口是否总是显示在其他窗口之前
webPreferences: {
// devTools: true, // 是否打开调试工具,如果要打开调试,最好把窗口的默认大小调大一些,且不要设置初始位置
nodeIntegration: true,
contextIsolation: false,
}
})
// 通过获取用户屏幕的宽高来设置悬浮球的初始位置,最好把窗口的默认大小调大一些,且不要设置初始位置
const size = screen.getPrimaryDisplay().workAreaSize
ballWin.setPosition(size.width - 150, size.height - 200) // 设置悬浮球位置
if (process.env.WEBPACK_DEV_SERVER_URL) {
await ballWin.loadURL(`${process.env.WEBPACK_DEV_SERVER_URL}#/ball`)
// ballWin.webContents.openDevTools(); // 打开调试工具
} else {
await ballWin.loadURL('app://./index.html#/ball')
}
ballWin.on('close', () => {
ballWin = null
})
}
// 监听 open-mainWin-window 消息, 显示 主窗口
ipcMain.on('open-mainWin-window', () => {
if (!mainWin) {
return
}
mainWin.maximize() // 窗口最大化
})
// 显示 工具球窗口
ipcMain.on('open-ballWin-window', () => {
if (ballWin) {
ballWin.show()
} else {
// 若窗口不存在,则先创建窗口再显示
createSBallWindow()
ballWin.show()
}
});
// 隐藏 工具球窗口
ipcMain.on('hide-ballWin-window', () => {
ballWin.hide()
});
// 保存主窗口的当前路由
ipcMain.on('route-change', (event, route) => {
mainWinRoutePath = route;
});
// 返回保存的路由信息
ipcMain.handle('get-mainWin-route', () => {
return mainWinRoutePath;
});
// 进入 主窗口的某个页面
ipcMain.on('change-page', async (event, url) => {
if (process.env.WEBPACK_DEV_SERVER_URL) {
await mainWin.loadURL(`${process.env.WEBPACK_DEV_SERVER_URL}#${url}`)
} else {
// 这里是为了配合router的mode: hash 一起使用
await ballWin.loadURL(url.format({
pathname: path.join(__dirname, '/index.html'),
protocol: 'file',
slashes: true,
hash: "#/ball"
}))
}
})
悬浮球页面 ball.vue
<template>
<div class="ball" @click="goFast">
<!-- dblclick双击 -->
<span>工具球</span>
</div>
</template>
<script>
// 判断是否在 electron 平台的环境当中
let ipcRenderer = null
const isElectron = navigator.userAgent.toLowerCase().indexOf(' electron/') >= 0
if (isElectron) {
ipcRenderer = require('electron').ipcRenderer
}
export default {
name: "ball",
data() {
return {
}
},
created() {
},
methods: {
goFast() {
if (isElectron) {
ipcRenderer.invoke('get-mainWin-route').then((route) => {
if (route != '/moral/record') {
// 消息名称、传参arg
ipcRenderer.send('change-page', '/ball')
}
// 发送 open-ballWin-window 消息
ipcRenderer.send('open-mainWin-window')
});
}
}
},
}
</script>
<style>
body {
background: transparent;
display: flex;
justify-content: center;
height: 100%;
// 这个是用于 移动窗口
/* -webkit-app-region: drag; */
}
.ball {
width: 60px;
height: 60px;
line-height: 60px;
border-radius: 50%;
box-sizing: border-box;
background: var(--primary-color);
box-shadow: 0 3px 5px 0 rgba(0, 0, 0, 0.4);
color: #fff;
background: linear-gradient(#40a9ff, #096dd9);
text-align: center;
cursor: pointer;
font-size: 14px;
/* -webkit-app-region: no-drag; */
}
</style>
router.js
import { createRouter, createWebHistory } from 'vue-router';
// 判断是否在 electron 平台的环境当中
const isElectron = navigator.userAgent.toLowerCase().indexOf(' electron/') >= 0
let ipcRenderer = null
if (isElectron) {
ipcRenderer = require('electron').ipcRenderer
}
const router = createRouter({
routes: [
{
path: '/index',
component: () => import('@/views/index'),
meta: { title: '首页' },
},
{
path: '/ball',
component: () => import('@/views/ball'),
meta: { title: '工具球' }
},
...
],
mode: 'hash', // 这里必须是hash
history: createWebHashHistory('/')
});
// 路由守卫
router.beforeEach((to, from, next) => {
if (isElectron) {
ipcRenderer.send('route-change', to.path)
}
next()
})
export default router;
注意:
1、如果使用了screen,一定要引入,否则会启动失败
2、打包后页面空白的,无法通过win.loadURL进行跳转
router的mode必须是hash,如果使用history,则通过win.loadURL(app://./index.html#${url}
)无法跳转
这是因为vue是单页面应用,但打包完成后,只有一个index.html,如果使用history,那么无法读取到index.html后面的路径(win.loadURL(app://./index.html#${url}
)),如果改成win.loadURL(app://./index/${url}
),打包后是就是空白的,没有进入到页面页面中,但调试的时候没问题
我的理解是,vue是单页面应用,所以打包后只有一个index.html页面,其他的都是组件,使用index.html才能显示主页面,然后通过#/url访问路由,显示某个组件中
4、窗口置顶
// 置顶并显示 主窗口
ipcMain.on('open-mainWin-window', () => {
if (!mainWin) {
return
}
mainWin.maximize() // 窗口最大化
// 当 ballWin 失去置顶状态时,将 mainWin 的置顶级别设为较高值
mainWin.setAlwaysOnTop(true, 'normal', 1);
mainWin.focus(); // 让窗口获得焦点
mainWin.setAlwaysOnTop(false); // 如果不希望它一直保持在最上层,可以之后立即取消置顶,即点击其他应用时,当前窗口会下移一层
})
5、绑定全局快捷键
这里会有个问题,注册之后,只要这个窗口没有隐藏,快捷键响应后,还是会触发事件
例如下方是F5,假设你在QQ应用上,点了F5,当前这个应用也会刷新
import { globalShortcut } from 'electron'
let mainWin, ballWin
...
// 全局快捷键 刷新
globalShortcut.register('F5', () => {
if (mainWin && ballWin) {
mainWin.webContents.reloadIgnoringCache();
ballWin.webContents.reloadIgnoringCache();
}
})
6、托盘、打包后的应用名称和图标logo
1、设置打包后的应用名称和图标logo
background.js 窗口的图标
import { createWindow } from 'electron'
const path = require('path');
const isDevelopment = process.env.NODE_ENV !== 'production'
const logo = isDevelopment ? path.join(process.cwd(), 'public/logo.png') : path.join(__dirname, 'logo.png')
async function createWindow() {
mainWin = new BrowserWindow({
width: 1200,
height: 800,
// 窗口的图标
icon: logo,
...
})
}
注意图标的最小尺寸是 256*256
第一步:需安装 electron-icon-builder才能改变应用图标
npm i electron-icon-builder --save-dev
第二步:在package.json的scripts中补充命令后,再进行 npm i后会生成一个build文件夹,可以将图标放在该文件夹下
"name": "project name", // 打包后这个将作为 应用安装后的文件夹名称
"productName": "智慧校园", // 默认以这个来作为应用的名称
"version": "1.0.0",
"private": true,
"scripts": {
...
"electron:build": "vue-cli-service electron:build --mode test", // 打包
"electron:generate-icons": "electron-icon-builder --input=./public/favicon.png --output=build --flatten" // 应用图标的打包
}
第三步:新建一个electron-builder.json与package.json同层级
electron-builder.json
{
"appId": "your.app.id",
"productName": "应用名称", // 如果这里不设置,则默认以package.json中的productName来命名
"directories": {
"output": "dist_electron/bundled"
},
"files": [
"dist_electron/bundled/**/*"
],
// 图标位置,我这里没有放在build文件夹下
"icon": "public/logo.png"
}
2、托盘,实现关闭窗口时不是直接关闭整个应用,只是隐藏,就像QQ一样在任务栏右边有个小图标,点击图标时再显示窗口
background.js
import { app, dialog, ...., Tray } from 'electron'
const isDevelopment = process.env.NODE_ENV !== 'production'
const logo = isDevelopment ? path.join(process.cwd(), 'public/logo.png') : path.join(__dirname, 'logo.png')
let mainWin, ballWin, tray
async function createWindow() {
......
// 监听 主窗口 右上角的关闭事件
mainWin.on('close', (e) => {
e.preventDefault(); // 阻止窗口默认的关闭行为
// 隐藏应用
mainWin.hide()
})
}
....
app.on('ready', async () => {
// 托盘
tray = new Tray(logo)
// 托盘的右键菜单
const contextMenu = Menu.buildFromTemplate([
{
label: '退出', type: 'normal', click: () => {
// 退出整个应用
app.quit()
}
}
])
tray.setToolTip(process.env.VUE_APP_NAME)
tray.setContextMenu(contextMenu)
// 托盘的点击事件:弹出主窗口
tray.on('click', () => {
mainWin.show()
})
}
在打包前测试的时候能正常显示托盘栏图标,但是在打包后启动时显示托盘图标为空白,并且过了一会儿就自动消失了,但是程序仍在后台运行并没有退出。
值得一提的是左侧箭头指向的任务栏图标为默认图标是因为没有配置
通过观察编译后的文件夹就可以观察出,打包的时候并没有将托盘栏图标打包进去,而想要将其打包进去只要在vue.config.js配置一下electronBuilder即可:
module.exports = {
pluginOptions: {
electronBuilder: {
nodeIntegration: true,
builderOptions: {
//这部分是为了解决打包后的托盘栏图标不显示的问题
//功能是将public文件夹下的img复制到打包后的文件夹下的public文件夹下
extraResources: [
{
from: './public',
to: './public',
}
]
}
},
}
}
7、多次双击桌面的应用图标,导致启动了多个相同的应用程序
在第一次双击桌面的应用图标,开启应用程序时,会加载并显示窗口,第二次及之后再次双击应用图标,又会打开一个全新的应用程序。
官方给的解释是需要强制手动退出,且缺少一行代码,event.preventDefault();
// 判断是否已开启了第一实例:即已经打开了应用
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
app.quit()
} else {
// 试图运行第二个实例
app.on('second-instance', (event) => {
// 显示第一个实例的窗口
if (mainWin) {
if (mainWin.isMinimized()) mainWin.restore()
mainWin.focus()
mainWin.show()
}
// 并阻止第二个实例的启动(默认行为)
event.preventDefault();
})
app.whenReady().then(async () => {
await createWindow();
});
}
完整代码background.js
包含:
1、窗口的菜单、右键菜单、全局快捷键
2、监听窗口的关闭事件,阻止默认行为
3、创建悬浮窗口、窗口的隐藏/显示,促使其他窗口路径跳转
4、窗口置顶事件
5、区分当前环境是electron,还是浏览器
6、打包后页面空白的,无法通过win.loadURL进行跳转
7、托盘
8、改变打包后的exe文件的应用图标logo、名称
9、多次双击桌面的应用图标,导致启动了多个程序
'use strict'
import { app, protocol, BrowserWindow, ipcMain, dialog, Menu, Tray, screen } from 'electron';
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib';
import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer';
const path = require('path');
const url = require('url');
const isDevelopment = process.env.NODE_ENV !== 'production';
const logo = isDevelopment ? path.join(process.cwd(), 'public/logo.png') : path.join(__dirname, 'logo.png');
let mainWin, ballWin, tray
let shouldQuitApp = false
// 判断是否已开启了第一实例:即已经打开了应用
const gotTheLock = app.requestSingleInstanceLock()
// 右键菜单
const contextMenu = Menu.buildFromTemplate([
{ label: '复制', role: 'copy' },
{ label: '粘贴', role: 'paste' }
]);
// 主窗口的顶栏菜单
const template = [
{
label: '刷新',
click: () => {
mainWin.webContents.reloadIgnoringCache();
}
},
];
protocol.registerSchemesAsPrivileged([
{ scheme: 'app', privileges: { secure: true, standard: true } }
]);
// 创建主窗口
async function createWindow() {
mainWin = new BrowserWindow({
width: 1200,
height: 800,
icon: logo,
webPreferences: {
nodeIntegration: process.env.ELECTRON_NODE_INTEGRATION,
contextIsolation: !process.env.ELECTRON_NODE_INTEGRATION
}
});
mainWin.webContents.on('context-menu', () => {
contextMenu.popup({ window: mainWin }); // 在指定窗口上显示上下文菜单
});
// 禁用顶栏菜单
mainWin.setMenu(Menu.buildFromTemplate([]));
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
if (isDevelopment) {
await mainWin.loadURL(process.env.WEBPACK_DEV_SERVER_URL);
if (!process.env.IS_TEST) mainWin.webContents.openDevTools();
} else {
createProtocol('app');
mainWin.loadURL('app://./index.html');
}
createSBallWindow();
// 监听 主窗口 右上角的关闭事件
mainWin.on('close', (e) => {
if (!shouldQuitApp) {
e.preventDefault(); // 阻止窗口默认的关闭行为
// quit()
mainWin.hide();
}
});
mainWin.once('ready-to-show', () => {
mainWin.show();
});
}
// 创建 工具球窗口
async function createSBallWindow() {
ballWin = new BrowserWindow({
width: 65, // 悬浮窗口的宽度 比实际DIV的宽度要多5px
height: 65, // 悬浮窗口的高度
type: 'toolbar', //创建的窗口类型为工具栏窗口
frame: false, // 是否创建有边框的窗口(含最大化/最小化/关闭功能),false创建无边框窗口
resizable: false, // 是否允许窗口大小缩放
movable: true, // 是否可以移动
show: true, // 是否在创建完成后,让窗口显示
transparent: true, // 设置透明
hasShadow: false, // 是否显示阴影
alwaysOnTop: true, // 窗口是否总是显示在其他窗口之前
webPreferences: {
// devTools: true, // 是否打开调试工具,打开调试则最好调整窗口的大小
nodeIntegration: true,
contextIsolation: false,
}
});
// 通过获取用户屏幕的宽高来设置悬浮球的初始位置
const size = screen.getPrimaryDisplay().workAreaSize
ballWin.setPosition(size.width - 200, size.height - 200) // 设置悬浮球位置
if (isDevelopment) {
await ballWin.loadURL(`${process.env.WEBPACK_DEV_SERVER_URL}#/ball`);
// ballWin.webContents.openDevTools(); // 打开调试工具
} else {
await ballWin.loadURL(url.format({
pathname: path.join(__dirname, '/index.html'),
protocol: 'file',
slashes: true,
hash: "#/ball"
}));
}
ballWin.on('close', () => {
ballWin = null
})
}
/**
* 显示主窗口
* @param {*} maximize 是否最大化
* @returns
*/
function showMainWin(maximize) {
if (!mainWin) {
return
}
if (!mainWin.isMinimized() && mainWin.isVisible()) {
mainWin.hide();
return
}
if (maximize) {
mainWin.maximize()
} else {
mainWin.show()
}
if (ballWin) {
ballWin.on('always-on-top-changed', (event, isAlwaysOnTop) => {
if (isAlwaysOnTop) {
// 当 ballWin 处于最顶端时,将 mainWin 的置顶级别设为较低值
mainWin.setAlwaysOnTop(true, 'normal', -1);
} else {
// 当 ballWin 失去置顶状态时,将 mainWin 的置顶级别设为较高值
mainWin.setAlwaysOnTop(true, 'normal', 1);
mainWin.setAlwaysOnTop(false); // 如果不希望它一直保持在最上层,可以之后立即取消置顶
}
})
ballWin.setAlwaysOnTop(true, 'normal', 1);
} else {
mainWin.setAlwaysOnTop(true);
mainWin.setAlwaysOnTop(false);
}
}
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
// 显示主窗口
ipcMain.on('show-mainWin', (event, maximize) => {
showMainWin(maximize)
})
// 进入 主窗口的某个页面
ipcMain.on('change-page', async (event, url) => {
if (isDevelopment) {
// 去除url开头的/ 避免造成 http://localhost:8080//...
await mainWin.loadURL(`${process.env.WEBPACK_DEV_SERVER_URL}#${url}`);
} else {
await mainWin.loadURL(`app://./index.html#${url}`);
}
})
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
})
app.on('ready', async () => {
// 托盘
tray = new Tray(logo)
const contextMenu = Menu.buildFromTemplate([
{
label: '退出', type: 'normal', click: () => {
shouldQuitApp = true; // 设置标记为 true,表示应该退出应用
app.quit();
}
}
])
tray.setToolTip(process.env.VUE_APP_NAME);
tray.setContextMenu(contextMenu);
tray.on('click', () => {
mainWin.show();
})
if (isDevelopment && !process.env.IS_TEST) {
try {
await installExtension(VUEJS_DEVTOOLS)
} catch (e) {
console.error('Vue Devtools failed to install:', e.toString())
}
}
// 注册 全局快捷键 刷新
// globalShortcut.register('F5', () => {
// if (mainWin && ballWin) {
// mainWin.webContents.reloadIgnoringCache();
// ballWin.webContents.reloadIgnoringCache();
// }
// })
})
if (!gotTheLock) {
app.quit()
} else {
app.on('second-instance', (event) => {
// 有人试图运行第二个实例,我们应该关注我们的窗口
if (mainWin) {
if (mainWin.isMinimized()) mainWin.restore()
mainWin.focus()
mainWin.show()
}
// 并阻止第二个实例的启动
event.preventDefault();
})
app.whenReady().then(async () => {
await createWindow();
});
}
if (isDevelopment) {
if (process.platform === 'win32') {
process.on('message', (data) => {
if (data === 'graceful-exit') {
app.quit();
}
})
} else {
process.on('SIGTERM', () => {
app.quit();
})
}
}
打包问题
electron-builder打包electron-v12.2.3-win32-x64.zip下载失败,这种情况通常是有electron的4个包,无法下载成功,手动下载后放入到系统对应的文件夹中即可。
分别是
electron-v12.2.3-win32-x64.zip
winCodeSign-2.6.0.7z.zip
nsis-resources3.4.1
nsis-3.4.1
INFO Building app with electron-builder:
• electron-builder version=22.14.13 os=10.0.22621
• description is missed in the package.json appPackageFile=D:\web\campus\wisdom-campus-desktop\dist_electron\bundled\package.json
• author is missed in the package.json appPackageFile=D:\web\campus\wisdom-campus-desktop\dist_electron\bundled\package.json
• writing effective config file=dist_electron\builder-effective-config.yaml
• packaging platform=win32 arch=x64 electron=12.2.3 appOutDir=dist_electron\win-unpacked
⨯ Get "https://github.com/electron/electron/releases/download/v12.2.3/electron-v12.2.3-win32-x64.zip": proxyconnect tcp: dial tcp :0: connectex: The requested address is not valid in its context.
按照错误提示,是electron-v12.2.3-win32-x64.zip这个文件下载失败,可以利用报错信息中的https://github.com/electron/electron/releases/download/v12.2.3/electron-v12.2.3-win32-x64.zip
这个地址进行下载,或者到淘宝的镜像地址里去下载,
1、electron-v12.2.3-win32-x64.zip
到淘宝的镜像地址里去下载,https://registry.npmmirror.com/binary.html?path=electron/,依次找到 12.2.3版本下 -> electron-v12.2.3-win32-x64.zip
通常系统会默认隐藏AppData,关闭隐藏即可
下载后,将压缩包解压到这个目录里:
C:\Users\你的用户名\AppData\Local\electron\Cache
解压后的文件
2、winCodeSign-2.6.0.7z.zip
https://registry.npmmirror.com/binary.html?path=electron-builder-binaries/winCodeSign-2.6.0/winCodeSign-2.6.0.7z
下载后,将压缩包解压解压到这个目录里:
C:\Users\你的用户名\AppData\Local\electron-builder\Cache\winCodeSign
注意:这里最后一层是winCodeSign,有的文章里显示的是在Cache文件里解压,这样是错误的
解压后的:
3、nsis 和 nsis-resources
https://registry.npmmirror.com/binary.html?path=electron-builder-binaries/
下载后,将2个压缩包都解压到nsis文件价中,没有nsis文件夹就自己创建一个:
C:\Users\你的用户名\AppData\Local\electron-builder\Cache\nsis
解压后的
最后再改一下镜像
用cnpm
npm install -g cnpm --registry=https://registry.npm.taobao.org
或者直接用npm
npm config set registry https://registry.npmmirror.com
https://registry.npm.taobao.org 淘宝镜像在2024年1月份已经过期了,目前虽然还能使用,
但是可以观察到,最终还是代理到了 https://registry.npmmirror.com,所以不如直接用 ** https://registry.npmmirror.com**
再次打包electron 应该就没有什么问题了