使用electron-builder在windows上打包并自动更新

17

使用electron-builder在windows上打包并自动更新


心得:从一个小白开始独自一人了解electron这个玩具,实现其打包到自动更新,查阅各种资料,做了各种尝试,现在将自己经历过的一些东西写下来,供参考!

注意:文章适合了解electron是什么,知道如何简单打包的读者,文章中代码只是一种思路参考,并不完整,不能直接使用。

打包

尝试过用electron-packager和electron-builder打包,最终选择了electron-builder,原因后面会说;

关于网络,在使用electron-packager或者electron-builder打包的过程中,可能会下载electron包以及其它依赖,有些包和依赖可能服务器在国外,会很慢,所以需要设置代理,electron可以通过设置淘宝镜像快速下载ELECTRON_MIRROR="https://npm.taobao.org/mirrors/electron/"(windows下通过set设置该参数);

这有个有趣的事情,我通过代理下载的时候有个叫codeSign的包怎么都下不下来,总是提示timeout,后来我用笔记本连接手机热点,手机开启shadowsocks代理,通过蜂窝网络这种方式居然下载下来了,遇到类似问题都可以尝试;

electron-packager

简单用法:

具体用法参考官网:electron-packger
"scripts": {
    "package": "electron-packager ./ --overwrite -all"
}

注意事项:

  1. 直接这么打包会将node_modules文件夹全部打包进去,包会很大,需要自行进行忽略某些文件或者文件夹,因为作者使用webpack将代码打包,所以我是在webpack打包好的文件夹,独立建立了一个package.json文件,将需要的包和文件放进去,在该文件夹下打包;
  2. 注意 package.json 的额外字段 —— productName、author 和 description,虽然这几个字段并不是打包必备的,但它们会在 Windows 的 Squirrel 安装包(用于自动更新,后面会说)中使用到。

特点:

  1. 在macOS系统下,打的包是app文件(也就是平常通过dmg文件安装后的应用程序,拖到应用程序能直接使用),不能直接打包dmg文件;(不记得能不能打windows的包了,网上参考资料说不能打windows包);
  2. 在windows系统下,能打包免安装的包,类似于我们网上下载绿色版软件,直接点击exe文件就运行软件;不能直接打包exe安装文件;(能否打app文件自行探索,蛤)
  3. 在自动更新的时候支持electron自带的autoUpdatersquirrel的方式(squirrel在windows下需要自己安装某些文件);

electron-builder

简单用法:

具体用法参考官网:electron-builder
"build": {
    "appId": "com.xxx.app",
    "mac": {
      "target": ["dmg","zip"]
    },
    "win": {
      "target": ["nsis","zip"]
    }
},
"scripts": {
    "dist": "electron-builder -wm"
},
注意事项:参考electron-packager

特点:

  1. 在macOS系统和windows系统都可以到dmg和exe安装文件;
  2. windows可以使用nsis打包安装文件,具体配置可以参考官网相关配置;
  3. 几乎支持了所有平台的所有格式;
  4. 自动更新的时候,不支持squirrel.Windows的方式,并且不使用electron自带的autoUpdater,需要使用 electron-updater包;

自动更新(windows下)

这边不详细描述mac下的自动更新,因为mac下无论哪种方法都需要为应用执行(Code-signing)这是squirrel.Mac必要条件,由于没有mac下的需要,所以需要的自行查阅资料(我不会告诉你是因为mac的开发者账号贵,没钱,蛤蛤蛤);自动更新这里提供两个参考,作者也是一开始参考的这两个网页(非常详细):
  1. Electron 自动更新的完整教程(Windows 和 OSX)
  2. electron在windows系统下实现自动更新

方法一:使用electron-packager配合grunt-electron-installer

  • 步骤一:使用electron-packager打包应用,打包出一个免安装包,确认可以执行应用;
  • 步骤二:

安装electron-squirrel-startup:(用于生成应用快捷方式)

npm install electron-squirrel-startup

==========================华丽的分割线=============================

使用grunt的作用是可以简单理解为将electron-packager打的包,再次打包生成安装文件exe,并且添加关于自动更新的文件,例如:RELEASES文件,setup程序等

安装 grunt-electron-installer:(使用grunt打包,配合squirrel实现自动更新)

npm install -g grunt-cli

npm install grunt grunt-electron-installer --save-dev

  • 步骤三:

在项目根目录下建Gruntfile.js(直接使用package.js中关于version等的配置就好了,不需要gruntPackage.json文件了)

参考:grunt-electron-installer

var grunt=require('grunt');

//配置
grunt.config.init({
    pkg: grunt.file.readJSON('./package.json'),
    'create-windows-installer': {
        x64:{
            authors:'xxxx',
            projectUrl:'',
            appDirectory:'./OutApp/Client-win32-x64',//要打包的输入目录
            outputDirectory:'./OutPut',//grunt打包后的输出目录
            exe:'Client.exe',
            description:'Client',
            setupIcon:"xxxx.ico",
            noMsi:true
        }
    }
});

//加载任务
grunt.loadNpmTasks('grunt-electron-installer');

//设置为默认
grunt.registerTask('default', ['create-windows-installer']);
  • 步骤四:

在主进程main.js中添加squirrel安装打包配置以及自动更新检测和事件监听:

参考:

Handling Squirrel Events

auto-updater

const electron = require('electron')
//自动更新
const autoUpdater = electron.autoUpdater

//生成和删除应用快捷方式,用于安装和卸载,直接在app.on('ready',() => {startupEventHandle()})的时候执行;
function startupEventHandle(){
  if(require('electron-squirrel-startup')) return;
  var handleStartupEvent = function () {
    if (process.platform !== 'win32') {
      return false;
    }
    var squirrelCommand = process.argv[1];
    switch (squirrelCommand) {
      case '--squirrel-install':
      case '--squirrel-updated':
        install();
        return true;
      case '--squirrel-uninstall':
        uninstall();
        app.quit();
        return true;
      case '--squirrel-obsolete':
        app.quit();
        return true;
    }
      // 安装
    function install() {
      var cp = require('child_process');    
      var updateDotExe = path.resolve(path.dirname(process.execPath), '..', 'update.exe');
      var target = path.basename(process.execPath);
      var child = cp.spawn(updateDotExe, ["--createShortcut", target], { detached: true });
      child.on('close', function(code) {
          app.quit();
      });
    }
    // 卸载
    function uninstall() {
      var cp = require('child_process');    
      var updateDotExe = path.resolve(path.dirname(process.execPath), '..', 'update.exe');
      var target = path.basename(process.execPath);
      var child = cp.spawn(updateDotExe, ["--removeShortcut", target], { detached: true });
      child.on('close', function(code) {
          app.quit();
      });
    }
  };
  if (handleStartupEvent()) {
    return ;
  }
}

// 检测更新,在你想要检查更新的时候执行,renderer事件触发后的操作自行编写
function updateHandle(){
    let message={
      error:'检查更新出错',
      checking:'正在检查更新……',
      updateAva:'检测到新版本,正在下载……',
      updateNotAva:'现在使用的就是最新版本,不用更新',
    };
    const os = require('os');
    autoUpdater.setFeedURL('放最新版本文件的文件夹的服务器地址');
    autoUpdater.on('error', function(error){
      sendUpdateMessage(message.error)
    })
    .on('checking-for-update', function(e) {
      sendUpdateMessage(message.checking)
    })
    .on('update-available', function(e) {
        sendUpdateMessage(message.updateAva)
    })
    .on('update-not-available', function(e) {
        sendUpdateMessage(message.updateNotAva)
    })
    .on('update-downloaded',  function (event, releaseNotes, releaseName, releaseDate, updateUrl, quitAndUpdate) {
        ipcMain.on('isUpdateNow', (e, arg) => {
            //some code here to handle event
            autoUpdater.quitAndInstall();
        })
        mainWindow.webContents.send('isUpdateNow')
    });
    
    //执行自动更新检查
    autoUpdater.checkForUpdates();
}

// 通过main进程发送事件给renderer进程,提示更新信息
// mainWindow = new BrowserWindow()
function sendUpdateMessage(text){
    mainWindow.webContents.send('message', text)
}
  • 步骤五:

执行grunt,在你指定的目录生成三个文件:exe 文件是安装包,RELEASES 包含安装及版本信息(自动更新就是通过对比该文件进行自动更新),nupkg 文件

至此,完成了一个版本的打包,双击exe文件后,弹出绿色的安装动画,无法选择安装目录,会自动安装在C:UsersAdministratorAppDataLocal下,安装结束后动画消失(一定要等安装的绿色动画消失才能对应用进行操作,不然可能会报错,也不能手动关闭安装程序),并自动创建了快捷方式。

如何进行版本更新?

  1. 改变package.json中的version属性,例如:改为 version: "1.0.1" (之前为1.0.0);
  2. 然后再次执行步骤一和步骤五,打包一个新的版本,多出两个文件,改变了RELEASES文件,除了上述三个文件外还有个delta文件,这个是增量包;
  3. 将新版本的上述文件(只需要nupkg文件和RELEASES文件)放到autoUpdater.setFeedURL(url)该方法中设置的url指向的文件夹下就可以了,执行更新检查的时候,autoUpdate会自动到该目录下查看 RELEASES 文件的版本信息,与安装目录下的 RELEASES 文件进行比对,进行更新下载;
会发现使用squirrel.Windows的时候,服务器仅仅作为文件服务器放置更新文件,不要编写其它文件,使用squirrel.Mac需要服务器编写接口文件进行检测判断,两者不太一样。

总结:如果是简单更新大概就是 -> 配置好 Gruntfile.js 和 main.js(包括与renderer的通信) 文件,electron-packager打包,grunt打包electron-packager打包生成的文件,会生成安装文件和版本信息,将grunt打包的东西放到一个文件服务器上,在本地安装的app上启动检查更新。

方法二:使用electron-builder配合electron-updater实现自动更新

参考:Auto Update

后来通过对比放弃了方法一,使用了方法二;主要原因有以下几点:(从我使用的情况来看,也可能是我自己没找到解决以下问题的方法,如果有欢迎告诉作者,谢谢)
  1. 因为方法一打包出来的安装文件 exe 比较大,原因是不是使用nsis方式压缩打包的;
  2. 并且electron自带的autoUpdater中事件和方法都没有electron-updater中的丰富,有不少实现不了,比如:使用原生autoUpdater下载更新时的进度条做不了,但是electron-updater提供了download-progerss事件,该事件提供了下载速度(bytesPerSecond)、已下载百分比(percent)、已传输(transferred)、文件大小(total)等参数,非常方便;
  3. autoUpdater一旦启动checkForUpdates(),有更新时就会自动下载更新,没办法忽略版本的更新,但是electron-updater提供了 appUpdater.autoDownload = false 的属性配置,可以不自动下载更新,需要下载的时候执行 appUpdater.downloadUpdate() 方法就可以了,完美解决我的困惑。
  4. 并且方法二只需要执行electron-builder的打包程序就可以了,会生成一个类似方法一中的RELEASES文件的latest.yml文件,不需要执行两次打包;更新的时候和方法一同样简单,只需要把electron-builder打包出来的文件放到文件服务器就可以了。
具体步骤如下
  • 步骤一:安装 electron-updater 包模块
npm install electron-updater --save
  • 步骤二:

配置package.json文件,只需要在 build 参数中添加 publish 配置就好了,具体参考PublishConfiguratio

注意:只有配置了publish才能生成latest.yml文件;
"build": {
    "appId": "com.xxx.app",
    "publish": [
        {
            "provider": "generic",
            
            //类似于autoUpdater.setFeedURL(url)中的url,用于自动更新的文件地址
            "url": "http://www.xxx.com/"
        }
    ],
    "mac": {
      "target": ["dmg","zip"]
    },
    "win": {
      "target": ["nsis","zip"]
    }
},
"scripts": {
    "dist": "electron-builder -wm"
},
  • 步骤三:

配置main.js文件,引入 electron-updater 文件,后面用法和electron自带的autoUpdater类似;

// 注意这个autoUpdater不是electron中的autoUpdater
import { autoUpdater } from "electron-updater"

// 检测更新,在你想要检查更新的时候执行,renderer事件触发后的操作自行编写
function updateHandle(){
    let message={
      error:'检查更新出错',
      checking:'正在检查更新……',
      updateAva:'检测到新版本,正在下载……',
      updateNotAva:'现在使用的就是最新版本,不用更新',
    };
    const os = require('os');
    autoUpdater.setFeedURL('放最新版本文件的文件夹的服务器地址');
    autoUpdater.on('error', function(error){
      sendUpdateMessage(message.error)
    });
    autoUpdater.on('checking-for-update', function() {
      sendUpdateMessage(message.checking)
    });
    autoUpdater.on('update-available', function(info) {
        sendUpdateMessage(message.updateAva)
    });
    autoUpdater.on('update-not-available', function(info) {
        sendUpdateMessage(message.updateNotAva)
    });
    
    // 更新下载进度事件
    autoUpdater.on('download-progress', function(progressObj) {
        mainWindow.webContents.send('downloadProgress', progressObj)
    })
    autoUpdater.on('update-downloaded',  function (event, releaseNotes, releaseName, releaseDate, updateUrl, quitAndUpdate) {
        ipcMain.on('isUpdateNow', (e, arg) => {
            //some code here to handle event
            autoUpdater.quitAndInstall();
        })
        mainWindow.webContents.send('isUpdateNow')
    });
    
    //执行自动更新检查
    autoUpdater.checkForUpdates();
}

// 通过main进程发送事件给renderer进程,提示更新信息
// mainWindow = new BrowserWindow()
function sendUpdateMessage(text){
    mainWindow.webContents.send('message', text)
}
  • 步骤四:

执行electron-builder进行打包,会生成安装包exelatest.yml等文件,执行exe安装软件;

如何进行更新?

  1. 改变package.json中的version属性,例如:改为 version: "1.0.1" (之前为1.0.0);
  2. 再次执行electron-builder打包,将新版本latest.yml文件和exe文件放到package.jsonbuild -> publish中的url对应的地址下;
  3. 在应用中触发更新检查,electron-updater自动会通过对应url下的yml文件检查更新;

总结:通过对比发现,方法二比方法一更加简单方便,并且打包生成的exe安装文件小很多(作者通过方法一生成安装文件70M,方法二仅仅 36M),唯一一个问题是,通过方法二没有产生delta文件,也就是没有增量包。

结语:这只是一个小白的实践总结,其中有不少是自己的理解,可能不对,有许多需要改进的地方,希望通过这个总结,自己在以后回顾有机会有更多不同的收获。

你可能感兴趣的

19 条评论
漱柯 · 2017年07月27日

真棒!

回复

随遇而安 · 2017年09月04日

技术用的很溜,文章写的很垃圾,一点儿都不具体。

回复

0

嗯嗯,批评得不错

木木俞 作者 · 2017年09月20日
pinkie · 2017年10月06日

有个问题想请教一下作者。在main.js中插入的import { autoUpdater } from "electron-updater"会报语法错误,import无法识别。作者没有这个错误吗?

回复

0

没有,而且在main.js中我用了不少import的方式

木木俞 作者 · 2017年10月18日
0

@木木俞 好吧。。也许是我少了什么东西

pinkie · 2017年10月27日
0

你好,你的问题解决了吗 我遇到了和你一样的问题

zz撒大苏打实打实 · 2017年11月30日
joey · 4月13日

electron-update 停留在正在下载,但是没有下载进度通知,是怎么回事

回复

大灰狼wow · 4月16日

在main.js中插入的import { autoUpdater } from "electron-updater"会报语法错误,import无法识别
同样遇到这个问题

回复

0

const autoUpdater = require('electron-updater').autoUpdater
这样子就可以了

大灰狼wow · 4月16日
苏格兰折耳猫 · 5月7日

build>publish>url和uploadUrl有什么区别呢?我打包后download-progress会触发两次(0到100),之后update-downloaded,却不会触发,会是什么原因呢

回复

Tip · 5月16日

作者您好,我在electron-builder遇到了这个问题:Error output:
!include: could not find: "F:workspace桶装水项目trunkcodefront_endpcmerch-endelectron-appnode_moduleselectron-builder-libtemplatesnsisincludeStdUtils.nsh"
Error in script "<stdin>" on line 1 -- aborting creation process
能帮我解答下吗?

回复

howartin · 5月21日

我想问下用mac能打出exe么

回复

健儿 · 6月5日

我检查更新的时候 提示这个 大家有见过吗?
Latest version (from update server) is not valid semver version: "null

回复

健儿 · 6月5日

重点是 我配置了publish 依然没有生产lates.yml

回复

shoyuf · 8月21日

codeSign 之类的包无法下载时可到相应的git仓库下载,解压到相关文件夹就行

回复

载入中...