electron应用自动更新

4,699 阅读5分钟

项目现状:每次有新版本发布时,需要用户自己去手动点击下载,然后手动一步步操作安装,每次都要去重新选择安装目录等,过程较为耗时,也需要用户去手动操作,用户体验不佳。

方案: 采用electron-builder配合electron-updater实现自动更新

electron 的打包可以采用electron-packager及electron-builder两种方式实现,经过对比之后发现electron-builder有比electron-packager有更丰富的的功能,支持更多的平台,同时也支持了自动更新。除了这几点之外,由electron-builder打出的包更为轻量,并且可以打包出不暴露源码的setup安装程序,所以将方案选为后者。

步骤:

A. 安装 electron-updater 包模块

  npm install electron-updater --save

B. 配置package.json文件里的build

"build": {
    "productName": "uWorker",   //项目名 这也是生成的exe文件的前缀名
    "appId": "xxxx",   //项目的appid
    "copyright": "xxx", //版权  信息
    "directories": {
      "output": "dist"  // 输出的文件夹
    },
    "publish": [
      {
        "provider": "generic",  //服务器提供商 也可以是GitHub等等  
        "url": "http://127.0.0.1/",//更新服务器地址,用本地服务做测试
      }
    ],
    "dmg": {
      "background": "../build/body-background.jpg",
      "window": {
        "x": 400,
        "y": 100,
        "width": 1100,
        "height": 709
      }
    }
    "mac": {
      "icon": "build/icons/icon.icns", // mac的图标路径
      "artifactName": "${productName}_setup_${version}.${ext}" // 打出来的包所带的版本信息名
    },
    "win": {
      "icon": "build/icons/icon.ico", // windows的图标路径,
      // 打包出来之后的结果
      "target": [
        "nsis",
        "zip"
      ],
      "artifactName": "${productName}_setup_${version}.${ext}" // 打出来的包所带的版本信息名
    },
    "nsis": {
        "oneClick": false, // 一键安装
        "perMachine": true, // 是否开启安装时权限限制(此电脑或当前用户)
        "allowElevation": true, // 允许请求提升。 如果为false,则用户必须使用提升的权限重新启动安装程序。
        "allowToChangeInstallationDirectory": true, // 允许修改安装目录
        "installerIcon": "../build/icons/icon.icns", // 安装图标
        "uninstallerIcon": "../build/icons/icon.icns", //卸载图标
        "installerHeaderIcon": "../build/icons/icon.icns", // 安装时头部图标
        "createDesktopShortcut": true, // 创建桌面图标
        "createStartMenuShortcut": true, // 创建开始菜单图标
    }
    "linux": {
      "icon": "build/icons",
      "artifactName": "${productName}_setup_${version}.${ext}"
    }
  }
  "scripts": {
    "builder": "electron-builder"
  }
  • 要打包成安装程序的话我们有两种方式,a.使用NSIS工具对我们的文件夹再进行一次打包,打包成exe; b.通过electron-builder的nsis直接打包成exe.这个nsis的配置指的是安装过程的重要配置,如果不配置nsis那么应用程序就会自动的安装在C盘。没有用户选择的余地,这样是不友好的(这里的nsis是一些基础的的配置,支持引入自定义的nsis脚本,来完成一些定制化的需求)。

注意: 配置了publish会生成latest.yml文件,用于自动更新的配置信息;不要自己轻易去修改该文件。如果文件有误,需要重打包获取新的latest.yml文件。

C. 打包操作

 npm run builder

配置了publish之后在mac上会生成.dmg的包以及latest-mac.yml文件; 在windows上会生成.exe的包以及latest.yaml, 这是实现自动更新的关键。根据latest.yaml里的版本信息去判断是否有新版本,需要进行下载安装。

// windows 下的latest.yml
version: 2.8.0

files:

        - url: 2.8.0.exe

        sha512:  FmSi6nU1PJ1LRQIBjuvaw0TG32KHPM76FlGMRcWrSNOs7XGeaUALspgOKknTFYzuqmjEJk6JiHGNOm/UH+wDLw==

size: 42463133

// 当target选择nsis和zip等时候,此时的files里会有多项,所以path是指定下载时候对于服务器上的下载文件地址,如果是mac的话指的的.zip.   sha512参数是在下载时候做校验的,以确保下载的是正确的新版本
path: 2.8.0.exe

sha512: FmSi6nU1PJ1LRQIBjuvaw0TG32KHPM76FlGMRcWrSNOs7XGeaUALspgOKknTFYzuqmjEJk6JiHGNOm/UH+wDLw==

releaseDate: '2020-01-21T02:24:38.716Z'

D. 配置主进程main.js文件,引入 electron-updater 文件,添加自动更新检测和事件监听(一定要是主进程main.js文件(或主进程main中的index.js文件),否则会报错)

const {app, BrowserWindow,Menu,ipcMain} = require('electron')
const path = require('path')
const package = {
    version:"2.8.1",
    productEnv:"development",// development products
    feedUrl:"http://127.0.0.1/"
};

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let mainWindow;

// ===============================================================更新区
// ===============================================================更新区
// ===============================================================更新区
const autoUpdater = require('electron-updater').autoUpdater;
// 检测更新,在你想要检查更新的时候执行,renderer事件触发后的操作自行编写
function updateHandle() {
    let message = {
        error: 'Check update is error.',
        checking: 'checking update now.',
        updateAva: 'has new packer is downing...',
        updateNotAva: 'is the lasted packer. do not to update   !',
    };
    const os = require('os');

    autoUpdater.setFeedURL(package.feedUrl);
    autoUpdater.on('error', function (error) {
        console.log('出错了', error)
        sendUpdateMessage({cmd:'error',message:error})
    });
    autoUpdater.on('checking-for-update', function (message) {
        sendUpdateMessage({cmd:'checking-for-update',message:message})
    });
    autoUpdater.on('update-available', function (message) {
        sendUpdateMessage({cmd:'update-available',message:message})
    });
    autoUpdater.on('update-not-available', function (message) {
        sendUpdateMessage({cmd:'update-not-available',message:message})
    });

    // 更新下载进度事件
    autoUpdater.on('download-progress', function (progressObj) {
        sendUpdateMessage({cmd:'download-progress',message:progressObj})
    })
    autoUpdater.on('update-downloaded', function (event, releaseNotes, releaseName, releaseDate, updateUrl) {
        sendUpdateMessage({cmd:'update-downloaded',message:{
            releaseNotes, releaseName, releaseDate, updateUrl
        }})
    });

    ipcMain.on('isUpdateNow', (e, arg)=>{
        // sendUpdateMessage({cmd:'isUpdateNow',message:arg})
        //some code here to handle event
        autoUpdater.quitAndInstall();
    });

    ipcMain.on("checkForUpdate",(e, arg)=>{
        //执行自动更新检查
        // sendUpdateMessage({cmd:'checkForUpdate',message:arg})
        autoUpdater.checkForUpdates();
    })

}

// 通过main进程发送事件给renderer进程,提示更新信息
function sendUpdateMessage(data) {
    mainWindow.webContents.send('message', data)
}
此处是在主进程中调用自动更新的api(事件触发可以在渲染进程中自定义并且通过ipcRenderer知会主进程),主进程中可以在对应的事件钩子里拿到对应的状态参数,如是否需要更新,下载包的进度,下载是否成功等。然后将这些通讯给渲染进程,可以将其反馈在界面上显示。
const ipcRenderer = require('electron').ipcRenderer;
ipcRenderer.on("staticData",function(event,data){
    // 获取配置项数据
    console.log('获取配置项数据', data)
    if(data.version) {
        Upd_version.innerText = data.version;
        if(typeof window.updateVerson == 'function') _app.innerText = data.version
    }
})

ipcRenderer.on('message',(event,data) => {
    // 初始化参数
    data = data || {};
    var message = data.message || {};
    console.log(data)

    // 需更新
    if(data.cmd == 'update-available'){
        Upd_app.style.display = "block";
        if(message.version) {
            console.log("正在更新到最新版本:v "+message.version);
            Upd_version.innerText = message.version;
        }
    }
    // 更新异常
    if(data.cmd == 'error'){
        console.log('error', data)
        Upd_lock = false;
        Upd_btn.innerText = "立即更新";

        if(message.errno == -4058){
            Upd_app.style.display = "none";
        }
    }
    // 下载中
    if(data.cmd == 'download-progress'){
        Upd_progress.value = Number(message.percent);
        Upd_percent = Number(message.percent);
    }
    // 下载完成
    if(data.cmd == 'update-downloaded'){
        // 下载完成 开始安装
        if(Upd_percent == 100){
            Upd_btn.innerText = "安装重启中...";
            ipcRenderer.send('isUpdateNow');
        }
    }
});
setTimeout(function(){
    autoUpdate()
},500);
function autoUpdate(){
    if(Upd_lock) return false;
    Upd_lock = true;
    Upd_btn.innerText = "版本更新中.";
    ipcRenderer.send('checkForUpdate');
}
渲染进程接受这些状态然后进行反馈,当下载成功时进询问用户。

E. 现在可以去模拟测试了,以windows的为例,安装刚刚打包出来的2.8.0的应用,然后再将版本号改为2.8.1,然后进行打包,并且部署到publish里设置的url服务目录下(用了IIS开了本地服务,不过刚开始一直失败,原来是要为.yml添加MIME的扩展类型~)

windows安装示例

效果如上视频所示,autoUpdater会检测到有新版本并且进行下载,在下载完成后可以自己选择弹框询问用户是否立即更新,用户选择确定之后,会去调用autoUpdater.quitAndInstall(),会退出当前的应用并且安装刚刚下载好的新版本