← Frontend

微信小程序前端面试全栈教程

微信小程序开发与前端面试知识的系统教程

学习目标:从 JavaScript 基础出发,掌握微信小程序原生开发、工程化、性能优化、安全、测试与上线,并能在面试中清楚解释原理、权衡和项目成果。

默认技术栈:原生微信小程序 + JavaScript/TypeScript。框架对比会涉及 Taro、uni-app。平台能力会更新,API 参数与限制以上线时的微信开放文档为准。

目录

  1. 面试能力地图与学习方法
  2. 前端基础:HTML、CSS、JavaScript、TypeScript
  3. 小程序架构、目录与配置
  4. WXML、WXSS、事件与渲染
  5. 生命周期、路由与组件化
  6. 状态管理、通信与数据流
  7. 网络、登录、鉴权与安全
  8. 缓存、文件、媒体与常用能力
  9. 工程化、跨端框架与质量保障
  10. 性能优化与故障诊断
  11. 项目实战:电商小程序
  12. 高频面试题与标准答法
  13. 手写题与编码题
  14. 八周学习计划与面试冲刺
  15. 自测卷与答案

1. 面试能力地图与学习方法

1.1 面试官真正考什么

能力 初级要求 中高级要求
语言基础 会写 ES6、异步、数组操作 解释闭包、原型、事件循环、内存与边界
小程序 API 会查文档并调用 理解双线程、通信成本、兼容与降级
页面开发 能还原 UI 组件抽象、状态设计、可维护性
网络与登录 会请求接口 讲清 code、服务端 session、自定义 token、安全边界
性能 会压图、懒加载 能定位首屏、setData、包体与长列表问题
工程质量 会 Git、基本调试 TypeScript、规范、测试、监控、CI/CD
项目表达 说做了哪些页面 讲问题、约束、方案、权衡、数据和复盘

核心方法:每个知识点都练习四层表达:它是什么 → 为什么需要 → 如何实现 → 有什么代价。

1.2 一道题的高分回答结构

以“如何优化 setData”为例:

  1. 先下定义:setData 把逻辑层数据同步到视图层并触发渲染。
  2. 讲原理:逻辑层与视图层之间存在序列化、通信和渲染成本。
  3. 给方案:只更新必要路径、合并更新、避免高频更新大对象、控制数据体积。
  4. 讲验证:用性能面板比较通信次数、耗时、首屏时间和内存。
  5. 补边界:不能为了少调用而把所有数据长期堆在页面 data 中。

2. 前端基础:必须补齐的底座

2.1 JavaScript 数据类型与判断

JavaScript 原始类型包括 string、number、bigint、boolean、undefined、symbol、null;对象类型包括普通对象、数组、函数、日期等。

typeof 1               // 'number'
typeof null            // 'object',历史遗留问题
typeof []              // 'object'
Array.isArray([])      // true
Object.prototype.toString.call(new Date()) // '[object Date]'
易错点:typeof null 不是 'null';NaN !== NaN,判断它应使用 Number.isNaN。

2.2 作用域、闭包与内存

闭包是函数与其词法环境的组合。内部函数即使离开创建它的作用域,仍可访问当时的外部变量。

function createCounter() {
  let count = 0
  return () => ++count
}
const next = createCounter()
next() // 1
next() // 2

用途:数据私有化、函数工厂、保存状态。风险:未解除的计时器、事件监听或全局引用可能让闭包长期保留大对象。

2.3 this、箭头函数与绑定

  • 普通函数的 this 取决于调用方式。
  • obj.fn() 中通常指向 obj。
  • call/apply/bind 可显式绑定。
  • 箭头函数没有自己的 this,它捕获外层词法 this。
const user = {
  name: 'Ada',
  normal() { return this.name },
  delayed() {
    setTimeout(() => console.log(this.name), 0)
  }
}

2.4 原型与继承

对象读取属性时,先查自身;找不到时沿 原型链 查找,直到 null。class 是基于原型机制的语法糖。

class Animal {
  constructor(name) { this.name = name }
  speak() { return `${this.name} speaks` }
}
class Cat extends Animal {}

2.5 事件循环与 Promise

同步代码先执行。一个任务结束后,环境会先清空当前轮次的微任务,再进入后续任务。Promise 回调通常属于微任务,计时器回调属于后续任务。

console.log('A')
setTimeout(() => console.log('B'), 0)
Promise.resolve().then(() => console.log('C'))
console.log('D')
// A D C B
面试表达:不要只背“宏任务、微任务”。先说执行顺序,再说队列与清空时机,最后说明宿主环境细节可能不同。

2.6 async/await 与错误处理

async 函数返回 Promise;await 暂停当前异步函数的后续代码,但不会阻塞整个线程。

async function load() {
  try {
    const [user, goods] = await Promise.all([getUser(), getGoods()])
    return { user, goods }
  } catch (error) {
    report(error)
    throw error
  } finally {
    hideLoading()
  }
}

并发无依赖任务使用 Promise.all;允许部分失败可考虑 Promise.allSettled。连续 await 会把本可并行的请求串行化。

2.7 深浅拷贝、不可变更新

展开运算符只做一层浅拷贝。JSON.parse(JSON.stringify(x)) 会丢失 undefined、函数、Symbol,并错误处理部分特殊对象和循环引用。支持时可用 structuredClone,但工程中仍应根据数据结构选择策略。

const next = {
  ...state,
  profile: { ...state.profile, city: 'Shanghai' }
}

2.8 防抖与节流

  • 防抖 debounce:停止触发一段时间后执行,适合搜索联想。
  • 节流 throttle:一段时间最多执行一次,适合滚动、拖动上报。
function debounce(fn, wait = 300) {
  let timer
  return function (...args) {
    clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), wait)
  }
}

2.9 TypeScript 面试核心

interface Product {
  id: string
  title: string
  price: number
  tags?: string[]
}

type ApiResult<T> =
  | { ok: true; data: T }
  | { ok: false; code: string; message: string }

function getById<T extends { id: string }>(items: T[], id: string): T | undefined {
  return items.find(item => item.id === id)
}

必须理解:联合类型、交叉类型、泛型、类型收窄、keyof、映射类型、Partial/Pick/Omit/Record。any 关闭类型检查;不确定外部输入时优先用 unknown 并主动收窄。


3. 小程序架构、目录与配置

3.1 为什么小程序不是普通网页

网页通常在浏览器页面上下文中运行并直接操作 DOM。微信小程序由宿主提供运行环境、组件和 API,业务代码不能随意操作浏览器 DOM。

核心架构:传统小程序常用 逻辑层 与 视图层 分离理解:

flowchart LR
  A["逻辑层:JavaScript / data"] -->|"setData:序列化与通信"| B["视图层:WXML / WXSS"]
  B -->|"用户事件"| A
  A --> C["微信客户端原生能力"]
  C --> A

这解释了三个高频现象:不能直接操作 DOM;频繁传输大数据成本高;原生组件与 Web 组件行为并不完全相同。

3.2 最小目录

miniprogram/
├── app.js                 # 全局逻辑
├── app.json               # 页面、窗口、分包等全局配置
├── app.wxss               # 全局样式
├── sitemap.json
├── pages/
│   └── home/
│       ├── index.js
│       ├── index.json
│       ├── index.wxml
│       └── index.wxss
├── components/
├── services/              # 请求与业务接口
├── utils/
└── types/

3.3 全局与页面配置

{
  "pages": ["pages/home/index", "pages/cart/index"],
  "window": {
    "navigationBarTitleText": "商城",
    "navigationBarBackgroundColor": "#ffffff"
  },
  "tabBar": {
    "list": [
      { "pagePath": "pages/home/index", "text": "首页" },
      { "pagePath": "pages/cart/index", "text": "购物车" }
    ]
  }
}

页面 .json 可覆盖部分全局窗口配置。组件需要声明 "component": true;引用自定义组件常在 usingComponents 中配置。

3.4 rpx 与适配

rpx是小程序的响应式长度单位,屏幕宽度按 750rpx 处理。设计稿若为 750 宽,标注值通常可直接用作 rpx。

.card {
  width: 686rpx;
  margin: 24rpx 32rpx;
  padding: 24rpx;
  box-sizing: border-box;
}

注意安全区、横屏、平板、系统字体缩放和自定义导航栏差异。不要把“750rpx”理解成物理像素。


4. WXML、WXSS、事件与渲染

4.1 数据绑定与条件渲染

<view class="page">
  <view wx:if="{{loading}}">加载中...</view>
  <view wx:elif="{{error}}">{{error}}</view>
  <product-list wx:else items="{{products}}" />
</view>

wx:if 会根据条件创建或销毁节点,适合切换不频繁、初始条件可能为假的场景。hidden 通常保留节点,仅改变显示状态,适合频繁切换但节点不重的场景。

4.2 列表与 key

<view wx:for="{{products}}" wx:key="id" wx:for-item="product">
  <text>{{product.title}}</text>
</view>

稳定唯一的 key 帮助框架识别节点身份。不要在可增删、排序的列表里随意使用索引作为 key。

4.3 事件系统

<button data-id="{{product.id}}" bind:tap="onAdd">加入购物车</button>
Page({
  onAdd(event) {
    const { id } = event.currentTarget.dataset
    this.addToCart(id)
  }
})
  • target:最初触发事件的节点。
  • currentTarget:当前绑定处理函数的节点。
  • bind:事件可继续冒泡。
  • catch:阻止事件继续冒泡。
  • mut-bind:用于某些互斥事件场景,按文档确认支持范围。

4.4 WXSS 与样式隔离

WXSS 支持大部分 CSS 能力并增加 rpx。自定义组件一般具有样式隔离,应通过组件设计暴露合理的 class、属性或外部样式能力,而不是依赖脆弱的跨层选择器。

.product-card {
  display: flex;
  gap: 20rpx;
  border-radius: 16rpx;
  background: #fff;
}

.title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

4.5 setData 的正确姿势

this.setData({
  [`products[${index}].selected`]: true,
  totalPrice: nextTotal
})

原则:

  1. 只同步视图真正依赖的数据。
  2. 使用数据路径更新局部字段,避免每次传整个数组或大对象。
  3. 合并同一业务时刻的更新。
  4. 高频输入、滚动等使用节流,并判断是否必须进入视图层。
  5. 非视图数据可放在页面实例普通字段,不必全部塞进 data。
易错点:直接修改 this.data.x 不会可靠地更新界面;超大对象整体 setData 会产生序列化、通信和渲染成本。

5. 生命周期、路由与组件化

5.1 生命周期

应用常见生命周期:onLaunch、onShow、onHide、onError。页面常见生命周期:onLoad、onShow、onReady、onHide、onUnload。

典型顺序:首次打开应用时应用初始化,随后进入页面加载、显示、就绪。页面跳转方式不同,会决定旧页面是隐藏还是卸载。

Page({
  data: { id: '', product: null },
  onLoad(options) {
    this.setData({ id: decodeURIComponent(options.id || '') })
    this.loadProduct()
  },
  onShow() {
    this.refreshCartBadge()
  },
  onUnload() {
    if (this.requestTask) this.requestTask.abort()
    clearInterval(this.timer)
  }
})

职责划分:onLoad 读取参数并做一次性初始化;onShow 刷新每次返回都可能变化的数据;onReady 适合首次渲染后的节点操作;onUnload 清理资源。

5.2 路由 API 对比

API 页面栈行为 典型用途
navigateTo 保留当前页,压入新页 列表到详情
redirectTo 替换当前页 不需要返回的中间页
navigateBack 弹出若干页面 返回
switchTab 切换 tabBar 页 主导航
reLaunch 重建页面栈 登录态重置、回首页

路由参数会进入 URL,应做编码、长度控制和输入校验;敏感数据不应放在 URL 中。

5.3 自定义组件

Component({
  properties: {
    product: { type: Object, value: null },
    disabled: { type: Boolean, value: false }
  },
  data: { pressed: false },
  observers: {
    'product.id': function (id) {
      if (id) this.prepare(id)
    }
  },
  methods: {
    onTap() {
      if (this.data.disabled) return
      this.triggerEvent('select', { id: this.data.product.id })
    }
  }
})
<product-card product="{{item}}" bind:select="onSelect" />

组件设计遵循:输入明确、输出事件化、内部状态最小、业务依赖可注入。高复用组件不要直接发业务请求或强依赖全局状态。

5.4 behaviors、slot 与抽象边界

  • behaviors 可复用组件间共同的属性、数据、生命周期和方法。
  • slot 让调用方注入内容,适合布局型组件。
  • 页面级业务尽量在页面或业务容器中;视觉组件保持纯粹。

面试常问“什么时候拆组件”:重复出现、职责独立、状态边界清楚、可独立测试时拆。只出现一次但逻辑复杂,也可为可维护性拆分。


6. 状态管理、通信与数据流

6.1 状态分层

层级 示例 建议位置
组件局部状态 展开、选中、输入值 组件 data
页面状态 列表、筛选、分页 页面 data
跨页会话状态 用户、购物车摘要 store 或受控全局状态
持久状态 token、主题、草稿 本地存储,注意过期与安全
服务端状态 商品、订单 请求层 + 缓存策略

单向数据流:状态产生视图;用户事件触发动作;动作更新状态;状态再次驱动视图。

6.2 页面通信方式

  1. URL 参数:少量、可序列化、非敏感信息。
  2. eventChannel:打开新页面时传较复杂的一次性数据或回传事件。
  3. 全局 store:跨多个页面共享、生命周期较长的状态。
  4. 本地缓存:需要跨启动保存的数据。
  5. 服务端:需要多端一致、可信和可恢复的数据。

不要把所有东西放到 getApp().globalData;它缺少明确变更入口,容易出现隐式耦合和状态过期。

6.3 简易 Store 思路

function createStore(initialState) {
  let state = initialState
  const listeners = new Set()
  return {
    getState: () => state,
    setState(updater) {
      state = typeof updater === 'function' ? updater(state) : updater
      listeners.forEach(fn => fn(state))
    },
    subscribe(fn) {
      listeners.add(fn)
      return () => listeners.delete(fn)
    }
  }
}

真实项目还需处理选择器、批量更新、持久化、调试和取消订阅。若项目复杂,可评估 MobX 等方案,但要能解释引入成本。


7. 网络、登录、鉴权与安全

7.1 封装请求层

const BASE_URL = 'https://api.example.com'

function request({ url, method = 'GET', data, auth = true }) {
  return new Promise((resolve, reject) => {
    const token = wx.getStorageSync('access_token')
    wx.request({
      url: BASE_URL + url,
      method,
      data,
      header: auth && token ? { Authorization: `Bearer ${token}` } : {},
      timeout: 10000,
      success(res) {
        if (res.statusCode >= 200 && res.statusCode < 300) {
          resolve(res.data)
        } else {
          reject({ type: 'http', status: res.statusCode, data: res.data })
        }
      },
      fail(error) { reject({ type: 'network', cause: error }) }
    })
  })
}

完善版还需要:请求 ID、统一错误映射、token 刷新、并发刷新锁、幂等重试、取消请求、日志脱敏、环境配置。

7.2 登录链路

sequenceDiagram
  participant U as 用户
  participant M as 小程序
  participant W as 微信服务
  participant S as 业务服务端
  U->>M: 打开或点击登录
  M->>W: wx.login()
  W-->>M: 临时 code
  M->>S: 提交 code
  S->>W: 服务端用 code 换取会话信息
  W-->>S: openid / session_key 等
  S->>S: 建立业务用户与会话
  S-->>M: 自定义 token
  M->>S: 后续请求携带 token
安全红线:AppSecret、服务端密钥和可信权限判断绝不能放在小程序代码中。客户端可被分析和篡改,服务端必须重新校验身份、权限、价格、库存和订单金额。

7.3 token 刷新并发控制

当多个请求同时收到未授权响应,不能同时发起多次刷新。应让第一个请求创建 refreshPromise,其余请求等待同一个 Promise;刷新成功后统一重放,失败则清理登录态并跳转。

let refreshPromise = null

async function ensureFreshToken() {
  if (!refreshPromise) {
    refreshPromise = refreshToken().finally(() => { refreshPromise = null })
  }
  return refreshPromise
}

只对安全、可重试的请求自动重放;支付、下单等写操作应使用幂等键和服务端幂等设计。

7.4 错误分类与用户体验

  • 网络错误:无网、超时、DNS,提示检查网络并允许重试。
  • HTTP 错误:401 重新认证;403 无权限;404 资源不存在;429 限流;5xx 服务异常。
  • 业务错误:库存不足、优惠券失效,要展示服务端可读信息。
  • 解析错误:响应结构异常,记录版本、请求 ID,不把内部堆栈展示给用户。

7.5 安全清单

  1. 仅使用 HTTPS,配置合法域名。
  2. 客户端输入全部不可信,服务端校验类型、范围、权限。
  3. 日志不得记录 token、手机号、身份证、支付信息。
  4. 防止越权:资源查询必须带当前用户权限条件。
  5. 防重放与重复提交:nonce、时间窗或幂等键,按风险选择。
  6. 展示富文本时采用可信白名单策略,避免恶意链接和内容。
  7. 本地缓存不存长期高敏感秘密;退出登录要清理相关数据。
  8. 隐私能力按平台规范告知并在必要场景请求授权,不提前索取。

8. 缓存、文件、媒体与常用能力

8.1 缓存策略

缓存记录应含版本与过期时间:

function setCache(key, value, ttlMs) {
  wx.setStorageSync(key, { value, expiresAt: Date.now() + ttlMs, version: 1 })
}

function getCache(key) {
  const item = wx.getStorageSync(key)
  if (!item || item.version !== 1 || Date.now() > item.expiresAt) {
    wx.removeStorageSync(key)
    return null
  }
  return item.value
}

常用策略:

  • Cache First:静态配置、变化很少的字典。
  • Network First:订单、库存等强时效数据。
  • Stale While Revalidate:先展示旧缓存,再后台更新,适合首页内容流。

8.2 图片优化

使用合适尺寸与格式、CDN 裁剪、懒加载、占位图、失败回退。列表缩略图不要直接加载原始大图。图片宽高应尽量固定,降低布局跳动。

8.3 上传文件

上传流程要考虑:类型与大小前置校验、压缩、进度、取消、失败重试、服务端签名、安全扫描、断点续传需求。客户端后缀名和 MIME 都不可信,服务端仍需校验。

8.4 授权设计

区分登录、用户资料、位置、相册、相机等能力。遵循“用到再申请”,拒绝后解释用途并提供不授权时的可用路径。不要把“授权失败”当成程序异常。


9. 工程化、跨端框架与质量保障

9.1 推荐分层

pages/          页面编排与路由
components/     可复用 UI 与业务组件
services/       API 定义与服务端数据转换
store/          跨页面状态
utils/          无业务或低业务工具
constants/      常量与枚举
types/          TypeScript 类型
config/         环境与功能开关

请求返回不要直接散落到视图。可在 service 层把后端字段转换为页面模型,隔离命名、空值与版本变化。

9.2 环境与发布

至少区分开发、测试、预发布、生产环境。API 地址和功能开关由构建配置注入;密钥永不打包。发布需要版本号、变更记录、回滚方案、灰度策略和监控观察窗。

9.3 原生、Taro、uni-app 对比

维度 原生小程序 Taro uni-app
平台贴合 最高 较高,依赖适配 跨端能力广
学习成本 熟悉小程序语法 需 React/Vue 等 通常偏 Vue 生态
跨端复用 较弱 强 强
新 API 跟进 通常最快 等框架适配或写平台代码 等适配或条件编译
调试复杂度 低一层 多编译与运行差异 多平台差异与条件分支

选择不是站队:单一微信平台、重性能与新能力可优先原生;已有 React 团队、多小程序目标可评估 Taro;Vue 团队且覆盖 App/H5 可评估 uni-app。最终用团队能力、目标端、组件生态、性能和长期维护成本决策。

9.4 测试金字塔

  1. 单元测试:纯函数、价格计算、数据转换、状态逻辑。
  2. 组件测试:输入属性、事件输出、条件渲染。
  3. 集成测试:登录、请求、缓存、页面协作。
  4. 端到端测试:下单、支付前后、异常恢复等关键链路。

测试重点放在高风险业务,而不是追求虚假的 100% 覆盖率。

9.5 监控与可观测性

记录:JS 错误、Promise 未处理错误、接口失败率与耗时、页面打开与首屏、关键业务漏斗、客户端版本、基础库版本、网络类型、请求 ID。日志必须采样、限流、脱敏。


10. 性能优化与故障诊断

10.1 性能优化流程

先测量,再定位,再改动,再回归。不要把“加缓存、压图片”当万能答案。

flowchart LR
  A["定义指标"] --> B["采集基线"] --> C["定位瓶颈"] --> D["最小改动"] --> E["对照验证"] --> F["监控回归"]

10.2 启动与首屏

优化方向:

  1. 控制主包体积,将非首屏页面与资源分包。
  2. 首屏只请求必要数据,其余延迟或并行加载。
  3. 避免应用启动时执行大量同步计算。
  4. 骨架屏应匹配真实布局,减少感知等待。
  5. 合理使用分包预下载,但要权衡流量与命中率。
  6. 图片使用 CDN 尺寸裁剪和懒加载。

10.3 渲染与长列表

  • 减少不必要节点、深层嵌套和复杂选择器。
  • 控制 setData 的频率与载荷。
  • 分页加载,避免一次渲染数千项。
  • 超长列表使用虚拟列表:只渲染可视区与缓冲区,滚动时复用或替换节点。
  • 固定或预估行高可显著简化虚拟列表位置计算;动态高度需要测量和缓存。

10.4 包体与分包

主包放启动必需页面、公共基础代码;业务域放独立分包。公共依赖的归属要根据构建结果评估,避免复制。分包不是越多越好:过碎会增加管理和加载复杂度。

10.5 内存与资源泄漏

页面卸载时清理:计时器、订阅、事件监听、观察器、进行中的请求、大数组引用、媒体资源。缓存应有数量与空间上限,不能无限增长。

10.6 性能题回答模板

“我先用性能工具和埋点确认慢在启动、网络、逻辑计算还是渲染。若是首屏,检查主包体积、关键请求瀑布和资源大小;若交互卡顿,检查 setData 频率、数据量和节点数;长列表则评估分页或虚拟化。优化后用同设备、同网络、同版本做对照,并观察线上 P50/P90/P95,而不是只报一次本地最快值。”


11. 项目实战:电商小程序

11.1 功能范围

  • 首页:Banner、分类、推荐、分页。
  • 搜索:历史、联想、防抖、结果页。
  • 商品详情:规格、库存、收藏、分享。
  • 购物车:选中、数量、失效商品、价格计算。
  • 下单:地址、优惠券、运费、幂等提交。
  • 订单:状态列表、详情、取消、确认收货。
  • 我的:登录态、设置、反馈。

11.2 数据模型

interface Sku {
  id: string
  specValues: Record<string, string>
  priceCent: number
  stock: number
}

interface CartItem {
  skuId: string
  quantity: number
  selected: boolean
  snapshotPriceCent: number
}

金额使用整数分存储和计算,展示时再格式化,避免浮点误差。

function formatMoney(cent) {
  if (!Number.isInteger(cent)) throw new TypeError('cent must be integer')
  return (cent / 100).toFixed(2)
}

11.3 购物车派生状态

总价、全选、已选数量属于可从基础状态计算的 派生状态。应在单一入口计算,避免各处分别维护导致不一致。

function deriveCart(items) {
  const valid = items.filter(item => !item.disabled)
  const selected = valid.filter(item => item.selected)
  return {
    allSelected: valid.length > 0 && selected.length === valid.length,
    selectedCount: selected.reduce((n, item) => n + item.quantity, 0),
    totalCent: selected.reduce((n, item) => n + item.priceCent * item.quantity, 0)
  }
}

11.4 下单一致性

客户端展示价格只是预估。提交订单时服务端根据 SKU、数量、地址、优惠规则重新计算价格并校验库存。客户端提交唯一幂等键,避免网络重试生成重复订单。

11.5 项目难点范例:规格选择

问题:多规格组合中,选择一个值后要禁用无库存组合。

方案:把每个 SKU 的规格值集合建立索引;当前已选择集合与候选值组合后,判断是否存在库存大于零且包含该组合的 SKU。SKU 很多时可预计算组合键映射。

权衡:全量遍历实现简单但复杂度高;索引增加初始化和内存成本,却降低每次点击的计算量。

11.6 项目难点范例:请求竞态

搜索词从 a 快速变为 ab,旧请求可能后返回并覆盖新结果。解决方式:取消旧请求,或给每次请求递增序号,只接受最后一次响应。

let latestRequestId = 0
async function search(keyword) {
  const id = ++latestRequestId
  const result = await api.search(keyword)
  if (id !== latestRequestId) return
  this.setData({ result })
}

11.7 STAR 项目讲述模板

  • S(情境):首页商品流在中低端设备滚动卡顿,首屏也偏慢。
  • T(任务):在不改变业务功能的前提下改善首屏与滚动体验。
  • A(行动):采集基线;拆分主包;首屏接口并行;图片按容器尺寸裁剪;减少大对象 setData;长列表分页与虚拟化;补充线上指标。
  • R(结果):使用真实测量数据表达,例如“测试机 P90 首屏从 X 降至 Y”。没有数据就诚实说“本地对照显著改善,线上指标尚未建立”,不要编数字。
  • 复盘:说明代价、尚未解决的问题和下一步。

12. 高频面试题与标准答法

12.1 小程序与 H5 有什么区别?

小程序运行在微信宿主提供的环境中,通过 WXML/WXSS、组件和受控 API 开发,通常不能直接操作 DOM;H5 运行在浏览器模型中,使用标准 Web API。小程序有平台登录、分享等能力与包体、域名、审核约束;H5 开放度更高、跨浏览器,但平台能力接入路径不同。

12.2 为什么频繁 setData 会卡?

因为数据需要从逻辑侧序列化并同步到视图侧,再触发渲染。频率高、载荷大、节点多时,通信和渲染成本叠加。优化要减少次数和字段、局部路径更新、节流高频事件,并减少视图节点。

12.3 wx:if 与 hidden 的区别?

wx:if 侧重条件创建与销毁,切换成本可能较高但初始为假时不创建;hidden 通常保留节点,仅控制显示,初始成本存在但频繁切换更合适。还要看组件是否很重以及切换频率。

12.4 页面生命周期怎么选?

一次性参数读取和初始化放 onLoad;每次页面重新可见都要刷新的逻辑放 onShow;首屏渲染后节点相关操作放 onReady;离开但保留页面时是 onHide;彻底离开时 onUnload 清理资源。

12.5 如何做登录?

客户端通过 wx.login 获取短期 code,发送给业务服务端;服务端与微信服务交换会话信息、绑定业务用户,并向客户端发自己的会话 token。后续接口带 token,服务端鉴权。AppSecret 与 session_key 不下发或硬编码在客户端。

12.6 token 过期怎么办?

统一请求层识别未授权,使用单例刷新 Promise 避免并发刷新风暴;刷新成功后按安全规则重放请求,失败则清理会话并引导登录。写请求要结合幂等机制,不能盲目重试。

12.7 如何设计组件?

先明确职责,属性作为输入,事件作为输出,内部状态保持最少;展示组件不直接依赖业务服务;使用 slot 解决内容扩展。抽象依据是变化原因和复用边界,不是文件行数。

12.8 如何优化首屏?

先测量启动、下载、请求和渲染各阶段;然后控制主包、分包首屏外代码、缩短关键请求链、并行无依赖请求、压缩和裁剪图片、减少初始化同步计算和 setData 载荷;最后同条件复测并监控分位数。

12.9 如何避免重复提交?

前端按钮 loading 和短期禁用改善体验,但不构成可靠保证;服务端用幂等键记录处理结果,对相同键返回同一结果,并在数据库层保证唯一约束。支付和订单状态还要以服务端查询或可靠回调为准。

12.10 分包有什么利弊?

分包可降低主包下载和启动压力,并按需加载业务;代价是依赖归属、跨包引用、预下载策略和版本管理更复杂。应该按业务域和启动路径拆,不为数字好看而过度切碎。

12.11 为什么不能信任客户端价格?

客户端代码和请求都可被修改。服务端必须从可信商品、库存、优惠和运费规则重新计算,验证用户资格,并用事务或一致性策略处理库存与订单。

12.12 你如何排查线上白屏?

先看影响范围、版本、机型、基础库和网络;检查 JS 异常、资源/接口失败、启动耗时与路由参数;通过请求 ID 串联服务端日志;尝试最小复现。必要时关闭功能开关或回滚,再修复并补监控和回归用例。

12.13 原生和跨端框架怎么选?

按目标平台数、团队栈、性能、新 API 依赖、组件生态和维护成本决策。单微信且追求平台特性优先原生;多端且团队已有 React/Vue 能力可考虑跨端,但必须预留平台差异层。

12.14 什么是请求竞态?

多个请求并发时,响应顺序不保证与发起顺序一致,旧响应可能覆盖新状态。可取消旧请求、使用序号或 Abort 语义、校验查询参数,并为加载状态按请求维度管理。

12.15 如何做灰度与回滚?

发布前确保兼容旧接口;通过版本或用户分桶控制功能开关;监控错误率、接口与业务漏斗;异常时先关开关或回滚。数据库和协议变更应前后兼容,避免只能代码回滚但数据无法回退。


13. 手写题与编码题

13.1 并发限制器

async function mapLimit(items, limit, worker) {
  const results = new Array(items.length)
  let cursor = 0

  async function run() {
    while (true) {
      const index = cursor++
      if (index >= items.length) return
      results[index] = await worker(items[index], index)
    }
  }

  const count = Math.min(limit, items.length)
  await Promise.all(Array.from({ length: count }, run))
  return results
}

追问:失败是立即停止还是收集全部结果?是否需要重试、取消、公平性?生产实现必须先定义语义。

13.2 重试与指数退避

async function retry(fn, {
  retries = 3,
  baseDelay = 300,
  shouldRetry = () => true
} = {}) {
  let lastError
  for (let attempt = 0; attempt <= retries; attempt++) {
    try {
      return await fn(attempt)
    } catch (error) {
      lastError = error
      if (attempt === retries || !shouldRetry(error)) throw error
      const delay = baseDelay * 2 ** attempt + Math.random() * 100
      await new Promise(resolve => setTimeout(resolve, delay))
    }
  }
  throw lastError
}

网络瞬断、超时、部分 5xx 或 429 可能重试;参数错误、权限错误通常不重试。非幂等写操作不能简单自动重试。

13.3 LRU 缓存

class LRUCache {
  constructor(capacity) {
    this.capacity = capacity
    this.map = new Map()
  }
  get(key) {
    if (!this.map.has(key)) return undefined
    const value = this.map.get(key)
    this.map.delete(key)
    this.map.set(key, value)
    return value
  }
  set(key, value) {
    if (this.map.has(key)) this.map.delete(key)
    this.map.set(key, value)
    if (this.map.size > this.capacity) {
      this.map.delete(this.map.keys().next().value)
    }
  }
}

13.4 树转扁平数组

function flattenTree(nodes) {
  const result = []
  const stack = nodes.map(node => ({ node, depth: 0 })).reverse()
  while (stack.length) {
    const { node, depth } = stack.pop()
    result.push({ ...node, depth, children: undefined })
    const children = node.children || []
    for (let i = children.length - 1; i >= 0; i--) {
      stack.push({ node: children[i], depth: depth + 1 })
    }
  }
  return result
}

13.5 必练算法清单

  • 数组/哈希:两数之和、去重、分组、最长无重复子串。
  • 双指针:有序数组、移动零、区间合并。
  • 栈/队列:括号匹配、单调栈、滑动窗口。
  • 树:DFS、BFS、最大深度、路径、最近公共祖先。
  • 动态规划:爬楼梯、零钱兑换、最长递增子序列。
  • 排序与二分:边界二分、Top K、归并思想。

做题时必须说:输入输出、边界、朴素方案、复杂度、优化、测试用例。


14. 八周学习计划与面试冲刺

第 1 周:JavaScript 底座

  • 每天 2 小时:类型、作用域、闭包、this、原型、异步。
  • 手写 debounce、throttle、Promise 并发限制。
  • 每天 2 道简单算法,口述复杂度。
  • 验收:能不看稿解释事件循环,并现场写一个请求封装。

第 2 周:原生小程序基础

  • 完成首页、列表、详情三个页面。
  • 掌握 WXML、WXSS、事件、路由、生命周期、rpx。
  • 验收:能解释 wx:if/hidden、路由 API、页面栈。

第 3 周:组件与状态

  • 写商品卡片、弹窗、空状态、规格选择器。
  • 做页面、组件、跨页三层状态设计。
  • 验收:组件有清晰输入输出,离开页面能清理资源。

第 4 周:网络、登录、安全

  • 封装请求层、错误映射、登录、刷新锁。
  • 实现缓存过期和退出清理。
  • 验收:画出完整登录时序图,讲出五条安全红线。

第 5 周:完整项目

  • 完成购物车、下单前确认、订单列表。
  • 处理空态、骨架屏、无网、超时、重复点击、请求竞态。
  • 验收:录制 3 分钟演示,README 有架构图与决策记录。

第 6 周:性能与工程质量

  • 分包、图片优化、局部 setData、分页或虚拟列表。
  • 引入 TypeScript、lint、单元测试、错误监控设计。
  • 验收:保留优化前后证据,不能只写“性能提升”。

第 7 周:面试专项

  • 每天 15 道高频题,回答限制在 90 秒。
  • 每天 3 道算法,至少一道中等。
  • 整理 5 个项目难点,每个用 STAR + 权衡 + 数据。
  • 验收:进行两次完整模拟面试并录音复盘。

第 8 周:投递与查漏补缺

  • 简历每个技术点都准备追问树。
  • 针对岗位描述补 React/Vue、Node、跨端框架或业务领域。
  • 保持每日算法与口述,不在面试前大规模更换技术栈。

每日 4 小时模板

时间 任务
60 分钟 学原理并做自己的解释卡片
90 分钟 项目编码与调试
45 分钟 算法与手写题
30 分钟 高频题口述录音
15 分钟 复盘:今天犯了什么错

15. 模拟面试自测卷

A. 基础题

  1. let、const、var 的作用域和提升有何区别?
  2. 为什么 0.1 + 0.2 !== 0.3?金额应怎样处理?
  3. 解释闭包并给一个内存泄漏风险案例。
  4. Promise 链中的错误如何传播?finally 有什么语义?
  5. 浅拷贝为什么不能隔离嵌套对象?

B. 小程序题

  1. 画出逻辑层、视图层和原生能力的关系。
  2. setData 为什么要控制体积和频率?
  3. navigateTo 与 redirectTo 如何选择?
  4. 页面返回后为什么常在 onShow 更新数据?
  5. 组件如何向父级传值?如何避免组件与业务耦合?

C. 网络与安全题

  1. 画出登录流程,哪些步骤必须在服务端?
  2. 10 个请求同时 401 时怎样避免刷新风暴?
  3. 为什么客户端禁用按钮不能保证不重复下单?
  4. 哪些错误可以重试?为什么要指数退避和抖动?
  5. 本地存储 token 有哪些风险和清理策略?

D. 性能与项目题

  1. 首页慢,你如何定位?
  2. 一万个列表项如何渲染?动态高度有什么难点?
  3. 讲一个你做过的技术权衡,而不是只讲结果。
  4. 线上某版本白屏,第一小时怎么处理?
  5. 你的项目如果用户量增长十倍,先出现什么瓶颈?

自测答案要点

  1. var 函数作用域且存在声明提升;let/const 块级作用域,有暂时性死区;const 约束绑定,不等于对象不可变。
  2. 二进制浮点无法精确表示部分十进制小数;金额用整数最小单位或可靠十进制方案。
  3. 函数保留词法环境;未清理监听/计时器并引用大对象可造成长期保留。
  4. 未处理的 reject 沿链传播;finally 无论成功失败执行,通常用于清理,若其抛错会改变链结果。
  5. 浅拷贝只复制第一层引用;嵌套对象仍共享。
  6. 逻辑层维护状态和业务,视图层渲染;双方通过受控机制通信,平台提供原生能力。
  7. 序列化、跨层通信与渲染均有成本。
  8. 是否需要保留当前页并返回,是关键判断。
  9. 返回时页面可能只是隐藏后重新显示,不会再次触发 onLoad。
  10. 属性输入、triggerEvent 输出;UI 组件不直接依赖业务 API。
  11. code 交换、密钥、业务用户绑定、可信鉴权必须在服务端。
  12. 单例刷新 Promise,其余请求等待;成功后安全重放,失败统一退出。
  13. 请求可被伪造或重放;需服务端幂等键和唯一约束。
  14. 临时网络错误、部分 5xx/429;退避避免持续压垮服务,抖动避免客户端同时重试。
  15. 客户端存储可被分析;控制有效期、最小权限、退出清理,敏感判断仍在服务端。
  16. 先拆启动、网络、逻辑、渲染阶段,记录基线,再针对瓶颈改并复测。
  17. 虚拟列表;动态高度需要测量、位置缓存和滚动锚定。
  18. 必须包含约束、候选方案、选择依据、代价和结果。
  19. 确认范围、止损、日志与版本定位、回滚或关开关、最小复现、修复与复盘。
  20. 根据架构分析接口容量、缓存一致性、包体与首屏、日志成本、热点数据等,不可空泛回答。

16. 面试前最终清单

简历上的每个项目都能回答

  • 为什么选这个技术栈?
  • 最难的问题是什么?根因如何确认?
  • 还比较过哪些方案?为什么没选?
  • 优化前后如何测量?数据是否可信?
  • 出过什么故障?如何止损与复盘?
  • 如果重做,会改什么?

必须能现场写

  • 防抖、节流、深层路径更新思路。
  • Promise 并发控制与错误处理。
  • 请求封装、token 刷新锁。
  • 列表去重、分组、树遍历、LRU。
  • 购物车总价与全选派生逻辑。

面试当天

  1. 先确认题意与边界,不急着编码。
  2. 先给可行方案,再逐步优化。
  3. 不会时说已知事实、推导过程和验证方法,不编 API。
  4. 代码写完主动测空数组、空值、重复值、并发和失败。
  5. 项目回答一定包含个人贡献,不把团队成果全部说成自己完成。
最后的记忆锚点:基础讲机制,API 讲边界,项目讲权衡,性能讲数据,安全讲服务端可信,故障讲止损与复盘。