小程序 面试指南
面试者视角回答
小程序是一种不需要下载即可使用的应用,用户扫一扫或搜一下即可打开。当前国内主流小程序平台包括微信、支付宝、百度、抖音等。本面试指南以微信小程序为主,其他平台原理类似。
核心概念
小程序 vs H5 vs 原生 App
| 特性 | 小程序 | H5 | 原生 App |
|---|---|---|---|
| 加载方式 | 首次下载,缓存运行 | 实时加载 | 安装运行 |
| 体验 | 接近原生 | 一般 | 最佳 |
| 开发成本 | 中等 | 低 | 高 |
| 更新 | 增量更新 | 实时 | 需审核更新 |
| 入口 | 多入口 | 浏览器 | 应用商店 |
| 分享 | 方便 | 一般 | 方便 |
双线程模型
┌─────────────────┐ ┌─────────────────┐
│ 视图层 │ │ 逻辑层 │
│ (WebView) │ │ (V8 引擎) │
│ │ │ │
│ 渲染 WXML │ │ 运行 JS │
│ 渲染 WXSS │ │ 数据处理 │
│ 交互事件 │ │ API 调用 │
└────────┬────────┘ └────────┬────────┘
│ │
│ Native 微信客户端 │
│ (性能体验保障) │
└──────────────┬───────────┘
│
appService为什么双线程?
- 隔离性:避免开发者直接操作 DOM,保证性能
- 安全性:防止 XSS 攻击
- Native 桥接:通过微信客户端转发消息
项目结构
├── project.config.json # 项目配置
├── sitemap.json # SEO 配置
├── app.js # 全局入口
├── app.json # 全局配置
├── app.wxss # 全局样式
├── pages/
│ └── index/
│ ├── index.js # 页面逻辑
│ ├── index.json # 页面配置
│ ├── index.wxml # 页面结构
│ └── index.wxss # 页面样式
└── components/ # 组件目录配置文件
app.json
json
{
"pages": ["pages/index/index", "pages/logs/logs"],
"window": {
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTitleText": "微信",
"navigationBarTextStyle": "black",
"backgroundColor": "#eeeeee",
"enablePullDownRefresh": false
},
"tabBar": {
"color": "#999",
"selectedColor": "#1890ff",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页",
"iconPath": "images/home.png",
"selectedIconPath": "images/home-active.png"
}
]
},
"style": "v2",
"sitemapLocation": "sitemap.json"
}page.json
json
{
"navigationBarTitleText": "页面标题",
"enablePullDownRefresh": true,
"navigationBarBackgroundColor": "#1890ff",
"usingComponents": {
"my-component": "/components/my-component"
}
}生命周期
应用生命周期
javascript
// app.js
App({
onLaunch(options) {
console.log("小程序初始化", options);
},
onShow(options) {
console.log("小程序显示", options);
},
onHide() {
console.log("小程序隐藏");
},
onError(error) {
console.error("小程序错误", error);
},
onPageNotFound(res) {
console.log("页面不存在", res);
},
onUnhandledRejection() {},
onThemeChange() {},
});页面生命周期
javascript
// pages/index/index.js
Page({
data: {
name: "初始数据",
},
onLoad(options) {
console.log("页面加载", options);
},
onShow() {
console.log("页面显示");
},
onReady() {
console.log("页面首次渲染完成");
},
onHide() {
console.log("页面隐藏");
},
onUnload() {
console.log("页面卸载");
},
onPullDownRefresh() {
console.log("下拉刷新");
wx.stopPullDownRefresh();
},
onReachBottom() {
console.log("上拉触底");
},
onPageScroll(e) {
console.log("页面滚动", e.scrollTop);
},
onShareAppMessage(e) {
return {
title: "分享标题",
path: "/pages/index/index",
imageUrl: "/images/share.png",
};
},
});组件化开发
组件定义
javascript
// components/my-component.js
Component({
properties: {
title: {
type: String,
value: "默认标题",
},
value: {
type: Number,
value: 0,
},
},
data: {
internalData: "内部数据",
},
methods: {
handleClick() {
this.setData({
internalData: "更新后的数据",
});
this.triggerEvent("myevent", { data: "传递给父组件的数据" });
},
},
lifetimes: {
created() {
console.log("组件创建");
},
attached() {
console.log("组件挂载");
},
ready() {
console.log("组件渲染完成");
},
moved() {
console.log("组件移动");
},
detached() {
console.log("组件卸载");
},
},
});组件使用
xml
<!-- page.wxml -->
<my-component
title="自定义标题"
value="{{parentValue}}"
bind:myevent="onMyEvent"
/>javascript
// page.js
Page({
data: {
parentValue: 100,
},
onMyEvent(e) {
console.log("收到组件事件", e.detail);
},
});数据绑定与渲染
WXML 模板语法
xml
<!-- 文本渲染 -->
<text>{{name}}</text>
<!-- 条件渲染 -->
<view wx:if="{{condition}}">显示</view>
<view wx:elif="{{condition2}}">条件2</view>
<view wx:else>隐藏</view>
<!-- 列表渲染 -->
<view wx:for="{{list}}" wx:key="id">
{{index}}: {{item.name}}
</view>
<!-- 模板引用 -->
<template name="myTemplate">
<view>{{name}}</view>
</template>
<template is="myTemplate" data="{{...item}}" />WXS 脚本
WXS(WeiXin Script)是小程序的一套脚本语言,用于增强 WXML 的数据处理能力。
xml
<!-- wxml -->
<wxs module="utils">
module.exports = {
formatPrice: function(price) {
return '¥' + price.toFixed(2);
},
truncate: function(str, len) {
return str.length > len ? str.slice(0, len) + '...' : str;
}
};
</wxs>
<text>{{utils.formatPrice(price)}}</text>API 与交互
微信 API 分类
| 分类 | 说明 | 示例 |
|---|---|---|
| 事件监听 API | 以 on 开头 | wx.onWindowResize |
| 同步 API | 以 Sync 结尾 | wx.getStorageSync |
| 异步 API | 回调/Promise | wx.request |
| 云开发 API | 云函数/云数据库 | wx.cloud.callFunction |
常用 API 示例
javascript
// 显示加载提示
wx.showLoading({ title: "加载中..." });
// 获取用户信息
wx.getUserProfile({
desc: "用于完善用户资料",
success: (res) => {
this.setData({ userInfo: res.userInfo });
},
});
// 本地存储
wx.setStorageSync("key", "value");
const value = wx.getStorageSync("key");
// 路由跳转
wx.navigateTo({ url: "/pages/detail/detail?id=1" });
wx.redirectTo({ url: "/pages/detail/detail" });
wx.reLaunch({ url: "/pages/index/index" });
wx.switchTab({ url: "/pages/home/home" });
// 页面传参
// 接收:onLoad(options) { console.log(options.id); }请求封装
javascript
const BASE_URL = "https://api.example.com";
function request(options) {
return new Promise((resolve, reject) => {
const token = wx.getStorageSync("token");
wx.request({
url: BASE_URL + options.url,
method: options.method || "GET",
data: options.data,
header: {
"Content-Type": "application/json",
Authorization: token ? `Bearer ${token}` : "",
},
success: (res) => {
if (res.statusCode === 200) {
resolve(res.data);
} else if (res.statusCode === 401) {
wx.removeStorageSync("token");
wx.reLaunch({ url: "/pages/login/login" });
} else {
reject(res.data);
}
},
fail: reject,
});
});
}
module.exports = { request };性能优化
1. 首屏加载优化
javascript
// 分包加载
// app.json
{
"subPackages": [
{
"root": "packageA",
"pages": [
"pages/cat/cat",
"pages/dog/dog"
]
}
]
}
// 预加载分包
wx.preloadSubpackage({
root: 'packageA',
name: 'game'
});2. 图片优化
xml
<!-- 使用 CDN 图片 -->
<image
src="{{item.imageUrl}}"
mode="aspectFill"
lazy-load="{{true}}"
show-menu-by-longpress="{{true}}"
/>
<!-- 本地图片放不同目录 -->
<!-- images/ 放首页图片 -->
<!-- static/ 放子包图片 -->3. setData 优化
javascript
// 不好:频繁 setData 大数据
this.setData({
list: this.data.list.concat(newItems),
});
// 好:使用路径减少数据量
this.setData({
"list[0].name": "新名字",
});
// 更好:使用数据索引
const index = 0;
const key = `list[${index}].name`;
this.setData({ [key]: "新名字" });
// 批量更新
this.setData({
a: 1,
b: 2,
// 不要分散多次调用
});4. 长列表优化
javascript
// 使用虚拟列表
// 只渲染可视区域内的元素
const ITEM_HEIGHT = 100;
const VISIBLE_COUNT = 10;
Page({
data: {
visibleList: [],
startIndex: 0,
},
onPageScroll(e) {
const startIndex = Math.floor(e.scrollTop / ITEM_HEIGHT);
const visibleList = this.data.list.slice(startIndex, startIndex + VISIBLE_COUNT);
this.setData({
startIndex,
visibleList,
});
},
});面试题精选
面试题 1:小程序的渲染原理是什么?
参考答案:
小程序采用双线程模型,这与传统 H5 有本质区别:
双线程架构:
视图层(WebView)
- 负责渲染 WXML 和 WXSS
- 处理用户交互事件
- 每个页面有一个 WebView
逻辑层(V8/JavaScriptCore)
- 运行小程序逻辑代码
- 处理业务逻辑和数据
- 无法直接操作 DOM
Native 微信客户端
- 充当桥接层
- 转发视图层和逻辑层消息
- 提供微信原生能力
消息传递流程:
视图层事件 → Native桥接 → 逻辑层处理 → setData → Native桥接 → 视图层更新为什么不用单线程?
单线程会导致性能问题:
- JS 执行会阻塞渲染
- 无法保证用户体验
- Native 能力无法高效调用
面试题 2:小程序和 H5 的区别是什么?
参考答案:
| 维度 | 小程序 | H5 |
|---|---|---|
| 运行环境 | 微信客户端 | 浏览器 |
| 渲染方式 | 双线程,WebView + JS | 单线程,浏览器渲染 |
| 系统权限 | 丰富(支付、分享等) | 有限 |
| 性能 | 接近原生 | 依赖网络 |
| 更新 | 增量更新 | 实时更新 |
| 入口 | 多入口(扫码、搜索等) | URL/书签 |
| 开发限制 | 受限于微信规则 | 自由 |
| 包大小 | 单包 2MB,总包 12MB | 无限制 |
性能差异原因:
- 预加载:小程序启动时预加载 WebView
- 缓存:JS 代码和静态资源可缓存
- 双线程:逻辑执行不阻塞渲染
- Native 优化:微信客户端做了大量优化
面试题 3:小程序如何实现分包加载?
参考答案:
分包是小程序性能优化的重要手段:
分包配置(app.json):
json
{
"pages": ["pages/index/index", "pages/home/home"],
"subPackages": [
{
"root": "packageA",
"name": "packageA",
"pages": ["pages/cat/cat", "pages/dog/dog"]
},
{
"root": "packageB",
"pages": ["pages/detail/detail"]
}
]
}分包结构:
├── pages/ # 主包
├── packageA/ # 分包 A
│ ├── pages/cat/
│ └── pages/dog/
└── packageB/ # 分包 B
└── pages/detail/分包加载触发:
javascript
// 主动触发分包下载
wx.loadSubPackage({
root: "packageA",
success: () => {
console.log("分包加载成功");
},
});
// 进入分包页面时自动下载
wx.navigateTo({
url: "packageA/pages/cat/cat",
});分包预加载:
javascript
// 预下载可能访问的分包
wx.preloadSubpackage({
root: "packageA",
name: "game",
});面试题 4:小程序中如何获取用户信息?
参考答案:
新版获取方式(2021年后):
javascript
// 必须先通过 button 组件让用户主动授权
// <button open-type="getUserProfile" bindtap="getUserInfo">获取头像</button>
getUserInfo() {
wx.getUserProfile({
desc: '用于完善用户资料',
success: (res) => {
this.setData({
userInfo: res.userInfo,
hasUserInfo: true
});
}
});
}旧版 API(已废弃):
javascript
// wx.getUserInfo 直接获取用户信息
// 2021 年后不再返回真实数据
wx.getUserInfo({
success: (res) => {
// 返回的 userInfo 是默认头像和昵称
console.log(res.userInfo);
},
});用户登录流程:
javascript
async function login() {
// 1. 获取 code
const { code } = await wx.login();
// 2. 发送到服务器换取 session
const res = await request({
url: "/api/login",
method: "POST",
data: { code },
});
// 3. 保存 token
wx.setStorageSync("token", res.token);
}面试题 5:小程序如何实现页面间数据传递?
参考答案:
方式一:URL 参数
javascript
// A 页面跳转时携带参数
wx.navigateTo({
url: '/pages/detail/detail?id=123&name=test'
});
// B 页面 onLoad 中接收
onLoad(options) {
console.log(options.id); // '123'
console.log(options.name); // 'test'
}方式二:事件总线
javascript
// utils/eventBus.js
class EventBus {
constructor() {
this.events = {};
}
on(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
}
emit(event, data) {
const callbacks = this.events[event] || [];
callbacks.forEach((cb) => cb(data));
}
off(event, callback) {
const callbacks = this.events[event] || [];
const index = callbacks.indexOf(callback);
if (index > -1) callbacks.splice(index, 1);
}
}
module.exports = new EventBus();
// A 页面
const eventBus = require("/utils/eventBus");
eventBus.on("dataUpdate", (data) => {
console.log("收到数据", data);
});
// B 页面
eventBus.emit("dataUpdate", { value: "test" });方式三:页面栈
javascript
// A 页面
const pages = getCurrentPages();
const prevPage = pages[pages.length - 2];
prevPage.setData({
returnedData: "来自 B 页面",
});
// 适用于 navigateBack
wx.navigateBack();面试题 6:小程序如何实现下拉刷新?
参考答案:
配置方式:
json
// page.json
{
"enablePullDownRefresh": true,
"navigationBarTitleText": "刷新示例"
}页面逻辑:
javascript
Page({
data: {
list: [],
page: 1,
pageSize: 10,
hasMore: true,
},
onLoad() {
this.loadData();
},
onPullDownRefresh() {
// 重置数据
this.setData({
list: [],
page: 1,
hasMore: true,
});
this.loadData().finally(() => {
wx.stopPullDownRefresh();
});
},
onReachBottom() {
if (!this.data.hasMore) return;
this.setData({ page: this.data.page + 1 });
this.loadData();
},
async loadData() {
const res = await request({
url: "/api/list",
data: {
page: this.data.page,
pageSize: this.data.pageSize,
},
});
this.setData({
list: this.data.page === 1 ? res.list : this.data.list.concat(res.list),
hasMore: res.list.length >= this.data.pageSize,
});
},
});面试题 7:小程序和 WebView 如何交互?
参考答案:
小程序内嵌 H5 页面需要使用 WebView:
xml
<!-- 小程序 wxml -->
<web-view src="{{h5Url}}" bind:message="onMessage"></web-view>javascript
// 小程序 JS
Page({
data: {
h5Url: "https://m.example.com/page",
},
onMessage(e) {
console.log("收到 H5 发来的消息", e.detail);
},
// 向 H5 发送消息
postMessage() {
const webview = this.selectComponent("#webview");
webview.postMessage({ data: "来自小程序" });
},
});H5 页面引用 JSSDK:
html
<script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>javascript
// H5 JS
wx.miniProgram.postMessage({
data: { from: "h5", value: "test" },
});
// 接收小程序消息
wx.miniProgram.onMessage((data) => {
console.log("收到小程序消息", data);
});
// 跳转小程序页面
wx.miniProgram.navigateTo({ url: "/pages/index/index" });
wx.miniProgram.switchTab({ url: "/pages/home/home" });
wx.miniProgram.navigateBack();面试题 8:小程序性能优化有哪些手段?
参考答案:
1. 代码层面
javascript
// 避免频繁 setData
// 合并多次 setData 为一次
this.setData({ a: 1 });
this.setData({ b: 2 });
// 改为
this.setData({ a: 1, b: 2 });
// 使用数据路径更新深层数据
this.setData({ "obj.key": "value" });
// 合理使用 component 替代 page2. 图片优化
xml
<image
src="{{img}}"
mode="aspectFill"
lazy-load="{{true}}"
/>
<!-- 合适尺寸 -->
<!-- 压缩图片 -->
<!-- 使用 CDN -->3. 分包加载
json
{
"subPackages": [...]
}4. 请求优化
javascript
// 合并请求
// 使用本地缓存
// 请求加 loading 避免重复点击5. 渲染优化
xml
<!-- 合理使用 wx:if vs hidden -->
<view wx:if="{{条件}}">偶尔显示</view>
<view hidden="{{!条件}}">频繁切换</view>
<!-- 列表渲染加 key -->
<view wx:for="{{list}}" wx:key="id">6. 启动优化
json
// app.json
{
"preloadRule": {
"pages/index/index": {
"network": "all",
"packages": ["packageA"]
}
}
}面试题 9:小程序如何实现分享功能?
参考答案:
方式一:onShareAppMessage
javascript
Page({
onShareAppMessage(res) {
if (res.from === "button") {
console.log("来自页面内转发按钮");
}
return {
title: "自定义分享标题",
path: "/pages/index/index?id=" + this.data.id,
imageUrl: "/images/share.png",
};
},
});方式二:button 组件
xml
<button open-type="share">转发</button>方式三:合成海报分享
javascript
async function shareToPoster() {
const posterConfig = {
width: 300,
height: 400,
backgroundColor: "#fff",
};
const canvas = wx.createCanvasContext("posterCanvas");
// 绘制背景
canvas.setFillStyle("#fff");
canvas.fillRect(0, 0, 300, 400);
// 绘制二维码
const qrcode = await getQRCode();
canvas.drawImage(qrcode, 100, 250, 100, 100);
// 绘制文字
canvas.setFontSize(16);
canvas.fillText("长按识别", 100, 380);
canvas.draw();
// 导出图片
const poster = await wx.canvasToTempFilePath({
canvasId: "posterCanvas",
});
// 保存到相册
await wx.saveImageToPhotosAlbum({
filePath: poster.tempFilePath,
});
}面试题 10:小程序登录流程是怎样的?
参考答案:
完整登录流程:
用户点击登录
↓
wx.getUserProfile() 获取用户授权
↓
wx.login() 获取 code
↓
发送 code 到开发者服务器
↓
服务器调用微信 API 用 code 换取 session_key 和 openid
↓
服务器生成自定义登录态 token 返回给小程序
↓
小程序保存 token 到 Storage
↓
后续请求携带 token 验证身份代码实现:
javascript
// 1. 小程序端
async function login() {
// 获取用户授权
const profileRes = await wx.getUserProfile({
desc: "用于完善用户资料",
});
// 获取 code
const loginRes = await wx.login();
// 发送到服务器
const serverRes = await request({
url: "/api/login",
method: "POST",
data: {
code: loginRes.code,
encryptedData: profileRes.encryptedData,
iv: profileRes.iv,
},
});
// 保存登录态
wx.setStorageSync("token", serverRes.token);
wx.setStorageSync("userInfo", serverRes.userInfo);
}
// 2. 服务器端(伪代码)
async function login(code, encryptedData, iv) {
// 用 code 换 session
const session = await wx.codeToSession(code);
// 解密用户数据(可选)
const userInfo = decrypt(encryptedData, iv, session.session_key);
// 生成自定义 token
const token = generateToken(session.openid);
return {
token,
userInfo,
};
}面试题 11:小程序如何实现支付功能?
参考答案:
支付流程:
1. 用户下单 → 服务器创建订单
2. 服务器调用微信支付统一下单 API
3. 服务器返回 prepay_id
4. 小程序调起支付
5. 用户输入密码支付
6. 微信回调服务器
7. 服务器验证并处理
8. 小程序查询订单状态小程序端代码:
javascript
async function requestPayment(orderId) {
// 1. 向服务器获取支付参数
const payRes = await request({
url: "/api/createOrder",
method: "POST",
data: { orderId },
});
// 2. 调起微信支付
await wx.requestPayment({
timeStamp: payRes.timeStamp,
nonceStr: payRes.nonceStr,
package: payRes.package,
signType: payRes.signType,
paySign: payRes.paySign,
});
// 3. 支付成功
wx.showToast({ title: "支付成功" });
}面试题 12:小程序如何处理登录态过期?
参考答案:
检测登录态:
javascript
// request 封装
function request(options) {
return new Promise((resolve, reject) => {
wx.request({
...options,
success: (res) => {
if (res.statusCode === 401) {
// token 过期
handleTokenExpire();
reject("登录已过期");
} else {
resolve(res.data);
}
},
fail: reject,
});
});
}
function handleTokenExpire() {
// 清除本地登录态
wx.removeStorageSync("token");
// 跳转到登录页
wx.reLaunch({ url: "/pages/login/login" });
}自动续期:
javascript
// 服务器返回 token 时附带过期时间
// 前端定时检测,快过期时自动刷新
Page({
data: {
tokenExpireTime: 0,
},
onShow() {
this.checkTokenExpire();
},
checkTokenExpire() {
const tokenExpireTime = wx.getStorageSync("tokenExpireTime");
const now = Date.now();
if (tokenExpireTime - now < 30 * 60 * 1000) {
// 30 分钟内过期,刷新 token
this.refreshToken();
}
},
async refreshToken() {
try {
const res = await request({ url: "/api/refreshToken" });
wx.setStorageSync("token", res.token);
wx.setStorageSync("tokenExpireTime", res.expireTime);
} catch (e) {
// 刷新失败,跳转登录
wx.reLaunch({ url: "/pages/login/login" });
}
},
});