概述
先介绍一下技术构成,项目是完整的vue项目,在现有项目上完成electron打包及更新。
打包使用的vue-cli-plugin-electron-builder插件,增量更新为具体的代码实现,并没有用到electron-updater。
思路
1、比较本地和远程的版本
本地的版本可以放在一个app.txt文件中,或者通过变量获取。
远程的版本通过服务端文件(yml或者json)获取
比较版本,若相同则不更新,返回。
若不同,则提示更新,或直接进入更新流程
2、更新流程:通过线上增量包文件地址,进行下载
3、将zip压缩包解压缩
4、重新加载
详解
一、vue-cli-plugin-electron-builder
安装vue-cli-plugin-electron-builder
vue add electron-builder@2.0.0-rc.6
版本选择9.0.0
二、新建vue.config.js
1、重点是:"asar": false,
这句话的意思是,打包的时候resources
下就不产生app.asar
,而是一个app文件夹,而这个文件夹呢是可以直接进行替换的,里面的内容就是我们的前端整个项目的打包,就是渲染进程。所以,我们每次只需要替换前端的打包就可以了,而不需要每次都安装替换整个app
2、将app图标放到public文件夹下
module.exports = {
pluginOptions: {
electronBuilder: {
builderOptions: {
//productName: "ebopo", // 项目名,这也是生成的exe文件的前缀名
copyright: 'Copyright © Ambow', // 应用程序版权行
publish: [{
"provider": "generic",
"channel": "latest",
"url": "http://ambow-ebopo.oss-cn-beijing.aliyuncs.com/common/electron/",
}],
"win": {//win相关配置
"icon": "./public/app.ico",//图标,当前图标在根目录下,注意这里有两个坑
"target": [
{
"target": "nsis",//利用nsis制作安装程序
"arch": [
"x64",//64位
"ia32"//32位
]
}
]
},
"asar": false,
"nsis": {
"oneClick": false, // 是否一键安装
"allowElevation": true, // 允许请求提升。 如果为false,则用户必须使用提升的权限重新启动安装程序。
"allowToChangeInstallationDirectory": true, // 允许修改安装目录
"installerIcon": "./public/app.ico",// 安装图标
"uninstallerIcon": "./public/app.ico",//卸载图标
"installerHeaderIcon": "./public/app.ico", // 安装时头部图标
"createDesktopShortcut": true, // 创建桌面图标
"createStartMenuShortcut": true,// 创建开始菜单图标
"shortcutName": "ebopo", // 图标名称
},
}
}
}
}
三、配置信息
background.js
let win
function createWindow() {
win = new BrowserWindow({
width: 800,
height: 600,
icon: `${__static}/app.ico`, // app内左上角图标
webPreferences: {
webSecurity: false, //取消跨域限制
nodeIntegration:true, //开启node
}
})
}
package.json
// 打包exe前缀名称,及项目内左上角显示名称
"name": "ebopo",
// 版本号
"version": "1.0.0",
"author": "leecss",
"description": "electronApp",
四、新增electron目录及设置菜单
在src文件夹下新建electron目录,在目录中新建menu.js
import { BrowserWindow, Menu, app} from 'electron'
let template = [{
label: '查看',
submenu: [{
label: '重载',
accelerator: 'CmdOrCtrl+R',
click: (item, focusedWindow) => {
if (focusedWindow) {
// 重载之后, 刷新并关闭所有之前打开的次要窗体
if (focusedWindow.id === 1) {
BrowserWindow.getAllWindows().forEach(win => {
if (win.id > 1) win.close()
})
}
focusedWindow.reload()
}
}
}, {
label: '切换全屏',
accelerator: (() => {
if (process.platform === 'darwin') {
return 'Ctrl+Command+F'
} else {
return 'F11'
}
})(),
click: (item, focusedWindow) => {
if (focusedWindow) {
focusedWindow.setFullScreen(!focusedWindow.isFullScreen())
}
}
}]
}, {
label: '窗口',
role: 'window',
submenu: [{
label: '最小化',
accelerator: 'CmdOrCtrl+M',
role: 'minimize'
}, {
label: '关闭',
accelerator: 'CmdOrCtrl+W',
role: 'close'
}]
}, {
label: '帮助',
role: 'help',
submenu: [
{
label: `版本` + app.getVersion(),
enabled: false
},
{
label: '切换开发者工具',
accelerator: (() => {
if (process.platform === 'darwin') {
return 'Alt+Command+I'
} else {
return 'Ctrl+Shift+I'
}
})(),
click: (item, focusedWindow) => {
if (focusedWindow) {
focusedWindow.toggleDevTools()
}
}
}
]
}]
var list = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(list)
修改background.js ,在主进程中引入菜单
async function createWindow() {
+ require('./electron/menu.js')
}
五、添加调试及日志文件输出
npm i electron-log
在electron目录下新建log.js
import log from 'electron-log'
log.transports.file.level = 'silly'
log.transports.console.level = false // 禁用console输出
export default log
在background.js 中使用
import log from './electron/log'
log.warn('hello')
日志文件查看:文件main.log存储位置
C:\Users\{用户}\AppData\Roaming\{appName}\logs
六、更新流程:比较版本
安装并引入axios
npm i axios
background.js
import axios from 'axios'
比较版本,若相同则返回,不相同则更新
async function createWindow() {
require('./electron/menu.js')
// Create the browser window.
win = new BrowserWindow({
width: 800,
height: 600,
icon: `${__static}/app.ico`, // app内左上角图标
webPreferences: {
webSecurity: false, //取消跨域限制
nodeIntegration:true, //开启node
}
})
if (process.env.WEBPACK_DEV_SERVER_URL) {
// Load the url of the dev server if in development mode
await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL)
if (!process.env.IS_TEST) win.webContents.openDevTools()
} else {
createProtocol('app')
// Load the index.html when not in development
win.loadURL('app://./index.html')
log.warn('hello')
// 判断是否热更新,版本对比
const updateUrl = 'http://ambow-ebopo.oss-cn-beijing.aliyuncs.com/common/electron'
const currentVersion = app.getVersion()
log.warn('currentVersion',currentVersion)
axios({
url: updateUrl + '/latest.yml',
method: 'GET'
}).then(res => {
const remoteVersion = JSON.stringify(res.data).split('\\n')[0].split(' ')[1]
log.warn('remoteVersion',remoteVersion)
if (currentVersion === remoteVersion) {
log.warn('版本相同,无需更新')
return;
}
// 热更新
const updateZipUrl = updateUrl + '/app.zip'
increment(updateZipUrl)
})
}
}
七、更新流程:下载远程更新包app.zip
在electron目录中新建downloadFile.js
下载文件需要两个参数:下载路径,保存路径
下载文件返回一个参数:保存路径
安装fs-extra
npm i fs-extra
const request = require('request')
const fs = require('fs')
const fse = require('fs-extra')
const path = require('path')
import log from './log'
function download(url, targetPath, cb = () => { }) {
let status
const req = request({
method: 'GET',
uri: encodeURI(url)
})
try {
const stream = fs.createWriteStream(targetPath)
let len = 0
let cur = 0
req.pipe(stream)
req.on('response', (data) => {
len = parseInt(data.headers['content-length'])
})
req.on('data', (chunk) => {
cur += chunk.length
const progress = (100 * cur / len).toFixed(2)
status = 'progressing'
cb(status, progress)
})
req.on('end', function () {
if (req.response.statusCode === 200) {
if (len === cur) {
console.log(targetPath + ' Download complete ')
status = 'completed'
cb(status, 100)
} else {
stream.end()
removeFile(targetPath)
status = 'error'
cb(status, '网络波动,下载文件不全')
}
} else {
stream.end()
removeFile(targetPath)
status = 'error'
cb(status, req.response.statusMessage)
}
})
req.on('error', (e) => {
stream.end()
removeFile(targetPath)
if (len !== cur) {
status = 'error'
cb(status, '网络波动,下载失败')
} else {
status = 'error'
cb(status, e)
}
})
} catch (error) {
console.log(error)
}
}
function removeFile(targetPath) {
try {
fse.removeSync(targetPath)
} catch (error) {
console.log(error)
}
}
export default async function downloadFile({ url, targetPath, folder = './' }, cb = () => { }) {
log.warn('download-url',url)
log.warn('download-saveurl',targetPath)
if (!targetPath || !url) {
throw new Error('targetPath or url is nofind')
}
try {
await fse.ensureDirSync(path.join(targetPath, folder))
} catch (error) {
throw new Error(error)
}
return new Promise((resolve, reject) => {
const name = url.split('/').pop()
const filePath = path.join(targetPath, folder, name)
download(url, filePath, (status, result) => {
if (status === 'completed') {
resolve(filePath)
}
if (status === 'error') {
reject(result)
}
if (status === 'progressing') {
cb && cb(result)
}
})
})
}
八、更新流程:将下载的app.zip解压缩并删除
在electron目录中新建increment.js
解压缩需要一个参数:加压压缩包的路径
在文件下载完成后执行
安装adm-zip
npm i adm-zip
import downloadFile from './downloadFile'
import { app } from 'electron'
const fse = require('fs-extra')
const AdmZip = require('adm-zip')
import log from './log'
export default async (updateZipUrl) => {
const resourcesPath = process.resourcesPath
downloadFile({ url: updateZipUrl, targetPath: resourcesPath }).then(async (filePath) => {
log.warn('unzip-filePath',filePath)
const zip = new AdmZip(filePath)
zip.extractAllToAsync(resourcesPath, true, (err) => {
if (err) {
console.error(err)
return
}
fse.removeSync(filePath)
setTimeout(() => {
log.warn('relaunch')
app.relaunch()
app.exit(0)
}, 2000);
})
}).catch(err => {
console.log(err)
})
}
在background.js引入并调用
import increment from './electron/increment'
// 热更新
const updateZipUrl = updateUrl + '/app.zip'
increment(updateZipUrl)
九、更新流程:将更新重启替换为重新加载
在electron目录下新建global.js
global.sharedObject = {
win: ''
}
export default global
在background.js引入并调用
import global from './electron/global'
async function createWindow() {
require('./electron/menu.js')
// Create the browser window.
win = new BrowserWindow({
width: 800,
height: 600,
icon: `${__static}/app.ico`, // app内左上角图标
webPreferences: {
webSecurity: false, //取消跨域限制
nodeIntegration:true, //开启node
}
})
if (process.env.WEBPACK_DEV_SERVER_URL) {
// Load the url of the dev server if in development mode
await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL)
if (!process.env.IS_TEST) win.webContents.openDevTools()
} else {
createProtocol('app')
// Load the index.html when not in development
win.loadURL('app://./index.html')
log.warn('hello')
// 判断是否热更新,版本对比
const updateUrl = 'http://ambow-ebopo.oss-cn-beijing.aliyuncs.com/common/electron'
const currentVersion = app.getVersion()
log.warn('currentVersion',currentVersion)
axios({
url: updateUrl + '/latest.yml',
method: 'GET'
}).then(res => {
const remoteVersion = JSON.stringify(res.data).split('\\n')[0].split(' ')[1]
log.warn('remoteVersion',remoteVersion)
if (currentVersion === remoteVersion) {
log.warn('版本相同,无需更新')
return;
}
// 热更新
const updateZipUrl = updateUrl + '/app.zip'
increment(updateZipUrl)
})
}
global.sharedObject.win = win
win.on('closed', () => {
win = null
global.sharedObject.win = null
})
}
在increment.js中引入并调用
import downloadFile from './downloadFile'
import global from './global'
const fse = require('fs-extra')
const AdmZip = require('adm-zip')
import log from './log'
export default async (updateZipUrl) => {
const resourcesPath = process.resourcesPath
downloadFile({ url: updateZipUrl, targetPath: resourcesPath }).then(async (filePath) => {
log.warn('unzip-filePath',filePath)
const zip = new AdmZip(filePath)
zip.extractAllToAsync(resourcesPath, true, (err) => {
if (err) {
console.error(err)
return
}
fse.removeSync(filePath)
setTimeout(() => {
log.warn('relaunch')
global.sharedObject.win.webContents.reloadIgnoringCache()
}, 2000);
})
}).catch(err => {
console.log(err)
})
}
setTimeout是为了演示效果,最后去掉。
十、遇到的问题
更新时报错,如下:
Error: EPERM: operation not permitted
原因是没有管理员权限,且安装时选择的为所有用户安装。
方案一:运行时选择:以管理员身份运行
方案二:下载时,仅为当前用户安装
解决方案:
在设置上,不给用户选择为所有用户安装的机会,默认一键安装,设置如下
"nsis": {
"oneClick": true, // 是否一键安装
"installerIcon": "./public/app.ico",// 安装图标
"uninstallerIcon": "./public/app.ico",//卸载图标
"installerHeaderIcon": "./public/app.ico", // 安装时头部图标
"createDesktopShortcut": true, // 创建桌面图标
"createStartMenuShortcut": true,// 创建开始菜单图标
"shortcutName": "ebopo", // 图标名称
},
十一、在ts项目中使用
electron-log的使用要用ts文件,并且用any声明,其他相似问题同理。
log.ts
const log:any = require('electron-log');
log.transports.file.level = 'silly'
log.transports.console.level = false // 禁用console输出
export default log
GitHub地址
参考
https://xuxin123.com/electron/increment-update1/