微信小程序前端面试全栈教程
微信小程序开发与前端面试知识的系统教程
学习目标:从 JavaScript 基础出发,掌握微信小程序原生开发、工程化、性能优化、安全、测试与上线,并能在面试中清楚解释原理、权衡和项目成果。
默认技术栈:原生微信小程序 + JavaScript/TypeScript。框架对比会涉及 Taro、uni-app。平台能力会更新,API 参数与限制以上线时的微信开放文档为准。
目录
- 面试能力地图与学习方法
- 前端基础:HTML、CSS、JavaScript、TypeScript
- 小程序架构、目录与配置
- WXML、WXSS、事件与渲染
- 生命周期、路由与组件化
- 状态管理、通信与数据流
- 网络、登录、鉴权与安全
- 缓存、文件、媒体与常用能力
- 工程化、跨端框架与质量保障
- 性能优化与故障诊断
- 项目实战:电商小程序
- 高频面试题与标准答法
- 手写题与编码题
- 八周学习计划与面试冲刺
- 自测卷与答案
1. 面试能力地图与学习方法
1.1 面试官真正考什么
| 能力 | 初级要求 | 中高级要求 |
|---|---|---|
| 语言基础 | 会写 ES6、异步、数组操作 | 解释闭包、原型、事件循环、内存与边界 |
| 小程序 API | 会查文档并调用 | 理解双线程、通信成本、兼容与降级 |
| 页面开发 | 能还原 UI | 组件抽象、状态设计、可维护性 |
| 网络与登录 | 会请求接口 | 讲清 code、服务端 session、自定义 token、安全边界 |
| 性能 | 会压图、懒加载 | 能定位首屏、setData、包体与长列表问题 |
| 工程质量 | 会 Git、基本调试 | TypeScript、规范、测试、监控、CI/CD |
| 项目表达 | 说做了哪些页面 | 讲问题、约束、方案、权衡、数据和复盘 |
核心方法:每个知识点都练习四层表达:它是什么 → 为什么需要 → 如何实现 → 有什么代价。
1.2 一道题的高分回答结构
以“如何优化 setData”为例:
- 先下定义:
setData把逻辑层数据同步到视图层并触发渲染。 - 讲原理:逻辑层与视图层之间存在序列化、通信和渲染成本。
- 给方案:只更新必要路径、合并更新、避免高频更新大对象、控制数据体积。
- 讲验证:用性能面板比较通信次数、耗时、首屏时间和内存。
- 补边界:不能为了少调用而把所有数据长期堆在页面
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
})
原则:
- 只同步视图真正依赖的数据。
- 使用数据路径更新局部字段,避免每次传整个数组或大对象。
- 合并同一业务时刻的更新。
- 高频输入、滚动等使用节流,并判断是否必须进入视图层。
- 非视图数据可放在页面实例普通字段,不必全部塞进
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 页面通信方式
- URL 参数:少量、可序列化、非敏感信息。
eventChannel:打开新页面时传较复杂的一次性数据或回传事件。- 全局 store:跨多个页面共享、生命周期较长的状态。
- 本地缓存:需要跨启动保存的数据。
- 服务端:需要多端一致、可信和可恢复的数据。
不要把所有东西放到 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
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 安全清单
- 仅使用 HTTPS,配置合法域名。
- 客户端输入全部不可信,服务端校验类型、范围、权限。
- 日志不得记录 token、手机号、身份证、支付信息。
- 防止越权:资源查询必须带当前用户权限条件。
- 防重放与重复提交:nonce、时间窗或幂等键,按风险选择。
- 展示富文本时采用可信白名单策略,避免恶意链接和内容。
- 本地缓存不存长期高敏感秘密;退出登录要清理相关数据。
- 隐私能力按平台规范告知并在必要场景请求授权,不提前索取。
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 测试金字塔
- 单元测试:纯函数、价格计算、数据转换、状态逻辑。
- 组件测试:输入属性、事件输出、条件渲染。
- 集成测试:登录、请求、缓存、页面协作。
- 端到端测试:下单、支付前后、异常恢复等关键链路。
测试重点放在高风险业务,而不是追求虚假的 100% 覆盖率。
9.5 监控与可观测性
记录:JS 错误、Promise 未处理错误、接口失败率与耗时、页面打开与首屏、关键业务漏斗、客户端版本、基础库版本、网络类型、请求 ID。日志必须采样、限流、脱敏。
10. 性能优化与故障诊断
10.1 性能优化流程
先测量,再定位,再改动,再回归。不要把“加缓存、压图片”当万能答案。
flowchart LR
A["定义指标"] --> B["采集基线"] --> C["定位瓶颈"] --> D["最小改动"] --> E["对照验证"] --> F["监控回归"]
10.2 启动与首屏
优化方向:
- 控制主包体积,将非首屏页面与资源分包。
- 首屏只请求必要数据,其余延迟或并行加载。
- 避免应用启动时执行大量同步计算。
- 骨架屏应匹配真实布局,减少感知等待。
- 合理使用分包预下载,但要权衡流量与命中率。
- 图片使用 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. 基础题
let、const、var的作用域和提升有何区别?- 为什么
0.1 + 0.2 !== 0.3?金额应怎样处理? - 解释闭包并给一个内存泄漏风险案例。
- Promise 链中的错误如何传播?
finally有什么语义? - 浅拷贝为什么不能隔离嵌套对象?
B. 小程序题
- 画出逻辑层、视图层和原生能力的关系。
- setData 为什么要控制体积和频率?
navigateTo与redirectTo如何选择?- 页面返回后为什么常在
onShow更新数据? - 组件如何向父级传值?如何避免组件与业务耦合?
C. 网络与安全题
- 画出登录流程,哪些步骤必须在服务端?
- 10 个请求同时 401 时怎样避免刷新风暴?
- 为什么客户端禁用按钮不能保证不重复下单?
- 哪些错误可以重试?为什么要指数退避和抖动?
- 本地存储 token 有哪些风险和清理策略?
D. 性能与项目题
- 首页慢,你如何定位?
- 一万个列表项如何渲染?动态高度有什么难点?
- 讲一个你做过的技术权衡,而不是只讲结果。
- 线上某版本白屏,第一小时怎么处理?
- 你的项目如果用户量增长十倍,先出现什么瓶颈?
自测答案要点
var函数作用域且存在声明提升;let/const块级作用域,有暂时性死区;const约束绑定,不等于对象不可变。- 二进制浮点无法精确表示部分十进制小数;金额用整数最小单位或可靠十进制方案。
- 函数保留词法环境;未清理监听/计时器并引用大对象可造成长期保留。
- 未处理的 reject 沿链传播;
finally无论成功失败执行,通常用于清理,若其抛错会改变链结果。 - 浅拷贝只复制第一层引用;嵌套对象仍共享。
- 逻辑层维护状态和业务,视图层渲染;双方通过受控机制通信,平台提供原生能力。
- 序列化、跨层通信与渲染均有成本。
- 是否需要保留当前页并返回,是关键判断。
- 返回时页面可能只是隐藏后重新显示,不会再次触发
onLoad。 - 属性输入、
triggerEvent输出;UI 组件不直接依赖业务 API。 - code 交换、密钥、业务用户绑定、可信鉴权必须在服务端。
- 单例刷新 Promise,其余请求等待;成功后安全重放,失败统一退出。
- 请求可被伪造或重放;需服务端幂等键和唯一约束。
- 临时网络错误、部分 5xx/429;退避避免持续压垮服务,抖动避免客户端同时重试。
- 客户端存储可被分析;控制有效期、最小权限、退出清理,敏感判断仍在服务端。
- 先拆启动、网络、逻辑、渲染阶段,记录基线,再针对瓶颈改并复测。
- 虚拟列表;动态高度需要测量、位置缓存和滚动锚定。
- 必须包含约束、候选方案、选择依据、代价和结果。
- 确认范围、止损、日志与版本定位、回滚或关开关、最小复现、修复与复盘。
- 根据架构分析接口容量、缓存一致性、包体与首屏、日志成本、热点数据等,不可空泛回答。
16. 面试前最终清单
简历上的每个项目都能回答
- 为什么选这个技术栈?
- 最难的问题是什么?根因如何确认?
- 还比较过哪些方案?为什么没选?
- 优化前后如何测量?数据是否可信?
- 出过什么故障?如何止损与复盘?
- 如果重做,会改什么?
必须能现场写
- 防抖、节流、深层路径更新思路。
- Promise 并发控制与错误处理。
- 请求封装、token 刷新锁。
- 列表去重、分组、树遍历、LRU。
- 购物车总价与全选派生逻辑。
面试当天
- 先确认题意与边界,不急着编码。
- 先给可行方案,再逐步优化。
- 不会时说已知事实、推导过程和验证方法,不编 API。
- 代码写完主动测空数组、空值、重复值、并发和失败。
- 项目回答一定包含个人贡献,不把团队成果全部说成自己完成。