uni-app 高频面试题与解答

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 / 小程序前端岗面试自测与背诵。

用法:第一部分是纯题目清单,先遮住答案自己答一遍;第二部分逐题对照解答。


第一部分:题目清单(自测用)

  1. uni-app 跨端原理是什么?
  2. Vue2 和 Vue3 在 uni-app 中的区别?
  3. 页面生命周期与组件生命周期执行顺序?
  4. onLoad 和 onShow 的区别与适用场景?
  5. 小程序为什么没有 DOM?如何替代?
  6. 五种路由 API 及使用区别?
  7. switchTab 为什么不能传参?怎么解决?
  8. 页面栈上限是多少?超限怎么办?
  9. 如何封装 uni.request?
  10. 401 怎么统一处理?
  11. 微信小程序登录流程?
  12. code 能在前端直接换 openid 吗?为什么?
  13. 什么是分包?为什么要分包?主包限制?
  14. 分包预下载有什么用?
  15. rpx 是什么?设计稿怎么对应?
  16. 条件编译语法?常用平台标识?
  17. 如何在 H5 和小程序写不同代码?
  18. Vuex 和 Pinia 区别?uni 推荐哪个?
  19. 跨页面传值有哪些方式?
  20. uni.$on 需要注意什么?
  21. 父子组件通信方式?
  22. provide/inject 适用场景?
  23. 小程序支付流程?谁生成签名?
  24. 如何做路由拦截/登录校验?
  25. 长列表如何优化?
  26. setData 为什么有性能成本?如何优化?
  27. v-for 为什么必须 key?能用 index 吗?
  28. 真机调试为什么必要?
  29. 事件总线内存泄漏怎么避免?
  30. 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/BOMdocumentwindowlocalStorage 均不存在。

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/节点 ,需等 onReadynextTick
  • 页面切走触发 onHide,再回来触发 onShow组件不会重建 ,故 created/mounted 不会重复执行。
  • 因此"返回列表刷新"这类需求必须写在页面 onShow,而不是组件 mounted
  • 组件层小程序没有 onShow ,只有 Vue 的 created/mounted;需要感知页面显示,可在页面 onShowuni.$emit('pageShow'),组件内 uni.$on 监听。

Q4. onLoad 和 onShow 的区别与适用场景?

核心结论onLoad 页面创建时执行一次(取参、初始化);onShow 每次显示都执行(刷新数据)。

钩子 执行次数 典型场景
onLoad(options) 一次 接收 URL 参数、首次请求、初始化表单
onShow 每次显示 从详情/编辑页返回后刷新列表、刷新登录态
  • 参数只在 onLoadoptions 里能拿到,且值永远是字符串 ,数字需 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.jsontabBar.list 声明过的页面,且不能传参
  • 提交成功页这类"不希望用户返回"的场景用 redirectTo;切换身份/重新登录后清栈用 reLaunch
  • 传参:url: '/pages/detail/detail?id=123',目标页 onLoad(options) 接收,值均为字符串。

Q7. switchTab 为什么不能传参?怎么解决?

核心结论 :tabBar 页面由客户端原生管理、常驻不销毁,跳转时 URL 参数会被直接忽略,与页面栈机制有关。

  • 解决思路:把数据放到"页面之外"的共享位置。
    1. 状态管理 :Vuex / Pinia 写入,目标 tab 页在 onShow 读取后清空。
    2. 本地缓存uni.setStorageSync('tmpParam', val),读完 removeStorageSync
    3. 事件总线uni.$emit 发送(仅适合同时机存在的页面)。
    4. globalDataApp.vue 里挂全局对象。
  • 变通:若必须带参跳转,改用 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
  • 实战:合格证开具流程做完提交后 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 计数开关、baseURLprocess.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 + appsecretopenid/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"]
    }
  }
}
  • networkall(任意网络)/ wifi(仅 Wi-Fi,省流量)。
  • 适用:首页有明确入口指向分包页时,预下载命中率最高。
  • 注意:预下载会占用用户带宽、与主包请求竞争,不要把所有分包都设为 all;低频分包按需加载即可。
  • 同一页面进入只触发一次预下载,分包已下载过则不再重复。

Q15. rpx 是什么?设计稿怎么对应?

核心结论 :rpx 是小程序自适应单位,750rpx 恒等于屏幕宽度,因此 750px 宽的设计稿可 1:1 直接写。

  • 换算公式:px = rpx × (屏幕宽度 / 750)。以 iPhone 6/7/8(375px)为例:1rpx = 0.5px750rpx = 375px
  • 设计稿 750px 出图 → 标注多少 px 就写多少 rpx,无需换算,这是 rpx 最大的工程价值。
  • 若是 375px 设计稿(如部分 UI 工具默认),则数值 ×2 写成 rpx。
  • 注意:
    • 部分机型存在 rounding 误差 (小数取整导致 1px 错位),边框、细线、固定间距等关键尺寸可用 px 兜底;
    • font-size 用 rpx 会随屏幕缩放,长文本建议配合 line-height 测试多机型;
    • 1px 物理细线用 0.5pxtransform: scaleY(0.5) 处理。

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-WEIXINMP-ALIPAYMP-BAIDUMP-TOUTIAOH5APP-PLUSAPPMP(所有小程序)、MP-360QUICKAPP-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 用 .syncv-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.vueonLaunch/onShow 补一次登录态校验。
  • 接口层的 401 处理(Q10)是最后一道防线,两者都要有。

Q25. 长列表如何优化?

核心结论 :核心是减少单次渲染与传输的数据量------分页加载、按需渲染、懒加载图片。

  • 分页 / 触底加载onReachBottompage++ 追加数据,首屏只请求 10~20 条。
  • 虚拟列表 :只渲染可视区 ± 缓冲区的节点(可用 uni-table 之外的第三方虚拟列表,或 uv-uiz-paging 等组件),千条以上数据必备。
  • 图片 :CDN + 压缩 + <image lazy-load>,列表用缩略图,详情才加载原图。
  • 避免一次性 setData 大数组 :追加用 this.list = this.list.concat(newList),必要时分片更新。
  • v-for 加稳定 key(用 id,不用 index)。
  • 骨架屏 / loading 提升感知性能。
  • 实战:合格证列表按批次分页 + 搜索条件缓存,onShow 不做全量重拉,只刷新变更项。

Q26. setData 为什么有性能成本?如何优化?

核心结论 :小程序逻辑层与渲染层线程分离setData 要把数据序列化后跨线程传输,再由视图层做 diff 与渲染------数据越大、频率越高,开销越大。

  • 优化手段
    1. 减小体积 :只传视图真正需要的字段,不要把接口返回的完整大对象塞进 data;不在 data 里放不参与渲染的数据(可挂在实例非响应属性上)。
    2. 合并更新 :多次赋值合并成一次,避免循环里 setData
    3. 降低频率 :滚动、输入等高频事件节流(throttle)/ 防抖,用 requestAnimationFrameuni.$emit 批量派发。
    4. 局部更新 :小程序原生支持路径更新 this.setData({ 'list[0].num': 1 }),避免整表重传。
    5. 长列表虚拟滚动(见 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.codeid + 时间戳 组合,实在没有再考虑 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() } }
}
  • 要点
    1. $off 必须传与原回调同一个函数引用,写匿名函数无法解绑;
    2. 采用"先 off 再 on",杜绝多次进入页面导致一次 emit 触发 N 次;
    3. Vue3 用 beforeUnmount,Vue2 / 小程序页面用 onUnload
    4. 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 成本较高,不建议盲目跟风。
相关推荐
梦曦i10 小时前
@meng-xi/create-uni-app vs unibest:轻量脚手架与重型框架的定位之争
前端·uni-app
2501_915106321 天前
iOS数据采集技术详解:从性能监控到崩溃分析的全链路实践
android·ios·小程序·https·uni-app·iphone·webview
我爱写代码i2 天前
AI对话绘画数字人源码 - uniapp前端
前端·人工智能·uni-app
ModyQyW2 天前
vite-plugin-uni-pages 更新了什么
前端·uni-app
2501_915918412 天前
Flutter项目配置iOS混淆的详细步骤与工具推荐
android·flutter·ios·小程序·uni-app·cocoa·iphone
梦曦i2 天前
create-uni-app v1.2.0:交互体验优化
前端·uni-app
lhldsg3 天前
智慧场馆解决方案小程序开发实战:从架构设计到部署指南
java·小程序·uni-app
skiyee3 天前
🚀 用 17 行代码给 UniApp 加上全局登录拦截
前端·uni-app
Mark108593 天前
【踩坑】Uni-app微信小程序自定义组件在 Flex 布局中未铺满整行
微信小程序·小程序·uni-app