Incremental update and non-inductive update of electron version update

        The previous article introduced the full update of electron version update (vue)_Zoie_ting's Blog-CSDN Blog

        But the full update package is too large, and it is not necessary to change a lot of things every time. Is there any way to update it in small quantities? Based on the previous article, this article describes how to perform incremental updates. 

        The words do not convey the meaning, take the picture as an example:

1. Get

        Packaged project, there is a bundled in dist, this directory is the part we often modify, including css, js, pictures, html and other files. Install it locally, open the location of the file, and you will find that there is an app.asar in the resources directory. In fact, this file compresses all the contents of the bundled together.

        Maybe readers will question it, so let's do it in practice:

  • First install asar: npm install -g asar
  • Switch to the directory where app.asar is located and execute: asar extract app.asar ./app-bundled
  • After that, you will see an app-bundled folder in resources, the content below is almost the same as the bundled in dist!

In fact, win.loadURL('app://./index.html')         in the main process is the index.html in the running file.

        Therefore, in theory, we only need to take out the files that need to be changed in app.asar, and modify the index.html loading path of the main process.

Second, subcontracting

        Here I want to explain in particular that the version update needs to have a trigger mechanism, whether it is interface acquisition, websocket push, or configuration file declaration. This should be easy to understand. The interface acquisition and websocket push are relatively simple. When the acquired data is different from the user's local one, an update can be triggered. The author uses the configuration file (hotVersion.json) to explain it here.

        The main idea of ​​configuring the packaged subpackage files in vue.config.js is to put the frequently modified files in app.asar.unpacked, take out the hotVersion.json for version comparison, and put the rest of the files in it app.asar.

        The content in hotVersion.json is just a version number for comparison: {"version": "2.5.7"}.

        The files and extraResources configuration items in asar and builderOptions are used here:

  • asar: Whether to use Electron's archive format to package the source code of the application into the archive , must be set to false
  • The file added by file is the file in app.asar in the new package
  • extraResources中:
  1. from means from which path in the finished package, here is dist/bundled
  2. to indicates which path to output these packages, app.asar.unpacked indicates in the app.asar.unpacked folder under the resources folder, ./ indicates under the resources folder
  3. The filter matches the file name. It is important to note that the files in app.asar.unpacked must be filtered out from bundled, and the files in app.asar.unpacked must not be in file, because once the app There is this file in .asar, then the program will automatically use the file instead of the app.asar.unpacked we want

        The configuration is as follows:

  pluginOptions: {
    electronBuilder: {
      //...
      asar: false,
      builderOptions: {
        productName: "test", //包名

        extraResources: [
          {
            from: "dist/bundled",
            to: "app.asar.unpacked",
            filter: [
              "!**/node_modules",
              "!**/background.js",
              "!**/background.js.LICENSE.txt",
              "!**/favicon.ico",
              "!**/package.json",
              "!**/hotVersion.json",
            ],
          },
          {
            from: "dist/bundled",
            to: "./",
            filter: [
              "**/hotVersion.json"
            ],
          },
        ],
        files: [
          "**/node_modules/**/*",
          "**/background.js",
          "**/background.js.LICENSE.txt",
          "**/favicon.ico",
          "**/package.json",
        ],

        win: {
          publish: [
            {
              provider: "generic",
              url: "https:xxx", //更新服务器地址,可为空
            },
          ],
          //...
        },
      },
    },
  },

        According to the above configuration, after packaging and installing locally, there will be more hotVersion.json and app.asar.unpacked folders under the resources folder . The content under app.asar.unpacked is the frequently modified ones filtered out by our configuration. Those ones. Execute asar extract app.asar ./app-bundled, you can see that there are only those files in the files we configured in the extra app-bundled folder. Again, app.asar cannot exist in app.asar.unpacked same file.

        So far, we just separated the installed files, and did not make subpackages. Continue to configure:

         Common Configuration - electron-builder There are several hook functions in electron-builder. Here we need to use afterPack . The functions here will run after packaging. I need to create subpackages here. Adm-zip is used here, please readers Install by yourself.

        Create a new afterPack.js and write the following code:

  • targetPath indicates the path of resources installed locally, which is the path of app.asar.unpacked, and unpacked is the path of app.asar.unpacked, which is the path of the files we need to modify each time. Use adm-zip to download the path All the files in .
  • Perform a write operation on dist/hotVersion.json (if there is no such file, it will be automatically generated), and the written content is exactly the content of our hotVersion.json
const path = require("path");
const AdmZip = require("adm-zip");
const fs = require("fs");

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"));

  fs.writeFile(
    path.join(context.outDir, "hotVersion.json"),
    JSON.stringify(
      {
        version: require("./hotVersion.json").version
      },
      null,
      2
    ),
    (err, data) => {}
  );
};

        At this time, afterPack needs to be added to the vue.config.js configuration:

//...
files:[
    //...
]
afterPack: "./afterPack.js",
//...

        At this point, unpack.zip and hotVersion.json will be generated in the packaged dist folder. Unpack.zip is the package we update each time . Compare the version numbers of .json. If they are inconsistent, download unpack.zip, extract it to app.asar.unpacked, and refresh the page to complete the update.

        Don’t forget, electron runs index.html in app.asar by default. After the above operation, there is no index.html in app.asar, so it will be a white screen after opening. Don't panic, modify the loading path.

        The original configuration for loading index.html is as follows:

import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'

//...
    createProtocol('app')
    win.loadURL('app://./index.html')

          No longer refer to createProtocol of vue-cli-plugin-electron-builder/lib, create a new createProtocol.js:

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

export const createProtocol = (scheme, customProtocol, serverPath = __dirname) => {
  (customProtocol || 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 })
      })
    }
  )
}

        Modify the loading path to load index.html in app.asar.unpacked instead:

let createProtocol = require("./config/createProtocol.js").createProtocol;

//...
   createProtocol(
      "app",
      "",
      path.join(process.resourcesPath, "./app.asar.unpacked")
    );
    win.loadURL("app://./index.html");

        So far, running electron after packaging and installing is not a white screen.

        If you need to release an incremental version, you only need to modify the content of hotVersion.json, put hotVersion.json and unpack.zip on the server after packaging, and update if the local hotVersion.json is inconsistent with the server during detection.

3. Update

        Note: Here is the logic of the full update from the previous article. If you are unclear, you can read the previous article: Full update of electron version update (vue)_Zoie_ting's Blog-CSDN Blog

        Start to check whether there is a full update:

  if (process.env.WEBPACK_DEV_SERVER_URL) {
    //...
  } else {
    createProtocol(
      "app",
      "",
      path.join(process.resourcesPath, "./app.asar.unpacked")
    );
    win.loadURL("app://./index.html");
    checkForUpdates();
  }
function checkForUpdates() {
  autoUpdater.checkForUpdates();
}

        If there is an update package, it will be automatically downloaded from the url configured by autoUpdater.setFeedURL(url), and it will be processed in update-downloaded after the download is complete. Among them, defaultId is the configuration default option, and the value is the index of buttons. For example, the configuration here is 0, which means "no". cancelId is also configured as 0, which means that if you do not select the value of buttons but directly close the pop-up confirmation box, select "No" for processing:

import {
    //...
    dialog
} from "electron";
autoUpdater.on("update-downloaded", () => {
    dialog.showMessageBox({
          type: "info",
          buttons: ["否", "是"],
          title: "应用更新",
          message: "更新包下载完成",
          detail: "请选择是否立即更新",
          defaultId: 0,
          cancelId: 0,
        }).then((res) => {
        if (res.response === 1) {
            autoUpdater.quitAndInstall();
        } else {
        }
    });
});

        If there is no full update, check whether there is an incremental update version:

let currentIncrementUpdate = ""; //本地版本
autoUpdater.on("update-not-available", () => {
  // 读取本地hotVersion
  fs.readFile(
    path.join(process.resourcesPath, "./hotVersion.json"),
    "utf8",
    (err, data) => {
      if (err) {
        //...
      } else {
        //记录本地的版本号,因为我们需要比对本地版本号和线上是否相同再触发更新
        currentIncrementUpdate = JSON.parse(data).version;
        incrementUpdate();
      }
    }
  );
});

        Detect whether there is an incremental update, and if so, download the online version package: 

let obsIncrementUpdate = ""; //服务器版本
let currentIncrementUpdate = ""; //本地版本

// 增量更新
async function incrementUpdate() {
  let oldPath = process.resourcesPath + "/app.asar.unpacked";
  let targetPath = process.resourcesPath + "/unpacked.zip";

  request(
    {
      method: "GET",
      uri: "https://xxx/hotVersion.json",
    },
    function (err, response, body) {
      if (response.statusCode == 200) {
        // 服务器版本
        obsIncrementUpdate = JSON.parse(body).version;
        //两个版本号不同,触发更新
        if (currentIncrementUpdate != obsIncrementUpdate) {
          let req = request({
            method: "GET",
            uri: "https://xxx/unpacked.zip", //增量更新包在服务器上的路径
          });

          try {
            let out = fs.createWriteStream(targetPath);
            let received_bytes = 0;
            let total_bytes = 0;
            req.pipe(out);

            req.on("response", function (data) {
              total_bytes = parseInt(data.headers["content-length"]);
            });

            req.on("data", function (chunk) {
              received_bytes += chunk.length;
            });

            req.on("end", function () {
              if (req.response.statusCode === 200) {
                if (received_bytes === total_bytes) {
                  updateAtOnce(oldPath, targetPath, obsIncrementUpdate);
                } else {
                  out.end();
                  //...省略错误处理
                }
              } else {
                //网络波动,下载文件不全
                out.end();
                //...省略错误处理
              }
            });
            req.on("error", (e) => {
              out.end();
              //网络波动,下载文件不全
              if (received_bytes !== total_bytes) {
               //...省略错误处理
              } else {
               //...省略错误处理
              }
            });
          } catch (err) {
            //...省略错误处理
          }
        } else {
        }
      } else {
        //读取线上的hotVersion错误
        //...省略错误处理
      }
    }
  );
}

        The online incremental update package has been downloaded, and a prompt is given, whether to update immediately:

async function updateAtOnce(oldPath, targetPath, obsIncrementUpdate) {
      dialog
        .showMessageBox({
          type: "info",
          buttons: ["否", "是"],
          title: "应用更新",
          message: "更新包下载完成",
          detail: "请立即完成更新",
          defaultId: 0,
          cancelId: 0,
        })
        .then((res) => {
          if (res.response === 1) {
            //立即更新
            handleIncreaseUpdate(oldPath, targetPath, obsIncrementUpdate);
          } else {
          }
        });
}

        Process immediate update: determine whether app.asar.unpacked.old exists, and if so, delete this folder and all files under this folder, back up app.asar.unpacked as app.asar.unpacked.old first, and decompress it Downloaded unpack.zip to app.asar.unpacked, once an exception occurs during this process, restore app.asar.unpacked.old to app.asar.unpacked.

        The reason why this function is written separately is for subsequent optimization. This function mainly deals with the downloaded installation. If there is already an installation package, it can be upgraded, just execute this function.

//删除目标文件夹以及文件夹下的所有文件
function deleteOld(url) {
  var files = [];
  if (fs.existsSync(url)) {
    files = fs.readdirSync(url);
    files.forEach(function (file, index) {
      var curPath = path.join(url, file);
      if (fs.statSync(curPath).isDirectory()) {
        deleteOld(curPath);
      } else {
        fs.unlinkSync(curPath);
      }
    });
    fs.rmdirSync(url);
  }
}
async function handleIncreaseUpdate(
  oldPath,
  targetPath,
  obsIncrementUpdate,
  reload = true
) {
  //删除目标文件夹以及文件夹下的所有文件
  deleteOld(oldPath + ".old");

  // 建立.old备份
  fs.rename(oldPath, oldPath + ".old", (err) => {
    if (err) {
      //...省略错误处理
      return;
    }
    // 解压
    let zip = new AdmZip(targetPath);
    // 把整个压缩包完全解压到 app.asar.unpacked 目录中
    zip.extractAllToAsync(oldPath, true, (err) => {
      if (err) {
        //恢复
        fs.rename(oldPath + ".old", oldPath, (err) => {});
        return;
      }
      //解压完之后别忘了要修改本地hotVersion文件的版本号,否则会一直触发更新
      fs.writeFile(
        path.join(process.resourcesPath, "./hotVersion.json"),
        JSON.stringify(
          {
            version: obsIncrementUpdate,
          },
          null,
          2
        ),
        (err, data) => {
          if (err) {
            //...省略错误处理
          } else {
            currentIncrementUpdate = obsIncrementUpdate;
            if (reload) {
              //重启应用
              app.relaunch();
              app.exit(0);
            } else {
            }
          }
        }
      );
    });
  });
}

4. Regular update

        The author has already introduced above that the update process has been processed under normal circumstances, but this is detected once when the user starts it. If the user has not exited the software after starting it, there is actually no way to update it according to the above, so you need to Add regular updates.

        It should be noted that if the full package has already been installed, the user should not be allowed to download it, that is to say, the update will no longer be checked.

        The author here assumes that the detection is once every two hours (reserve a long enough time to ensure that the full update package can be downloaded within this time (because the incremental update package is smaller than the full update)), and the modification is as follows:

let timeInterval = null; //检测更新
var updateDownloading = false; //正在下载全量更新包

function checkForUpdates() {
  /防止如果有其他的触发机制,每次先清除定时器,每次触发则重新计时
  clearInterval(timeInterval);
  timeInterval = null;
  // 已下载完成或尚未下载
  if (!updateDownloading) {
    autoUpdater.checkForUpdates();
    timeInterval = setInterval(() => {
      if (!updateDownloading) autoUpdater.checkForUpdates();
    }, 7200000);
  } else {
  }
}
autoUpdater.on("update-available", (info) => {
  updateDownloading = true; 
});
autoUpdater.on("update-downloaded", () => {
  updateDownloading = false; 
}

        Modify incrementUpdate():

let obsIncrementUpdate = ""; //服务器版本
let currentIncrementUpdate = ""; //本地版本
let hasCheckWaitUpdate = false; //稍后更新
let downloadApplying = false;

// 增量更新
async function incrementUpdate() {
  let oldPath = process.resourcesPath + "/app.asar.unpacked";
  let targetPath = process.resourcesPath + "/unpacked.zip";
  if (hasCheckWaitUpdate) {
      dialog
        .showMessageBox({
          type: "info",
          buttons: ["否", "是"],
          title: "应用更新",
          message: "更新包下载完成",
          detail: "请立即完成更新",
          defaultId: 0,
          cancelId: 0,
        })
        .then((res) => {
          if (res.response === 1) {
            handleIncreaseUpdate(oldPath, targetPath, obsIncrementUpdate);
          }
        });
    }
    return;
  }

  if (downloadApplying) {
    dialog
      .showMessageBox({
        type: "info",
        buttons: ["我知道了"],
        title: "应用更新",
        message: "更新包正在下载",
        detail: "请耐心等待",
      })
      .then((res) => {});
    return;
  }
  request(
    {
      method: "GET",
      uri: "https://xxx/hotVersion.json",
    },
    function (err, response, body) {
      if (response.statusCode == 200) {
        // 服务器版本
        obsIncrementUpdate = JSON.parse(body).version;
        //两个版本号不同,触发更新
        if (currentIncrementUpdate != obsIncrementUpdate) {
          downloadApplying = true;
          let req = request({
            method: "GET",
            uri: "https://xxx/unpacked.zip", //增量更新包在服务器上的路径
          });

          try {
            let out = fs.createWriteStream(targetPath);
            let received_bytes = 0;
            let total_bytes = 0;
            req.pipe(out);

            req.on("response", function (data) {
              total_bytes = parseInt(data.headers["content-length"]);
            });

            req.on("data", function (chunk) {
              received_bytes += chunk.length;
            });

            req.on("end", function () {
              if (req.response.statusCode === 200) {
                if (received_bytes === total_bytes) {
                  updateAtOnce(oldPath, targetPath, obsIncrementUpdate);
                } else {
                  out.end();
                  //...省略错误处理
                  downloadApplying = false;
                }
              } else {
                //网络波动,下载文件不全
                out.end();
                //...省略错误处理
                downloadApplying = false;
              }
            });
            req.on("error", (e) => {
              out.end();
              //网络波动,下载文件不全
              if (received_bytes !== total_bytes) {
               //...省略错误处理
              } else {
               //...省略错误处理
              }
              downloadApplying = false;
            });
          } catch (err) {
            //...省略错误处理
            downloadApplying = false;
          }
        } else {
        }
      } else {
        //读取线上的hotVersion错误
        //...省略错误处理
      }
    }
  );
}

        Modify updateAtOnce():

async function updateAtOnce(oldPath, targetPath, obsIncrementUpdate) {
    hasCheckWaitUpdate = true;
    //...
}

        Modify handleIncreaseUpdate():

let decompressing = false; //正在解压
let hasCheckWaitUpdate = false; //稍后更新
let downloadApplying = false;
async function handleIncreaseUpdate(
  oldPath,
  targetPath,
  obsIncrementUpdate,
  reload = true
) {
  if (!fs.existsSync(targetPath)) {
    hasCheckWaitUpdate = false;
    downloadApplying = false;
    return;
  }
  // 不能重复处理文件
  if (decompressing) {
    return;
  }
  decompressing = true;

  //...

  fs.rename(oldPath, oldPath + ".old", (err) => {
    if (err) {
      //...省略错误处理
      hasCheckWaitUpdate = false;
      downloadApplying = false;
      decompressing = false;
      return;
    }
    // ...
    zip.extractAllToAsync(oldPath, true, (err) => {
      if (err) {
        //...
        downloadApplying = false;
        decompressing = false;
        return;
      }
      //解压完之后别忘了要修改本地hotVersion文件的版本号,否则会一直触发更新
      fs.writeFile(
        //...
        (err, data) => {
          if (err) {
            //...省略错误处理
          } else {
            currentIncrementUpdate = obsIncrementUpdate;
            hasCheckWaitUpdate = false;
            if (reload) {
              //刷新页面
              win.webContents.reloadIgnoringCache();
            } else {
            }
          }
          downloadApplying = false;
          decompressing = false;
        }
      );
    });
  });
}

5. Optimization and bottom line

5.1 Forced update

        When switching accounts or logging out of the system, check whether the installation package has been downloaded but not updated, and force the update:

ipcMain.on("mustUpdate", (event, args) => {
  if (hasCheckWaitUpdate) {
    let oldPath = process.resourcesPath + "/app.asar.unpacked";
    let targetPath = process.resourcesPath + "/unpacked.zip";
    handleIncreaseUpdate(oldPath, targetPath, obsIncrementUpdate);
    setTimeout(() => {
      closeWinAll();
    }, 2000);
  } else {
    closeWinAll();
  }
});

5.2 Reduction of pop-up windows

        Above we are regularly checking for updates, and will give an immediate update confirmation box every time, but the user can choose not to close it, which will cause the pop-up box to pop up repeatedly, and he can click on each pop-up box, but when he clicks one of them When a pop-up box is displayed, we have already updated it, so subsequent clicks may report an error. To solve this problem, we can limit the pop-up box to appear only once:

let messageBox = null; //立即更新提示框
async function incrementUpdate() {
  //...
  if (hasCheckWaitUpdate) {
    if (!messageBox) {
      messageBox = dialog
        .showMessageBox({
          //...
        })
        .then((res) => {
          messageBox = null;
          //...
        });
    }
    return;
  }
  //...
}
async function updateAtOnce(oldPath, targetPath, obsIncrementUpdate) {
    //...
    if (!messageBox) {
      messageBox = dialog
        .showMessageBox({
          //...
        })
        .then((res) => {
          messageBox = null;
          //...
        });
    }
}

Summarize

        The above is the author's main introduction to the incremental update and optimization of electron. If there are any incomplete records, please ask questions~

Guess you like

Origin blog.csdn.net/sxww_zyt/article/details/131006833