微信小程序 - 03 工程实践层与综合 Demo

本文目标

本文从真实团队项目角度,讲解微信小程序工程化实践,并设计一个覆盖入门、中级、高级知识点的综合 Demo。重点解决"能不能把项目长期维护好"的问题。

本章节知识图谱

基础能力: 页面/组件/API
工程结构: 分层目录
请求层: 统一封装
状态层: 用户/订单/配置
组件层: 通用组件/业务组件
错误处理: Loading/Empty/Error/Retry
缓存策略: 内存/本地/服务端
设计规范: 样式/交互/可复用
监控: 日志/埋点/异常
发布: CI/体验版/灰度/正式版
面试: 项目经验/工程治理

1. 工程化项目结构

是什么

工程化项目结构是把页面、组件、接口、状态、配置、工具、静态资源、测试和发布脚本按职责拆分,形成稳定边界。

为什么重要

小程序一旦进入多人协作和持续迭代,最大风险不是单个页面写不出来,而是功能越来越多后依赖混乱、重复封装、难以测试、难以排查问题。

底层原理

工程结构本质是依赖方向控制。页面可以依赖服务层和组件层,服务层可以依赖请求基础设施,基础设施不应反向依赖业务页面。

使用场景

  • 团队项目初始化。
  • 旧项目重构。
  • 面试项目工程化升级。
  • 多模块业务并行开发。

推荐结构

text 复制代码
miniprogram/
  app.js
  app.json
  app.wxss
  config/
    env.js
  constants/
    status.js
  services/
    request.js
    auth.service.js
    product.service.js
    order.service.js
  store/
    user.store.js
    cart.store.js
  components/
    base-button/
    empty-state/
    product-card/
    order-card/
  pages/
    home/
    product-detail/
    cart/
    order-list/
    order-detail/
    profile/
  packages/
    order/
    marketing/
  utils/
    format.js
    validator.js
    logger.js
  assets/
  tests/

常见错误

  • 页面直接调用 wx.request
  • 工具方法里混入业务逻辑。
  • 组件既请求接口又控制页面跳转。
  • 所有状态都放到 app.globalData

最佳实践

  • 页面层只做数据装配和交互控制。
  • 服务层只负责业务 API。
  • 请求层只负责协议、鉴权、错误处理。
  • 组件层通过属性和事件通信。
  • 全局状态只放真正跨页面共享的数据。

关联知识点

关联请求封装、状态管理、组件化、分包和架构设计。

面试考察方式

面试官常要求描述你负责项目的目录结构,并追问模块边界和依赖方向。

2. 请求层与错误处理

是什么

请求层负责统一管理接口调用、鉴权、错误处理、Loading、重试、日志和环境切换。

为什么重要

真实业务中接口失败不可避免。统一请求层能让错误处理一致,避免页面重复代码和遗漏安全逻辑。

底层原理

请求层对 wx.request 做 Promise 化包装,形成"请求前处理 -> 发起请求 -> HTTP 状态处理 -> 业务状态处理 -> 错误归一化 -> 日志上报"的流水线。

使用场景

  • 所有服务端接口调用。
  • 登录失效统一跳转。
  • 网络失败重试。
  • 接口耗时监控。

代码示例

js 复制代码
// config/env.js
const ENV = "dev";

const map = {
  dev: "https://dev-api.example.com",
  test: "https://test-api.example.com",
  prod: "https://api.example.com"
};

module.exports = {
  ENV,
  baseURL: map[ENV]
};
js 复制代码
// services/request.js
const { baseURL } = require("../config/env");

class ApiError extends Error {
  constructor(message, detail) {
    super(message);
    this.name = "ApiError";
    this.detail = detail;
  }
}

function request(options) {
  const startedAt = Date.now();
  const token = wx.getStorageSync("token");

  return new Promise((resolve, reject) => {
    wx.request({
      url: `${baseURL}${options.url}`,
      method: options.method || "GET",
      data: options.data || {},
      header: {
        Authorization: token ? `Bearer ${token}` : "",
        "content-type": "application/json",
        ...options.header
      },
      success(res) {
        const cost = Date.now() - startedAt;
        console.log("[api]", options.url, cost);

        if (res.statusCode === 401) {
          wx.removeStorageSync("token");
          wx.reLaunch({ url: "/pages/login/index" });
          reject(new ApiError("登录已失效", res));
          return;
        }

        if (res.statusCode < 200 || res.statusCode >= 300) {
          reject(new ApiError(`网络错误 ${res.statusCode}`, res));
          return;
        }

        const body = res.data || {};
        if (body.code !== 0) {
          reject(new ApiError(body.message || "业务处理失败", body));
          return;
        }

        resolve(body.data);
      },
      fail(err) {
        reject(new ApiError("网络不可用,请稍后重试", err));
      }
    });
  });
}

module.exports = {
  request,
  ApiError
};

常见错误

  • 只处理成功路径。
  • 所有错误都提示"系统错误"。
  • 401 在每个页面单独处理。
  • 没有记录接口耗时和失败上下文。

最佳实践

  • 错误类型归一化。
  • 登录失效统一处理。
  • 页面保留错误态和重试能力。
  • 关键接口记录耗时和 Trace ID。

关联知识点

关联登录、安全、监控、故障排查。

面试考察方式

常要求现场设计请求封装,并追问 Token 过期、接口重试、错误提示如何做。

3. 状态管理

是什么

状态管理用于维护跨页面共享数据,如用户信息、购物车、门店、权限、全局配置。

为什么重要

没有状态管理,页面之间会通过 globalData、缓存、路由参数互相传递数据,导致状态来源混乱。

底层原理

状态管理应遵循单一数据源和显式更新。页面订阅状态变化,状态更新后通知页面刷新局部视图。

使用场景

  • 用户信息跨页面共享。
  • 购物车数量同步。
  • 门店切换影响多个页面。
  • 权限和配置动态变化。

代码示例

js 复制代码
// store/user.store.js
const listeners = [];

const state = {
  userInfo: null,
  token: wx.getStorageSync("token") || ""
};

function notify() {
  listeners.forEach(listener => listener({ ...state }));
}

function subscribe(listener) {
  listeners.push(listener);
  listener({ ...state });
  return () => {
    const index = listeners.indexOf(listener);
    if (index >= 0) listeners.splice(index, 1);
  };
}

function setUserInfo(userInfo) {
  state.userInfo = userInfo;
  notify();
}

function setToken(token) {
  state.token = token;
  wx.setStorageSync("token", token);
  notify();
}

module.exports = {
  subscribe,
  setUserInfo,
  setToken
};

页面订阅:

js 复制代码
const userStore = require("../../store/user.store");

Page({
  data: {
    userInfo: null
  },

  onLoad() {
    this.unsubscribeUser = userStore.subscribe(state => {
      this.setData({
        userInfo: state.userInfo
      });
    });
  },

  onUnload() {
    if (this.unsubscribeUser) {
      this.unsubscribeUser();
    }
  }
});

常见错误

  • 所有共享数据都放 globalData
  • 页面卸载后没有取消订阅。
  • 状态更新入口分散。
  • 缓存和内存状态不一致。

最佳实践

  • 明确状态归属。
  • 更新状态必须通过方法。
  • 页面卸载取消订阅。
  • 本地缓存只作为恢复状态的来源,不作为唯一真相。

关联知识点

关联架构设计、组件通信、缓存策略。

面试考察方式

常问小程序没有 Vuex/Redux 时如何管理跨页面状态。

4. 分包与包体治理

是什么

分包是把小程序代码按业务模块拆成主包和子包。主包负责启动和公共能力,子包按需加载。

为什么重要

小程序有包体限制。包体过大会影响启动速度和审核发布,也会让低端设备首屏体验变差。

底层原理

启动时先加载主包,进入分包页面时再下载对应分包。公共资源放主包会被多个分包复用,但放太多会拖慢首屏。

使用场景

  • 订单、营销、会员、设置等低频模块。
  • 大型小程序包体优化。
  • 多业务模块独立迭代。

代码示例

json 复制代码
{
  "pages": [
    "pages/home/index",
    "pages/profile/index"
  ],
  "subpackages": [
    {
      "root": "packages/order",
      "pages": [
        "list/index",
        "detail/index"
      ]
    },
    {
      "root": "packages/marketing",
      "pages": [
        "coupon/index",
        "activity/index"
      ]
    }
  ],
  "preloadRule": {
    "pages/home/index": {
      "network": "wifi",
      "packages": ["packages/order"]
    }
  }
}

常见错误

  • 主包放太多业务页面。
  • 分包之间互相引用页面代码。
  • 公共组件重复打包。
  • 分包预加载不看业务路径和网络条件。

最佳实践

  • 首页、登录、Tab 页放主包。
  • 低频重业务放分包。
  • 公共组件和基础工具谨慎放主包。
  • 分包预加载只服务高概率路径。

关联知识点

关联性能优化、架构模块拆分、发布治理。

面试考察方式

高频问题:如何优化小程序包体?分包怎么拆?

5. 组件库与样式规范

是什么

组件库是沉淀通用 UI 和交互模式的工程资产,样式规范用于统一视觉、间距、字体、颜色和交互状态。

为什么重要

没有组件库,团队会重复实现按钮、弹窗、卡片、空态、表单,造成体验不一致和维护成本上升。

底层原理

组件库通过属性定义可配置能力,通过事件暴露交互,通过样式隔离降低污染,通过文档和示例保证一致使用。

使用场景

  • 多页面复用 UI。
  • 团队统一设计语言。
  • 快速搭建业务页面。

代码示例

js 复制代码
// components/empty-state/index.js
Component({
  properties: {
    title: {
      type: String,
      value: "暂无数据"
    },
    actionText: {
      type: String,
      value: ""
    }
  },

  methods: {
    handleAction() {
      this.triggerEvent("action");
    }
  }
});
xml 复制代码
<view class="empty">
  <image class="icon" src="/assets/empty.png" mode="aspectFit" />
  <view class="title">{{title}}</view>
  <button wx:if="{{actionText}}" bindtap="handleAction">{{actionText}}</button>
</view>

常见错误

  • 组件参数过多,变成万能组件。
  • 通用组件里写具体业务文案。
  • 组件没有空态、禁用态、加载态。

最佳实践

  • 组件保持职责单一。
  • 通用组件不直接请求业务接口。
  • 重要组件提供示例和使用约束。
  • 对按钮、表单、弹窗、列表、空态优先建设。

关联知识点

关联 UI 规范、可维护性、团队协作。

面试考察方式

常问如何抽象组件、通用组件和业务组件如何区分。

综合 Demo 项目:本地生活服务小程序

项目背景

设计一个面向社区用户的本地生活服务小程序,用户可以浏览门店、购买服务、预约时间、支付订单、接收订阅消息、评价服务。商家端可通过后台管理商品、订单和运营活动。

业务目标

  • 支撑真实服务预约和支付闭环。
  • 覆盖列表、详情、登录、订单、支付、消息、评价等核心业务。
  • 体现工程化、性能、安全、稳定性和架构设计能力。
  • 可作为学习项目、团队培训项目和面试讲解项目。

功能列表

  • 首页:推荐服务、附近门店、活动入口。
  • 门店:门店列表、门店详情、地图定位。
  • 服务:服务列表、服务详情、规格选择。
  • 购物车:服务套餐、数量、价格计算。
  • 订单:创建订单、订单列表、订单详情、取消订单。
  • 支付:拉起微信支付、支付状态查询、失败重试。
  • 消息:订阅消息授权、订单状态通知。
  • 用户:登录、手机号授权、地址管理、评价。
  • 运营:优惠券、活动页、分享。
  • 稳定性:错误页、空态、重试、日志上报。

技术选型

  • 原生微信小程序:适合深入理解平台机制。
  • JavaScript 或 TypeScript:团队成熟后推荐 TypeScript。
  • 自研轻量 Store:满足学习和中型项目。
  • 服务端:Node.js/Java 均可,接口遵循 REST 或 BFF。
  • 监控:接口耗时、页面错误、业务埋点。

项目结构

text 复制代码
local-life-miniapp/
  miniprogram/
    pages/
      home/
      store-list/
      store-detail/
      service-detail/
      cart/
      login/
      profile/
    packages/
      order/
        list/
        detail/
        checkout/
      marketing/
        coupon/
        activity/
    components/
      service-card/
      store-card/
      order-card/
      price-bar/
      empty-state/
    services/
      request.js
      auth.service.js
      store.service.js
      service.service.js
      order.service.js
      payment.service.js
      message.service.js
    store/
      user.store.js
      cart.store.js
    utils/
      format.js
      validator.js
      logger.js
    config/
      env.js

核心模块设计

用户模块:

  • 负责 wx.login、手机号授权、Token 存储、用户资料。
  • 登录态由服务端签发,前端只保存业务 Token。

商品服务模块:

  • 首页推荐、门店服务列表、详情。
  • 支持缓存和分页。

购物车模块:

  • 本地状态为主,提交订单前由服务端重新校验价格和库存。

订单模块:

  • 创建订单、查询订单、取消订单、支付状态同步。
  • 订单状态以后端为准。

支付模块:

  • 前端拉起支付。
  • 支付结果通过服务端回调确认。
  • 前端支付后主动查询订单状态做兜底。

消息模块:

  • 在用户明确操作时申请订阅消息。
  • 服务端根据订单状态触发消息。

关键代码实现:订单创建与支付

js 复制代码
// services/order.service.js
const { request } = require("./request");

function createOrder(payload) {
  return request({
    url: "/orders",
    method: "POST",
    data: payload
  });
}

function getOrderDetail(orderId) {
  return request({
    url: `/orders/${orderId}`
  });
}

module.exports = {
  createOrder,
  getOrderDetail
};
js 复制代码
// services/payment.service.js
const { request } = require("./request");

function createPayment(orderId) {
  return request({
    url: `/orders/${orderId}/payment`,
    method: "POST"
  });
}

function requestPayment(payParams) {
  return new Promise((resolve, reject) => {
    wx.requestPayment({
      ...payParams,
      success: resolve,
      fail: reject
    });
  });
}

module.exports = {
  createPayment,
  requestPayment
};
js 复制代码
// packages/order/checkout/index.js
const orderService = require("../../../services/order.service");
const paymentService = require("../../../services/payment.service");

Page({
  data: {
    submitting: false,
    items: [],
    addressId: ""
  },

  async submitOrder() {
    if (this.data.submitting) return;

    this.setData({ submitting: true });
    try {
      const order = await orderService.createOrder({
        items: this.data.items,
        addressId: this.data.addressId
      });

      const payParams = await paymentService.createPayment(order.id);
      await paymentService.requestPayment(payParams);

      wx.redirectTo({
        url: `/packages/order/detail/index?id=${order.id}&from=pay`
      });
    } catch (err) {
      wx.showToast({
        title: err.message || "提交失败",
        icon: "none"
      });
    } finally {
      this.setData({ submitting: false });
    }
  }
});

配置说明

json 复制代码
{
  "permission": {
    "scope.userLocation": {
      "desc": "用于展示附近门店"
    }
  },
  "requiredPrivateInfos": ["getLocation"],
  "subpackages": [
    {
      "root": "packages/order",
      "pages": ["list/index", "detail/index", "checkout/index"]
    },
    {
      "root": "packages/marketing",
      "pages": ["coupon/index", "activity/index"]
    }
  ]
}

运行方式

  1. 使用微信开发者工具导入项目。
  2. 配置 AppID 和后端 API 地址。
  3. 在开发环境中开启"不校验合法域名"或配置合法域名。
  4. 启动 Mock 服务或连接测试环境接口。

测试方式

  • 单元测试:测试 utils、服务层数据转换、状态管理。
  • 接口测试:Mock 登录、订单、支付参数。
  • 冒烟测试:首页、登录、详情、下单、订单列表。
  • 真机测试:定位、支付、订阅消息、低端机性能。

部署方式

  • 开发者工具上传体验版。
  • 小程序 CI 上传版本。
  • 体验版测试通过后提交审核。
  • 正式发布前检查接口域名、隐私协议、版本号、灰度配置。

扩展方向

  • 接入优惠券和会员等级。
  • 增加商家客服和售后。
  • 加入 WebSocket 实时订单状态。
  • 增加运营活动配置平台。
  • 接入性能监控和自动报警。

对应知识图谱

  • 基础概念:页面、组件、配置。
  • 核心能力:路由、请求、登录、存储。
  • 工程实践:分层、组件库、状态管理、分包。
  • 高级应用:支付、订阅消息、定位。
  • 架构设计:订单、支付、消息模块边界。
  • 性能安全:包体、缓存、支付安全、错误兜底。

实战案例:订单列表加载慢

业务背景

订单列表页需要展示用户历史订单,包含门店、服务、价格、状态、操作按钮。上线后用户反馈进入订单页慢,滚动卡顿。

问题描述

  • 首次进入订单列表白屏 2 秒以上。
  • 一次接口返回 100 条订单。
  • 页面一次性 setData 全量订单。
  • 每个订单卡片包含多张图片。

技术难点

  • 数据量大。
  • 图片资源多。
  • setData 数据过大。
  • 列表渲染节点多。

方案分析

方案一:只加 Loading。体验改善有限,不解决根因。

方案二:分页加载。降低单次请求和渲染成本。

方案三:图片压缩和懒加载。减少网络和渲染压力。

方案四:订单卡片字段裁剪。列表页只展示必要字段,详情页再查完整数据。

最终方案

  • 接口改为分页,每页 10 条。
  • 列表页只返回摘要字段。
  • 图片使用缩略图。
  • 下拉刷新重置第一页,上拉加载下一页。
  • setData 只追加新页数据。

核心代码

js 复制代码
Page({
  data: {
    orders: [],
    page: 1,
    pageSize: 10,
    hasMore: true,
    loading: false
  },

  onLoad() {
    this.loadOrders(true);
  },

  onReachBottom() {
    if (this.data.hasMore && !this.data.loading) {
      this.loadOrders(false);
    }
  },

  async loadOrders(reset) {
    const page = reset ? 1 : this.data.page;
    this.setData({ loading: true });
    try {
      const result = await Promise.resolve({
        list: [],
        hasMore: false
      });
      this.setData({
        orders: reset ? result.list : this.data.orders.concat(result.list),
        page: page + 1,
        hasMore: result.hasMore
      });
    } finally {
      this.setData({ loading: false });
    }
  }
});

性能、安全、可维护性权衡

  • 性能:分页和摘要字段显著降低渲染压力。
  • 安全:订单详情仍需服务端校验用户权限。
  • 可维护性:列表接口和详情接口职责更清晰。

可能的坑

  • 下拉刷新时未重置页码。
  • 快速上拉触发重复请求。
  • 删除订单后列表分页状态不一致。

线上故障排查思路

  • 看接口耗时和返回体大小。
  • setData 数据大小和频率。
  • 真机观察图片加载和滚动帧率。
  • 对比低端机和高端机表现。

面试中如何讲

先说明业务现象,再给出数据证据,然后解释根因是接口过大、渲染节点多和 setData 成本高,最后说明分页、摘要字段、图片优化和状态防重复提交的组合方案。

本章节面试题

题目:你会如何设计微信小程序项目结构?

  • 难度:中级
  • 高频:是
  • 考察点:工程化、分层、可维护性。
  • 标准答案:按页面、组件、服务、状态、工具、配置、静态资源拆分。页面层负责展示和交互,服务层负责业务接口,请求层负责协议和错误处理,状态层维护跨页面数据,组件层沉淀复用 UI。
  • 深度扩展:可以进一步说明依赖方向、分包策略和团队协作规范。
  • 常见错误回答:只列 pagescomponents,没有说明职责边界。
  • 面试官追问:业务组件和通用组件如何区分?
  • 项目应用场景:中大型业务小程序。
  • 对应知识点:工程结构、组件库、服务层。

题目:如何封装一个可靠的请求层?

  • 难度:中级
  • 高频:是
  • 考察点:接口封装、错误处理、登录态。
  • 标准答案:对 wx.request 做 Promise 封装,统一 baseURL、Header、Token、HTTP 状态、业务状态码、错误归一化、登录失效、耗时日志和重试策略。页面只感知业务数据和可展示错误。
  • 深度扩展:高级回答会区分网络失败、HTTP 错误、业务错误和风控错误,并说明 Trace ID 和监控。
  • 常见错误回答:只把回调改成 Promise。
  • 面试官追问:401 和 403 如何处理?
  • 项目应用场景:所有接口调用。
  • 对应知识点:请求层、登录鉴权、监控。

题目:小程序状态管理怎么做?

  • 难度:中级
  • 高频:是
  • 考察点:跨页面状态、数据一致性。
  • 标准答案:简单项目可用 app.globalData 和本地缓存;中型项目应建立状态模块,提供订阅、更新方法和清理机制。用户、购物车、门店、权限等跨页面数据应有明确归属,页面卸载时取消订阅。
  • 深度扩展:可以讨论状态持久化、缓存恢复、登录退出清理。
  • 常见错误回答:所有东西都放 globalData
  • 面试官追问:购物车数量如何在多个 Tab 同步?
  • 项目应用场景:用户中心、购物车、门店切换。
  • 对应知识点:状态管理、缓存、组件通信。

题目:如何做小程序分包?

  • 难度:中级
  • 高频:是
  • 考察点:包体优化、业务拆分。
  • 标准答案:首页、Tab、登录和公共基础能力放主包;订单、营销、设置等低频或重业务放分包。公共组件谨慎放主包,分包预加载要基于真实路径和网络条件。
  • 深度扩展:可以说明独立分包、分包预下载和公共代码重复问题。
  • 常见错误回答:为了分包而分包,不考虑业务访问路径。
  • 面试官追问:分包之间能不能直接互相引用?
  • 项目应用场景:大型商城、本地生活、政企服务小程序。
  • 对应知识点:分包、性能优化、架构拆分。

题目:订单列表卡顿你怎么排查?

  • 难度:高级
  • 高频:是
  • 考察点:性能定位、工程经验。
  • 标准答案:先看接口耗时、返回体大小、首屏数据量、图片数量、setData 频率和大小,再看渲染节点数量和真机表现。常见方案包括分页、摘要字段、图片压缩和懒加载、骨架屏、局部更新、避免重复请求。
  • 深度扩展:专家回答会结合双线程通信成本解释 setData 为什么影响性能。
  • 常见错误回答:只说"加缓存"。
  • 面试官追问:如果接口很快但页面仍卡,下一步看什么?
  • 项目应用场景:订单、商品、消息、评论长列表。
  • 对应知识点:列表性能、setData、工程监控。

工程高级知识点库

文档定位

本文扩展工程实践、高级能力和真实项目治理知识点,适合团队开发规范、项目重构、面试项目讲解和技术方案评审。

工程高级知识图谱

工程结构
请求治理
状态治理
组件治理
鉴权/重试/错误
缓存/一致性
组件库/设计规范
监控/日志
团队协作
发布/灰度/复盘
架构演进

1. 环境配置治理

是什么

环境配置治理是把开发、测试、预发、生产的接口域名、开关、日志级别和 Mock 策略统一管理。

为什么重要

环境混乱会导致测试连生产、生产连测试、日志泄露、接口行为不一致。

代码示例

js 复制代码
const ENV = "dev";

const configs = {
  dev: {
    baseURL: "https://dev-api.example.com",
    mock: true,
    logLevel: "debug"
  },
  test: {
    baseURL: "https://test-api.example.com",
    mock: false,
    logLevel: "info"
  },
  prod: {
    baseURL: "https://api.example.com",
    mock: false,
    logLevel: "warn"
  }
};

module.exports = configs[ENV];

常见错误

  • 在页面里硬编码接口地址。
  • 生产环境打开 Mock。
  • 日志级别不区分环境。

最佳实践

  • 环境配置集中管理。
  • 发布前自动检查环境。
  • 敏感配置不放前端。
  • 远程开关要有默认值。

2. API 分层设计

分层模型

层级 职责 示例
请求基础层 HTTP、Token、错误、日志 request.js
服务层 业务接口语义 order.service.js
页面层 调用服务并渲染 pages/order
状态层 跨页面状态 cart.store.js

反例

js 复制代码
Page({
  onLoad() {
    wx.request({
      url: "https://api.example.com/orders",
      success: res => this.setData({ orders: res.data.data })
    });
  }
});

问题:页面耦合域名、协议、返回结构和错误处理。

正确写法

js 复制代码
const orderService = require("../../services/order.service");

Page({
  async onLoad() {
    const orders = await orderService.listOrders();
    this.setData({ orders });
  }
});

3. 错误状态模型

是什么

页面不应只有 loading 和 success,还应明确 empty、error、offline、forbidden 等状态。

状态模型

js 复制代码
const PageStatus = {
  LOADING: "loading",
  SUCCESS: "success",
  EMPTY: "empty",
  ERROR: "error",
  FORBIDDEN: "forbidden"
};

页面示例

js 复制代码
Page({
  data: {
    status: "loading",
    list: []
  },

  async loadData() {
    this.setData({ status: "loading" });
    try {
      const list = await Promise.resolve([]);
      this.setData({
        list,
        status: list.length ? "success" : "empty"
      });
    } catch (err) {
      this.setData({ status: "error" });
    }
  }
});

面试追问

  • 为什么不建议所有错误都 Toast?
  • 空态和错误态有什么区别?
  • 页面错误态如何设计重试?

4. 组件分层与复用边界

组件分类

类型 职责 示例
基础组件 通用视觉和交互 Button、Empty
业务组件 绑定业务语义 OrderCard
容器组件 聚合状态和数据 CartPanel
页面组件 页面局部结构 HomeSection

判断标准

  • 多个页面重复出现,可抽基础或业务组件。
  • 只在一个页面出现但结构复杂,可抽页面组件。
  • 组件需要直接请求业务接口时,优先评估是否应该放页面或容器。

组件设计清单

  • 输入是否通过 properties
  • 输出是否通过 triggerEvent
  • 是否有 loading、empty、disabled 状态。
  • 是否写死业务文案。
  • 样式是否污染外部。
  • 是否可单独预览和测试。

5. 购物车状态设计

业务问题

购物车涉及跨页面数量同步、价格计算、库存校验、登录态、服务端一致性。

推荐设计

  • 本地 store 维护当前选择。
  • 本地缓存用于恢复。
  • 提交订单前由服务端重新计算价格和库存。
  • 商品变价、下架、库存不足必须以后端为准。

代码示例

js 复制代码
const state = {
  items: []
};

function addItem(service) {
  const found = state.items.find(item => item.id === service.id);
  if (found) {
    found.count += 1;
  } else {
    state.items.push({ ...service, count: 1 });
  }
  wx.setStorageSync("cart", state.items);
}

function getTotalPrice() {
  return state.items.reduce((sum, item) => sum + item.price * item.count, 0);
}

module.exports = {
  state,
  addItem,
  getTotalPrice
};

6. 分包策略扩展

拆分原则

模块 建议位置 原因
首页 主包 启动入口
登录 主包 高频基础流程
Tab 页面 主包 平台要求和高频
订单详情 分包 相对低频
营销活动 分包 资源重、变动频繁
设置页 分包 低频

分包风险

  • 公共依赖过大导致主包变大。
  • 分包首进有加载等待。
  • 分包间引用路径混乱。
  • 预加载策略过度,反而浪费流量。

7. Mock 与接口联调

是什么

Mock 是在后端未完成或接口不稳定时,用模拟数据支持前端开发和测试。

实践建议

  • Mock 数据结构必须贴近真实接口。
  • Mock 和真实接口共用服务层。
  • Mock 不应污染生产环境。
  • 复杂流程用场景化 Mock,例如支付成功、支付失败、库存不足。

示例

js 复制代码
function mockOrderDetail(id) {
  return Promise.resolve({
    id,
    status: "PENDING_PAY",
    amount: 9900,
    items: [{ name: "深度清洁服务", count: 1 }]
  });
}

8. 日志与埋点

日志分类

类型 用途 示例
技术日志 排查错误 JS 异常、接口失败
行为埋点 分析路径 点击、曝光、转化
性能日志 优化体验 首屏耗时、接口耗时
业务日志 追踪链路 下单、支付、退款

关键字段

  • 用户匿名 ID。
  • 页面路径。
  • 基础库版本。
  • 设备型号。
  • 网络类型。
  • 接口 Trace ID。
  • 错误码和堆栈。

代码示例

js 复制代码
function report(event, payload) {
  const system = wx.getSystemInfoSync();
  console.log("[report]", {
    event,
    payload,
    page: getCurrentPages().slice(-1)[0]?.route,
    sdk: system.SDKVersion,
    model: system.model,
    time: Date.now()
  });
}

module.exports = { report };

9. 小程序 CI 与发布流程

标准流程

  1. 本地开发自测。
  2. 分支代码审查。
  3. 自动静态检查。
  4. 上传体验版。
  5. QA 冒烟测试。
  6. 提交审核。
  7. 灰度或观察发布。
  8. 监控错误率。

发布检查清单

  • API 域名正确。
  • AppID 正确。
  • 版本号和更新说明正确。
  • 隐私协议和权限声明完整。
  • 支付、登录、订阅消息真机验证。
  • 关键页面无白屏。
  • 远程开关默认值可兜底。

10. 工程面试题扩展

题目:为什么页面不应该直接调用 wx.request

  • 难度:中级
  • 高频:是
  • 考察点:工程分层。
  • 标准答案:页面直接调用会耦合域名、协议、Token、错误处理和返回结构,导致重复代码和维护困难。应通过请求层和服务层统一封装。
  • 深度扩展:请求层还能接入日志、Trace ID、重试和登录失效处理。
  • 常见错误回答:直接调更方便。
  • 面试官追问:服务层和请求层的区别是什么?
  • 项目应用场景:团队项目、长期迭代项目。
  • 对应知识点:API 分层、请求治理。

题目:如何判断一个组件是否应该抽象?

  • 难度:中级
  • 高频:是
  • 考察点:组件设计。
  • 标准答案:看复用频率、变化方向、职责是否单一、输入输出是否清晰。如果只是代码片段相似但业务语义不同,不一定要抽;如果多个页面共享同一交互和视觉规范,应抽组件。
  • 深度扩展:过早抽象会让组件参数膨胀,降低可维护性。
  • 常见错误回答:重复两次就抽。
  • 面试官追问:万能组件有什么问题?
  • 项目应用场景:组件库、业务卡片、弹窗。
  • 对应知识点:组件治理。

题目:如何设计小程序发布前检查?

  • 难度:高级
  • 高频:否
  • 考察点:质量保障。
  • 标准答案:从配置、权限、接口、真机、关键链路、性能、错误监控、灰度开关和回滚方案检查。支付、登录、定位、订阅消息必须真机验证。
  • 深度扩展:发布不是上传动作,而是风险控制流程。
  • 常见错误回答:开发者工具能跑就提交审核。
  • 面试官追问:线上出现严重问题如何止血?
  • 项目应用场景:正式上线、版本迭代。
  • 对应知识点:CI、灰度、稳定性。
相关推荐
小徐_23332 小时前
Wot UI v1 升级 v2?这份迁移指南帮你少踩坑!
前端·微信小程序·uni-app
优睿远行3 小时前
微信小程序云开发环境搭建与REST API混合架构实战
微信小程序·小程序
Greg_Zhong4 小时前
解决绘制的雷达图在页面有滚动时,雷达图出现`轻微上下偏移`的问题
微信小程序·canvans绘制雷达图
空中海4 小时前
微信小程序 - 02 基础概念层与核心能力层
微信小程序·小程序
無名路人6 小时前
小程序点餐页吸顶滚动
前端·微信小程序·ai编程
游戏开发爱好者87 小时前
使用Fiddler设置HTTPS抓包诊断Power Query网络问题
android·ios·小程序·https·uni-app·iphone·webview
七月的冰红茶8 小时前
【开发工具】使用cursor实现点单小程序
小程序
Greg_Zhong8 小时前
微信小程序中使用canvas实现雷达图及标签对角显示(实现雷达图标签的标准做法)
微信小程序·小程序canvas实现雷达图·标签不通过canvas绘制
码农客栈9 小时前
小程序学习(十八)之“骨架屏”
小程序