Electron实际开发相关问题解决

887 阅读7分钟

Electron实际开发相关问题解决

【前言】此文档是作者在Electron实际开发中所遇到的问题,且已经解决的,因为技术有限,可能有很多地方,很多代码可能都还存在漏洞,如有疑问或者有更好的解决方式,以及有代码可以优化的地方,欢迎留言,多谢大神指点。

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封装成一个公共的组件,使用传值的方式加载相对应的srcpreload

<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-loadingWebView链接加载时触发
  • 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);
            },
            //  获取用户 token
            getToken: () => {
                return electronCookie.get('userInfo').access_token
            },
            //  设置用户 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)    //  使用 $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(() => {
        //  托盘设置,因为只需要在Windows系统上设置,所以需要排除mac系统
        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)
            //  单击托盘展示窗口,仅针对Windows系统
            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-CLIoutputDir那样

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"
                }
            }
        }
    }
}