Vue项目迁移Electron打包、发布、踩坑全纪录,满满的血泪史!

5,824 阅读8分钟

这篇文章我会持续更新,建议收藏。如果方便,请点个赞。小弟在这里谢谢了。

2021年11月25日更新(ICON锯齿解决方案)

如果你的项目发现桌面端的图标有锯齿:

image.png
VS
企业微信截图_16378111572821.png

你可能用的是256 * 256的ICO. 你需要让UI设计一个128 * 128大小的PNG,然后生成ICO

我找了很多的PNG转ICO的地址,转换出来都没有用。现在把下面这个地址分享给大家:

www.convertico.com/#google_vig…

2021年10月22日更新(electron下载失败解决方案)

遇到安装依赖无法安装的问题,可以使用淘宝的镜像。使用方式如下:

在根目录下新建.npmrc, 写入代码如下

electron_mirror="https://npm.taobao.org/mirrors/electron/"

建议使用这个方法,下文中的解决方案可以不用看了。

前言

你好, 我是阿飞

在过去的一年里面,我用Vue + Electron开发了一个桌面应用,目前有大约1000余客户在使用。

一直想写一个关于Electron的踩坑记录,今天终于下决心写下了这篇文章,希望可以帮小伙伴们开发的时候节约一些时间,先上一张项目图片。

img

今天的内容,主要是讲一下,我是如何完成这个项目的,又踩了那些坑。

如果你未来有可能要写一个Electron的项目,你可以收藏一下。

为什么要做这个桌面端应用?

这是一个对接第三方平台(饿了么、美团、京东到家、京东药寄送、达达、顺丰等)的项目,刚开始的时候,我们做的是一个网页版的应用。后来有两个需求,不得不转到桌面端。

  1. 来单的声音提醒
  2. 来单使用打印机自动打印

浏览器对播放声音的自动播放有限制,必须用户和页面交互之后

Chrome对音频调整策略说明

其次,我们需要做来单自动打印,浏览器的window.print打印无法实现自动打印。如果要实现自动打印,只能使用C-lodop来实现打印。

其实这个插件还是非常好的解决了我的问题。

但是有两个缺陷:

  • 自动打印的时候,小票底部有一个“该打印由Clodup提供的字样”,去除需要付费
  • 打印需要额外安装一个C-lodup的插件

开始

我在这之前没有做过桌面端,当时我们的web端已经完全做完了,如果在重新写桌面端,成本就太高了。所以只能考虑,如何把当前的代码,从vue的web端转成桌面版。

我查询一番之后,发现通过vue-cli-plugin-electron-builder可以实现vue向Electron的完美迁移。

这里其实很简单,直接yarn add vue-cli-plugin-electron-builder,之后就可以使用npm run electron:serve启动本地项目了。

这边文章主要讲踩坑记录,安装的过程就不多说了。网上的文章很多,这里我放一个链接在这里。

其实就是安装依赖,然后执行命令就可以了。

文件变化

安装完之后,我们会发现我们的项目中,有一些文件发生了变化,多了一个文件。

  • package.json发现变化

img

  • 增加了background.js文件

img

打包

我们能看到,在package.json的里面,多了electron:serveelectron:build的选项。

但是打包的时候,并不是执行npm run electron:build那么简单的。

因为资源的原因,下载的时候,会出现各种超时,导致打包失败。

如果你是在windows下面对electron的文件进行打包,可能需要做如下 操作

第一步:

npm run build后,第一次报错需要下载 electron-v2.0.18-win32-x64.zip(我这里是需要该版本的文件,根据自己的错误信息,来选择对应的版本下载即可),在镜像中选取该版本号 2.0.18,点击进入,并选择下载 electron-v2.0.18-win32-x64.zip 和 SHASUMS256.txt, 下载完成后,将SHASUMS256.txt文件改成 SHASUMS256.txt-2.0.18,然后将两个文件拷入如图位置:

img

第二步:

完成step1后,继续npm run build,发现又有文件下载失败 winCodeSign-2.4.0(我这里是需要该版本的文件,根据自己的错误信息,来选择对应的版本下载即可),然后自己手动下载github.com/electron-us…

img

然后拷贝到下面的文件夹中:

img

第三步:

完成step2后,继续npm run build,发现又有文件下载失败 nsis-3.0.3.2(同上),然后自己手动下载github.com/electron-us…

img

然后拷贝到下面的文件夹中:

img

第四步:

step4:完成step3后,继续npm run build,发现又有文件下载失败 nsis-resources-3.3.0,但是按照上面的方法操作,最后还是会报错,然后我尝试,用step3中下载解压后的这个nsis-3.0.3.2版本试试,拷贝如图位置所有文件:

img

然后拷贝到下面的文件夹中:

img

这个时候,npm run electron:build就可以了。

使用代理的方式安装打包依赖

按照上面的方式,只能在当前电脑进行打包,换了电脑依旧会出现该问题。

后来我找到一个新的办法,通过代理。

但是常规的代理方式,是没有用的,因为翻墙的时候,npm run electron:build的时候,并不会通过代理去下载相应的国外资源。

所以这里我们需要设置linux的代理。

在terminal中运行下面的命令

export http_proxy=http://127.0.0.1:端口

这里的端口是你的FQ的工具代理的端口。

比如我的就应该是下面的这个端口:

这个方法我在Mac上面使用可以,windows上没有尝试。有兴趣的小伙伴可以自行尝试一下。尝试之后,可以留言哦!

如何区分32位打包和64位打包, 以及环境变量的设置

在我的package.json里面有下面的命令

"electron:build32": "vue-cli-service electron:build --win --ia32 --mode prod",
"electron:buildPre32": "vue-cli-service electron:build --win --ia32 --mode pre",
"electron:build64": "vue-cli-service electron:build --mode prod",
"electron:buildTest32": "vue-cli-service electron:build --win --ia32 --mode development",
"electron:buildTest64": "vue-cli-service electron:build --mode development",
"electron:serve": "vue-cli-service electron:serve",

--win: 代表打包windows版本 --ia32: 代表打包32位版本,如果不设置,默认打包64位版本

32位版本的打包结果可以兼容64位电脑使用,但是64位的项目不兼容32位。

--mode:是我配置的环境变量,和vue-cli3官方文档中配配置意思一致。只需要在根目录下创建相应的文件即可:

img

自动打印

img

网上关于自动打印的文章不多,且零零散散。

最后在我自己的研究下,还是搞定了。

自动打印涉及几个问题点:

  • 如何获取设备?
  • 打印模板怎么编写?
  • 如何自动触发设备打印?

获取设备

先来讲获取设备:

在App.js,触发获取打印机设备

const { ipcRenderer } = window.require('electron');
// 引入ipcRenderer对象,该对象和主线程的ipcMain通讯
ipcRenderer.send('getPrinterList');

在主进程的createWindow当方中,执行获取设备方法

ipcMain.on('getPrinterList', event => {
    // 主线程获取打印机列表
    const list = win.webContents.getPrinters();
    // 通过webContents发送事件到渲染线程,同时将打印机列表也传过去
    win.webContents.send('printerList', list);
});

App.js中的mounted的时候接受获取到的打印机列表

ipcRenderer.once('printerList', (event, data) => {
    console.log('打印机列表', data)
    // 设置打印机列表
    ...
});

ipcMain 和 ipcRenderer分别是主进程和渲染进程,他们的交互方式是通过on和send方法。也就事监听观察者模式吧,不知道我有没有说错。

类似于vue中的eventBus的事件传递方式。

通过什么打印?

在electron中,打印需要通过webview的形式进行打印。

将写好的打印模板放在远程服务器,或者在本地。

img

放在远程服务器的好处是,更改模板的时候,不需要重新打包。

同样是在App.js中,插入webview(因为我的打印是全局的,所以放在这里)

img

在点击打印的时候,执行下面的方法:

handlePrintTest (res) {
    let activeDevice = localStorage.getItem('activeDevice');
    if(!activeDevice) {
        this.$Message.error('请选择打印机!')
        return;
    }
    this.$Message.success('打印中, 请稍后')
    // 当vue节点渲染完成后,获取<webview>节点
    const webview = this.$refs.printWebview;
    webview.send('webview-print-render', {
        printName: activeDevice,
        // style: res.styleStr,
        html: res.htmlStr
    });

}

注意,下面是关键: print.html中代码如下:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
</head>

<body id="bd"></body>
<script>
    const { ipcRenderer } = require('electron')
    ipcRenderer.on('webview-print-render', (event, info) => {
        // 执行渲
        console.log('info')
        var strBodyStyle = `css代码...自行编写`;
        // let styleStr =
        var nod = document.createElement('style')
        nod.type='text/css';
        if (nod.styleSheet) { //ie下
            nod.styleSheet.cssText = strBodyStyle;
        } else {
           nod.innerHTML = strBodyStyle; //或者写成 nod.appendChild(document.createTextNode(str))
        }
        document.getElementsByTagName('head')[0].appendChild(nod);
        // 这里的info.html就是在点击的时候,传入的html。这是因为我这边的订单信息是动态的,所以html每次都是动态生成的。
        document.getElementById('bd').innerHTML = info.html
        ipcRenderer.sendToHost('webview-print-do')
    })
</script>

自动打印

自动打印的触发条件是:websocket接受到消息,就触发打印事件。这里可以根据各位小伙伴的实际情况去处理。

如果有什么问题,可以留言给我。

软件自动升级

软件自动升级,网上教程也有不少,但是都不是很完善。

我这边做的是,每10分钟,监听一次。如果发现远程服务器有新的版本,就会自动升级。

自动更新需要用到electron-updater包。

这个包直接安装即可。 完成效果如下:

img

img

自动升级的步骤

第一步:

创建窗口的时候,执行更新方法:

import { autoUpdater } from 'electron-updater';
function createWindow() {
	// 根据不同的环境,配置不同的远程下载地址
    // 32位 64位 以及预发布环境
	if ((process.argv[5] && process.argv[5] === 'pre') || (process.argv[6] && process.argv[6] === 'pre')) {
        if(process.arch==='x64') {
            uploadUrl = 'https://******/upload/backend/template/dist_electron';
        } else {
            uploadUrl = 'https://******/upload/backend/template/dist_electron32';
        }
    } else {
        if(process.arch==='x64') {
            uploadUrl = 'https://******/upload/backend/template/slyf64';
        } else {
            uploadUrl = 'https://******/upload/backend/template/slyf32';
        }
    }
    autoUpdater.setFeedURL(uploadUrl);
    autoUpdater.autoDownload = false;
	updateHandle();
}

function updateHandle() {
    console.log('执行检查更新');
    let updaterCacheDirName = 'electron-admin-updater';
    const updatePendingPath = path.join(autoUpdater.app.baseCachePath, updaterCacheDirName, 'pending');
    fs.emptyDir(updatePendingPath);
    let message = {
        error: '检查更新出错',
        checking: '正在检查更新……',
        updateAva: '检测到新版本,正在下载……',
        updateNotAva: '现在使用的就是最新版本,不用更新'
    };
    const os = require('os');

    autoUpdater.setFeedURL(uploadUrl);
    autoUpdater.autoDownload = false;
    autoUpdater.on('error', function(err) {
        // console.log('出现错误');
        sendUpdateMessage(err);
    });
    autoUpdater.on('checking-for-update', function() {
        console.log('检查更新');
        sendUpdateMessage(message.checking);
    });
    autoUpdater.on('update-available', function(info) {
        console.log('发现新的版本。。', info);
        sendUpdateMessage(info)
    });
    autoUpdater.on('update-not-available', function(info) {
        console.log('无需更新');
        sendUpdateMessage(message.updateNotAva);
    });

    // 更新下载进度事件
    autoUpdater.on('download-progress', function (progressObj) {
        win.webContents.send('downloadProgress', progressObj)
    })

    autoUpdater.on('update-downloaded', function (event, releaseNotes, releaseName, releaseDate, updateUrl, quitAndUpdate) {
        autoUpdater.quitAndInstall();
    });
    ipcMain.on('startDownload', () => {
        autoUpdater.downloadUpdate()
    })

    ipcMain.on('checkForUpdate', () => {
        // 执行自动更新检查
        autoUpdater.checkForUpdates();
    });
}

在background中,获取到当前的版本信息,发送给渲染进程。

// 通过main进程发送事件给renderer进程,提示更新信息
function sendUpdateMessage(text) {
    win.webContents.send('message', text);
}

渲染进程获取到更新信息,判断是否需要染出更新窗口

<Modal
    v-model="modal"
    title="版本更新提示"
    @on-ok="ok"
    @on-cancel="cancel">
    <p>有新的版本等待更新, 版本号为:{{version}}</p>
    <p v-if="versionDes">版本更新内容:{{versionDes}}</p>
</Modal>
<Modal
    v-model="progressModal"
    footer-hide
    title="下载进度">
    <Progress :percent="downloadPercent" status="active" />
</Modal>
mounted () {
	// 页面刷新的时候,执行检查更新操作
	ipcRenderer.send('checkForUpdate');
    // 每隔10分钟再次检查
    setInterval(() =>{
        // 每 10 分钟检查一次是否需要更新
        ipcRenderer.send('checkForUpdate');
    }, 600000)
    // // 注意:“downloadProgress”事件可能存在无法触发的问题,只需要限制一下下载网速就好了
    ipcRenderer.on('downloadProgress', (event, progressObj) => {
    	// 如果发现有新的版本需要更新,立即弹出更新对话框
        this.progressModal = true;
        this.downloadPercent = progressObj.percent.toFixed(2) || 0;
    });
    
    ipcRenderer.on("message", (event, text) => {
        // 检测到新版本
        if(text.version) {
            this.modal = true;
            this.version = text.version;
            this.versionDes = text.versionDes ? text.versionDes : '';
            this.tips = text;
        }
    });
}

需要注意的是, 如果想要手动更新,一定要再createWindow的时候,添加autoUpdater.autoDownload = false;。否则一旦发现有新的版本需要更新,就会立即更新。

关于latest.yml的说明

项目打包完成之后,会生成这两个文件。

img

version: 1.1.6
files:
  - url: 门店看板 Setup 1.1.6.exe
    sha512: uWV2SYo7kTK4aYcBeyQx9Y5c62fW50VLQcs96cvgj/ygz15YrEFsc4GVWOmGOJ8LbpKhw5gHYCjBj0s6yGwBpg==
    size: 59999596
path: 门店看板 Setup 1.1.6.exe
sha512: uWV2SYo7kTK4aYcBeyQx9Y5c62fW50VLQcs96cvgj/ygz15YrEFsc4GVWOmGOJ8LbpKhw5gHYCjBj0s6yGwBpg==
releaseDate: '2020-12-30T08:59:40.271Z'
自定义参数:
versionDes: '这是新版本的说明'

这个文件中,可以在获取更新信息的时候,获取到,你可以在这添加的你的新版本信息。

我在上面标红的两个文件需要放到远程服务器,autoupdater插件会读取latest.yml文件,判断是否需要升级。

需要注意的是,这两个文件是相互匹配的,如果没有不是同时打包的,是无法更新的。

其他打包后的问题

electron打包的软件只支持win7+的系统。有xp需求的请绕道。

win7部分电脑打开白屏

win7白屏问题,是因为缺少.net frameork包导致的,安装后即可解决

  1. 如果是 win 7 及以上系统,安装 4.8 版本的.net 包后,再重新打开看板

官网地址如下:dotnet.microsoft.com/download/do…

img

  1. 部分未更新的 win 7 系统安装.net 包后仍不支持,此时可以用 win 7 补丁更新系统后再次尝试安装

img

  1. 部分机型无法安装.netframework

安装.net 包时弹出“无法建立到信任根颁发机构的证书链”,则按以下步骤安装信任证书后重新安装.net 包 根证书下载地址: download.microsoft.com/download/2/…

1)下载根证书并安装

img

2)点击开始-运行或 win+R 键,输入 mmc

img

3)文件-添加删除管理单元-双击证书

img

4)选择计算机账户-下一步-完成即可

img

img

5)导入证书-浏览需要导入的证书-下一步-完成即可

img

6)完成以上步骤之后,重新安装.net 包即可

xx应用 已停止工作

遇到这个问题不要慌,这是软件的兼容性问题。按照下面的解决方案就可以了。

a)选中右击-点击属性-打开兼容性

img

b)点击以兼容模式运行这个程序-选择 Windows server 2008-点击以管理员身份运行此程序-最后点击应用、确认

img

其他问题

打印机相关问题非常多,有驱动的问题,有串口还是usb的问题,这里我不一一列举。

小伙伴遇到打印机的问题,可以留言,我将在能力范围内,帮你解决问题。

我在第二个Electron应用中遇到的一个奇葩问题

我的另外一个项目,使用到了node-adodb,出现了十分诡异的问题。

就是我在本地环境electron:serve的时候,可以正常读取access数据库的资源,但是打包之后告诉我找不到这个模块

当然,网上我查了整整一天,都没有找到解决方案。不止这个问题,log-4的日志插件也会有这个问题

img

最后我找到了解决方案。

  1. 第一:查看您的插件是不是在生产依赖
  2. 第二:如果在生产依赖下,渲染进程中使用window.require的方式引入第三方插件,打包后会出现找不到依赖的情况,所以你需要吧require的模块卸载background的主进程文件中
  3. 第三:部分插件,打包后因为asar的原因找不到模块

img

具体的编写位置如下:

img

// 这里的配置卸载vue.config.js
pluginOptions: {
        electronBuilder: {
        builderOptions: {
            productName: '门店看板',
            appId: '123',
             win: {
                icon: './public/desttop.ico'
             },
            publish: [
                {
                    provider: 'generic',
                    url: 'xxxxx'
                }
            ],
            "nsis": {
              "oneClick": false, // 是否一键安装
                "perMachine": true,
              "allowElevation": true, // 允许请求提升。 如果为false,则用户必须使用提升的权限重新启动安装程序。
              "allowToChangeInstallationDirectory": true, // 允许修改安装目录
                "installerSidebar": './public/installer.bmp',
              // "installerIcon": './public/desttop.ico',// 安装图标
              // "uninstallerIcon": './public/desttop.ico',//卸载图标
              // "installerHeaderIcon": './public/desttop.ico', // 安装时头部图标
              "createDesktopShortcut": true, // 创建桌面图标
              "createStartMenuShortcut": true,// 创建开始菜单图标
              "shortcutName": "门店看板", // 图标名称
              // "include": "build/script/installer.nsh", // 包含的自定义nsis脚本
            }
        }
    }
},

其他的基础配置

electron的基础配置,我把代码贴在这里,大家想了解,去看一些electron的官方介绍,我不多说了。

设置托盘

const setAppTray = () => {
    // 托盘对象
    let appTray = null;
    // 系统托盘右键菜单
    let trayMenuTemplate = [
        {
            label: '退出',
            click: function() {
                // ipc.send('close-main-window');
                // isQuit = true;
                app.quit()
                // win.close('close')
                // console.log('执行了。。。。')
                // win.on('closed', () => {})
            }
        }
    ];
    // 系统托盘图标目录
    // let trayIcon = path.join(__dirname, '../public')
    // const iconPath = path.join(__static, './logo.png');

    appTray = new Tray(`${__static}/logo.png`)
    // 图标的上下文菜单
    const contextMenu = Menu.buildFromTemplate(trayMenuTemplate)

    appTray.on('click', function (Event) {
        win.show();
    })

    // 设置此托盘图标的悬停提示内容
    appTray.setToolTip('邻医快药')

    // 设置此图标的上下文菜单
    appTray.setContextMenu(contextMenu)
}

隐藏菜单

function createMenu() {
   // darwin表示macOS,针对macOS的设置

   if (process.platform === 'darwin') {
       const template = [
       {
           label: 'App Demo',
           submenu: [
               {
                   role: 'about'
               },
               {
                   role: 'quit'
               }]
       }]
       let menu = Menu.buildFromTemplate(template)
       Menu.setApplicationMenu(menu)
   } else {
       // windows及linux系统
       Menu.setApplicationMenu(null)
   }
}

createWindow的时候配置以及devtools的打开关闭

这里包括:

  • 打开最大化窗口
  • 通过F12来打开调试工具等等
function createWindow() {
    // Create the browser window.
    win = new BrowserWindow({
        // fullscreen: true,
        show: false,
        frame: true,
        webPreferences: {
            // Use pluginOptions.nodeIntegration, leave this alone
            // See nklayman.github.io/vue-cli-plugin-electron-builder/guide/security.html#node-integration for more info
            webSecurity: false,
            webviewTag: true,
            nodeIntegration: true
        },
        // eslint-disable-next-line no-undef
        icon: `${__static}/logo.ico`
    });
    win.maximize();
    win.show();
    updateHandle();
    
    ipcMain.on('toggleDevTools', event => {
        win.webContents.toggleDevTools();
    })

    if (process.env.WEBPACK_DEV_SERVER_URL) {
        // Load the url of the dev server if in development mode
        win.loadURL(process.env.WEBPACK_DEV_SERVER_URL);
        // if (!process.env.IS_TEST) win.webContents.openDevTools();
        // win.webContents.openDevTools();
    } else {
        createProtocol('app');
        // Load the index.html when not in development
        win.loadURL('app://./index.html');
        // win.webContents.openDevTools();
    }
    win.on('closed', (e) => {
        win = null;
    });
    // 去除菜单
    createMenu()
    // 设置托盘
    setAppTray()
}