前言
想做一个桌面应用程序,一直没有找到简单快速可上手的框架。刚好有点前端的底子,就发现了Electron。关于Electron的介绍,请移步 https://www.electronjs.org/ 查阅。
简单来说,引用官网的话,Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架,就是可以用前端代码开发跨平台的桌面应用程序(GUI);又因为有CSS的存在,所以可以引入很多的UI样式库或者手搓样式,让应用美观一些。
我相信很多朋友也可能喜欢写点桌面应用程序或者称为小工具,所以本篇适合想用前端来实现功能的伙伴,因此需要至少有html、css、js、Vue2基础。
废话少说,先看下完成图。
很简单的功能,就当作一个Demo吧。
其中关键在于如何组织Vue路由、创建子窗口、在 Vue Component 中如何与主进程通信(渲染进程与主进程)、如何设置图标等内容,东西不多不少,刚好够起步。当把这些配置好了就可以专注于应用功能的开发了。
共19000左右字的Demo教程,准备好纸巾,让我们开始吧!
目录
4.2 窗口进阶,使某个窗口加载指定 Vue Component
零、项目源码
hyf/utils-hub-demo - 码云 - 开源中国https://gitee.com/fan-hongyu/utils-hub-demo一、基本环境说明
- npm版本:8.6.0
- node版本:v18.0.0
- Vue/cli版本:@vue/cli 5.0.8
- IDE:WebStorm
二、技术栈(框架)
- Vue 2
- ElementUI
- Electron 13
三、环境搭建
为避免篇幅冗长(最终还是冗长了),基础环境搭建请移步我的另一篇文章
【桌面应用程序】Vue-Electron 环境构建、打包与测试(Windows)-CSDN博客
环境搭建好了如图
注释掉自动安装 VUEJS_DEVTOOLS,否则可能导致启动缓慢。
src/background.js
启动示例
四、分析与开发
4.1 自定义菜单栏的实现
4.1.1 菜单显示
首先我们来实现菜单栏的显示。
在 src 下新建一个 menu.js 文件,用来实现菜单的逻辑。
src/menu.js
import {Menu} from "electron";
const template = [
{
label: '帮助',
submenu: [
{
label: '关于',
accelerator: 'CmdOrCtrl+H'
}
]
}
]
Menu.setApplicationMenu(Menu.buildFromTemplate(template))
代码解释:从 electron 将 Menu 导入进来;定义一个菜单栏模板,里面包含了一级菜单帮助,他有一个子选项 关于,快捷键为 Mac(Command)/ Windows(Ctrl) + H 。通过 Menu.setApplicationMenu传入一个Menu.buildFromTemplate(模板),即可实现自定义菜单栏实现。
现在菜单就定义完了,如何应用到窗口上呢?
在 backgroud.js 中引入即可。
src/backgroud.js
//引入自定义菜单
require('./menu')
启动测试,可以看到已经实现了。需要什么,就在 模板数组 中添加即可。
4.1.2 公共窗口创建
为了使点击 “关于” 选项,可以弹出新窗口。那么我们需要一个创建窗口的方法。
思考:应用可能会随着开发功能越来越多,那么每次创建窗口都要写一个单独的方法吗?这样会导致代码冗余,难以维护。
所以要创建一个公共的创建窗口的方法。
在 src 下新建一个 windowManager.js 文件,并填写以下代码。
src/windowManager.js
/** 窗口管理器 */
import {BrowserWindow} from "electron";
const path = require('path')
/**
*
* @param param 窗口参数对象
*/
const winURL = process.env.NODE_ENV === 'development'
? 'http://localhost:8080'
: `file://${__dirname}/index.html`
let win;
function commonCreateWindow(param) {
let win = new BrowserWindow({
width: param.width || 400,
height: param.height || 200,
autoHideMenuBar: param.isAutoHideMenuBar || false,
title: param.title || 'utils-hub',
show: param.show || false,
minWidth: param.minWidth || 400,
minHeight: param.minHeight || 200,
//如果没有传icon,那么就使用默认的图标,在 public/下
icon: path.join(__dirname, param.iconName || 'app.ico'),
minimizable: param.minimizable,
maximizable: param.maximizable,
resizable: param.resizable,
closable: true,
webPreferences: {
preload: path.resolve(__dirname, './preload.js'),
nodeIntegration: false,
contextIsolation: true,
enableRemoteModule: false, // 禁用 remote 模块以提高安全性
}
})
// win.webContents.on('did-finish-load', () => {
// win.setTitle(param.title); // 设置窗口标题
// win.show();
// })
win.loadURL(`${winURL}` + "/#/sub-win/" + param.url);
// console.log(win)
win.once('ready-to-show', () => {
if (param.isMax) {
win.maximize();
}
win.setTitle(param.title)
win.show(); // 显示窗口
});
// 打开开发者工具(仅在开发环境中启用)
if (process.env.NODE_ENV === 'development') {
win.webContents.openDevTools();
}
win.on('closed', () => {
console.log('执行')
win = null;
});
}
export default commonCreateWindow;
让我们来解读以下以上代码:
- 定义了一个winUrl,判断当前是否为开发环境。如果是,那么窗口就加载 http://localhost:8080(当启动electron时,开发环境会启动本地服务器,从而使页面加载);如果不是,那么通过file协议访问打包后的通过node构建路径(__dirname)来找到你的index.html,并让窗口加载此文件。由于我们的Vue实例是挂载到index.html中,所以可以显示Vue组件内容。(构建的文件将会被自动注入)
- 定义了一个方法,接受窗口参数并创建窗口。调用 electron 的 new BrowserWindow 来实现一个窗口的创建。
- 窗口参数解释
属性 | 释义 |
---|---|
width | 窗口宽度 |
height | 窗口高度 |
autoHideMenuBar | 自动隐藏菜单栏 |
title | 窗口标题 |
show | 是否显示窗口 |
minWidth | 窗口最小宽度 |
minHeight | 窗口最小高度 |
icon | 窗口图标,没有则使用 public/favicon.icon |
minimizable | 窗口是否可以被最小化 |
maximizable | 窗口是否可以被最大化 |
webPreferences.preload | 预加载脚本,实现渲染进程与主进程通信 |
webPreferences.nodeIntegration | 禁用 Node.js 集成以提高安全性 |
webPreferences.contextIsolation | 启用上下文隔离以进一步增强安全性 |
webPreferences.enableRemoteModule | 禁用 remote 模块以提高安全性 |
- win.once('ready-to-show',()=>{}),在该方法中调用show方法,用于避免electron启动后白屏或闪屏现象。
- param.isMax,自定义的属性。窗口启动后是否立即最大化显示。
- win.webContents.openDevTools(); 该上下行代码表示,是否启动开发者工具,建议开发环境开启此选项,用于排查错误。
- win.loadURL(`${winURL}` + "/#" + param.url); ,用于加载窗口的路径,此处使用Vue 路由,并且 mode 为 hash。参考 electron实现打开子窗口,窗口加载vue路由指定的组件页面_vue electron单独打开子窗口-CSDN博客
- win.on('closed', () =>{}),监听窗口关闭事件,将win置为null。
- || xxx 为默认值。
通过解读以上代码,大家应该了解了窗口的创建过程。下面我们创建预加载脚本。
在 src 下新建 preload.js
src/preload.js
console.log('预加载脚本执行')
打印测试。
当然现在还不能启动,因为启动了也会报找不到 preload.js。
配置 vue.config.js
vue.config.js
const {defineConfig} = require('@vue/cli-service')
module.exports = defineConfig({
// 禁用eslint
lintOnSave: false,
transpileDependencies: true,
//添加预加载脚本
pluginOptions:{
electronBuilder:{
preload: {
preload: 'src/preload.js' // 确保路径正确
},
}
}
})
这样,就定义好了。然后执行
npm run build
好了,现在预加载脚本就配置好了。
4.1.3 功能实现
现在创建窗口的方法有了,预加载脚本也有了,是不是能实现点击 “关于” 选项,弹出窗口了呢?
测试一下,让我们回到 menu.js ,并在 关于 中添加 click 。
src/menu.js
import {Menu} from "electron";
//导入公共创建窗口的方法
import commonCreateWindow from "@/windowManager";
const template = [
{
label: '帮助',
submenu: [
{
label: '关于',
accelerator: 'CmdOrCtrl+H',
//添加单击方法
click: () => {
//构建 关于 窗口参数对象
const aboutWindowObject = {
//宽高使用默认值
//自动隐藏菜单栏
isAutoHideMenuBar: true,
title: '关于',
//默认隐藏
//最大宽度和最大高度使用默认值
//TODO 对于ICON,最后处理
//禁止最大化最小化
minimizable: false,
maximizable: false,
//禁止窗口调整大小
resizable:false,
//禁止启动后最大化
isMax: false,
url: 'about-win'
}
//调用公共创建窗口的方法
commonCreateWindow(aboutWindowObject)
}
}
]
}
]
Menu.setApplicationMenu(Menu.buildFromTemplate(template))
启动程序测试。
启动后,发现 dist_electron目录中多了preload.js,这说明已经预加载脚本配置好了。
点击 帮助 -> 关于,或者使用快捷键 Ctrl+h。
此时可以看到,窗口已经成功弹出了,并且预加载脚本也执行了。
4.2 窗口进阶,使某个窗口加载指定 Vue Component
当我们打开了 “关于” 窗口后,发现了一个问题,这个窗口所显示的内容并不是我们想要的。
我需要让这个窗口加载我的Vue页面(About.vue)。
先看下我们的 windowManger.js 中的commonCreateWindow方法中的一段代码。
win.loadURL(`${winURL}` + "/#" + param.url);
我们在上面创建 “关于” 窗口时,aboutWindowObject 对象中没有 url属性。所以我们的窗口访问路径就是 http://localhost:8080/#(开发环境)
所以打开浏览器访问这个地址,显示出的页面和关于页面一致,这是没问题的。
好了,现在我们要加载自己的内容。
4.2.1 安装并配置Vue路由
//安装适用于Vue2的Vue-Router
npm install vue-router@3 --save
在 src 下,新建 router 目录,并且新建 index.js
路由文件创建好了,先不管。
4.2.2 布局组件与子窗口
我们要使用electron创建桌面应用,不仅有窗口,还有页面。为了将窗口与页面文件区分开,我们在 src 下新建 views 目录,并在里面新建 pages 和 windows。
- src/views
- pages
- windows
那就可以直接写功能页面/窗口了吗?不,我们还需要一个布局组件。
在 src 下,我们可以发现有一个 components 目录,就它了。
在 src/components 下,新建 BaseLayout.vue 与 SubWindowLayout.vue。一个用于页面的布局,一个用于窗口的布局。
全部新建为 Options API 组件(只会Vue2)
里面放点啥呢?
简单来说,如果要是 SubWindowLayout.vue 中添加了一个按钮,那么如果你的嵌套路由中以此组件为布局组件后,所有依赖此路由的子路由的窗口都会有这个按钮。
添加 <router-view></router-view> 标签
<div>
<router-view></router-view>
</div>
如需了解更多Vue-Router信息,请移步 Vue Router | The official Router for Vue.js
接下来,我们创建 AboutWin.vue(为了与页面文件区分,所有窗口采用 xxxWin.vue 的命名方式)
在 src/views/windows/ 新建 AboutWin.vue。如果你喜欢更加清晰的层次,可以再新建一层about目录。
src/views/windowsAboutWin.vue
<script>
export default {
name: "AboutWin"
}
</script>
<template>
<div class="desc">
<p>这是使用Electron开发的一款集成了多个工具的软件。</p>
<p>版本:v1.0</p>
</div>
</template>
<style scoped>
.desc {
margin-top: 40px;
font-size: 12px;
}
</style>
现在布局组件有了,关于 窗口也有了,那就配置下路由吧。
src/router/index.js
import Vue from 'vue';
import Router from 'vue-router';
/** 引入窗口布局组件 */
import SubWinLayout from "@/components/SubWindowLayout.vue";
/** 引入窗口 */
import AboutWin from "@/views/windows/AboutWin.vue";
Vue.use(Router)
const routes = [
{
path: '/sub-win',
component: SubWinLayout,
children: [
{
path: 'about-win', component: AboutWin
}
]
}
]
const router = new Router({
mode: 'hash',
routes
});
export default router;
简单解释一下:引入布局组件和关于窗口,定义路由规则。其中,mode必须为hash,使用history模式的话会找不到路径。参考
electron实现打开子窗口,窗口加载vue路由指定的组件页面_vue electron单独打开子窗口-CSDN博客
在 src/main.js 中引入路由。
src/main.js
import Vue from 'vue'
import App from './App.vue'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import router from './router/index.js';
Vue.use(ElementUI)
Vue.config.productionTip = false
new Vue({
router,
render: h => h(App),
}).$mount('#app')
现在我们要稍微改动一下 src/menu.js 中的一段代码
src/menu.js
// 旧的
win.loadURL(`${winURL}` + "/#" + param.url);
// 新的
win.loadURL(`${winURL}` + "/#/sub-win/" + param.url);
由于我所有的子窗口路由都基于 sub-win 这个父路由,所以可以把这写死。如果有多个父路由,请保持旧有的即可,然后在传参时,传父+子(或者使用逻辑判断)。
现在我们修改 src/menu.js 中的菜单栏 关于 选项的click方法,添加 url 参数。
src/menu.js
url: 'about-win'
修改 src/App.vue,删除其他内容,添加 <router-view></router-view>
src/App.vue
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'App',
}
</script>
<style>
</style>
启动测试。
4.3 页面、功能窗口的实现
好了,帮助 -> 关于 窗口弹出并显示指定内容已经完成了。
接下来实现一下页面上的功能。目前程序启动之后,页面是空白的,因为App.vue中仅有一个<router-view></router-view>,路由中没有指定根路由,所以什么都没有。
创建根页面
在 src/views/ 下,创建index.vue,用于首页。
src/views/index.vue
<script>
import Dashboard from "@/views/pages/Dashboard.vue";
import CommonUtils from "@/views/pages/CommonUtils.vue";
export default {
name: "index",
components: {CommonUtils, Dashboard},
data() {
return {
activeIndex: 'dashboard',
currentDate:''
}
},
methods: {
handleSelect(val) {
console.log(val)
this.activeIndex = val;
},
getCurrentDate(){
const now = new Date();
// 使用 toLocaleString 格式化日期和时间
this.currentDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
}
},
created() {
this.getCurrentDate();
setInterval(() => {
this.getCurrentDate();
}, 1000);
}
}
</script>
<template>
<div>
<div><h3><i class="el-icon-time"> {{currentDate}}</i></h3></div>
<nav>
<!--菜单拦内容-->
<el-menu :default-active="activeIndex" class="el-menu-demo" mode="horizontal" @select="handleSelect">
<el-menu-item index="dashboard"> <i class="el-icon-odometer"></i>控制板</el-menu-item>
<el-menu-item index="common-utils"> <i class="el-icon-suitcase"></i>常用工具</el-menu-item>
</el-menu>
</nav>
<!-- 动态显示Component-->
<br>
<div>
<Dashboard v-show="activeIndex==='dashboard'"></Dashboard>
<CommonUtils v-show="activeIndex==='common-utils'"></CommonUtils>
</div>
</div>
</template>
<style scoped>
</style>
简单解释一下:引入了两个自定义的组件,Dashboard.vue 和 CommonUtils.vue ,一个用于显示Dashboard,一个用于显示常用工具。通过ElementUI的 <el-menu></el-menu> 实现切换。
还有一个动态时间,实现每秒刷新。
以下是两个组件的内容
src/views/pages/Dashboard.vue
<script>
export default {
name: "Dashboard",
data(){
return{
}
}
}
</script>
<template>
<div>
<p>Welcome to Utils-Hub.</p>
</div>
</template>
<style>
</style>
Dashboard.vue 页面很简单,就一个p标签。
src/views/pages/CommonUtils.vue
<template>
<div>
<div v-for="(val) in utilsList">
<el-card class="box-card" @click.native="createUtilsWin(val.flag)">
<div slot="header" class="clearfix">
<h2><i :class="val.icon"></i> {{ val.title }}</h2>
</div>
<div class="descCls">
{{ val.desc }}
</div>
</el-card>
</div>
</div>
</template>
<script>
export default {
name: "CommonUtils",
methods: {
createUtilsWin(flag) {
if(flag === "other"){
this.$message.warning('暂无更多需求!')
return false;
}
//创建窗口
window.api.createUtilsWindow(flag);
}
},
data() {
return {
utilsList: [
{
id: '1',
flag: 'transfer',
title: '大小写转换',
desc: '键入字符,将其转换为大写字符或小写字符',
icon: 'el-icon-refresh'
},
// {id: '2', flag: 'regexp', title: '正则表达式', desc: '使用简单的表达式完成复杂的需求', icon: 'el-icon-cpu'},
// {id: '3', flag: 'cronExp', title: 'Cron表达式', desc: '? * * * * *', icon: 'el-icon-cpu'},
// {
// id: '4',
// flag: 'TableNameExtract',
// title: '表名提取',
// desc: '仅适用于MySQL。选择标准的.sql格式文件,从中提取表名',
// icon: 'el-icon-top'
// },
// {
// id: '5',
// flag: 'uuidGenerator',
// title: 'UUID生成器',
// desc: '生成标准的uuid。',
// icon: 'el-icon-s-opportunity'
// },
{id: '6', flag: 'other', title: 'Other', desc: '需求加载中...', icon: 'el-icon-loading'},
]
}
}
}
</script>
<style scoped>
.box-card {
width: 250px;
height: 200px;
float: left;
margin-left: 43px;
margin-top: 40px;
}
.box-card:hover {
border: 1px solid #409EFF;
cursor: pointer;
}
.descCls {
font-size: 12px;
}
</style>
CommonUtils.vue 页面也很简单,让我们来解读一下:
定义了一个 utilsList 的工具列表数组,里面写的是一些常用的工具对象。使用v-for将其遍历到el-card 上,从而实现一个工具对象对应一个卡片。当点击一个卡片时,弹出对应工具卡片功能的窗口。
其中,需要注意的是,每个工具对象中,有一个 flag属性,用于窗口标识。
整段代码中,最关键的部分就是
window.api.createUtilsWindow(flag);
为便于理解,请看以下图示。
4.3.1 渲染进程与主进程通信
简单来说,就是Vue组件中定义一个函数,用于处理页面的事件;然后此函数再调用由preload.js中暴露出来的方法,通过window对象调用;preload.js中通过ipcRederer与主进程ipcMain通信。
对于ipcRenderer 有两种方式通信方法,对应ipcMain两种接收的方法
- ipcRenderer.send 发送给主进程消息,不关心处理结果,类似void。主进程通过ipcMain.on来接收,随后处理。
- ipcRenderer.invoke 发送给主进程调用方法,需要返回值。主进程通过ipcMain.handle来处理,随后返回返回值。
4.3.2 工具页面点击创建窗口实现
现在回过头来看看我们的页面怎么样了。
什么都没有。是的,如果有东西就奇怪了。
添加路由
src/router/index.js
import Vue from 'vue';
import Router from 'vue-router';
/** 引入窗口布局组件 */
import SubWinLayout from "@/components/SubWindowLayout.vue";
import BaseLayout from "@/components/BaseLayout.vue";
/** 引入窗口 */
import AboutWin from "@/views/windows/AboutWin.vue";
/** 引入页面 */
import index from '@/views/index.vue'
import Dashboard from "@/views/pages/Dashboard.vue";
import CommonUtils from "@/views/pages/CommonUtils.vue";
Vue.use(Router)
const routes = [
{
path: "/",
component: BaseLayout,
children: [
{path: '', component: index},
{path: 'dashboard', component: Dashboard},
{path: 'common-utils', component: CommonUtils},
]
},
{
path: '/sub-win',
component: SubWinLayout,
children: [
{
path: 'about-win', component: AboutWin
}
]
}
]
const router = new Router({
mode: 'hash',
routes
});
export default router;
启动测试。
直接点击 大小写转换 卡片将提示 createUtilsWindow 是未定义的。
现在打开我们的preload.js,并加入以下内容
src/preload.js
const {contextBridge, ipcRenderer} = require('electron')
contextBridge.exposeInMainWorld('api',{
createUtilsWindow:(flag)=>{
ipcRenderer.send('create-utils-window',flag)
}
})
由于我们只需要创建一个窗口,并不需要返回值,所以使用ipcRenderer.send方法。
现在的情况是,渲染进程(Vue Component)中定义好创建窗口的方法了,预加载脚本(preload.js)也定义好了,那么现在就差主进程了。
项目中肯定不只有一个渲染进程与主进程的通信方法,所以我们把监听逻辑放在一个单独的文件中。
src 下新建 listener.js 。
src/listener.js
/** 主进程监听与处理 */
import {ipcMain} from 'electron'
ipcMain.on('create-utils-window',(e,flag)=>{
console.log('主进程方法被调用了')
})
顺便提一句,如果觉得会有很多 ipcRenderer.send 和 ipcMain.on,两个文件来回跑怕打错的话,可以将其定义为常量。
伪代码
- const CREATE_UTILS_WINDOW = 'create-utils-window';
- ipcRenderer.send(CREATE_UTILS_WINDOW);
- ipcMain.on(CREATE_UTILS_WINDOW);
继续,在background.js 中引入 listener.js,并且设置preload.js
//引入监听
require('./listener')
//预加载脚本
preload:path.resolve(__dirname,'./preload.js'),
启动测试。
可以看到,成功打印了。主进程在终端打印,渲染进程在控制台打印。
让我们修改一下listener.js监听方法的内容,实现窗口的创建。
src/listener.js
/** 主进程监听与处理 */
import {ipcMain} from 'electron'
import commonCreateWindow from '@/windowManager'
ipcMain.on('create-utils-window', (e, flag) => {
// console.log('主进程方法被调用了')
//创建窗口
let windowObject = {}
//判断flag
switch (flag) {
case "transfer":
windowObject.width = 800;
windowObject.height = 600;
windowObject.isAutoHideMenuBar = true;
windowObject.title = '大小写转换';
windowObject.minWidth = 800;
windowObject.minHeight = 600;
//TODO ICON稍后处理
windowObject.isMax = true;
windowObject.url = 'transfer-win';
break;
default:
break;
}
commonCreateWindow(windowObject)
})
定义好了,现在缺的是路由和窗口页面。
新建
src/views/windows/commonUtils/TransferWin.vue
<script>
export default {
methods: {
/** 复制内容到剪切板 */
async copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
this.$message.success('内容已复制!')
} catch (err) {
this.$message.error('复制失败!')
}
},
/** 转大写 */
async handleTransferToUpper() {
this.destText = this.srcText.toUpperCase();
await this.copyToClipboard(this.destText)
},
/** 转小写 */
async handleTransferToLower() {
this.destText = this.srcText.toLowerCase();
await this.copyToClipboard(this.destText)
},
/** 清空 */
handleClear() {
this.destText = '';
this.srcText = '';
},
},
name: "TransferWin",
data() {
return {
srcText: '',
destText: ''
}
}
}
</script>
<template>
<div>
<el-card class="box-card">
<div slot="header" class="clearfix">
<h2>大小写转换</h2>
</div>
输入小写或大写字符,将其转换为大写或小写。
</el-card>
<el-input
class="common"
type="textarea"
:rows="6"
placeholder="请输入需要转换的内容"
v-model="srcText">
</el-input>
<div class="common">
<el-button type="primary" icon="el-icon-refresh-right" @click="handleTransferToUpper">转大写</el-button>
<el-button type="primary" icon="el-icon-refresh-left" @click="handleTransferToLower">转小写</el-button>
<el-button type="danger" icon="el-icon-delete-solid" @click="handleClear">清空</el-button>
</div>
<el-input
class="common"
type="textarea"
:rows="6"
readonly
placeholder="Console"
v-model="destText">
</el-input>
</div>
</template>
<style scoped>
.common {
margin-top: 20px;
}
</style>
功能实现很简单,就是输入字符,将其转为大写或小写,并自动复制到剪切板,可清空。
添加路由
import TransferWin from "@/views/windows/commonUtils/TransferWin.vue";
{path: 'transfer-win', component: TransferWin},
启动测试。
到这功能基本就完事了,剩下还有一些小细节需要处理一下。
4.4 窗口图标与托盘显示
现在我们每个窗口的图标都是一样的,原因在于
没传图标参数。
4.4.1 主窗口图标设置
主窗口的图标设置会关联到应用的首页图标和任务栏图标。
为了使图标管理起来不那么混乱,我们需要一个文件来存储icon的名称。
在 src 下新建 iconManager.js
src/iconManager.js
/** 图标管理器 */
export const ICON_PATHS = {
//应用程序的图标,首页左上角角标、任务栏角标、托盘角标
APP_ICON: 'winIcon/app.ico',
//关于窗口的图标
ABOUT_WIN_ICON:'winIcon/about.ico',
//大小写转换窗口左上角角标
TRANSFER_WIN_ICON: 'winIcon/transfer.ico',
//右下角角标 打开 icon
OPEN_ICON:'winIcon/open.ico',
//右下角角标 退出 icon
EXIT_ICON:'winIcon/exit.ico',
}
此处需要注意,当是开发环境的时候,图标从 dist_electron 目录读取;当打包后,图标是从 public 目录获取。(因为__dirname)
所以需要在 public 和 dist_electron 目录中新建 winIcon 目录
将对应的图标放到这两个文件夹中。
打开 src/background.js,设置应用图标。
src/background.js
import * as iconManger from '@/iconManager'
icon:path.resolve(__dirname,iconManger.ICON_PATHS.APP_ICON)
启动测试。
4.4.2 子窗口设置图标
子窗口图标只涉及左上角角标
打开
src/listener.js
import * as iconManger from '@/iconManager'
//TODO ICON稍后处理
windowObject.iconName = iconManger.ICON_PATHS.TRANSFER_WIN_ICON;
启动测试。
4.4.3 托盘设置
悬浮提示文字
右键菜单
新建文件
src/tray.js
// tray.js
const { Tray, Menu } = require('electron');
const path = require('path');
import * as iconSupport from '@/iconManager'
let appTray = null;
let win = null; // 你需要从外部传递窗口实例
function createTray(app,mainWindow,nativeImage) {
win = mainWindow;
// 打开图标缩小设置
const openResizedIcon = nativeImage.createFromPath(path.join(__dirname, iconSupport.ICON_PATHS.OPEN_ICON)).resize({
width: 16,
height: 16
});
// 退出图标缩小设置
const exitResizedIcon = nativeImage.createFromPath(path.join(__dirname, iconSupport.ICON_PATHS.EXIT_ICON)).resize({
width: 16,
height: 16
});
// 系统托盘右键菜单
let trayMenuTemplate = [
{
label: '打开',
icon: openResizedIcon,
click: function () {
win.show();
win.maximize();
}
},
{
label: '退出',
icon: exitResizedIcon,
click: function () {
app.quit();
app.quit();
}
}
];
appTray = new Tray(path.join(__dirname, iconSupport.ICON_PATHS.APP_ICON));
const contextMenu = Menu.buildFromTemplate(trayMenuTemplate);
appTray.setToolTip('utils-hub');
appTray.setContextMenu(contextMenu);
// 点击托盘图标时显示窗口
appTray.on('click', function () {
win.show();
win.maximize();
});
}
export default createTray;
解读一下:
首先将两个图标进行缩放,因为太大了不好看;然后定义了一个托盘菜单模板,有 打开 和 退出功能;最后设置托盘图标,将菜单应用上。当单击菜单时,显示程序。创建托盘的方法需要三个参数,应用实例,窗口以及一个用于处理图片的对象。
我们在
src/background.js中调用一下这个方法。
src/background.js
import { app, protocol, BrowserWindow,nativeImage } from 'electron'
import createTray from '@/tray'
createTray(app,win,nativeImage);
启动测试。
托盘就设置好了,但是现在有一个问题就是当我们点击主窗口的关闭时,程序就退出了,一般的应用可能会询问用户是退出还是驻托盘。
咱们这里不搞那么复杂了,就默认关闭按钮是驻托盘,想要退出的话使用托盘右键退出。
修改
src/background.js 的逻辑
首先将 win 从 const 改为 let
src/background.js
// 部分代码
let win = new BrowserWindow({})
win.on('close', (e) => {
if (win.isMinimized()) {
win = null;
} else {
e.preventDefault();
win.minimize();
win.hide();
}
})
解读一下:监听关闭事件,如果窗口当前是最小化的,那么就置为null;如果不是,那么就最小化并隐藏(不显示窗口)
启动测试。
可以看到窗口虽然已经关闭了,但是托盘区还是有程序存在的。单击或右键打开,都可以使窗口重新显示。
到现在,开发工作完成了。
五、打包
5.1 打包Electron程序
首先进行打包配置
vue.config.js
builderOptions: {
appId: 'com.utils.hub',
productName: 'UtilsHubDemo',
directories: {
output: 'build'
},
win: {
// 应用图标,这里要确保图标文件存在且路径正确,一般为.ico 格式
icon: 'public/winIcon/app.ico',
// 目标架构,可以是 x64、ia32 等,根据实际需求选择
target: [
{
target: 'nsis', // 使用 NSIS 打包
arch: ['x64'] // 指定架构
}
]
},
}
执行吗?
我先执行试试(此处不贴图,打包n次中)
一番操作,发现两个问题。
- 关于窗口没有图标,忘设置了。
import * as iconManager from '@/iconManager'
iconName:iconManager.ICON_PATHS.APP_ICON,
- ElementUI 图标没有了
参考 [已解决]electron-builder vue 打包后iconfont/element-ui字体图标不显示问题_vue打包后element图标没有了-CSDN博客
在项目 public 目录下新建 element-ui 目录
然后打开项目的node_modules目录,搜索element-ui,将其中的theme-chalk目录拷贝到 public/element-ui 下
修改
public/element-ui/theme-chalk/index.css
搜索fonts,在前面都添加 ./
修改前
修改后
修改
public/index.html
加入以下内容
<!-- 添加此行 -->
<link rel="stylesheet" href="<%= BASE_URL %>element-ui/theme-chalk/index.css">
至此,问题应该都会解决了,重新打包测试。
完成!
5.2 使用inno setup 打包为安装程序
当electron打包完成后,目录下会自动生成一个 Setup.exe,双击即可安装。
但是没法选择目录什么的。(应该是需要配置nsis脚本,奈何不会呀!)
所以我们看见上面还有一个文件夹,顾名思义应该是未打包的(直译)。那么我的理解就是,把这个文件夹拿到哪,里面的程序都能正常运作(Windows平台)。
所以下载 Inno Setup
可以使用图形化的方式进行打包过程。
下载
默认没有中文(不是软件的中文,是用于你的应用安装时的中文)。
需要下载中文包
选择简体中文下载即可。
保存下载语言包之后(注意后缀名不要是txt),打开inno setup的安装目录,放到language目录下即可。
开始打包
这里选择前边说的win-unpacked/下面的UtilsHubDemo.exe
此处需要注意,因为仅有这一个exe是跑不起来,它还需要win-unpacked目录中的文件支持。所以我们在其他地方新建一个文件夹 test。
然后将win-unpacked中除 UtilsHubDemo.exe 之外的文件拷贝到 test 文件夹中。
继续打开inno setup操作
选择刚刚的 test 文件夹
该应用没有相关的后缀文件
建议勾选最后一个选项,勾选之后,用户安装的时候会询问
仅为我安装还是为所有用户安装(为所有用户安装需要管理员权限)。
默认即可
我这里选择仅支持简体中文。
输出目录、输出文件名、图标
保存脚本文件
六、安装测试
打开两个虚拟机,Win10和Win7测试效果。
Win10
Win7
七、项目源码
hyf/utils-hub-demo - 码云 - 开源中国https://gitee.com/fan-hongyu/utils-hub-demo
结语
至此,就从开发到打包到安装全都实现啦!由于笔者水平有限,其中涉及性能、安全、高级语法的问题可能未进行过多说明,望包涵!:)
非常感谢您能看到最后!
希望可以帮到你!
最后,感谢开源!