因为我们的项目需要在electron-updater的基础做一些其他工作,并且频繁的改动node_module文件很麻烦,所以思前想后还是自己实现一下。代码支持直接复制使用,可能存在一些小问题,但关键的地方已经内测过,没有问题。如果有新的见解,欢迎前来讨论。
electron自动更新流程:下载yml文件—>下载安装包—>检验秘钥—>触发安装
文件目录
Event.js
export class Event{
constructor() {
this.events = {};
}
on(event, callback){
let callbacks = this.events[event] || [];
callbacks.push(callback);
this.events[event] = callbacks;
}
off(event){
this.events[event] = [];
}
offAll(){
this.events = {};
};
emit(event, ...args){
const callbacks = this.events[event];
callbacks.forEach(fn => fn.apply(this, args));
}
}
AutoUpdater
const os = require('os');
const request = require('request');
const { resolve } = require('path');
const fs = require('fs');
const crypto = require('crypto');
const hash = crypto.createHash('sha512');
const FILESIZEUNIT = 1024 * 1024;
const RATE = 100;
const DOWNLOADPACKAGE = 'downloadPackage'; // 下载安装包
const UPDATEAVAILABLE = 'update-available'; // 确认更新是否可用
const ERROR = 'error'; // 输出错误
const DOWNLOADPROGRESS = 'download-progress'; // 下载进度
const DOUPDATE = 'doUpdate'; // 开始更新
import { Event } from './event.js'
class autoUpdate extends Event {
osType;
osArch;
innerFullUrl;
outerFullUrl;
isAutoDownload;
isAutoInstallOnAppQuit;
currentVersion;
netEnvironment;
updateContent;
updatePackageLocation;
showProgressSpeed;
constructor(options) {
super();
// 检测参数类型
this.checkForArguments(options);
// 获取参数
this.getArguments(options);
// 添加监听事件
this.addListener();
};
checkForArguments(options) {
if (typeof options !== 'object' || typeof options === null) throw new TypeError('options应该是一个对象!');
};
getArguments() {
const {
innerFullUrl = '',
outerFullUrl = '',
isAutoDownload = false,
isAutoInstallOnAppQuit = false,
netEnvironment = true,
currentVersion = '',
updatePackageLocation = resolve(__dirname, 'download'), // 返回运行文件所在的目录
showProgressSpeed = 4,
} = options;
this.osType = os.type();
this.osArch = os.arch();
this.innerFullUrl = innerFullUrl;
this.outerFullUrl = outerFullUrl;
this.isAutoDownload = isAutoDownload;
this.isAutoInstallOnAppQuit = isAutoInstallOnAppQuit;
this.netEnvironment = netEnvironment;
this.currentVersion = currentVersion;
this.showProgressSpeed = showProgressSpeed;
this.updatePackageLocation = updatePackageLocation;
};
getURL() {
return this.netEnvironment ? this.innerFullUrl : this.outerFullUrl;
};
doUpdate(){
this.emit(DOUPDATE);
}
addListener() {
// 开始更新
this.on(DOUPDATE, async () => {
// 从服务器获取yml文件
await this.getYmlFile();
if(this.isAutoDownload){
// 自动下载
this.emit(DOWNLOADPACKAGE);
}
});
this.on(DOWNLOADPACKAGE, () => {
// 下载安装包
this.getInstallPackage();
});
};
getYmlFile() {
let callback = (res) => {
// 根据操作系统位数获取安全密钥和安装包名称
this.getUpdateContent(this.resolveString(res));
}
let err = (res) => {
throw new Error(res.data);
}
return new Promise((resolve, reject) => {
request({
url: this.getURL() + '/lastest.yml',
method: "get",
responseType: 'blob',
}, (error, response, body) => {
if (!error && response.statusCode === 200) {
resolve(response)
} else {
reject(response)
}
})
}).then((res) => {
callback(res)
}).catch((data) => {
err(data)
})
};
resolveString(string) {
let obj = {};
let str = JSON.stringify(string).substring(1, string.length - 1);
let strArray = str.split('\\r\\n');
for (let i = 0; i < strArray.length; i++) {
let s = strArray[i];
let dataGroup = s.split(':');
obj[dataGroup[0].trim()] = dataGroup[1].trim();
}
return obj;
};
// 项目需要,非必须
getUpdateContent(content) {
let sha512Array = content.sha512.split(';');
let pathArray = content.path.split(';');
if (this.osArch === 'x64') {
this.updateContent.path = pathArray[1];
this.updateContent.sha512 = sha512Array[1];
} else if (this.osArch === 'ia32') {
this.updateContent.path = pathArray[0]
this.updateContent.sha512 = sha512Array[0];
}
this.updateContent.version = content.version;
};
checkForVersion(version) {
let lastestVersionArray = version.split('.');
let currentVersionArray = this.currentVersion.split('.');
const latestVersionLength = lastestVersionArray.length;
const currentVersionLength = currentVersionArray.length;
const getVersionNumber = (num, cur, index, array) => {
// 比较方式
// 将版本号转换为10进制数,进行大小比较
num += +cur * Math.pow(10, (array.length - index - 1));
return num;
}
if (latestVersionLength !== currentVersionLength) {
this.emit(ERROR, '版本类型不匹配,需要版本类型:' + version + ',得到版本类型:' + this.currentVersion);
}
let latestVersionNumber = lastestVersionArray.reduce(getVersionNumber, 0);
let currentVersionNumber = currentVersionArray.reduce(getVersionNumber, 0);
if (latestVersionNumber > currentVersionNumber) this.emit(DOWNLOADPACKAGE);
else if (latestVersionNumber === currentVersionNumber) this.emit(UPDATEAVAILABLE, '当前版本已更新至最新版本:' + this.currentVersion);
else this.emit(UPDATEAVAILABLE, '版本号出现问题,请检查!需要版本类型:' + version + ',得到版本类型:' + this.currentVersion);
};
getInstallPackage() {
let totalSize = 0;
let count = 0;
let receiveLen = 0;
const req = request({
url: this.getURL() + '/' + this.updateContent.path,
method: "get",
})
if(!fs.existsSync(this.updatePackageLocation)) fs.mkdirSync(this.updatePackageLocation);
this.updatePackageLocation += '/' + this.updateContent.path.substring(0, this.updateContent.path.length - 3) + 'tmp';
// 先写成临时文件,等待秘钥加密完成,再生成真正的可执行文件
const out = fs.createWriteStream(this.updatePackageLocation);
req.pipe(out).on("close", function (err) {
if (err) this.emit(ERROR, err);
this.checkForSha512();
});
req.on('response', (data) => {
totalSize = parseInt(data.headers['content-length'], 10);
});
req.on('data', (chunk) => {
receiveLen += chunk.length;
count += chunk.length;
if (count >= (FILESIZEUNIT * this.showProgressSpeed)) {
let percentage = receiveLen / totalSize * RATE;
this.emit(DOWNLOADPROGRESS, {
percentage,
totalSize,
})
count = 0;
}
});
req.on('end', () => {
});
};
checkForSha512(){
let latestSafeSecret = '';
// 设置为base64编码
hash.setEncoding('base64');
let rs = fs.createReadStream(this.updatePackageLocation,{
highWaterMark: FILESIZEUNIT
})
rs.on('data', (data) => {
hash.update(data);
})
rs.on('end', () => {
hash.digest();
hash.end();
latestSafeSecret = hash.read();
if(this.updateContent.sha512 === latestSafeSecret){
// 安全秘钥通过
fs.renameSync(this.updatePackageLocation, this.updatePackageLocation.substring(0, this.updatePackageLocation.length - 3) + 'exe', (err) => {
if(err) throw err;
console.log('文件重命名完成!');
})
}else{
// 安全秘钥未通过
this.emit('error', '秘钥检测未通过,需要秘钥:' + this.updateContent.sha512 + ',得到秘钥:' + latestSafeSecret);
}
})
}
}
export default autoUpdate