【前言】此文档是本人在实际开发中所遇到的问题,且已经解决的,因为技术有限,可能有很多地方,很多代码可能都还存在漏洞,如有疑问或者有更好的解决方式,以及有代码可以优化的地方,欢迎留言,多谢大神指点。
ElectronBuild开发问题浅解
开机自启
之所以将这一项放在最前面,是因为这一点很重要,当需要主进程执行app.setLoginItemSettings 开机自启操作时,必须要将vue.config.js 中的"requestedExecutionLevel": "highestAvailable", 清除,否则将会导致app.setLoginItemSettings 失效
至于为什么要在主进程的监听中加入event.sender.send('setting-auto-start', true) 看似在重复发送通信,是因为在Electron API文档中有提到,因此个人认为加上也无妨
- 可以使用
event.reply(...) 将异步消息发送回发送者。 此方法将自动处理从非主 frame 发送的消息(比如: iframes)。相应的发送方法是: event.sender.send(...) 它将总是把消息发送到主 frame
ipcMain.on('setting-auto-start', async (event, flag) => {
if (flag) {
app.setLoginItemSettings({
openAtLogin: true,
openAsHidden: true,
path: process.execPath,
args: [
'--hideWindow', '"true"'
]
})
event.sender.send('setting-auto-start', true)
} else {
app.setLoginItemSettings({
openAtLogin: false,
openAsHidden: false,
path: process.execPath,
args: []
})
event.sender.send('setting-auto-start', false)
}
})
有关Webview
在使用webview 的preload时,需要注意的一点是preload需要传入的路径是一个协议路径,比如file:// 、http:// 等,而由于这里的path.join 获取的是编译后的路径,所以需要与当前环境区分开来,当然也可以将webview 封装成一个公共的组件,使用传值的方式加载相对应的src 和preload
<template>
<webview class="webview" :src="weburl" :preload="filepath" :title="title" nodeintegrationinsubframes disablewebsecurity></webview>
</template>
<script>
export default {
props: ['weburl', 'filepath', 'title']
}
</script>
if (process.env.NODE_ENV == 'production') {
this.info.preloadUrl = path.join(__dirname, 'preload.js')
} else {
this.info.preloadUrl = require('path').resolve('./src/utils/electron/preload.js')
}
Webview中常用监听方法和属性
webview.openDevTools() 开启访客端控制台did-start-loading 当WebView 链接加载时触发did-attach 嵌入WebView 内容时触发did-stop-loading WebView 加载完成时触发console-message WebView 访客端控制台信息接收ipc-message 接收WebView 页面的值dom-ready WebView 页面DOM加载完成时触发
在开发过程中,我们最有可能遇到的就是要对webview 访客界面中的事件、元素属性等进行操作。此时,我们可以通过使用webview.executeJavaScript() 函数将JS注入到访客页面中,当然这些也仅仅只是针对于一些公共的处理方法,如果有极个别的访客界面不需要这些公共的操作,那就需要进行特殊处理
比如这里需要对访客界面的所有超链接标签进行监听,点击超链接在客户端中新建一个tab打开标签中的链接,而其中也针对访客页面中的iframe界面进行处理:
const webview = document.querySelector('.webview')
webview.addEventListener("did-start-loading", () => {
webview.openDevTools()
webview.executeJavaScript(`
setTimeout(() => {
if (document) {
let nodeList = []
let type = 'default'
let tabContent = document.querySelectorAll('.el-tab-pane')
if (tabContent && tabContent.length) {
tabContent.forEach(item => {
if (item.firstChild.contentWindow) {
type = 'tdd'
nodeList = item.firstChild.contentWindow.document.querySelectorAll('body a')
}
})
} else {
nodeList = document.querySelectorAll('body a')
}
nodeList.length && nodeList.forEach(item => {
let attr_type = item.getAttribute('target')
if (attr_type != '_blank') return false
item.addEventListener('click', (event) => {
let info = {
title: type == 'tdd' ? '涂多多商城-' + event.srcElement.innerText : event.srcElement.innerText,
src: event.srcElement.href || event.srcElement.parentNode.href,
type: 'webview'
}
window.electron.send('ibi_href_click', info)
}, false)
})
}
}, 1500)
`)
})
有关webview用户登录
- 由于
webview 的监听事件都是在加载完访客链接地址后才能够执行,而在此之前我们需要将客户端登录的token 设置在访客页面的Cookie中,因此不能再使用webview.executeJavaScript() 的方式注入JS,这时,我们所引入的preload.js 就起来作用 - 因为每个
webview 都可以理解为每个单独的浏览器,相互之间的Cookie等数据都不能共同享用,因此为了能够达到所有的访客加载之前都能获取到客户端的token ,我个人这里是使用延迟加载的方式,将token 设置到访客额Cookie中,从而使访客可以在第一时间拿到token 进行登录后的操作
const electronStore = require('electron-store')
const electronCookie = new electronStore()
setTimeout(() => {
if (electronCookie.get('userInfo')) {
if (electronCookie.get('userInfo').access_token && document) {
document.cookie = 'token=' + electronCookie.get('userInfo').access_token
}
}
})
webview中调用ipcRenderer
当访客端需要向客户端发送通信指令时,可以在预加载文件(preload.js)中将electron 挂载到window上,从而使访客端可以直接通过window操作ipcRenderer 向客户端发送通信,同时也可以在preload 中设置相应的方法,以供访客端可以通过调用获取到客户端的数据信息
- 客户端设置
preload console.log('preload引入成功')
const {ipcRenderer, contextBridge} = require('electron')
const electronStore = require('electron-store')
const electronCookie = new electronStore()
setTimeout(() => {
if (electronCookie.get('userInfo')) {
if (electronCookie.get('userInfo').access_token && document) {
document.cookie = 'token=' + electronCookie.get('userInfo').access_token
}
}
})
contextBridge.exposeInMainWorld(
"electron", {
ipcRenderer: {
send: (channel, data) => {
ipcRenderer.send(channel, data);
},
},
send: (channel, data) => {
ipcRenderer.send(channel, data);
},
on: (channel, callback) => {
const newCallback = (_, data) => callback(_, data);
ipcRenderer.on(channel, newCallback);
},
once: (channel, callback) => {
const newCallback = (_, data) => callback(_, data);
ipcRenderer.once(channel, newCallback);
},
getToken: () => {
return electronCookie.get('userInfo').access_token
},
setUserToken: () => {
document.cookie = 'token=' + electronCookie.get('userInfo').access_token
}
}
);
- 访客端调用
preload if (window.electron) {
window.electron.send('check-login')
window.electron.ipcRenderer.send('check-login')
window.electron.on('check-login')
const token = window.electron.getToken()
}
封装进程监听
在实际开发中,我们可能会经常操作主进程和渲染进程之间的相互通信,而通信越多写的代码就会越多,如果不对这些监听进行分类和封装,久而久之会导致项目维护起来很痛苦,每次都要先去找到监听指令在哪,找到以后可能也并不知道这个指令是在操作下载还是用户退出,或者是链接跳转,因此,这里我个人是通过类的方式,将不同类型的通信处理区分开,这样就会一目了然,看网上有很多大佬都是通过js导入的方式,都是可以的
-
主进程封装 const { app, ipcMain } = require('electron')
exports.initCommon = function (win) {
ipcMain.on('setting-auto-start', async (event, flag) => {
if (flag) {
app.setLoginItemSettings({
openAtLogin: true,
openAsHidden: true,
path: process.execPath,
args: [
'--hideWindow', '"true"'
]
})
event.sender.send('setting-auto-start', true)
} else {
app.setLoginItemSettings({
openAtLogin: false,
openAsHidden: false,
path: process.execPath,
args: []
})
event.sender.send('setting-auto-start', false)
}
})
}
-
渲染进程 import { ipcRenderer } from 'electron'
import $public from '../public'
export default class CheckLogin {
constructor ($bus) {
this.about($bus)
}
about ($bus) {
ipcRenderer.on('check-login', () => {})
}
}
<script>
import OtherRender from './utils/renders/otherRender'
export default {
name: 'app',
mounted() {
// 其他渲染进程通信
new OtherRender(this.$bus)
// 下载渲染进程通信
new DownloadRender(this.$bus)
}
}
</script>
项目中使用 PubSub
如果在项目中使用 PubSub 你就会发现这里面有一个坑,就是当触发PubSub.publish 发布消息时,实际上PubSub.subscribe 可能会被多次订阅监听,因此,遇到这种问题是,应该先取消订阅
PubSub.publish('check-login')
PubSub.unsubscribe('check-login')
PubSub.subscribe('check-login', () => {
...
})
防止多任务
正常情况下,Electron允许打包后使用快捷方式开启多任务,而我们的需求是只能开启一个客户端端,并且不影响托盘,可以使用一下参考:
if (win) {
const isAppInstance = app.requestSingleInstanceLock()
if (!isAppInstance) {
app.quit()
} else {
app.on('second-instance', (event, argv, workingDirectory, additionalData, ackCallback) => {
if (win) {
if (win.isMinimized()) win.restore()
win.focus()
win.show()
}
})
}
setTimeout(() => {
if (!$platform.is_mac()) {
let iconImage;
const iconUrl = process.env.NODE_ENV === "development" ? path.join(__dirname, '..', "./src/assets/favicon.ico") : path.join(__dirname, "favicon.ico")
appTray = new Tray(nativeImage.createFromPath(iconUrl))
appTray.setToolTip(process.env.VUE_APP_NAME)
appTray.on('click', () => {
win.isVisible() ? win.hide() : win.show()
})
const contextMenu = Menu.buildFromTemplate([
{
label: '打开窗口',
click: () => {
win.show()
},
},
{
label: '退出',
click: () => {
process.platform === 'darwin' ? app.quit() : win.destroy()
},
},
])
appTray.setContextMenu(contextMenu)
}
}, 500)
}
ElectronBuild打包问题浅解
修改文件入口
一般情况下electron-build 的打包入口默认为background.js ,但如果有需求要修改入口文件的话需要在vue.config.js 中修改pluginOptions.electronBuilder.mainProcessFile 的配置
注意: 设置自定义入口文件以后,package.json 中的入口文件不需要改变,还是设置 background.js 作为入口就好,否则在打包时程序会报错找不到 background.js ,原因是因为vue-cli-plugin-electron-builder 在打包时,默认从 package.json 中寻找入口文件,而 vue-cli-plugin-electron-builder 的默认入口文件还是 background.js ,具体可以到 node_modules 中的 vue-cli-plugin-electron-builder 查看源码
module.exports = {
pluginOptions: {
electronBuilder: {
builderOptions: {
mainProcessFile: 'src/index.js'
}
}
}
}
{
"name": "demo",
"version": "1.0.0",
"private": true,
"description": "This is Desccript",
"main": "background.js",
"author": "Me",
"scripts": {
"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"
}
}
修改输出文件
默认情况下electron-build 打包好的文件都会生成在dist_electron 中,而在实际开发中,我们也可以通过pluginOptions.electronBuilder.builderOptions.outputDir 来自定义打包文件,就像Vue-CLI 的outputDir 那样
module.exports = {
pluginOptions: {
electronBuilder: {
builderOptions: {
"directories": {
"output": "dist" // 设置自定义输出文件
}
}
}
}
}
图标设置
在打包项目是通常会遇到,明明图标是设置了的,打包后却没有生效,或者打包报错、失败,是因为在打包时,Windows要求打包的ico 文件的尺寸需要在256*256 ,同时ico 文件不能直接有png 等图标格式文件修改后缀名得出,而是需要通过ico 处理的网站得出
打包生成安装文件
众所周知,我们使用electron 或者electron-bhild 最终目的就是要将项目打包成各个系统可以安装,并使用的安装文件,而这些也可以在pluginOptions.electronBuilder.builderOptions 中来客制化
module.exports = {
pluginOptions: {
electronBuilder: {
nodeIntegration: true,
enableRemoteModule: true,
removeElectronJunk: false,
mainProcessFile: 'src/index.js',
builderOptions: {
"appId": "com.example.app",
"productName": process.env.VUE_APP_NAME, // 项目名,也是生成的安装文件名,即 aDemo.exe
"copyright": "Copyright ? 2022", // 版权信息
"directories": {
"output": "dist" // 输出文件路径
},
"win": { // win相关配置
"icon": "./public/favicon.ico", // 图标,当前图标在根目录下,注意这里有两个坑
"target": [
{
"target": "nsis", // 利用nsis制作安装程序
"arch": [
"ia32", // 32位
"x64", // 64位
]
}
]
},
"dmg": {
"contents": [
{
"x": 410,
"y": 150,
"type": "link",
"path": "/Applications"
},
{
"x": 130,
"y": 150,
"type": "file"
}
]
},
"mac": {
"icon": "./src/assets/images/icon.icns"
},
"linux": {
"icon": "./src/assets/images/icon.icns"
}
}
}
}
}
|