Actualización incremental y actualización no inductiva de la versión de electrones

        El artículo anterior presentó la actualización completa de la actualización de la versión electrónica (vue)_Zoie_ting's Blog-CSDN Blog

        Pero el paquete de actualización completo es demasiado grande y no es necesario cambiar muchas cosas cada vez. ¿Hay alguna forma de actualizarlo en pequeñas cantidades? Basado en el artículo anterior, este artículo describe cómo realizar actualizaciones incrementales. 

        Las palabras no transmiten el significado, tome la imagen como ejemplo:

1. Obtener

        Proyecto empaquetado, hay un paquete en dist, este directorio es la parte que modificamos a menudo, incluidos css, js, imágenes, html y otros archivos. Instálelo localmente, abra la ubicación del archivo y encontrará que hay una app.asar en el directorio de recursos. De hecho, este archivo comprime todo el contenido del paquete.

        Tal vez los lectores lo cuestionen, así que hagámoslo en la práctica:

  • Primero instale asar: npm install -g asar
  • Cambie al directorio donde se encuentra app.asar y ejecute: asar extract app.asar ./app-bundled
  • Después de eso, verá una carpeta incluida en la aplicación en recursos, ¡el contenido a continuación es casi el mismo que el incluido en dist!

De hecho, win.loadURL('app://./index.html')         en el proceso principal es index.html en el archivo en ejecución.

        Por lo tanto, en teoría, solo necesitamos eliminar los archivos que deben cambiarse en app.asar y modificar la ruta de carga index.html del proceso principal.

En segundo lugar, la subcontratación

        Aquí quiero explicar en particular que la actualización de la versión debe tener un mecanismo de activación, ya sea la adquisición de la interfaz, la inserción del websocket o la declaración del archivo de configuración. Esto debería ser fácil de entender. La adquisición de la interfaz y el empuje del websocket son relativamente simples. Cuando los datos adquiridos son diferentes a los locales del usuario, se puede activar una actualización. El autor usa el archivo de configuración (hotVersion.json) para explicarlo aquí. .

        La idea principal de configurar los archivos de subpaquetes empaquetados en vue.config.js es colocar los archivos que se modifican con frecuencia en app.asar.unpacked, extraer hotVersion.json para comparar versiones y colocar el resto de los archivos en él. app.asar.

        El contenido de hotVersion.json es solo un número de versión para comparar: {"versión": "2.5.7"}.

        Los archivos y los elementos de configuración de extraResources en asar y builderOptions se usan aquí:

  • asar: ya sea para usar el formato de archivo de Electron para empaquetar el código fuente de la aplicación en el archivo , debe configurarse como falso
  • El archivo agregado por archivo es el archivo en app.asar en el nuevo paquete
  • recursos extra中:
  1. from significa desde qué ruta en el paquete terminado, aquí está dist/bundled
  2. para indica qué ruta enviar estos paquetes, app.asar.unpacked indica en la carpeta app.asar.unpacked debajo de la carpeta de recursos, ./ indica debajo de la carpeta de recursos
  3. El filtro coincide con el nombre del archivo. Es importante tener en cuenta que los archivos en app.asar.unpacked se deben filtrar del paquete y los archivos en app.asar.unpacked no deben estar en el archivo, porque una vez que la aplicación existe archivo en .asar, entonces el programa usará automáticamente el archivo en lugar de la app.asar.unpacked que queremos

        La configuración es la siguiente:

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

        De acuerdo con la configuración anterior, después de empaquetar e instalar localmente, habrá más carpetas hotVersion.json y app.asar.unpacked en la carpeta de recursos . El contenido de app.asar.unpacked es el que se modifica con frecuencia filtrado por nuestra configuración. Aquellos. Ejecute asar extract app.asar ./app-bundled, puede ver que solo hay esos archivos en los archivos que configuramos en la carpeta adicional del paquete de aplicaciones Nuevamente, app.asar no puede existir en app.asar.unpacked mismo archivo.

        Hasta ahora, solo separamos los archivos instalados y no creamos subpaquetes. Continuar para configurar:

         Configuración común - generador de electrones Hay varias funciones de enlace en el generador de electrones. Aquí necesitamos usar afterPack . Las funciones aquí se ejecutarán después del empaquetado. Necesito crear subpaquetes aquí. Adm-zip se usa aquí, por favor lectores Instale usted mismo .

        Cree un nuevo afterPack.js y escriba el siguiente código:

  • targetPath indica la ruta de los recursos instalados localmente, que es la ruta de app.asar.unpacked, y unpacked es la ruta de app.asar.unpacked, que es la ruta de los archivos que debemos modificar cada vez. para descargar la ruta Todos los archivos en .
  • Realice una operación de escritura en dist/hotVersion.json (si no existe tal archivo, se generará automáticamente), y el contenido escrito es exactamente el contenido de nuestro 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) => {}
  );
};

        En este momento, se debe agregar afterPack a la configuración de vue.config.js:

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

        En este punto, se generarán unpack.zip y hotVersion.json en la carpeta dist empaquetada. Unpack.zip es el paquete que actualizamos cada vez . Compare los números de versión de .json. Si no son coherentes, descargue unpack.zip, extraiga en app.asar.unpacked y actualice la página para completar la actualización.

        No olvide, electron ejecuta index.html en app.asar de manera predeterminada.Después de la operación anterior, no hay index.html en app.asar, por lo que será una pantalla blanca después de abrir. No se asuste, modifique la ruta de carga.

        La configuración original para cargar index.html es la siguiente:

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

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

          Ya no haga referencia a createProtocol de vue-cli-plugin-electron-builder/lib, cree un nuevo 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 })
      })
    }
  )
}

        Modifique la ruta de carga para cargar index.html en app.asar.unpacked en su lugar:

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

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

        Hasta ahora, ejecutar electrones después de empaquetar e instalar no es una pantalla blanca.

        Si necesita lanzar una versión incremental, solo necesita modificar el contenido de hotVersion.json, poner hotVersion.json y desempaquetar.zip en el servidor después del empaquetado y actualizar si el hotVersion.json local es inconsistente con el servidor durante la detección. .

3. Actualizar

        Nota: Esta es la lógica de la actualización completa del artículo anterior. Si no está seguro, puede leer el artículo anterior: Actualización completa de la actualización de la versión electrónica (vue)_Zoie_ting's Blog-CSDN Blog

        Comience a verificar si hay una actualización completa:

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

        Si hay un paquete de actualización, se descargará automáticamente de la URL configurada por autoUpdater.setFeedURL(url), y se procesará en la descarga de actualización una vez que se complete la descarga. Entre ellos, defaultId es la opción predeterminada de configuración, y el valor es el índice de los botones.Por ejemplo, la configuración aquí es 0, que significa "no". cancelId también está configurado como 0, lo que significa que si no selecciona el valor de los botones pero cierra directamente el cuadro de confirmación emergente, seleccione "No" para procesar:

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

        Si no hay una actualización completa, verifique si hay una versión de actualización incremental:

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

        Detecta si hay una actualización incremental, y de ser así, descarga el paquete de la versión online: 

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

        El paquete de actualización incremental en línea se ha descargado y se le indica si desea actualizar inmediatamente:

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

        Procesar actualización inmediata: determine si existe app.asar.unpacked.old y, de ser así, elimine esta carpeta y todos los archivos de esta carpeta, primero haga una copia de seguridad de app.asar.unpacked como app.asar.unpacked.old y descomprímalo Descargado unpack.zip a app.asar.unpacked, una vez que ocurra una excepción durante este proceso, restaure app.asar.unpacked.old a app.asar.unpacked.

        La razón por la que esta función se escribe por separado es para su posterior optimización.Esta función se ocupa principalmente de la instalación descargada. Si ya existe un paquete de instalación, se puede actualizar, simplemente ejecute esta función.

//删除目标文件夹以及文件夹下的所有文件
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. Actualización regular

        El autor ya ha introducido anteriormente que el proceso de actualización se ha procesado en circunstancias normales, pero esto se detecta una vez que el usuario lo inicia. Si el usuario no ha salido del software después de iniciarlo, en realidad no hay forma de actualizarlo de acuerdo con lo anterior, por lo que debe agregar actualizaciones periódicas.

        Cabe señalar que si ya se instaló el paquete completo, no se debe permitir que el usuario lo descargue, es decir, ya no se verificará la actualización.

        El autor aquí asume que se detecta una vez cada dos horas (reserve un tiempo lo suficientemente largo para asegurarse de que el paquete de actualización completo se pueda descargar dentro de este tiempo (porque el paquete de actualización incremental es más pequeño que la versión completa)), y la modificación es como sigue:

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

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

        Modificar updateAtOnce():

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

        Modificar 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. Optimización y resultado final

5.1 Actualización forzada

        Al cambiar de cuenta o cerrar sesión en el sistema, verifique si el paquete de instalación se descargó pero no se actualizó y fuerce la actualización:

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 Reducción de ventanas emergentes

        Arriba, comprobamos regularmente si hay actualizaciones y mostraremos un cuadro de confirmación de actualización inmediata cada vez, pero el usuario puede optar por no cerrarlo, lo que hará que el cuadro emergente aparezca repetidamente, y puede hacer clic en cada ventana emergente. cuadro emergente, pero cuando hace clic en uno de ellos Cuando se muestra un cuadro emergente, ya lo hemos actualizado, por lo que los clics posteriores pueden informar un error. Para resolver este problema, podemos limitar el cuadro emergente para que aparezca solo una vez :

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

Resumir

        Lo anterior es la introducción principal del autor a la actualización incremental y la optimización de electrones. Si hay registros incompletos, haga preguntas ~

Supongo que te gusta

Origin blog.csdn.net/sxww_zyt/article/details/131006833
Recomendado
Clasificación