Skip to content

小程序 面试指南

面试者视角回答

小程序是一种不需要下载即可使用的应用,用户扫一扫或搜一下即可打开。当前国内主流小程序平台包括微信、支付宝、百度、抖音等。本面试指南以微信小程序为主,其他平台原理类似。


核心概念

小程序 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 分类

分类说明示例
事件监听 APIon 开头wx.onWindowResize
同步 APISync 结尾wx.getStorageSync
异步 API回调/Promisewx.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 有本质区别:

双线程架构:

  1. 视图层(WebView)

    • 负责渲染 WXML 和 WXSS
    • 处理用户交互事件
    • 每个页面有一个 WebView
  2. 逻辑层(V8/JavaScriptCore)

    • 运行小程序逻辑代码
    • 处理业务逻辑和数据
    • 无法直接操作 DOM
  3. Native 微信客户端

    • 充当桥接层
    • 转发视图层和逻辑层消息
    • 提供微信原生能力

消息传递流程:

视图层事件 → Native桥接 → 逻辑层处理 → setData → Native桥接 → 视图层更新

为什么不用单线程?

单线程会导致性能问题:

  • JS 执行会阻塞渲染
  • 无法保证用户体验
  • Native 能力无法高效调用

面试题 2:小程序和 H5 的区别是什么?

参考答案:

维度小程序H5
运行环境微信客户端浏览器
渲染方式双线程,WebView + JS单线程,浏览器渲染
系统权限丰富(支付、分享等)有限
性能接近原生依赖网络
更新增量更新实时更新
入口多入口(扫码、搜索等)URL/书签
开发限制受限于微信规则自由
包大小单包 2MB,总包 12MB无限制

性能差异原因:

  1. 预加载:小程序启动时预加载 WebView
  2. 缓存:JS 代码和静态资源可缓存
  3. 双线程:逻辑执行不阻塞渲染
  4. 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 替代 page

2. 图片优化

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" });
        }
    },
});