전자 버전 업데이트의 증분 업데이트 및 비유도 업데이트

        이전 기사에서는 전자 버전 업데이트(vue)_Zoie_ting's Blog-CSDN Blog의 전체 업데이트를 소개했습니다.

        그런데 전체 업데이트 패키지가 너무 크고 매번 많은 것을 변경할 필요가 없는데 소량으로 업데이트할 수 있는 방법은 없을까요? 이전 문서를 기반으로 이 문서에서는 증분 업데이트를 수행하는 방법을 설명합니다. 

        단어는 의미를 전달하지 않습니다. 예를 들어 사진을 찍으십시오.

하나, 입수

        패키지 프로젝트에는 dist에 번들로 포함되어 있으며 이 디렉토리는 css, js, 그림, html 및 기타 파일을 포함하여 우리가 자주 수정하는 부분입니다. 로컬에 설치하고 파일이 있는 위치를 열어보면 리소스 디렉토리에 app.asar가 있는 것을 확인할 수 있는데, 사실 이 파일은 번들로 묶인 모든 내용을 압축하고 있다.

        독자들이 의문을 가질 수도 있으니 실제로 해봅시다.

  • 먼저 asar 설치: npm install -g asar
  • app.asar가 있는 디렉터리로 전환하고 다음을 실행합니다. asar extract app.asar ./app-bundled
  • 그 후 리소스에 앱 번들 폴더가 표시되며, 아래 내용은 dist에 번들된 것과 거의 동일합니다!

        사실 메인 프로세스의 win.loadURL('app://./index.html')은 실행 파일의 index.html입니다.

        따라서 이론적으로는 app.asar에서 변경이 필요한 파일만 꺼내고 메인 프로세스의 index.html 로딩 경로만 수정하면 된다.

둘째, 하도급

        여기에서는 버전 업데이트가 인터페이스 획득이든, websocket 푸시든, 구성 파일 선언이든 간에 트리거 메커니즘이 있어야 한다는 점을 특히 설명하고 싶습니다. 이것은 이해하기 쉬울 것입니다.인터페이스 획득 및 websocket 푸시는 비교적 간단합니다.획득된 데이터가 사용자의 로컬 데이터와 다른 경우 업데이트가 트리거될 수 있습니다.저자는 구성 파일(hotVersion.json)을 사용하여 여기에서 설명합니다. .

        vue.config.js에서 패키징된 하위 패키지 파일을 구성하는 주요 아이디어는 app.asar.unpacked에 자주 수정되는 파일을 넣고 버전 비교를 위해 hotVersion.json을 꺼내고 나머지 파일을 넣는 것입니다. app.asar.

        hotVersion.json의 내용은 비교를 위한 버전 번호입니다: {"version": "2.5.7"}.

        asar 및 builderOptions의 파일 및 extraResources 구성 항목이 여기에서 사용됩니다.

  1. from은 완성된 패키지의 경로를 의미합니다. 여기서는 dist/bundled입니다.
  2. 이러한 패키지를 출력할 경로를 나타내기 위해 app.asar.unpacked는 리소스 폴더 아래의 app.asar.unpacked 폴더를 나타내고 ./는 리소스 폴더 아래를 나타냅니다.
  3. 필터는 파일 이름과 일치합니다. app.asar.unpacked의 파일은 번들에서 필터링해야 하며 app.asar.unpacked의 파일은 파일에 없어야 합니다. 파일을 .asar에 넣으면 프로그램은 자동으로 원하는 app.asar.unpacked 파일 대신 파일을 사용합니다.

        구성은 다음과 같습니다.

  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", //更新服务器地址,可为空
            },
          ],
          //...
        },
      },
    },
  },

        위의 구성에 따라 로컬로 패키징하고 설치한 후 리소스 폴더 아래에 더 많은 hotVersion.json 및 app.asar.unpacked 폴더가 있을 것입니다 . 그것들. asar extract app.asar ./app-bundled를 실행하면 추가 app-bundled 폴더에 설정한 파일에 해당 파일만 있는 것을 확인할 수 있습니다.다시 말하지만 app.asar는 동일한 app.asar.unpacked 파일에 존재할 수 없습니다.

        지금까지는 설치된 파일만 분리하고 서브패키지는 만들지 않았습니다. 계속 구성:

         일반 구성 - electron-builder electron-builder에는 몇 가지 후크 기능이 있습니다. 여기서는 afterPack을 사용해야 합니다 . 여기의 기능은 패키징 후 실행됩니다. 여기에서 하위 패키지를 생성해야 합니다. 여기서는 Adm-zip을 사용합니다. 독자가 직접 설치하십시오. .

        새 afterPack.js를 만들고 다음 코드를 작성합니다.

  • targetPath는 app.asar.unpacked의 경로인 로컬에 설치된 리소스의 경로를 나타내고, unpacked는 매번 수정해야 하는 파일의 경로인 app.asar.unpacked의 경로를 나타냅니다. .
  • dist/hotVersion.json에 쓰기 작업을 수행하고(해당 파일이 없으면 자동으로 생성됨) 기록된 내용은 정확히 우리 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) => {}
  );
};

        이때 afterPack을 vue.config.js 구성에 추가해야 합니다.

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

        이때 unpack.zip과 hotVersion.json이 패키지된 dist 폴더에 생성됩니다. Unpack.zip은 매번 업데이트되는 패키지입니다 . .json의 버전 번호를 비교하십시오. 일치하지 않으면 unpack.zip을 다운로드하고 app.asar.unpacked로 이동하고 페이지를 새로 고쳐 업데이트를 완료합니다.

        잊지 마세요, electron은 기본적으로 app.asar에서 index.html을 실행합니다.위 작업 후 app.asar에는 index.html이 없으므로 열면 흰색 화면이됩니다. 당황하지 말고 로딩 경로를 수정하십시오.

        index.html을 로드하기 위한 원래 구성은 다음과 같습니다.

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

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

          더 이상 vue-cli-plugin-electron-builder/lib의 createProtocol을 참조하지 않고 새 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 })
      })
    }
  )
}

        대신 app.asar.unpacked에서 index.html을 로드하도록 로드 경로를 수정합니다.

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

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

        지금까지 패키징 및 설치 후 전자를 실행하는 것은 흰색 화면이 아닙니다.

        증분 버전을 릴리스해야 하는 경우 hotVersion.json의 내용을 수정하고 패키징 후 서버에 hotVersion.json 및 unpack.zip을 배치하고 감지 중에 로컬 hotVersion.json이 서버와 일치하지 않는 경우 업데이트하기만 하면 됩니다. .

3. 업데이트

        참고: 이전 기사의 전체 업데이트 논리는 다음과 같습니다. 확실하지 않은 경우 이전 기사를 읽을 수 있습니다: 전자 버전 업데이트 전체 업데이트(vue)_Zoie_ting의 블로그-CSDN 블로그

        전체 업데이트가 있는지 확인 시작:

  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();
}

        업데이트 패키지가 있는 경우 autoUpdater.setFeedURL(url)에서 설정한 url에서 자동으로 다운로드되며, 다운로드 완료 후 update-downloaded에서 처리됩니다. 그 중 defaultId는 구성 기본 옵션이고 값은 버튼의 인덱스입니다.예를 들어 여기서 구성은 "아니오"를 의미하는 0입니다. cancelId도 0으로 구성됩니다. 즉, 버튼 값을 선택하지 않고 팝업 확인 상자를 직접 닫는 경우 "아니오"를 선택하여 처리합니다.

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 {
        }
    });
});

        전체 업데이트가 없는 경우 증분 업데이트 버전이 있는지 확인합니다.

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();
      }
    }
  );
});

        증분 업데이트가 있는지 확인하고 업데이트가 있으면 온라인 버전 패키지를 다운로드합니다. 

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错误
        //...省略错误处理
      }
    }
  );
}

        온라인 증분 업데이트 패키지가 다운로드되었으며 즉시 업데이트할지 묻는 메시지가 표시됩니다.

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 {
          }
        });
}

        즉시 업데이트 처리: app.asar.unpacked.old가 존재하는지 확인하고 존재하는 경우 이 폴더 및 이 폴더 아래의 모든 파일을 삭제하고 app.asar.unpacked를 app.asar.unpacked.old로 먼저 백업한 후 압축 해제 다운로드 unpack.zip을 app.asar.unpacked로, 이 과정에서 예외가 발생하면 app.asar.unpacked.old를 app.asar.unpacked로 복원합니다.

        이 기능을 별도로 작성하는 이유는 차후 최적화를 위한 것으로 이 기능은 주로 다운로드 받은 설치를 다룬다. 이미 설치 패키지가 있는 경우 업그레이드할 수 있으며 이 기능을 실행하기만 하면 됩니다.

//删除目标文件夹以及文件夹下的所有文件
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. 정기 업데이트

        위의 작성자는 정상적인 상황에서 업데이트 프로세스가 진행되었지만 사용자가 소프트웨어를 시작할 때 한 번 감지된다고 소개했습니다. 정기적인 업데이트를 추가해야 합니다.

        전체 패키지가 이미 설치된 경우 사용자가 다운로드할 수 없어야 합니다. 즉, 업데이트가 더 이상 확인되지 않습니다.

        여기서 작성자는 탐지가 2시간마다 한 번이라고 가정하고(전체 업데이트 패키지가 이 시간 내에 다운로드될 수 있도록 충분한 시간을 예약합니다(증분 업데이트 패키지가 전체 업데이트보다 작기 때문에)) 수정은 다음과 같습니다. 다음과 같이:

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; 
}

        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错误
        //...省略错误处理
      }
    }
  );
}

        updateAtOnce() 수정:

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

        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. 최적화 및 결론

5.1 강제 업데이트

        계정을 전환하거나 시스템에서 로그아웃할 때 설치 패키지가 다운로드되었지만 업데이트되지 않았는지 확인하고 업데이트를 강제 적용합니다.

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 팝업 창 축소

        위에서 우리는 정기적으로 업데이트를 확인하고 있으며 매번 즉시 업데이트 확인 상자를 제공하지만 사용자가 닫지 않도록 선택할 수 있습니다. 그러면 팝업 상자가 반복적으로 팝업되며 각 팝업을 클릭할 수 있습니다. 팝업 상자가 표시되면 이미 업데이트되어 있으므로 후속 클릭에서 오류가 발생할 수 있습니다.이 문제를 해결하기 위해 팝업 상자가 한 번만 표시되도록 제한할 수 있습니다. :

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;
          //...
        });
    }
}

요약하다

        위는 전자의 증분 업데이트 및 최적화에 대한 저자의 주요 소개입니다. 불완전한 기록이 있으면 질문하십시오 ~

추천

출처blog.csdn.net/sxww_zyt/article/details/131006833