uni-app 高频面试题与解答
-
- 第一部分:题目清单(自测用)
- 第二部分:逐题解答
-
- [Q1. uni-app 跨端原理是什么?](#Q1. uni-app 跨端原理是什么?)
- [Q2. Vue2 和 Vue3 在 uni-app 中的区别?](#Q2. Vue2 和 Vue3 在 uni-app 中的区别?)
- [Q3. 页面生命周期与组件生命周期执行顺序?](#Q3. 页面生命周期与组件生命周期执行顺序?)
- [Q4. onLoad 和 onShow 的区别与适用场景?](#Q4. onLoad 和 onShow 的区别与适用场景?)
- [Q5. 小程序为什么没有 DOM?如何替代?](#Q5. 小程序为什么没有 DOM?如何替代?)
- [Q6. 五种路由 API 及使用区别?](#Q6. 五种路由 API 及使用区别?)
- [Q7. switchTab 为什么不能传参?怎么解决?](#Q7. switchTab 为什么不能传参?怎么解决?)
- [Q8. 页面栈上限是多少?超限怎么办?](#Q8. 页面栈上限是多少?超限怎么办?)
- [Q9. 如何封装 uni.request?](#Q9. 如何封装 uni.request?)
- [Q10. 401 怎么统一处理?](#Q10. 401 怎么统一处理?)
- [Q11. 微信小程序登录流程?](#Q11. 微信小程序登录流程?)
- [Q12. code 能在前端直接换 openid 吗?为什么?](#Q12. code 能在前端直接换 openid 吗?为什么?)
- [Q13. 什么是分包?为什么要分包?主包限制?](#Q13. 什么是分包?为什么要分包?主包限制?)
- [Q14. 分包预下载有什么用?](#Q14. 分包预下载有什么用?)
- [Q15. rpx 是什么?设计稿怎么对应?](#Q15. rpx 是什么?设计稿怎么对应?)
- [Q16. 条件编译语法?常用平台标识?](#Q16. 条件编译语法?常用平台标识?)
- [Q17. 如何在 H5 和小程序写不同代码?](#Q17. 如何在 H5 和小程序写不同代码?)
- [Q18. Vuex 和 Pinia 区别?uni 推荐哪个?](#Q18. Vuex 和 Pinia 区别?uni 推荐哪个?)
- [Q19. 跨页面传值有哪些方式?](#Q19. 跨页面传值有哪些方式?)
- [Q20. uni.on 需要注意什么?](#Q20. uni.on 需要注意什么?)
- [Q21. 父子组件通信方式?](#Q21. 父子组件通信方式?)
- [Q22. provide/inject 适用场景?](#Q22. provide/inject 适用场景?)
- [Q23. 小程序支付流程?谁生成签名?](#Q23. 小程序支付流程?谁生成签名?)
- [Q24. 如何做路由拦截/登录校验?](#Q24. 如何做路由拦截/登录校验?)
- [Q25. 长列表如何优化?](#Q25. 长列表如何优化?)
- [Q26. setData 为什么有性能成本?如何优化?](#Q26. setData 为什么有性能成本?如何优化?)
- [Q27. v-for 为什么必须 key?能用 index 吗?](#Q27. v-for 为什么必须 key?能用 index 吗?)
- [Q28. 真机调试为什么必要?](#Q28. 真机调试为什么必要?)
- [Q29. 事件总线内存泄漏怎么避免?](#Q29. 事件总线内存泄漏怎么避免?)
- [Q30. uni-app x 相比传统方案优势?](#Q30. uni-app x 相比传统方案优势?)
定位:uni-app / 小程序前端岗面试自测与背诵。
用法:第一部分是纯题目清单,先遮住答案自己答一遍;第二部分逐题对照解答。
第一部分:题目清单(自测用)
- uni-app 跨端原理是什么?
- Vue2 和 Vue3 在 uni-app 中的区别?
- 页面生命周期与组件生命周期执行顺序?
- onLoad 和 onShow 的区别与适用场景?
- 小程序为什么没有 DOM?如何替代?
- 五种路由 API 及使用区别?
- switchTab 为什么不能传参?怎么解决?
- 页面栈上限是多少?超限怎么办?
- 如何封装 uni.request?
- 401 怎么统一处理?
- 微信小程序登录流程?
- code 能在前端直接换 openid 吗?为什么?
- 什么是分包?为什么要分包?主包限制?
- 分包预下载有什么用?
- rpx 是什么?设计稿怎么对应?
- 条件编译语法?常用平台标识?
- 如何在 H5 和小程序写不同代码?
- Vuex 和 Pinia 区别?uni 推荐哪个?
- 跨页面传值有哪些方式?
- uni.$on 需要注意什么?
- 父子组件通信方式?
- provide/inject 适用场景?
- 小程序支付流程?谁生成签名?
- 如何做路由拦截/登录校验?
- 长列表如何优化?
- setData 为什么有性能成本?如何优化?
- v-for 为什么必须 key?能用 index 吗?
- 真机调试为什么必要?
- 事件总线内存泄漏怎么避免?
- uni-app x 相比传统方案优势?
第二部分:逐题解答
Q1. uni-app 跨端原理是什么?
核心结论:uni-app = Vue 语法 + 平台编译器 + 统一运行时,一套代码编译到多端。
- 编译器 :把
.vue编译成各端目标代码------小程序为wxml/wxss/js/json,H5 为标准 Vue,App 为 weex / uts。 - 运行时(runtime) :抹平各端 API 差异,
uni.xxx在底层映射到wx.xxx/navigator/plus。 - 条件编译 :
#ifdef MP-WEIXIN ... #endif处理差异化代码。 - 不是"套壳 webview" :小程序端是原生组件渲染,H5 端才是 webview。
- 小程序平台无 DOM/BOM ,
document、window、localStorage均不存在。
Q2. Vue2 和 Vue3 在 uni-app 中的区别?
核心结论:核心差异在编译器、响应式实现与写法,新项目首选 Vue3 + Vite + Pinia。
| 维度 | Vue2(Options) | Vue3(Composition) |
|---|---|---|
| 编译器 | vue-cli / HBuilderX | Vite(更快,Tree-shaking 更优) |
| 响应式 | Object.defineProperty |
Proxy(可监听新增属性、数组下标) |
| 写法 | data(){return{}} + methods |
setup() / <script setup> |
| 状态管理 | Vuex | Pinia |
| 现状 | 逐步进入维护态,老项目多 | 新项目默认 |
- 生命周期:组件钩子在 Vue3 中需
import { onMounted } from 'vue'。 - 实战:农产品合格证类业务项目若在维护期可沿用 Vue2;新建模块建议直接 Vue3,避免二次迁移。
Q3. 页面生命周期与组件生命周期执行顺序?
核心结论 :页面 onLoad → 组件 created → 组件 mounted → 页面 onReady,页面 onLoad 早于组件 mounted。
- 页面
onLoad触发时子组件尚未挂载,不能在此直接操作子组件 DOM/节点 ,需等onReady或nextTick。 - 页面切走触发
onHide,再回来触发onShow;组件不会重建 ,故created/mounted不会重复执行。 - 因此"返回列表刷新"这类需求必须写在页面
onShow,而不是组件mounted。 - 组件层小程序没有
onShow,只有 Vue 的created/mounted;需要感知页面显示,可在页面onShow里uni.$emit('pageShow'),组件内uni.$on监听。
Q4. onLoad 和 onShow 的区别与适用场景?
核心结论 :onLoad 页面创建时执行一次(取参、初始化);onShow 每次显示都执行(刷新数据)。
| 钩子 | 执行次数 | 典型场景 |
|---|---|---|
onLoad(options) |
一次 | 接收 URL 参数、首次请求、初始化表单 |
onShow |
每次显示 | 从详情/编辑页返回后刷新列表、刷新登录态 |
- 参数只在
onLoad的options里能拿到,且值永远是字符串 ,数字需Number()转换。 - 不要在
onShow做重活(大列表全量重拉、复杂计算),应做增量/条件刷新,否则返回页面会明显卡顿。 - 实战:合格证列表页
onShow只判断needRefresh标志位再决定是否重新请求。
Q5. 小程序为什么没有 DOM?如何替代?
核心结论 :小程序是双线程模型------渲染层(WebView)与逻辑层(JSCore)分离,逻辑层跑在纯 JS 引擎里,没有 DOM/BOM。
- 两层通过 Native 层中转通信 ,数据用
setData传递,无法直接操作节点。 - 替代方案:
| 需求 | DOM 写法 | uni 写法 |
|---|---|---|
| 取节点信息 | getBoundingClientRect |
uni.createSelectorQuery().select().boundingClientRect() |
| 本地存储 | localStorage |
uni.setStorageSync / uni.getStorageSync |
| 网络请求 | XMLHttpRequest |
uni.request |
| 跳转 | location.href |
uni.navigateTo 等 |
- 标签也需替换:
<div>→<view>、<span>→<text>、<img>→<image>;<text>内只能嵌套文本,放<view>不生效。 - 注意:写库时避免引入依赖
document的第三方包(部分图表、富文本库需选小程序专用版)。
Q6. 五种路由 API 及使用区别?
核心结论 :navigateTo 开新页可返回、redirectTo 替换当前页、switchTab 跳 tab 页、reLaunch 关全部重开、navigateBack 返回。
| API | 表现 | 可返回 | 栈上限 |
|---|---|---|---|
uni.navigateTo |
打开新页入栈 | 是 | 10 层 |
uni.redirectTo |
关闭当前页并替换 | 否 | --- |
uni.switchTab |
跳转 tabBar 页 | 否 | --- |
uni.reLaunch |
关闭所有页面重开 | 否 | --- |
uni.navigateBack |
返回上 N 页(delta) |
--- | --- |
switchTab只能跳pages.json里tabBar.list声明过的页面,且不能传参。- 提交成功页这类"不希望用户返回"的场景用
redirectTo;切换身份/重新登录后清栈用reLaunch。 - 传参:
url: '/pages/detail/detail?id=123',目标页onLoad(options)接收,值均为字符串。
Q7. switchTab 为什么不能传参?怎么解决?
核心结论 :tabBar 页面由客户端原生管理、常驻不销毁,跳转时 URL 参数会被直接忽略,与页面栈机制有关。
- 解决思路:把数据放到"页面之外"的共享位置。
- 状态管理 :Vuex / Pinia 写入,目标 tab 页在
onShow读取后清空。 - 本地缓存 :
uni.setStorageSync('tmpParam', val),读完removeStorageSync。 - 事件总线 :
uni.$emit发送(仅适合同时机存在的页面)。 - globalData :
App.vue里挂全局对象。
- 状态管理 :Vuex / Pinia 写入,目标 tab 页在
- 变通:若必须带参跳转,改用
uni.reLaunch({ url: '/pages/index/index?x=1' })------非 tabBar 跳转时会重建页面,参数可拿到,但会清空页面栈、tab 选中态需手动处理。 - 实战:从"我的"跳到"首页"并要求定位到某个合格证分类,用 Pinia 存一个
pendingCategory,首页onShow消费一次。
Q8. 页面栈上限是多少?超限怎么办?
核心结论 :微信小程序页面栈最多 10 层 ,navigateTo 超限会直接 fail(不报错也不跳)。
- 预防:跳转前判断栈深,超限自动降级。
js
function go(url) {
const pages = getCurrentPages()
if (pages.length >= 10) {
uni.redirectTo({ url }) // 替换当前页,栈深不变
} else {
uni.navigateTo({ url })
}
}
- 兜底策略 :
- 深层表单流程(如 A→B→C→D 填写)中段用
redirectTo收敛层级。 - 完成后用
uni.navigateBack({ delta: n })一次性回退,或reLaunch重开。 - 严禁在循环/批量场景里连续
navigateTo。
- 深层表单流程(如 A→B→C→D 填写)中段用
- 实战:合格证开具流程做完提交后
navigateBack({delta:2})直接回列表,而不是再开新页。
Q9. 如何封装 uni.request?
核心结论:统一封装为 Promise,集中处理 baseURL、token 注入、业务状态码与错误提示。
js
// utils/request.js
const BASE_URL = 'https://api.example.com' // 按环境切换
export function request(options = {}) {
return new Promise((resolve, reject) => {
uni.request({
url: BASE_URL + options.url,
method: options.method || 'GET',
data: options.data,
timeout: 15000,
header: {
'Content-Type': 'application/json',
'Authorization': uni.getStorageSync('token') || ''
},
success: (res) => {
if (res.statusCode === 200 && res.data.code === 0) {
resolve(res.data)
} else if (res.data.code === 401) {
handle401()
reject(res.data)
} else {
uni.showToast({ title: res.data.msg || '请求失败', icon: 'none' })
reject(res.data)
}
},
fail: (err) => {
uni.showToast({ title: '网络异常', icon: 'none' })
reject(err)
}
})
})
}
export default {
get: (url, data) => request({ url, data, method: 'GET' }),
post: (url, data) => request({ url, data, method: 'POST' })
}
- 四个必备能力:Promise 化 (支持
async/await)、请求拦截 (注入 token)、响应拦截 (统一状态码)、错误分层(网络错误 vs 业务错误)。 - 可加:
loading计数开关、baseURL按process.env.NODE_ENV切换、上传用uni.uploadFile单独封装。
Q10. 401 怎么统一处理?
核心结论:在响应拦截器里识别 401 → 清除登录态 → 跳登录页并记录回跳地址,而不是每个接口各写一遍。
js
let jumping = false // 防并发重复跳转
function handle401() {
if (jumping) return
jumping = true
const cur = getCurrentPages().pop()
const redirect = cur ? encodeURIComponent('/' + cur.route) : ''
uni.removeStorageSync('token')
uni.reLaunch({ url: `/pages/login/login?redirect=${redirect}` })
setTimeout(() => { jumping = false }, 1000)
}
- 判定条件:
res.statusCode === 401或业务码res.data.code === 401,两者都要覆盖。 - 并发陷阱:首页同时发 5 个请求会触发 5 次跳转 → 用标志位或防抖节流。
- 登录页拿到
redirect参数,登录成功后uni.redirectTo回跳,保证用户体验连贯。 - 若有 refreshToken,可先静默续期再重放原请求;失败再跳登录。
Q11. 微信小程序登录流程?
核心结论 :前端拿 code 交给后端,后端用 code + appid + appsecret 换 openid/session_key,再下发自定义 token。
1. 前端 uni.login() → 拿到临时凭证 code(5 分钟有效、只能用一次)
2. 前端把 code 发给后端 → POST /api/wx/login
3. 后端 code + appid + appsecret 调微信 jscode2session 接口
4. 后端拿到 openid / session_key,生成自定义 token 返回前端
5. 前端 uni.setStorageSync('token'),后续请求在 header 携带
js
uni.login({
provider: 'weixin',
success: async ({ code }) => {
const res = await request.post('/wx/login', { code })
uni.setStorageSync('token', res.data.token)
}
})
- 手机号授权需用
<button open-type="getPhoneNumber">+getPhoneNumber回调拿code,再由后端解密。 uni.checkSession()可校验session_key是否过期,过期需重新login。- 注意:必须先配置合法域名,且真机才能正常调起。
Q12. code 能在前端直接换 openid 吗?为什么?
核心结论 :不能。换 openid 必须用到 appsecret,而 appsecret 绝不能出现在前端。
- 安全原因 :小程序代码包可被反编译,
appsecret一旦泄露,攻击者可冒充服务端任意换取用户身份、调用微信接口。 session_key不能下发前端:它用于解密用户敏感信息(手机号、unionId),泄露等于数据裸奔。- code 本身有防护设计:一次性、5 分钟失效、只能被同一个 appid 使用一次,所以即便被截获风险也有限------这正是要交给后端立刻兑换的原因。
- 正确做法:前端只传
code,后端完成jscode2session并返回自家 JWT/自定义 token;后续鉴权走自家 token。 - 补充:微信官方明确将"前端调用 jscode2session"列为不规范用法,审核与安全扫描会点名。
Q13. 什么是分包?为什么要分包?主包限制?
核心结论 :分包是把部分页面/资源拆成独立包按需下载,用来突破小程序主包 2MB 的体积限制。
- 为什么要分包 :主包超过 2MB 会导致无法预览和上传;且主包越大,首次启动下载越慢。
- 配置方式 (
pages.json):
json
{
"pages": [{ "path": "pages/index/index" }],
"subPackages": [
{
"root": "packageA",
"pages": [{ "path": "list/list" }]
}
]
}
- 规则:
pages中的页面属于主包,subPackages中的属于分包;- 主包与单个分包各不超过 2MB,整体另有总量上限;
- 分包可引用主包资源(JS、组件、static),主包不能引用分包内资源;
- tabBar 页面必须在主包。
- 实战:合格证打印、统计报表、帮助中心等低频模块全部沉到分包,主包只留首页/列表/登录。
Q14. 分包预下载有什么用?
核心结论 :preloadRule 让用户在进入某个主包页面时提前后台下载指定分包,跳转分包页时不再等待,消除白屏。
json
{
"preloadRule": {
"pages/index/index": {
"network": "all",
"packages": ["packageA"]
},
"pages/mine/mine": {
"network": "wifi",
"packages": ["packageB"]
}
}
}
network:all(任意网络)/wifi(仅 Wi-Fi,省流量)。- 适用:首页有明确入口指向分包页时,预下载命中率最高。
- 注意:预下载会占用用户带宽、与主包请求竞争,不要把所有分包都设为
all;低频分包按需加载即可。 - 同一页面进入只触发一次预下载,分包已下载过则不再重复。
Q15. rpx 是什么?设计稿怎么对应?
核心结论 :rpx 是小程序自适应单位,750rpx 恒等于屏幕宽度,因此 750px 宽的设计稿可 1:1 直接写。
- 换算公式:
px = rpx × (屏幕宽度 / 750)。以 iPhone 6/7/8(375px)为例:1rpx = 0.5px,750rpx = 375px。 - 设计稿 750px 出图 → 标注多少 px 就写多少 rpx,无需换算,这是 rpx 最大的工程价值。
- 若是 375px 设计稿(如部分 UI 工具默认),则数值 ×2 写成 rpx。
- 注意:
- 部分机型存在 rounding 误差 (小数取整导致 1px 错位),边框、细线、固定间距等关键尺寸可用
px兜底; font-size用 rpx 会随屏幕缩放,长文本建议配合line-height测试多机型;- 1px 物理细线用
0.5px或transform: scaleY(0.5)处理。
- 部分机型存在 rounding 误差 (小数取整导致 1px 错位),边框、细线、固定间距等关键尺寸可用
Q16. 条件编译语法?常用平台标识?
核心结论 :用 #ifdef(仅在某平台编译)/ #ifndef(除某平台外都编译)包裹代码,编译期裁剪,不进运行包。
js
// #ifdef MP-WEIXIN
console.log('只在微信小程序编译')
// #endif
// #ifndef H5
console.log('除了 H5 都编译')
// #endif
// #ifdef MP-WEIXIN || MP-ALIPAY
console.log('微信或支付宝小程序')
// #endif
各位置写法:
| 位置 | 写法 |
|---|---|
| 模板 | <!-- #ifdef MP-WEIXIN --> ... <!-- #endif --> |
| 样式 | /* #ifdef MP-WEIXIN */ ... /* #endif */ |
| 脚本 | // #ifdef MP-WEIXIN ... // #endif |
- 常用标识:
MP-WEIXIN、MP-ALIPAY、MP-BAIDU、MP-TOUTIAO、H5、APP-PLUS、APP、MP(所有小程序)、MP-360、QUICKAPP-WEBVIEW。 pages.json中也支持条件编译,可为不同平台配不同的pages/tabBar。
Q17. 如何在 H5 和小程序写不同代码?
核心结论 :两种手段------编译期条件编译 (推荐,无冗余代码)和运行期平台判断(无法静态拆分的场景)。
js
// 方式一:条件编译(编译期裁剪,包体最干净)
// #ifdef MP-WEIXIN
uni.login({ provider: 'weixin' })
// #endif
// #ifdef H5
window.location.href = '/#/login'
// #endif
// 方式二:运行期判断(枚举/映射表场景更灵活)
const platform = uni.getSystemInfoSync().uniPlatform // 'mp-weixin' | 'web' | 'app'
- 典型差异点:
- 登录 :小程序走
uni.login+ code;H5 走账号密码 / 微信 OAuth 网页授权。 - 跨域 :H5 有跨域,需 devServer proxy 或后端 CORS;小程序无跨域但有合法域名白名单(必须 HTTPS)。
- 支付 :小程序
requestPayment(provider:'wxpay');H5 走 JSAPI 或跳转收银台。 - 存储/分享/扫码 :API 能力差异大,统一用
uni.xxx+ 条件编译兜底。
- 登录 :小程序走
- 建议:把平台差异收敛到
utils/platform.js一层,业务代码不散落#ifdef。
Q18. Vuex 和 Pinia 区别?uni 推荐哪个?
核心结论 :Pinia 去掉了 mutations、天然支持 Composition API 与 TS,uni-app Vue3 项目官方推荐 Pinia;Vue2 老项目继续用 Vuex。
| 维度 | Vuex | Pinia |
|---|---|---|
| 核心概念 | state / mutations / actions / getters |
state / actions / getters(无 mutations) |
| 异步 | actions 里写异步 | actions 直接写异步 |
| 模块化 | modules + 命名空间 |
多个独立 store 文件,天然扁平 |
| TS 支持 | 弱 | 强,类型推导完善 |
| 体积 | 较大 | 更轻 |
js
// Pinia 示例
export const useUserStore = defineStore('user', {
state: () => ({ token: '', info: null }),
getters: { isLogin: (s) => !!s.token },
actions: { setToken(v) { this.token = v } }
})
// 组件内保持响应式
const { token } = storeToRefs(useUserStore())
- 注意:Pinia 无
mutations后,异步 action 直接改 state 即可,代码量明显减少。 - 持久化:小程序端 token 建议同时落
storage,启动时回填,避免刷新/冷启动丢失。
Q19. 跨页面传值有哪些方式?
核心结论:按数据量与生命周期选------URL 参数最简单,全局状态最规范,缓存适合临时中转。
| 方式 | 适用场景 | 注意 |
|---|---|---|
URL 参数 ?id=1 |
简单 id、正向跳转 | 仅字符串;长度受限;复杂对象需 encodeURIComponent(JSON.stringify()) |
uni.setStorageSync |
临时中转、跨 tab 传值 | 读完即删,避免脏数据 |
| Vuex / Pinia | 用户信息、登录态、购物车等共享状态 | 最规范,可响应 |
uni.$emit/$on 事件总线 |
同栈页面反向通知(如详情页改数据通知列表) | 必须 $off |
getCurrentPages() |
取上一页实例直接调方法/改数据 | 强耦合,慎用 |
globalData |
极简全局量 | 非响应式 |
js
// 页面栈直取上一页(慎用)
const pages = getCurrentPages()
const prev = pages[pages.length - 2]
prev.$vm.refresh() // 直接调上一页方法
- 反向传值(返回上一页并带数据)优先选事件总线或"回调标志 + 全局状态"。
Q20. uni.$on 需要注意什么?
核心结论 :uni.$on 是全局事件总线,监听后必须 $off 销毁,否则页面实例被引用无法释放,造成内存泄漏与重复触发。
js
export default {
onLoad() {
uni.$on('listRefresh', this.onRefresh)
},
onUnload() {
uni.$off('listRefresh', this.onRefresh) // 必须传入同一个函数引用
},
methods: {
onRefresh(data) { this.fetchList() }
}
}
- 必须传第二个参数 :
uni.$off('listRefresh', this.onRefresh)只移除这一个监听;不传会清掉该事件的全部监听,可能误伤其他页面。 - 先 off 再 on:防止页面重复进入导致同一回调被注册多次,一次 emit 触发多次。
- 事件名加前缀 :如
cert:refresh,避免多模块命名冲突。 - 只适合"两端同时存在"的通信;跨页面长期状态用 Pinia 更稳妥。
Q21. 父子组件通信方式?
核心结论 :父传子用 props,子传父用 $emit,父调子方法用 ref,跨层用 provide/inject 或状态管理。
vue
<!-- 父组件 -->
<child :id="id" @change="onChange" ref="childRef" />
<!-- 子组件 -->
<script>
export default {
props: { id: { type: [String, Number], default: '' } },
methods: { submit() { this.$emit('change', { id: this.id }) } }
}
</script>
- 双向绑定:Vue2 用
.sync或v-model;Vue3 用defineModel/update:xxx。 this.$refs.child.fn()直接调子组件方法(如触发校验、打开弹窗)。$parent/$children:强耦合,不推荐。- uni 注意点:
- 小程序组件没有
onShow,只有created/mounted; - 自定义组件默认样式隔离 ,外部样式不生效 → 配
options: { styleIsolation: 'shared' }或用::v-deep。
- 小程序组件没有
- 非全局组件局部注册,减少主包体积。
Q22. provide/inject 适用场景?
核心结论 :用于跨多层级的祖孙组件通信,避免 props 层层透传;适合同一组件树内的"上下文"共享。
- 典型场景:
- 主题 / 语言 / 全局配置下发;
- 表单容器向深层表单项下发校验上下文(类似 Element UI 的
elForm); - 复杂业务组件(如合格证表单向导)内部跨层共享状态。
- 响应性 :
provide默认值不是响应式的 (Vue2 尤甚)。要响应需传一个响应式对象或computed:
js
// 父
provide() {
return { formModel: this.model, isEdit: computed(() => this.editable) }
}
// 子孙
inject: ['formModel', 'isEdit']
- 不适合的场景:需要被多处修改、需追踪变更来源的全局状态 → 用 Pinia(有 devtools、可调试)。
- 缺点:数据来源不直观、重构易断链,业务里建议配合注释与统一 key 常量。
Q23. 小程序支付流程?谁生成签名?
核心结论 :签名必须由后端生成 。后端调微信统一下单拿到 prepay_id 并二次签名,前端只负责拿参数调起支付。
1. 前端提交订单 → POST /api/order/create
2. 后端调微信「统一下单」接口 → 拿到 prepay_id
3. 后端按规则二次签名,返回 timeStamp/nonceStr/package/signType/paySign
4. 前端 uni.requestPayment 调起收银台
5. 用户支付 → 微信异步通知后端 → 后端改单(以此为准)
6. 前端轮询订单状态,展示结果页
js
uni.requestPayment({
provider: 'wxpay',
timeStamp: res.timeStamp,
nonceStr: res.nonceStr,
package: res.package, // 'prepay_id=xxx'
signType: res.signType, // 'RSA' / 'MD5'
paySign: res.paySign,
success: () => { /* 轮询确认后跳成功页 */ },
fail: (err) => { if (err.errMsg.includes('cancel')) { /* 用户取消 */ } }
})
- 支付结果以后端异步通知为准 ,不能只信前端
success,需轮询/查单确认。 - 前端不放任何商户密钥、不参与签名计算。
- 实战坑:支付必须真机 且当前微信号已登录;
package必须是prepay_id=xxx完整格式;App 端还需单独申请支付能力。
Q24. 如何做路由拦截/登录校验?
核心结论 :用 uni.addInterceptor 统一拦截跳转 API,比在每个页面写判断更可维护。
js
// main.js / permission.js
const WHITE_LIST = ['/pages/login/login', '/pages/index/index']
function checkLogin(url) {
if (WHITE_LIST.some(p => url.startsWith(p))) return true
return !!uni.getStorageSync('token')
}
;['navigateTo', 'redirectTo', 'reLaunch', 'switchTab'].forEach(method => {
uni.addInterceptor(method, {
invoke(args) {
if (!checkLogin(args.url)) {
uni.navigateTo({
url: `/pages/login/login?redirect=${encodeURIComponent(args.url)}`
})
return false // 返回 false 终止原跳转
}
return true
}
})
})
- 拦截
switchTab同样必要,tabBar 页(如"我的")常是重点鉴权页。 - 白名单 +
redirect回跳参数:登录成功后uni.redirectTo({ url: redirect })。 - 局限:只能拦 API 跳转,首次进入的首页 不会被拦(那是
pages.json的启动页)→ 需在App.vue的onLaunch/onShow补一次登录态校验。 - 接口层的 401 处理(Q10)是最后一道防线,两者都要有。
Q25. 长列表如何优化?
核心结论 :核心是减少单次渲染与传输的数据量------分页加载、按需渲染、懒加载图片。
- 分页 / 触底加载 :
onReachBottom中page++追加数据,首屏只请求 10~20 条。 - 虚拟列表 :只渲染可视区 ± 缓冲区的节点(可用
uni-table之外的第三方虚拟列表,或uv-ui、z-paging等组件),千条以上数据必备。 - 图片 :CDN + 压缩 +
<image lazy-load>,列表用缩略图,详情才加载原图。 - 避免一次性 setData 大数组 :追加用
this.list = this.list.concat(newList),必要时分片更新。 - v-for 加稳定 key(用 id,不用 index)。
- 骨架屏 / loading 提升感知性能。
- 实战:合格证列表按批次分页 + 搜索条件缓存,
onShow不做全量重拉,只刷新变更项。
Q26. setData 为什么有性能成本?如何优化?
核心结论 :小程序逻辑层与渲染层线程分离 ,setData 要把数据序列化后跨线程传输,再由视图层做 diff 与渲染------数据越大、频率越高,开销越大。
- 优化手段 :
- 减小体积 :只传视图真正需要的字段,不要把接口返回的完整大对象塞进
data;不在data里放不参与渲染的数据(可挂在实例非响应属性上)。 - 合并更新 :多次赋值合并成一次,避免循环里
setData。 - 降低频率 :滚动、输入等高频事件节流(
throttle)/ 防抖,用requestAnimationFrame或uni.$emit批量派发。 - 局部更新 :小程序原生支持路径更新
this.setData({ 'list[0].num': 1 }),避免整表重传。 - 长列表虚拟滚动(见 Q25)。
- 减小体积 :只传视图真正需要的字段,不要把接口返回的完整大对象塞进
- 反面案例:列表 500 条每条 20 字段,全量
setData一次即卡顿;应只渲染当前页数据。 - 排查:
uni.setEnableDebug+ 微信开发者工具 Audits / 体验评分面板。
Q27. v-for 为什么必须 key?能用 index 吗?
核心结论 :key 是 diff 算法识别节点的唯一标识,用于复用而非重建 节点;不建议用 index。
- 无 key 或使用 index 时,列表在插入、删除、排序、筛选 后,同一 index 会指向不同数据,Vue 会错误复用节点,导致:
- 输入框/勾选状态错位(第 2 项的内容跑到第 3 项);
- 小程序端出现渲染错乱、动画异常等难以复现的 bug。
- 正确做法:用稳定唯一 id(后端主键、业务编码):
vue
<view v-for="item in list" :key="item.id">{{ item.name }}</view>
- 无 id 时可用
item.code或id + 时间戳组合,实在没有再考虑 index。 - 注意:
key不要绑定随机数(Math.random()),会导致每次都全量重建,性能更差。
Q28. 真机调试为什么必要?
核心结论 :模拟器与真机的内核、API 能力、尺寸表现都存在差异,只跑模拟器上线必然出问题。
- 渲染差异:模拟器用 Chrome 内核,真机是各厂商 WebView;rpx 换算、字体、1px 边框、安全区(刘海屏/底部横条)表现不同。
- API 能力 :支付、扫码、蓝牙、定位、录音、
getPhoneNumber、分享等在模拟器上无法真实调起或行为失真。 - 性能表现:长列表滚动、图片加载、首屏耗时的真实手感只有真机能测,模拟器往往明显更快。
- 权限与域名 :
request合法域名校验、https证书、用户授权弹窗流程真机才完整。 - 建议流程 :开发用模拟器快速迭代 → 提交前用体验版真机验证(至少 1 台 iOS + 1 台 Android,含低配机)→ 再发版。
- 远程调试:
uni.setEnableDebug({ enableDebug: true })+ 真机调试/vConsole 查看日志。
Q29. 事件总线内存泄漏怎么避免?
核心结论 :uni.$on 注册的回调常驻全局,页面销毁后若不解绑,页面实例无法被回收 → 泄漏 + 重复触发。务必在销毁钩子里 uni.$off。
js
export default {
onLoad() {
uni.$off('cert:refresh', this.onRefresh) // 先解,防重复注册
uni.$on('cert:refresh', this.onRefresh)
},
onUnload() {
uni.$off('cert:refresh', this.onRefresh) // 必须传同一函数引用
},
beforeUnmount() { // Vue3
uni.$off('cert:refresh', this.onRefresh)
},
methods: { onRefresh(d) { this.fetchList() } }
}
- 要点 :
$off必须传与原回调同一个函数引用,写匿名函数无法解绑;- 采用"先 off 再 on",杜绝多次进入页面导致一次 emit 触发 N 次;
- Vue3 用
beforeUnmount,Vue2 / 小程序页面用onUnload; - tabBar 页面常驻不销毁,注意别在
onShow里反复$on。
- 更稳妥的替代:用 Pinia 承载共享状态 + 监听,天然随组件作用域释放。
Q30. uni-app x 相比传统方案优势?
核心结论 :uni-app x 用 uts (TypeScript 风格语言)直接编译为原生代码(Android→Kotlin、iOS→Swift),性能接近原生,省去 JS 桥接与运行时开销。
| 维度 | 传统 uni-app(编译型) | uni-app x |
|---|---|---|
| 语言 | JS / Vue | uts(强类型,TS 风格) |
| 产物 | JS + 运行时,跨端映射 | 原生 Kotlin / Swift |
| 渲染 | 小程序原生组件 / App 端 webview 或 nvue | 原生渲染 |
| 性能 | 良好,受 JS Bridge 限制 | 接近原生,尤其长列表/动画 |
| 生态 | 成熟,插件市场丰富 | 仍在完善,部分插件不兼容 |
| 跨端覆盖 | 小程序 / H5 / App 全覆盖 | 以 App 为主,小程序在推进 |
- 面试加分点:能讲清"编译型跨端(运行时映射) vs 原生型跨端(源码级编译)"的差异,说明选型要看业务------多端小程序优先仍用传统 uni-app,重性能 App 可评估 uni-app x。
- 趋势:Vue3 + Vite 已成新项目默认;uniCloud(serverless,前后端一体)降低后端门槛;旧项目迁移 uni-app x 成本较高,不建议盲目跟风。