uni-app 云打包报「打包时未添加 uni-privacy 模块」排查与修复
项目:Vue 3 + uni-app 3.0,同时打包微信小程序 + Android App 环境:HBuilderX 5.x 云打包 场景:云打包出 Android 包,装到手机启动瞬间弹一个「HTML5+ Runtime」警告,本地 HBuilderX 运行到手机/模拟器却完全不弹
一、为什么这份文档有必要
这个坑最容易把人带偏的地方在于——它只在云打包后出现,本地开发环境怎么都复现不了。你会怀疑是证书、是签名、是 manifest 没配模块,一路瞎改 manifest 里的 modules,结果越改越乱。
更要命的是,弹窗里那句「未添加 uni-privacy 模块」会让人本能地去 manifest 里加一个 uni-privacy 模块。但 uni-privacy 根本不是 HBuilderX 官方模块名,加进去也会被云打包静默丢弃,纯属无效配置。
所以这篇文档先讲清楚两件事:
- 为什么本地不复现、云打包才弹(基座差异);
- 真正的触发链路是什么(微信隐私 API 没做条件编译),以及怎么彻底修掉。
二、问题现象
云打包安装后,App 启动瞬间弹出系统级弹窗:
HTML5+ Runtime
打包时未添加uni-privacy模块,请参考 https://ask.dcloud.net.cn/article/283
[忽略] [查看详情]
特征:
- 只在云打包后的包里出现,HBuilderX「运行到手机/模拟器」本地调试时不弹;
- 点「忽略」能继续用,但每次冷启动都弹,体验极差,且上架审核必挂;
- 多台设备都会复现,与机型无关。
三、为什么本地不弹、云打包才弹
这是理解整个问题的关键,先把这个「灵异现象」解释清楚。
HBuilderX 有两套运行基座:
| 本地「运行到手机/模拟器」 | 云打包 | |
|---|---|---|
| 基座类型 | 标准基座(全功能) | 精简基座(按 manifest.json 的 modules 裁剪) |
| 模块情况 | 内置全部模块,什么都不缺 | 只打包你在 modules 里勾选的模块,没勾的一律没有 |
也就是说:本地调试用的是「什么都带」的标准基座,App 端调用隐私相关 API 时,5+ Runtime 能在标准基座里找到对应的隐私处理逻辑,所以不报;而云打包按 modules 裁剪,你既没勾隐私模块、uni-privacy 又不是合法模块名,于是 5+ Runtime 在 App 端检测到隐私 API 调用时,找不到对应模块实现,就弹了这个警告。
一句话:本地不弹不代表代码没问题,只是标准基座替你兜了底。真正的隐患在源码里,云打包一裁剪就暴露。
四、真正的根因:微信隐私 API 没做条件编译
uni-app 一套代码多端编译。隐私弹窗这块,微信小程序端和 App 端是两套完全不同的机制:
- 微信小程序端:需要你自己写组件 + 调
wx.getPrivacySetting/wx.onNeedPrivacyAuthorization/wx.openPrivacyContract这套wx.*API; - App 端(Android/iOS):隐私协议弹窗由
manifest.json里的app-plus.privacy.template配置自动处理,不需要任何 JS API 调用。
问题就出在:项目里为了做微信小程序隐私弹窗,写了一个 wx-privacy-popup.vue 组件,并在首页 onMounted 里调了 wx.* API,但这些代码没有用 #ifdef MP-WEIXIN 条件编译包裹。
结果 App 端编译时这些 wx.* 调用也被编进去了。5+ Runtime 在 App 端检测到隐私相关 API 被调用,就去查 manifest 有没有对应的隐私模块实现——查不到 uni-privacy,于是弹窗报警。
触发链路:
wx-privacy-popup.vue / index.vue 里调用 wx.getPrivacySetting 等
│ (未用 #ifdef MP-WEIXIN 包裹)
▼
App 端编译时这些 wx.* 调用也被编入
▼
App 启动执行到隐私 API 调用
▼
5+ Runtime 检测到 App 端调用隐私 API,查找 uni-privacy 模块
▼
云打包精简基座里没有该模块 → 弹「打包时未添加 uni-privacy 模块」
补充一个认知点:uni-privacy 不是 HBuilderX 官方模块名。官方 App Modules 列表里只有 Bluetooth、Contacts、Maps、Payment 等驼峰命名的模块(见 官方 manifest 文档 的「App Modules」章节)。在 modules 里写 "uni-privacy": {} 是无效配置,云打包会静默丢弃,加它没有任何作用。
五、解决方法
核心就一句话:把所有微信小程序专属的隐私 API 调用,用 #ifdef MP-WEIXIN 包起来,让 App 端编译时根本看不到这些代码。 分四步。
步骤 1:给 wx-privacy-popup.vue 全面包条件编译
这个组件里 template、mounted、openPrivacyContract、wx.showToast 全都要包。#ifdef MP-WEIXIN 在 template 里用 HTML 注释形式 <!-- #ifdef MP-WEIXIN -->,在 JS 里用 // #ifdef MP-WEIXIN。
<template>
<!-- #ifdef MP-WEIXIN -->
<view v-if="visible" class="privacy-mask">
<view class="privacy-popup">
<view class="title">隐私政策提示</view>
<view class="content">
请你仔细阅读《{{privacyContractName}}》,了解我们如何收集、使用你的个人信息
</view>
<view class="link-btn" @click="openPrivacyContract">查看完整隐私协议</view>
<view class="btn-group">
<view class="disagree-btn" @click="handleDisagree">拒绝</view>
<!-- 必须使用官方open-type -->
<button
class="agree-btn"
open-type="agreePrivacyAuthorization"
@agreeprivacyauthorization="handleAgree"
>同意</button>
</view>
</view>
</view>
<!-- #endif -->
</template>
<script>
export default {
name: 'WxPrivacyPopup',
// ...props / data 省略
mounted() {
// #ifdef MP-WEIXIN
if (wx.getPrivacySetting) {
wx.getPrivacySetting({
success: (res) => {
this.privacyContractName = res.privacyContractName || '《小程序隐私保护指引》'
}
})
}
// #endif
},
methods: {
// #ifdef MP-WEIXIN
openPrivacyContract() {
if (wx.openPrivacyContract) {
wx.openPrivacyContract({
fail: () => { wx.showToast({ title: '打开协议失败', icon: 'none' }) }
})
}
},
// #endif
handleAgree() {
this.$emit('agree')
this.$emit('update:visible', false)
},
handleDisagree() {
this.$emit('disagree')
// #ifdef MP-WEIXIN
wx.showToast({ title: '请先同意隐私协议', icon: 'none' })
// #endif
this.$emit('update:visible', false)
}
}
}
</script>
注意 handleAgree / handleDisagree 这种纯事件 emit 逻辑不用包(两端都要发事件),只把里面调 wx.* 的那一行包住即可。
步骤 2:给 index.vue 的组件使用和 API 调用包条件编译
首页里有两处要包:模板里用 <WxPrivacyPopup> 组件的地方,和 onMounted 里调 wx.* 的地方。
模板部分(修复前 → 修复后):
<!-- 修复前:App 端也会渲染这个微信组件 -->
<WxPrivacyPopup
v-model:visible="showPrivacyPopup"
@agree="onPrivacyAgree"
@disagree="onPrivacyDisagree"
/>
<!-- 修复后:只在微信小程序端渲染 -->
<!-- #ifdef MP-WEIXIN -->
<WxPrivacyPopup
v-model:visible="showPrivacyPopup"
@agree="onPrivacyAgree"
@disagree="onPrivacyDisagree"
/>
<!-- #endif -->
onMounted 里的隐私检测逻辑(修复前 → 修复后):
// 修复前:App 端也会执行 wx.* 调用
onMounted(() => {
uni.$on('login-success', onLoginSuccess)
const isAuthorized = uni.getStorageSync('privacy_authorized')
if (!isAuthorized && wx.getPrivacySetting) {
wx.getPrivacySetting({
success: (res) => {
if (res.needAuthorization) {
showPrivacyPopup.value = true
} else {
uni.setStorageSync('privacy_authorized', true)
}
}
})
if (wx.onNeedPrivacyAuthorization) {
wx.onNeedPrivacyAuthorization(() => {
showPrivacyPopup.value = true
})
}
}
})
// 修复后:整段 wx.* 检测逻辑只在小程序端运行
onMounted(() => {
uni.$on('login-success', onLoginSuccess)
const isAuthorized = uni.getStorageSync('privacy_authorized')
// #ifdef MP-WEIXIN
if (!isAuthorized && wx.getPrivacySetting) {
wx.getPrivacySetting({
success: (res) => {
if (res.needAuthorization) {
showPrivacyPopup.value = true
} else {
uni.setStorageSync('privacy_authorized', true)
}
}
})
if (wx.onNeedPrivacyAuthorization) {
wx.onNeedPrivacyAuthorization(() => {
showPrivacyPopup.value = true
})
}
}
// #endif
})
小坑提醒:第 43 行那句
import WxPrivacyPopup from '@/components/wx-privacy-popup.vue'不用包条件编译。因为import只是导入组件定义,只要模板里不渲染它就不会执行;给import包#ifdef反而会破坏 Vue 的 SFC 编译。包「使用处」即可。
步骤 3:清理 manifest 里无效的 uni-privacy 模块声明
如果之前因为看到弹窗提示,在 manifest.json(或模板 manifest.template.json)里手写过:
"modules": {
"uni-privacy": {}
}
删掉它。如前所述,uni-privacy 不是官方模块名,留着是无效配置。App 端的隐私弹窗由 app-plus.privacy.template 自动处理,不需要在 modules 里加任何东西:
"app-plus": {
"privacy": {
"template": {
"title": "服务协议和隐私政策",
"message": "请你仔细阅读《服务协议》和《隐私政策》..."
}
}
}
若项目用
manifest.template.json+prebuild-manifest.js生成manifest.json,改完模板后记得跑一遍node scripts/prebuild-manifest.js同步。
步骤 4:重新云打包验证
本地「运行到手机」验证不出来(标准基座兜底),必须重新云打包一次装到手机冷启动验证。修复后弹窗不再出现,说明微信隐私 API 已被正确隔离到小程序端。
六、条件编译清单速查
凡是「微信小程序专属、App 端不该执行」的隐私相关代码,都要按下表用 #ifdef MP-WEIXIN 包裹:
| 代码位置 | 内容 | 包裹方式 |
|---|---|---|
wx-privacy-popup.vue template | 整个弹窗模板(含 <button open-type="agreePrivacyAuthorization">) | <!-- #ifdef MP-WEIXIN --> ... <!-- #endif --> |
wx-privacy-popup.vue mounted | wx.getPrivacySetting | // #ifdef MP-WEIXIN ... // #endif |
wx-privacy-popup.vue openPrivacyContract | wx.openPrivacyContract / wx.showToast | // #ifdef MP-WEIXIN ... // #endif |
wx-privacy-popup.vue handleDisagree | wx.showToast | // #ifdef MP-WEIXIN ... // #endif |
index.vue template | <WxPrivacyPopup> 组件使用 | <!-- #ifdef MP-WEIXIN --> ... <!-- #endif --> |
index.vue onMounted | wx.getPrivacySetting / wx.onNeedPrivacyAuthorization 整段 | // #ifdef MP-WEIXIN ... // #endif |
index.vue import | import WxPrivacyPopup | 不包(包了破坏 SFC 编译,只包使用处即可) |
判断原则:凡是直接调用 wx.* 或使用微信专属 open-type 的代码,默认都要包 #ifdef MP-WEIXIN;纯 uni.* API(如 uni.setStorageSync、uni.$emit)两端通用,不用包。
七、核心教训
「本地不弹」不代表代码没问题。本地用标准基座(全功能),云打包用精简基座(按
modules裁剪)。凡是「本地正常、云打包才报模块缺失」类问题,第一反应应该是:有端专属代码没做条件编译隔离,而不是去 manifest 加模块。uni-privacy不是 HBuilderX 官方模块名。官方 App Modules 只有Bluetooth/Contacts/Maps/Payment等驼峰命名。看到「未添加 uni-privacy 模块」提示,别去 manifest 里加"uni-privacy": {},加了也会被云打包静默丢弃。App 端隐私弹窗由
app-plus.privacy.template自动处理,不需要任何 JS API。微信小程序端才需要wx.getPrivacySetting那套 API。两端机制完全不同,混用必须用#ifdef MP-WEIXIN隔离。条件编译包「使用处」而非「定义处」。组件的
import语句不用包(包了破坏编译),模板里的组件标签和 JS 里的 API 调用才需要包。验证必须走云打包。本地运行到手机验证不了这个问题,改完一定要重新云打包装一次,冷启动确认弹窗消失。
本文基于一次真实的「云打包 App 启动弹 uni-privacy 模块缺失」排查整理。核心动作不是去 manifest 加模块,而是把微信小程序专属的 wx.* 隐私 API 用 #ifdef MP-WEIXIN 隔离到小程序端,让 App 端编译时根本不包含这些调用——这样云打包的精简基座就不会再触发 5+ Runtime 的隐私模块检测告警。