uni-app App 热更新实现指南:从接口检查到下载安装重启(附带AI提示词)

在 App 开发中,用户不可能每次小改动都重新下载安装完整安装包。

比如修一个页面样式、改一段 JS 逻辑、更新几张图片,如果都发整包,体验会很重。

这时候就可以用 热更新

本文以 uni-app App 端为例,讲清楚:

  • 实现热更新需要准备什么
  • 热更新完整流程是什么
  • 如何请求版本接口
  • 如何下载 wgt 包
  • 如何安装并重启应用
  • 最后附一个可直接给 AI 使用的开发提示词

一、热更新需要准备哪些东西?

实现热更新,前后端都需要配合。

1. 当前 App 版本号

App 本地需要知道自己当前的版本号,例如:

json 复制代码
{
  "versionName": "1.1.0",
  "versionCode": 343
}

其中:

  • versionName:展示给用户看的版本号,例如 1.1.0
  • versionCode:用于程序比较的新旧版本号,例如 343

热更新接口一般会用 versionCode 来判断是否有新版本。


2. 后端版本检查接口

例如:

txt 复制代码
/api/xxx

接口返回示例:

json 复制代码
{
  "msg": "操作成功",
  "code": 200,
  "data": {
    "dataid": "f2a5c7dd-2b08-44ed-8e00-5317e383e75c",
    "versioncode": "344",
    "versionname": "1.1.1",
    "filepath": "https://xxx.com/update/app.wgt",
    "updatecontent": "更新文案",
    "enabledstate": 1,
    "priority": 1,
    "apptype": 3
  }
}

重点字段:

字段 说明
versioncode 服务端最新版本号
versionname 服务端最新版本名称
filepath wgt 热更新包下载地址
updatecontent 更新内容
priority 更新优先级,可用于后续强更逻辑

3. wgt 热更新包

uni-app App 热更新一般使用 .wgt 包。

你需要在打包平台或 HBuilderX 中生成 wgt 资源包,然后上传到服务器,后端接口返回它的下载地址。


4. App 端安装能力

uni-app App 端使用 5+ Runtime API:

js 复制代码
plus.runtime.install(filePath, options, successCallback, errorCallback)

安装成功后,建议调用:

js 复制代码
plus.runtime.restart()

因为新 JS、CSS、图片等资源需要重新加载后才能稳定生效。


二、热更新整体流程

#mermaid-svg-zfA62Y5cw6g2lNFd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-zfA62Y5cw6g2lNFd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zfA62Y5cw6g2lNFd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zfA62Y5cw6g2lNFd .error-icon{fill:#552222;}#mermaid-svg-zfA62Y5cw6g2lNFd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zfA62Y5cw6g2lNFd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zfA62Y5cw6g2lNFd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zfA62Y5cw6g2lNFd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zfA62Y5cw6g2lNFd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zfA62Y5cw6g2lNFd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zfA62Y5cw6g2lNFd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zfA62Y5cw6g2lNFd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zfA62Y5cw6g2lNFd .marker.cross{stroke:#333333;}#mermaid-svg-zfA62Y5cw6g2lNFd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zfA62Y5cw6g2lNFd p{margin:0;}#mermaid-svg-zfA62Y5cw6g2lNFd .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-zfA62Y5cw6g2lNFd .cluster-label text{fill:#333;}#mermaid-svg-zfA62Y5cw6g2lNFd .cluster-label span{color:#333;}#mermaid-svg-zfA62Y5cw6g2lNFd .cluster-label span p{background-color:transparent;}#mermaid-svg-zfA62Y5cw6g2lNFd .label text,#mermaid-svg-zfA62Y5cw6g2lNFd span{fill:#333;color:#333;}#mermaid-svg-zfA62Y5cw6g2lNFd .node rect,#mermaid-svg-zfA62Y5cw6g2lNFd .node circle,#mermaid-svg-zfA62Y5cw6g2lNFd .node ellipse,#mermaid-svg-zfA62Y5cw6g2lNFd .node polygon,#mermaid-svg-zfA62Y5cw6g2lNFd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zfA62Y5cw6g2lNFd .rough-node .label text,#mermaid-svg-zfA62Y5cw6g2lNFd .node .label text,#mermaid-svg-zfA62Y5cw6g2lNFd .image-shape .label,#mermaid-svg-zfA62Y5cw6g2lNFd .icon-shape .label{text-anchor:middle;}#mermaid-svg-zfA62Y5cw6g2lNFd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zfA62Y5cw6g2lNFd .rough-node .label,#mermaid-svg-zfA62Y5cw6g2lNFd .node .label,#mermaid-svg-zfA62Y5cw6g2lNFd .image-shape .label,#mermaid-svg-zfA62Y5cw6g2lNFd .icon-shape .label{text-align:center;}#mermaid-svg-zfA62Y5cw6g2lNFd .node.clickable{cursor:pointer;}#mermaid-svg-zfA62Y5cw6g2lNFd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-zfA62Y5cw6g2lNFd .arrowheadPath{fill:#333333;}#mermaid-svg-zfA62Y5cw6g2lNFd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-zfA62Y5cw6g2lNFd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-zfA62Y5cw6g2lNFd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zfA62Y5cw6g2lNFd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zfA62Y5cw6g2lNFd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zfA62Y5cw6g2lNFd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-zfA62Y5cw6g2lNFd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-zfA62Y5cw6g2lNFd .cluster text{fill:#333;}#mermaid-svg-zfA62Y5cw6g2lNFd .cluster span{color:#333;}#mermaid-svg-zfA62Y5cw6g2lNFd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-zfA62Y5cw6g2lNFd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zfA62Y5cw6g2lNFd rect.text{fill:none;stroke-width:0;}#mermaid-svg-zfA62Y5cw6g2lNFd .icon-shape,#mermaid-svg-zfA62Y5cw6g2lNFd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zfA62Y5cw6g2lNFd .icon-shape p,#mermaid-svg-zfA62Y5cw6g2lNFd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-zfA62Y5cw6g2lNFd .icon-shape .label rect,#mermaid-svg-zfA62Y5cw6g2lNFd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zfA62Y5cw6g2lNFd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zfA62Y5cw6g2lNFd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zfA62Y5cw6g2lNFd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否







App 启动或用户点击检查更新
获取当前 App versionCode
请求版本检查接口
服务端是否有更高版本?
结束;手动检查时提示已是最新版本
弹窗展示版本号和更新内容
用户是否立即更新?
取消更新
下载 filepath 对应的 wgt 包
下载是否成功?
提示下载失败
plus.runtime.install 安装 wgt
安装是否成功?
提示安装失败
关闭 loading
plus.runtime.restart 软重启
新资源生效


三、热更新核心代码

下面是一个较完整的封装示例。

js 复制代码
import operationApi from "@/api/operation/index.js";
import { showBrandToast } from "@/utils/method.js";

const APP_TYPE = 3;

let checkingPromise = null;
let downloading = false;
let downloadTask = null;

function toInt(value) {
  const n = Number.parseInt(String(value ?? "").trim(), 10);
  return Number.isFinite(n) ? n : 0;
}

function formatBytes(size) {
  const n = Number(size) || 0;
  if (n < 1024) return `${n}B`;
  if (n < 1024 * 1024) return `${Math.floor((n / 1024) * 100) / 100}KB`;
  return `${Math.floor((n / 1024 / 1024) * 100) / 100}MB`;
}

function getRuntimeProperty() {
  // #ifdef APP-PLUS
  return new Promise((resolve) => {
    if (typeof plus === "undefined" || !plus.runtime?.getProperty) {
      resolve({});
      return;
    }

    plus.runtime.getProperty(plus.runtime.appid, (info) => {
      resolve(info || {});
    });
  });
  // #endif

  // #ifndef APP-PLUS
  return Promise.resolve({});
  // #endif
}

export async function getCurrentAppVersion() {
  const runtimeInfo = await getRuntimeProperty();

  return {
    versionCode: String(runtimeInfo.versionCode || ""),
    versionName: String(runtimeInfo.version || ""),
  };
}

function normalizeUpdateInfo(data) {
  if (!data || typeof data !== "object") return null;

  const currentVersionCode = toInt(data.currentVersionCode);
  const nextVersionCode = toInt(data.versioncode);

  if (!nextVersionCode || nextVersionCode <= currentVersionCode) {
    return null;
  }

  return {
    versionCode: String(data.versioncode || ""),
    versionName: String(data.versionname || ""),
    filePath: String(data.filepath || "").trim(),
    updateContent: String(data.updatecontent || "").trim(),
    priority: toInt(data.priority),
  };
}

function showUpdateConfirm(updateInfo) {
  const content = [
    `发现新版本 V${updateInfo.versionName || updateInfo.versionCode}`,
    updateInfo.updateContent,
  ]
    .filter(Boolean)
    .join("\n\n");

  return new Promise((resolve) => {
    uni.showModal({
      title: "版本更新",
      content,
      cancelText: "稍后再说",
      confirmText: "立即更新",
      success: (res) => resolve(Boolean(res.confirm)),
      fail: () => resolve(false),
    });
  });
}

function closeWaiting(waiting) {
  try {
    if (waiting?.close) waiting.close();
  } catch {}

  try {
    uni.hideLoading();
  } catch {}

  // #ifdef APP-PLUS
  try {
    if (typeof plus !== "undefined" && plus.nativeUI?.closeWaiting) {
      plus.nativeUI.closeWaiting();
    }
  } catch {}
  // #endif
}

function createWaiting(title) {
  // #ifdef APP-PLUS
  if (typeof plus !== "undefined" && plus.nativeUI?.showWaiting) {
    return plus.nativeUI.showWaiting(title, { modal: true });
  }
  // #endif

  uni.showLoading({ title, mask: true });
  return null;
}

function updateWaiting(waiting, title) {
  if (waiting?.setTitle) {
    waiting.setTitle(title);
    return;
  }

  uni.showLoading({
    title,
    mask: true,
  });
}

function installWgt(filePath) {
  return new Promise((resolve, reject) => {
    // #ifdef APP-PLUS
    if (typeof plus === "undefined" || !plus.runtime?.install) {
      reject(new Error("当前环境不支持安装更新包"));
      return;
    }

    plus.runtime.install(
      filePath,
      { force: false },
      () => resolve(),
      (err) => reject(err || new Error("安装更新包失败"))
    );
    // #endif

    // #ifndef APP-PLUS
    reject(new Error("当前环境不支持安装更新包"));
    // #endif
  });
}

function restartApp() {
  // #ifdef APP-PLUS
  if (typeof plus !== "undefined" && plus.runtime?.restart) {
    plus.runtime.restart();
  }
  // #endif
}

function restartAfterInstall() {
  closeWaiting();

  setTimeout(() => {
    restartApp();
  }, 300);
}

function downloadAndInstall(updateInfo) {
  if (downloading) {
    showBrandToast("更新包下载中");
    return Promise.resolve(false);
  }

  if (!updateInfo.filePath) {
    showBrandToast("更新包地址为空");
    return Promise.resolve(false);
  }

  downloading = true;
  let waiting = createWaiting("准备下载...");

  return new Promise((resolve) => {
    downloadTask = uni.downloadFile({
      url: updateInfo.filePath,

      success: async (downloadResult) => {
        if (downloadResult.statusCode !== 200) {
          closeWaiting(waiting);
          downloading = false;
          downloadTask = null;
          showBrandToast("更新包下载失败");
          resolve(false);
          return;
        }

        try {
          updateWaiting(waiting, "正在安装...");
          await installWgt(downloadResult.tempFilePath);

          closeWaiting(waiting);
          waiting = null;

          restartAfterInstall();
          resolve(true);
        } catch (err) {
          console.error("安装更新包失败:", err);
          showBrandToast("更新包安装失败");
          resolve(false);
        } finally {
          if (waiting) closeWaiting(waiting);
          downloading = false;
          downloadTask = null;
        }
      },

      fail: (err) => {
        console.error("下载更新包失败:", err);
        closeWaiting(waiting);
        downloading = false;
        downloadTask = null;
        showBrandToast("更新包下载失败");
        resolve(false);
      },
    });

    downloadTask?.onProgressUpdate?.((res) => {
      const progress = Math.max(0, Math.min(100, Number(res.progress) || 0));
      const written = formatBytes(res.totalBytesWritten);
      const total = formatBytes(res.totalBytesExpectedToWrite);

      updateWaiting(waiting, `下载中 ${progress}%\n${written}/${total}`);
    });
  });
}

export function cancelAppUpdateDownload() {
  if (!downloadTask) return false;

  downloadTask.abort();
  downloadTask = null;
  downloading = false;
  uni.hideLoading();

  return true;
}

export async function checkAppUpdate(options = {}) {
  const { manual = false } = options;

  // #ifndef APP-PLUS
  if (manual) {
    const version = await getCurrentAppVersion();
    showBrandToast(`当前版本 V${version.versionName || "1.0.0"}`);
  }
  return null;
  // #endif

  if (checkingPromise) return checkingPromise;

  checkingPromise = (async () => {
    try {
      const current = await getCurrentAppVersion();
      const currentVersionCode = current.versionCode;

      if (!currentVersionCode) {
        if (manual) showBrandToast("未获取到当前版本号");
        return null;
      }

      const data = await operationApi.getAppVersionManagement(
        {
          versioncode: currentVersionCode,
          apptype: APP_TYPE,
        },
        {
          loading: manual,
          silent: true,
        }
      );

      const updateInfo = normalizeUpdateInfo({
        ...(data || {}),
        currentVersionCode,
      });

      if (!updateInfo) {
        if (manual) showBrandToast("已是最新版本");
        return null;
      }

      const confirmed = await showUpdateConfirm(updateInfo);

      if (!confirmed) {
        return updateInfo;
      }

      await downloadAndInstall(updateInfo);
      return updateInfo;
    } catch (err) {
      console.error("检查 App 更新失败:", err);

      if (manual) {
        showBrandToast("检查更新失败");
      }

      return null;
    } finally {
      checkingPromise = null;
    }
  })();

  return checkingPromise;
}

四、App 启动时检查更新

可以在 App.vue 中启动检查。

js 复制代码
// #ifdef APP-PLUS
import { checkAppUpdate } from "@/utils/appUpdate.js";
// #endif

export default {
  onLaunch() {
    // #ifdef APP-PLUS
    setTimeout(() => {
      checkAppUpdate({ manual: false });
    }, 1200);
    // #endif
  },
};

为什么加 setTimeout

因为 App 刚启动时,首页、权限、推送、登录态等逻辑可能也在初始化。

稍微延迟检查更新,可以减少和其他启动逻辑抢资源的问题。


五、设置页手动检查更新

比如设置页有一行:

vue 复制代码
<view class="settings-item" @click="handleCheckUpdate">
  <text class="item-label">版本更新</text>
  <view class="item-right">
    <text class="item-value">当前版本:V{{ version }}</text>
  </view>
</view>

脚本中:

js 复制代码
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { checkAppUpdate, getCurrentAppVersion } from "@/utils/appUpdate.js";

const version = ref("");

const handleCheckUpdate = () => {
  checkAppUpdate({ manual: true });
};

const refreshAppVersion = async () => {
  try {
    const appVersion = await getCurrentAppVersion();
    version.value = appVersion.versionName || "1.0.0";
  } catch {
    version.value = "1.0.0";
  }
};

onLoad(() => {
  refreshAppVersion();
});

这样用户可以主动点击检查更新。


六、最终 AI 辅助开发提示词

下面这段可以直接复制给 AI,让它帮你在 uni-app 项目中实现热更新。

txt 复制代码
你是一个资深 uni-app App 开发工程师,请帮我实现 App 热更新功能,要求代码质量高、结构清晰、可维护。

项目是 uni-app Vue 项目,运行端主要是 APP-PLUS。

需求如下:

1. 新增一个热更新工具模块,例如 src/utils/appUpdate.js。
2. App 当前版本号优先通过 plus.runtime.getProperty 获取:
   - versionCode 用于和服务端比较
   - versionName 用于展示
3. 新增版本检查接口:
   GET /api/xxxxx
   参数:
4. 接口返回示例:
   {
     "code": 200,
     "data": {
       "versioncode": "344",
       "versionname": "1.1.1",
       "filepath": "wgt下载地址",
       "updatecontent": "更新内容",
       "priority": 1,
       "apptype": 3
     }
   }
5. 如果服务端 versioncode 大于当前 versionCode,则认为有新版本。
6. 有新版本时弹窗提示用户:
   - 标题:版本更新
   - 内容展示 versionName 和 updatecontent
   - 按钮:稍后再说、立即更新
7. 用户点击立即更新后:
   - 使用 uni.downloadFile 下载 filepath 对应的 wgt 包
   - 展示下载进度
   - 下载成功后调用 plus.runtime.install 安装
   - 安装成功后关闭 loading
   - 调用 plus.runtime.restart() 软重启 App,使新资源生效
8. 下载失败、安装失败、filepath 为空时要有友好提示。
9. 要避免重复检查和重复下载。
10. 只在 APP-PLUS 端执行热更新逻辑;H5、小程序端不要调用 plus。
11. App.vue 启动后延迟 1 秒左右静默检查更新。
12. 设置页的"版本更新"按钮可以手动检查更新。
13. 代码中使用条件编译:
    #ifdef APP-PLUS
    #endif
14. 请输出完整代码,并说明每个核心函数的作用。

七、总结

uni-app App 热更新的核心其实就四步:

txt 复制代码
获取当前版本号 -> 请求服务端版本接口 -> 下载 wgt -> 安装并重启

真正要注意的是体验和边界:

  • 不要重复下载
  • 不要在非 App 端调用 plus
  • 最后调用 plus.runtime.restart() 让新资源生效

这样实现后,小版本修复就可以通过 wgt 包快速下发,用户不用重新安装完整 App,体验会轻很多。

复制代码
相关推荐
千逐687 小时前
Uniapp 鸿蒙实战之 AtomGit APP - 仓库详情与文件树浏览
服务器·microsoft·uni-app
anyup9 小时前
uView Pro 正式支持 MCP 协议!让 AI 秒懂你的组件库
前端·uni-app·ai编程
千逐6811 小时前
Uniapp 鸿蒙实战之 AtomGit APP - 首页设计与仓库列表实现
华为·uni-app·harmonyos
天丁o1 天前
Spring Boot + uni-app 智慧考勤闭环 Demo:打卡记录、异常状态和日统计如何复用到企业系统
spring boot·uni-app·mybatis plus·企业管理系统·考勤系统
这是个栗子1 天前
uni-app 微信小程序开发:常用事件指令(@xxx)(一)
微信小程序·小程序·uni-app
小徐_23335 天前
Wot UI 2.2.0 发布:Button 新增 subtle,VideoPreview 预览体验继续增强
前端·微信小程序·uni-app
宸翰6 天前
解决 uni-app App 端 vue-i18n 占位符丢失:封装跨端可用的 tf 格式化方法
前端·vue.js·uni-app
时光足迹7 天前
uni-app 视频通话实战:康复师与患者视频问诊的 6 个致命 Bug 与解决方案
android·ios·uni-app
时光足迹7 天前
腾讯云 TRTC UniApp SDK 从入门到上线
前端·vue.js·uni-app