手写Electron自动更新-实现绝大多数功能

因为我们的项目需要在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
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值