Development update incremental update of electronic from scratch (1)

Time:2021-4-30

Update – incremental update (1)

In the last issue, we have finished the full update of electron. In this issue, we introduce several schemes of how to modify only part of the files to achieve incremental update.

asar

After we install the electronic software, right-click to open the location of the file to enterresourcesDirectory (MAC shows package contents), you can seeapp.asarThis file is the main business file of the electron program. It’s not an encrypted file. It’s actually a compressed file. We can decompress this file with the NPM package

npm install -g asar
ASAR extract app. ASAR. / (run under the same level directory as app. ASAR. Note: if the students installed on Disk C can't unzip, run CMD as administrator to unzip)

After decompression, it is found thatdist_electron/bundledIn fact, if we only modify the things in the rendering process, we don’t need to do a complete package update. As long as we replace JS, HTML and CSS, our page will also be updated. Then we only need to update a few m files, and we don’t need to let users download a complete new package. The advantage of incremental update lies in this.

However, it’s easy to say, but there are still some problems in actual operation. If you set the packageasar:trueIf so, it should be done when the software startsapp.asarReplacement will find that it cannot be replaced (under win) and is being used by software. So this scheme will definitely not work with ordinary replacement. I will introduce several schemes for your reference.

7z-Asar7z

Here, I also provide a 7z plug-in to enable 7z to open ASAR,linkIf your 7z is installed on Disk C, putAsar.64.dll(64 bit system) put inC:\Program Files\7-Zip\Formats\Inside,FormatsIf not, create a new one yourself.
Development update incremental update of electronic from scratch (1)

Scheme 1, asar:false

This is a common way. For example, vscode uses this scheme. When packaging, you need to modify the packaging configuration (builderoptions in vue.config.js)asar:falseSo when you pack itresourcesIt won’t happen in the next few minutesapp.asar, but an app folder, and this folder can be replaced directly, so there is no problem that it cannot be replaced.

In short, settingasar:false, package, and enter the packaged green installation packagedist_electron/win-ia32-unpacked/resources(Win32), compress the app file intoapp.zipPut it on the server, the rendering process detects the incremental update, notifies the main process, and the main process downloads app.zip, decompresses and replaces it.

  • Advantages: simple and rough.
  • Disadvantages: the installation and full update are slow, the main process is exposed, the replacement is the whole download replacement.

The steps of this scheme are similar to those of scheme 2. For details, please refer to the method of scheme 2.

Scheme 2, app. ASAR. Unpacked + app. ASAR

app.asar.unpackedThis thing is quite common, becauseapp.asarFor example, the file is only readable, and some node commands can’t be used. We often use this for some third-party database or DLL files. To put it simply, we should put it in theapp.asarPut files in andapp.asarPeerapp.asar.unpackedIn the directory (actually similar to the app folder of scheme 1), so as to release theapp.asarIt’s very restrictive.

See here is not a new idea, sinceapp.asarIt can’t be moved. We can throw the changed documents inapp.asar.unpackedThe main process and some unchanging things are still in theapp.asar, replace on incremental updateapp.asar.unpackedThat’s it.

  • Advantages: you can keep the main process JS and other files in theapp.asarTo replace only render process files.
  • Disadvantages: since the main process JS does not move, the version number of the environment variable injected by the main process will not change, that is, the version number obtained by using the environment variable in the main process after the update is not the updated version number (you can get it from the rendering process).

Implementation steps:

1. Set app.asar.unpacked

First of all, set up the files we want to replace, which will be generated when packingdist_electron/bundledThis folder, and then use it againelectron-builderPackage this folder into our electron file.

Builderoptions of vue.config.js

extraResources: [{
  from: "dist_electron/bundled",
  to: "app.asar.unpacked",
  filter: [
    "!**/icons",
    "!**/preload.js",
    "!**/node_modules",
    "!**/background.js"
    ]
  }],
  files: [
    "**/icons/*",
    "**/preload.js",
    "**/node_modules/**/*",
    "**/background.js"
  ],

How about extraresourcesapp.asar.unpackedFiles is the settingapp.asarWhat we mean here is that we putdist_electron/bundledIn addition toiconsbackground.jsAnd so onapp.asar, and the rest are put inapp.asar.unpackedTake a look, take a lookapp.asar.unpackedIs it what we want.

2. Build incremental zip

Now we have itapp.asar.unpackedBut we can’t manually compress it in the installation free package every timeapp.asar.unpackedIt’s too much trouble. Here we use the packaged hook to automatically build incremental packages.
ADM zip is used to process zip package, FS extra is an extension of fs to process files

npm i adm-zip
npm i fs-extra

electron-builderProvide the completed hook in the packageafterPack

Add builderoptions of vue.config.js

afterPack: './afterPack.js',

./afterPack.js:
const path = require('path')
const AdmZip = require('adm-zip')

exports.default = async function(context) {
  let targetPath
  if(context.packager.platform.nodeName === 'darwin') {
    targetPath = path.join(context.appOutDir, `${context.packager.appInfo.productName}.app/Contents/Resources`)
  } else {
    targetPath = path.join(context.appOutDir, './resources')
  }
  const unpacked = path.join(targetPath, './app.asar.unpacked')
  var zip = new AdmZip()
  zip.addLocalFolder(unpacked)
  zip.writeZip(path.join(context.outDir, 'unpacked.zip'))
}

The resources of MAC and win are different. Now let’s pack them up,dist_electronThe directory will generate aunpacked.zipThis is our incremental package.

3. Loading policy modification

In the window startup section, we said that the HTML loading of our rendering process is throughapp://Protocol loading. This protocol used to be loaded withapp.asarFor the root directory, here the rendering process of the file to move outapp.asarIt’s too late,app://The protocol can’t find our rendering process HTML, so we need to modify itapp.asar.unpackedAs the root directory.

The main process is found in main / index.js
//Import {create protocol} from 'Vue cli plugin electronic builder / lib' we find this file and copy it,
//Modify readfile (path. Join (__ Dirname, pathname). You can see that this protocol reads__ Dirname (` app. ASAR '), we pass in a path to replace the original file__ dirname

Create createprotocol.js

import { protocol } from 'electron'
import * as path from 'path'
import { readFile } from 'fs'
import { URL } from 'url'

export default (scheme, serverPath = __dirname) => {
  protocol.registerBufferProtocol(
    scheme,
    (request, respond) => {
      let pathName = new URL(request.url).pathname
      pathName = decodeURI(pathName) // Needed in case URL contains spaces
      readFile(path.join(serverPath, pathName), (error, data) => {
        if (error) {
          console.error(
            `Failed to read ${pathName} on ${scheme} protocol`,
            error
          )
        }
        const extension = path.extname(pathName).toLowerCase()
        let mimeType = ''

        if (extension === '.js') {
          mimeType = 'text/javascript'
        } else if (extension === '.html') {
          mimeType = 'text/html'
        } else if (extension === '.css') {
          mimeType = 'text/css'
        } else if (extension === '.svg' || extension === '.svgz') {
          mimeType = 'image/svg+xml'
        } else if (extension === '.json') {
          mimeType = 'application/json'
        } else if (extension === '.wasm') {
          mimeType = 'application/wasm'
        }

        respond({ mimeType, data })
      })
    },
    (error) => {
      if (error) {
        console.error(`Failed to register ${scheme} protocol`, error)
      }
    }
  )
}

The main process introduces our modifiedcreateProtocol.js

import createProtocol from './services/createProtocol'
const resources = process.resourcesPath

Change the original create protocol ('app ') to
createProtocol('app', path.join(resources, './app.asar.unpacked'))

We can go through it nowapp://Protocol loadingapp.asar.unpackedUnder the file, a package to try to see if the page can load normally, of course, if you are directly usingfile://The protocol loading local file changes are similar, that is, change the loading address. After the preparatory work is completed, we begin to render the incremental update logic of the process.

4. Analog interface

I won’t say much here. Just like the previous full update, if you don’t know, you can go to the previous content, return and modify it with the HTTP server simulation interface.env.devby0.0.2, package generationunpacked.zipAnd put it in the server directory

{
  "code": 200,
  "success": true,
  "data": {
    "forceUpdate": false,
    "fullUpdate": false,
    "upDateUrl": "http://127.0.0.1:4000/unpacked.zip",
    "restart": false,
    "Message": "I want to upgrade to 0.0.2,",
    "version": "0.0.2"
  }
}

5. Incremental update of rendering process

The logic of the page here is similar to that of the previous full update. We have detected that the page is updated withwin-incrementSend update information to master process:

<template>
  <div class="increment">
    The current version is: {{config. Vue}_ APP_ VERSION }}</div>
    < a-button type = "primary" @ Click = "updateclick (true)" > detect updates < / a-button >
  </div>
</template>

<script>
import cfg from '@/config'
import update from '@/utils/update'
import { defineComponent, getCurrentInstance } from 'vue'

export default defineComponent({
  setup() {
    const { proxy } = getCurrentInstance()
    const config = cfg
    const api = proxy.$api
    const message = proxy.$message
    function upDateClick(isClick) {
      api('http://localhost:4000/index.json', {}, { method: 'get' }).then(res => {
        console.log(res)
        if (cfg.NODE_ENV !== 'development') {
          update(config.VUE_APP_VERSION, res).then(() => {
            if (!res.fullUpdate) {
              window.ipcRenderer.invoke('win-increment', res)
            }
          }).catch(err => {
            if (err.code === 0) {
              Isclick & & message. Success ('Is the latest version ')
            }
          })
        } else {
          Message. Success ('Please update in package environment ')
        }
      })
    }
    return {
      config,
      upDateClick
    }
  }
})
</script>

6. Main process processing

Ipcmain.js add
import increment from '../utils/increment'

ipcMain.handle('win-increment', (_, data) => {
  increment(data)
})

Incremental update processingincrement.js, byupDateUrlDownload the incremental package. After the download, we will download the originalapp.asar.unpackedRename the backup, if there is an error, you can restore it, and then unzip the download. After processing, we can use itreloadIgnoringCacheJust reload the page, of course you can also use itapp.relaunch()restart app

import downloadFile from './downloadFile'
import global from '../config/global'
import { app } from 'electron'
const path = require('path')
const fse = require('fs-extra')
const AdmZip = require('adm-zip')

export default (data) => {
  const resourcesPath = process.resourcesPath
  const unpackedPath = path.join(resourcesPath, './app.asar.unpacked')
  downloadFile({ url: data.upDateUrl, targetPath: resourcesPath }).then(async (filePath) => {
    backups(unpackedPath)
    const zip = new AdmZip(filePath)
    zip.extractAllToAsync(unpackedPath, true, (err) => {
      if (err) {
        console.error(err)
        reduction(unpackedPath)
        return
      }
      fse.removeSync(filePath)
      if (data.restart) {
        reLoad(true)
      } else {
        reLoad(false)
      }
    })
  }).catch(err => {
    console.log(err)
  })
}

function backups(targetPath) {
  If (FSE. Pathexistssync (targetpath + '. Back')) {// delete old backup
    fse.removeSync(targetPath + '.back')
  }
  if (fse.pathExistsSync(targetPath)) {
    FSE. Movesync (targetpath, targetpath + '. Back') // backup directory
  }
}

function reduction(targetPath) {
  if (fse.pathExistsSync(targetPath + '.back')) {
    fse.moveSync(targetPath + '.back', targetPath)
  }
  reLoad(false)
}

function reLoad(close) {
  if (close) {
    app.relaunch()
    app.exit(0)
  } else {
    global.sharedObject.win.webContents.reloadIgnoringCache()
  }
}

Packaged download filedownloadFile.js

const request = require('request')
const fs = require('fs')
const fse = require('fs-extra')
const path = require('path')

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, 'network fluctuation, incomplete download file')
        }
      } 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, 'network fluctuation, Download failure')
      } 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 = () => { }) {
  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)
      }
    })
  })
}

The basic logic of incremental update is completed. If you are using scheme 1, you can also refer to the process. Click the detection update of rendering process to see if the version changes0.0.2No,
Development update incremental update of electronic from scratch (1)
Development update incremental update of electronic from scratch (1)

Scheme defect treatment

As we said earlier, this scheme has a disadvantage that the environment variables in the main process will not change, so we can use theprocess.env.VUE_APP_VERSIONGet the version number or get the previous version number.
Our rendering process is repackaged, so its environment variables are accurate. At this time, we can send the configuration information from the rendering process to the main process when the page is loaded.

Renderer's app.vue:
import cfg from '@/config'
window.ipcRenderer.invoke('win-envConfig', cfg)

global.js:
global.envConfig = {}

Ipcmain.js of Main:
import global from '../config/global'
ipcMain.handle('win-envConfig', (_, data) => {
  global.envConfig = data
})

No longer usedprocess.env.VUE_APP_VERSIONTo get the version number information, useglobal.config.VUE_APP_VERSIONGet, and try a 0.0.2 package again.

Supplementary notes

  • Here is just a simple incremental update logic. If you want a download schedule, you can implement it yourself
  • Generally speaking, this kind of incremental update package will save the address to the database when it is uploaded. You can do some security processing, such as attaching MD5 or Sha to the file when it is saved, and then decompressing after the local verification of consistency after the incremental update download, so as to ensure the accuracy of the file.
  • Of course, there is decompression failure processing. If our incremental update package is damaged, although we have a backup, the restart will still pull the update package to update. If the restart update is used, it will fall into a dead cycle. Here, we can make a version update restart record. After more than several times, we will no longer process this version of the package.

Of course, there are other ways to achieve incremental update. We have finished too much in the first issue. We will continue with other solutions in the next issue.

This series of updates can only be sorted out by weekend and off-duty time. If there are more content, the update will be slow. I hope it can help you. Please support it with more star or like collection

Address:link
Address of this article:link