小程序全栈项目实战:从零构建电商应用

以电商小程序为例,完整演示从零构建全栈小程序项目的全流程,包括需求分析、架构设计、前后端开发、云开发集成与生产部署。

理论知识只有在实战中才能真正内化。本文将以一个完整的电商小程序项目为例,从需求分析、技术选型、架构设计、前后端开发到最终部署,完整呈现全栈小程序项目的构建过程。这个项目综合运用了前文讨论的组件化架构、状态管理、云开发和性能优化等技术,为读者提供一个可参考、可复用的工程化实践样本。

一、项目概述

1.1 需求范围

本项目实现一个简版 B2C 电商小程序,覆盖以下核心功能模块:

模块功能点技术重点
首页轮播图、分类入口、商品推荐流虚拟列表、骨架屏、数据缓存
商品详情轮播图、SKU 选择、规格联动、评价列表组件化、状态管理、事件通信
购物车商品管理、数量调整、价格计算、失效标记全局状态、持久化、批量操作
订单流程地址选择、优惠券、支付下单云函数、事务、微信支付
用户中心个人信息、订单列表、地址管理云数据库、权限控制
搜索与筛选关键词搜索、多维度筛选、历史记录全文索引、数据聚合

1.2 技术选型

层面选型理由
框架原生微信小程序直接调用平台能力,无抽象损耗
状态管理MobX + mobx-miniprogram响应式更新,开发体验佳
后端微信云开发Serverless,免运维,天然鉴权
数据库云数据库 (MongoDB)文档型,与 JSON 自然映射
存储云存储 CDN自动 CDN 加速,支持图片处理
支付微信支付原生集成,用户体验流畅

二、架构设计

2.1 目录结构

ecommerce-miniprogram/
├── cloudfunctions/              # 云函数
│   ├── product/                 # 商品相关接口
│   │   ├── index.js             # list / detail / search
│   │   └── config.json
│   ├── cart/                    # 购物车逻辑
│   ├── order/                   # 订单与支付
│   └── user/                    # 用户信息管理
├── components/                  # 公共组件
│   ├── product-card/
│   ├── count-down/
│   ├── sku-selector/
│   ├── address-picker/
│   ├── loading-skeleton/
│   └── price-display/
├── pages/                       # 页面
│   ├── index/                   # 首页
│   ├── category/                # 分类页
│   ├── search/                  # 搜索页
│   ├── product-detail/          # 商品详情
│   ├── cart/                    # 购物车
│   ├── order-confirm/           # 订单确认
│   ├── order-list/              # 订单列表
│   ├── user-center/             # 用户中心
│   └── address-list/            # 地址管理
├── store/                       # 状态管理
│   ├── index.js                 # Store 入口
│   ├── cart-store.js            # 购物车状态
│   ├── user-store.js            # 用户状态
│   └── order-store.js           # 订单状态
├── utils/                       # 工具函数
│   ├── request.js               # 请求封装
│   ├── format.js                # 格式化工具
│   ├── cache.js                 # 缓存策略
│   └── validators.js            # 校验规则
├── behaviors/                   # 公共行为
│   ├── trackable.js             # 埋点
│   └── validatable.js           # 表单校验
├── app.js                       # 应用入口
├── app.json                     # 全局配置
├── app.wxss                     # 全局样式
└── project.config.json

2.2 数据流架构

视图层 (WXML/WXSS)
    ↑↓ setData / 事件
逻辑层 (Page/Component)
    ↑↓ dispatch / subscribe
状态管理层 (MobX Store)
    ↑↓ 持久化 / 事件总线
服务层 (云函数调用)
    ↑↓ HTTP / 云调用
数据层 (云数据库 / 云存储)

所有数据变更都通过 MobX Store 进行集中管理,Store 中的 Action 负责调用云函数获取或修改数据。视图层只负责展示和触发 Action,不直接操作数据。这种单向数据流保证了状态变化的可预测性和可追踪性。

2.3 分包策略

{
  "pages": [
    "pages/index/index",
    "pages/category/category",
    "pages/user-center/user-center"
  ],
  "subpackages": [
    {
      "root": "package-product",
      "pages": [
        "pages/product-detail/product-detail",
        "pages/search/search"
      ]
    },
    {
      "root": "package-trade",
      "pages": [
        "pages/cart/cart",
        "pages/order-confirm/order-confirm",
        "pages/order-list/order-list",
        "pages/address-list/address-list"
      ],
      "preloadRule": {
        "pages/index/index": {
          "network": "all",
          "packages": ["package-product"]
        }
      }
    }
  ]
}

主包仅保留首页、分类页和用户中心三个 Tab 页面 ensures冷启动最快。商品详情和搜索归入商品分包,交易链路归入交易分包,通过 preloadRule 在用户浏览首页时预下载商品分包。

三、状态管理实现

3.1 购物车 Store

购物车状态是本项目中最复杂的状态之一,涉及跨页面同步、持久化和价格实时计算:

// store/cart-store.js
import { observable, action } from 'mobx-miniprogram';

export const cartStore = observable({
  items: [],          // 商品项数组
  selectedIds: [],    // 已勾选商品的 SKU ID
  invalidItems: [],   // 失效商品(下架/售罄)
  
  // 计算属性:已勾商品列表
  get selectedItems() {
    return this.items.filter(item => this.selectedIds.includes(item.skuId));
  },
  
  // 计算属性:总价(含优惠计算)
  get totalPrice() {
    return this.selectedItems.reduce((sum, item) => {
      const effectivePrice = item.promotionPrice || item.price;
      return sum + effectivePrice * item.quantity;
    }, 0);
  },
  
  // 计算属性:总数量
  get totalCount() {
    return this.selectedItems.reduce((sum, item) => sum + item.quantity, 0);
  },
  
  // 计算属性:是否全选
  get isAllSelected() {
    return this.items.length > 0 && 
           this.selectedIds.length === this.items.length;
  },
  
  // 添加商品
  addItem: action(function(product, sku, quantity = 1) {
    const existing = this.items.find(item => item.skuId === sku.id);
    if (existing) {
      existing.quantity += quantity;
      // 触发更新
      this.items = [...this.items];
    } else {
      this.items.push({
        productId: product.id,
        skuId: sku.id,
        name: product.name,
        skuName: sku.name,
        image: sku.image || product.mainImage,
        price: sku.price,
        promotionPrice: sku.promotionPrice,
        stock: sku.stock,
        quantity,
        addedAt: Date.now()
      });
    }
    this._persist();
  }),
  
  // 更新数量
  updateQuantity: action(function(skuId, quantity) {
    const item = this.items.find(i => i.skuId === skuId);
    if (!item) return;
    
    if (quantity <= 0) {
      this.removeItem(skuId);
    } else {
      item.quantity = Math.min(quantity, item.stock);
      this.items = [...this.items];
      this._persist();
    }
  }),
  
  // 移除商品
  removeItem: action(function(skuId) {
    this.items = this.items.filter(item => item.skuId !== skuId);
    this.selectedIds = this.selectedIds.filter(id => id !== skuId);
    this._persist();
  }),
  
  // 切换选中状态
  toggleSelect: action(function(skuId) {
    if (this.selectedIds.includes(skuId)) {
      this.selectedIds = this.selectedIds.filter(id => id !== skuId);
    } else {
      this.selectedIds = [...this.selectedIds, skuId];
    }
  }),
  
  // 全选/取消全选
  toggleSelectAll: action(function() {
    if (this.isAllSelected) {
      this.selectedIds = [];
    } else {
      this.selectedIds = this.items.map(item => item.skuId);
    }
  }),
  
  // 清空失效商品
  clearInvalid: action(function() {
    const invalidIds = this.invalidItems.map(item => item.skuId);
    this.items = this.items.filter(item => !invalidIds.includes(item.skuId));
    this.selectedIds = this.selectedIds.filter(id => !invalidIds.includes(id));
    this.invalidItems = [];
    this._persist();
  }),
  
  // 持久化到本地
  _persist() {
    wx.setStorageSync('cart_data', {
      items: this.items,
      selectedIds: this.selectedIds
    });
  },
  
  // 从本地恢复
  restore() {
    const saved = wx.getStorageSync('cart_data');
    if (saved) {
      this.items = saved.items || [];
      this.selectedIds = saved.selectedIds || [];
    }
  }
});

购物车 Store 采用 MobX 的响应式机制,任何商品的增删改都会自动触发绑定组件的重新渲染。selectedIdsitems 分离存储,使得勾选状态和价格计算的变更可以独立追踪。

3.2 云函数层设计

// cloudfunctions/product/index.js
const cloud = require('wx-server-sdk');
cloud.init();
const db = cloud.database();
const _ = db.command;
const $ = db.command.aggregate;

// 商品列表查询
async function listProducts(event) {
  const { category, page = 1, size = 20, sort = 'default' } = event;
  
  let query = db.collection('products').where({ status: 'on_sale' });
  
  if (category) {
    query = query.where({ categoryId: category });
  }
  
  const sortMap = {
    default: { sortOrder: -1 },
    price_asc: { price: 1 },
    price_desc: { price: -1 },
    sales: { salesCount: -1 },
    new: { createdAt: -1 }
  };
  
  const { data } = await query
    .orderBy(Object.entries(sortMap[sort] || sortMap.default)[0])
    .skip((page - 1) * size)
    .limit(size)
    .field({
      name: true,
      mainImage: true,
      price: true,
      promotionPrice: true,
      salesCount: true,
      tags: true
    })
    .get();
  
  return { success: true, list: data, page, size };
}

// 商品详情查询
async function getProductDetail(event) {
  const { productId } = event;
  
  const { data: product } = await db.collection('products').doc(productId).get();
  
  // 并行查询 SKU 和评价统计
  const [skuResult, reviewResult] = await Promise.all([
    db.collection('skus').where({ productId }).get(),
    db.collection('reviews').where({ productId }).count()
  ]);
  
  // 计算评分分布
  const ratingStats = await db.collection('reviews').aggregate()
    .match({ productId })
    .group({
      _id: '$rating',
      count: $.sum(1)
    })
    .end();
  
  return {
    success: true,
    data: {
      ...product,
      skus: skuResult.data,
      reviewCount: reviewResult.total,
      ratingStats: ratingStats.list
    }
  };
}

// 搜索商品
async function searchProducts(event) {
  const { keyword, filters, page = 1, size = 20 } = event;
  
  let query = db.collection('products').where({
    status: 'on_sale',
    $or: [
      { name: db.RegExp({ regexp: keyword, options: 'i' }) },
      { tags: _.in([keyword]) }
    ]
  });
  
  // 应用筛选条件
  if (filters?.priceRange) {
    query = query.where({
      price: _.gte(filters.priceRange[0]).and(_.lte(filters.priceRange[1]))
    });
  }
  
  const { data } = await query
    .orderBy('relevance', 'desc')
    .skip((page - 1) * size)
    .limit(size)
    .get();
  
  return { success: true, list: data, keyword };
}

exports.main = async (event, context) => {
  const { action } = event;
  switch (action) {
    case 'list': return listProducts(event);
    case 'detail': return getProductDetail(event);
    case 'search': return searchProducts(event);
    default: return { success: false, message: 'Unknown action' };
  }
};

云函数层采用 Action 分发模式,单个云函数通过 action 参数路由到不同的处理函数。这种模式减少了云函数数量,简化了部署和调用管理。对于性能敏感的查询(如商品列表),使用 field() 投影仅返回前端需要的字段,减少网络传输量。

四、核心页面实现

4.1 首页与虚拟列表

// pages/index/index.js
import { createStoreBindings } from 'mobx-miniprogram-bindings';
import { cartStore } from '../../store/cart-store';

Page({
  data: {
    banners: [],
    categories: [],
    products: [],
    page: 1,
    hasMore: true,
    loading: false,
    refreshing: false
  },

  onLoad() {
    this.storeBindings = createStoreBindings(this, {
      store: cartStore,
      fields: ['totalCount']
    });
    
    this.loadData();
    cartStore.restore();
  },

  onUnload() {
    this.storeBindings.destroyStoreBindings();
  },

  async loadData() {
    this.setData({ loading: true });
    
    try {
      const [bannerRes, categoryRes, productRes] = await Promise.all([
        this.loadBanners(),
        this.loadCategories(),
        this.loadProducts(1)
      ]);
      
      this.setData({
        banners: bannerRes,
        categories: categoryRes,
        products: productRes.list,
        page: 1,
        hasMore: productRes.list.length >= 20,
        loading: false
      });
    } catch (err) {
      console.error('Load data failed:', err);
      this.setData({ loading: false });
      wx.showToast({ title: '加载失败', icon: 'none' });
    }
  },

  async loadBanners() {
    const { result } = await wx.cloud.callFunction({
      name: 'banner',
      data: { action: 'list' }
    });
    return result.data || [];
  },

  async loadCategories() {
    const { result } = await wx.cloud.callFunction({
      name: 'category',
      data: { action: 'list' }
    });
    return result.data || [];
  },

  async loadProducts(page) {
    const { result } = await wx.cloud.callFunction({
      name: 'product',
      data: { action: 'list', page, size: 20 }
    });
    return result;
  },

  async onReachBottom() {
    if (!this.data.hasMore || this.data.loading) return;
    
    const nextPage = this.data.page + 1;
    this.setData({ loading: true });
    
    try {
      const { list } = await this.loadProducts(nextPage);
      this.setData({
        products: [...this.data.products, ...list],
        page: nextPage,
        hasMore: list.length >= 20,
        loading: false
      });
    } catch (err) {
      this.setData({ loading: false });
    }
  },

  onPullDownRefresh() {
    this.setData({ refreshing: true });
    this.loadData().finally(() => {
      this.setData({ refreshing: false });
      wx.stopPullDownRefresh();
    });
  },

  onProductTap(e) {
    const { id } = e.currentTarget.dataset;
    wx.navigateTo({ url: `/package-product/pages/product-detail/product-detail?id=${id}` });
  }
});

4.2 商品详情与 SKU 选择

商品详情页是转化率的关键页面。SKU 选择器的核心难点在于规格联动——当用户选择某个属性时,需要实时计算可选的剩余规格组合,并更新价格、库存和对应图片。

// components/sku-selector/sku-selector.js
Component({
  properties: {
    skus: Array,
    attributes: Array  // [{ name: '颜色', values: ['红', '蓝'] }, ...]
  },

  data: {
    selected: {},      // { '颜色': '红', '尺码': 'XL' }
    currentSku: null
  },

  observers: {
    'selected.**': function(selected) {
      this._updateCurrentSku(selected);
    }
  },

  methods: {
    onSelectAttribute(e) {
      const { attrName, value } = e.currentTarget.dataset;
      const { selected } = this.data;
      
      // 切换选择:已选则取消,未选则选中
      const newSelected = { ...selected };
      if (newSelected[attrName] === value) {
        delete newSelected[attrName];
      } else {
        newSelected[attrName] = value;
      }
      
      this.setData({ selected: newSelected });
    },

    _updateCurrentSku(selected) {
      const { skus } = this.data;
      
      // 查找匹配的 SKU
      const matched = skus.find(sku => {
        return Object.entries(selected).every(
          ([key, val]) => sku.attributes[key] === val
        );
      });
      
      this.setData({ currentSku: matched || null });
      
      // 通知父组件
      this.triggerEvent('change', {
        selected,
        sku: matched,
        valid: !!matched && matched.stock > 0
      });
    },

    // 判断某属性值是否可选
    isValueAvailable(attrName, value) {
      const { selected, skus } = this.data;
      
      // 模拟选择此属性值,检查是否有 SKU 匹配
      const testSelected = { ...selected, [attrName]: value };
      return skus.some(sku => {
        return Object.entries(testSelected).every(
          ([key, val]) => sku.attributes[key] === val
        ) && sku.stock > 0;
      });
    }
  }
});

4.3 订单确认与支付

// pages/order-confirm/order-confirm.js
import { createStoreBindings } from 'mobx-miniprogram-bindings';
import { cartStore } from '../../store/cart-store';

Page({
  data: {
    selectedItems: [],
    totalPrice: 0,
    address: null,
    coupon: null,
    remark: '',
    submitting: false
  },

  onLoad() {
    this.storeBindings = createStoreBindings(this, {
      store: cartStore,
      fields: {
        selectedItems: 'selectedItems',
        totalPrice: 'totalPrice'
      }
    });

    // 检查是否有选中商品
    if (cartStore.selectedItems.length === 0) {
      wx.showToast({ title: '请先选择商品', icon: 'none' });
      setTimeout(() => wx.switchTab({ url: '/pages/cart/cart' }), 1500);
      return;
    }

    this.loadDefaultAddress();
  },

  async loadDefaultAddress() {
    try {
      const { result } = await wx.cloud.callFunction({
        name: 'user',
        data: { action: 'getDefaultAddress' }
      });
      if (result.success) {
        this.setData({ address: result.data });
      }
    } catch (err) {
      console.error('Load address failed:', err);
    }
  },

  async onSubmitOrder() {
    const { address, remark, totalPrice } = this.data;
    
    if (!address) {
      wx.showToast({ title: '请选择收货地址', icon: 'none' });
      return;
    }

    this.setData({ submitting: true });

    try {
      // 1. 创建服务端订单
      const { result: orderResult } = await wx.cloud.callFunction({
        name: 'order',
        data: {
          action: 'create',
          items: cartStore.selectedItems.map(item => ({
            productId: item.productId,
            skuId: item.skuId,
            quantity: item.quantity
          })),
          address,
          remark,
          totalAmount: totalPrice
        }
      });

      if (!orderResult.success) {
        throw new Error(orderResult.message);
      }

      const { orderId, prepayId } = orderResult.data;

      // 2. 获取支付参数并调起支付
      const { result: payParams } = await wx.cloud.callFunction({
        name: 'order',
        data: { action: 'getPayParams', prepayId }
      });

      await wx.requestPayment({
        ...payParams.data,
        package: `prepay_id=${prepayId}`
      });

      // 3. 支付成功,清理购物车
      cartStore.selectedItems.forEach(item => {
        cartStore.removeItem(item.skuId);
      });

      // 4. 跳转到订单详情
      wx.redirectTo({
        url: `/package-trade/pages/order-detail/order-detail?id=${orderId}`
      });

    } catch (err) {
      console.error('Order failed:', err);
      wx.showToast({ title: err.message || '下单失败', icon: 'none' });
    } finally {
      this.setData({ submitting: false });
    }
  }
});

五、数据库设计

5.1 核心集合结构

// products 集合文档示例
{
  _id: "prod_123456",
  name: "无线蓝牙耳机 Pro",
  description: "主动降噪,长续航...",
  mainImage: "cloud://env-id.bucket/images/prod1.jpg",
  images: ["...", "..."],
  categoryId: "cat_electronics",
  categoryPath: ["数码", "音频", "耳机"],
  price: 299,
  promotionPrice: 249,  // 促销价,null 表示无促销
  status: "on_sale",    // on_sale | off_shelf | sold_out
  salesCount: 1850,
  tags: ["新品", "热销"],
  attributes: [
    { name: "颜色", values: ["白色", "黑色"] },
    { name: "版本", values: ["标准版", "降噪版"] }
  ],
  createdAt: ISODate("2024-06-01"),
  updatedAt: ISODate("2024-09-15")
}

// skus 集合文档示例
{
  _id: "sku_789012",
  productId: "prod_123456",
  skuCode: "PROD1-WHITE-STD",
  attributes: {
    "颜色": "白色",
    "版本": "标准版"
  },
  image: "cloud://env-id.bucket/images/sku1-white.jpg",
  price: 299,
  promotionPrice: 249,
  stock: 150,
  salesCount: 320
}

// orders 集合文档示例
{
  _id: "order_345678",
  _openid: "user_openid_xxx",
  orderNo: "E20240915000123",
  items: [
    {
      productId: "prod_123456",
      skuId: "sku_789012",
      name: "无线蓝牙耳机 Pro",
      skuName: "白色 / 标准版",
      image: "...",
      price: 249,
      quantity: 1
    }
  ],
  totalAmount: 249,
  freight: 0,
  discountAmount: 0,
  payAmount: 249,
  status: "paid",  // pending | paid | shipped | completed | cancelled | refunded
  address: {
    name: "张三",
    phone: "138****5678",
    province: "广东省",
    city: "深圳市",
    district: "南山区",
    detail: "科技园 xx 栋"
  },
  remark: "请发顺丰",
  timeline: [
    { status: "created", time: ISODate("2024-09-15T10:00:00Z") },
    { status: "paid", time: ISODate("2024-09-15T10:02:30Z") }
  ],
  createdAt: ISODate("2024-09-15T10:00:00Z"),
  updatedAt: ISODate("2024-09-15T10:02:30Z")
}

5.2 索引设计

// 商品查询索引
db.collection('products').createIndex({
  name: 'idx_products_category_status',
  keys: { categoryId: 1, status: 1, sortOrder: -1 }
});

// SKU 查询索引
db.collection('skus').createIndex({
  name: 'idx_skus_product',
  keys: { productId: 1 }
});

// 订单查询索引
db.collection('orders').createIndex({
  name: 'idx_orders_user_status',
  keys: { _openid: 1, status: 1, createdAt: -1 }
});

六、性能优化实践

6.1 启动优化

  • 主包仅保留 Tab 页面,商品和交易链路放入分包
  • 商品列表使用分页加载 + 骨架屏
  • 图片全部使用 CDN + 自适应压缩参数
  • 首页数据预拉取(wx.getBackgroundFetchData

6.2 渲染优化

  • 商品列表采用虚拟列表,DOM 节点数固定
  • 购物车使用纯数据字段 _persistTimer 避免 setData 循环
  • 评价列表图片使用 lazy-load

6.3 数据优化

  • 商品列表接口仅返回必要字段,排除详情 HTML 等大字段
  • 搜索结果本地缓存 5 分钟
  • 购物车状态本地持久化,应用启动时自动恢复

七、部署与发布

7.1 云函数部署

# deploy-cloud.sh
#!/bin/bash

ENV="prod"
FUNCTIONS=("product" "cart" "order" "user" "payment")

for fn in "${FUNCTIONS[@]}"; do
  echo "Deploying cloud function: ${fn}..."
  wx cloud functions:deploy --name ${fn} --env ${ENV}
done

echo "All cloud functions deployed."

7.2 小程序发布流程

  1. 开发阶段:本地开发 开发者工具预览 自测
  2. 提审阶段:代码上传 体验版测试 提交审核
  3. 发布阶段:审核通过 分阶段发布(10% 50% 100%)
  4. 监控阶段:小程序助手查看性能数据 异常告警 热修复

八、总结

本项目展示了从零构建全栈小程序工程的完整方法论。通过组件化拆分实现 UI 复用,通过 MobX 状态管理实现跨页面数据同步,通过微信云开发简化后端建设,通过分包加载和虚拟列表保障性能表现。在实际生产环境中,还应补充错误监控(如 Sentry)、运营埋点和 A/B 测试等能力。

电商小程序的复杂度远不止于此——营销系统(秒杀、拼团、优惠券)、物流追踪、售后退款、多商户平台等场景都会带来更高的架构挑战。但掌握本文所述的基础框架后,这些扩展功能都可以在稳固的地基上逐步搭建。小程序开发的本质是在有限资源约束下(包体积、启动速度、渲染性能)交付最优用户体验,每一个技术决策都应在理解平台特性的基础上做出权衡。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「miniprogram」更多文章

  1. 小程序自动化测试与 CI/CD 实践
  2. 小程序安全与合规实践
  3. 小程序性能优化全景指南