简介:本资源是一套可直接学习与二次开发的微信小程序商店完整实现代码,面向小程序初学者及希望构建自有小程序分发平台的开发者,解决从零搭建小程序展示、搜索、下载等核心功能的学习与实践难题。压缩包共57个文件,包含9个JS逻辑文件(处理页面交互与API调用)、8个WXML模板文件(定义界面结构)、8个WXSS样式文件(实现响应式UI)、9个JSON配置文件(管理路由与全局设置)以及17张PNG图标资源,整体仅131KB,轻量易读,目录结构清晰体现pages、utils、images等标准小程序模块划分。已有4041人学习下载,适合通过真实项目理解小程序生命周期、微信开放接口集成(如小程序码生成)、前后端协同设计及合规性实践。读者可快速掌握商店类应用的典型架构——从前端分类导航与搜索框实现,到后端数据模拟与下载行为模拟,再到README中的部署说明与运行指引,是一份兼具教学性与工程参考价值的入门级实战范例。
1. 微信小程序商店完整代码:不是模板套壳,而是能跑通商品上架、购物车、订单闭环的可调试工程
你搜“微信小程序商店代码”,十有八九点开是空壳页面——首页轮播图能动,点击商品跳转就报Page not found;购物车图标在右上角,但点进去是空数组[];更别提下单时卡在wx.request:fail,连 mock 数据都没配好。这份「微信小程序商店完整代码」不是 UI 演示包,它是一个真实可调试、带后端联调能力、覆盖从商品列表 → 加入购物车 → 提交订单 → 支付模拟全流程的工程级代码包。它用标准 WXML + WXSS + JS 三层结构组织,所有 API 接口路径已预留真实字段(如/api/goods/list,/api/order/create),并附带一份轻量 Node.js Express 后端参考实现(含数据库 schema 和 seed 数据)。适合两类人:一是刚学完小程序基础、卡在“写完页面不知道下一步怎么连数据”的开发者;二是需要快速验证一个电商类交互逻辑(比如优惠券叠加规则、库存扣减时序)的业务方。它不承诺上线即用,但承诺——你 clone 下来,改三处配置,就能看到真实商品滚动、购物车数量实时变化、订单号生成成功。
2. 项目结构与核心模块拆解:看清这 4 个目录,你就知道它为什么不是玩具
这份代码不是单文件堆砌,而是按小程序工程规范分层组织。我把它拉进开发工具后第一件事,就是打开根目录下的project.config.json确认基础库版本(要求 ≥ 2.25.0),再逐层看懂四个关键目录的作用。下面不是罗列文件名,而是告诉你每个目录里真正干活的模块在哪、为什么这么放。
2.1 pages/ 目录:页面逻辑不是写在 wxml 里,而藏在 page.js 的 onLoad 和 onShow 中
pages/下有index/(首页)、goods/(商品详情)、cart/(购物车)、order/(订单确认)四个主流程页。重点不是 wxml 结构,而是每个page.js文件里的生命周期函数:
// pages/cart/cart.js Page({ data: { cartItems: [], totalPrice: 0, totalNum: 0 }, onLoad() { // 关键:这里不是直接 this.setData,而是调用封装好的 cartService this.loadCartData(); }, onShow() { // 关键:页面每次显示都重新拉取,避免其他页面修改后状态不同步 this.loadCartData(); }, loadCartData() { // 调用统一服务层,而非散落的 wx.request cartService.getCartList().then(res => { this.setData({ cartItems: res.data.list || [], totalPrice: res.data.totalPrice || 0, totalNum: res.data.totalNum || 0 }); }); } });提示:
cartService是独立的服务模块(见utils/cartService.js),它把请求地址、token 拦截、错误重试逻辑全部收口。你改接口域名,只改这一处,不用满项目搜https://。
2.2 utils/ 目录:工具不是辅助,而是业务稳定器,尤其 network.js 和 storage.js
utils/里最值得细看的是network.js和storage.js。前者不是简单封装wx.request,而是实现了自动携带登录态 token、失败自动重试 2 次、超时统一设为 10s:
// utils/network.js function request(options) { return new Promise((resolve, reject) => { const token = wx.getStorageSync('auth_token') || ''; wx.request({ url: options.url, method: options.method || 'GET', data: options.data || {}, header: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, timeout: 10000, success: (res) => { if (res.statusCode === 200) { resolve(res); } else if (res.statusCode === 401) { // token 过期,跳转登录 wx.navigateTo({ url: '/pages/login/login' }); } else { reject(new Error(`HTTP ${res.statusCode}`)); } }, fail: (err) => { // 网络异常时,尝试重试(仅 GET 请求) if (options.method === 'GET' && !options.noRetry) { setTimeout(() => { request({ ...options, noRetry: true }).then(resolve).catch(reject); }, 1000); } else { reject(err); } } }); }); }storage.js则封装了wx.setStorageSync的安全写法——对敏感数据(如用户地址)自动加密,对大对象(如购物车全量)做 JSON 序列化容错:
// utils/storage.js export function set(key, value) { try { // 防止 value 为 undefined 或 null 导致 JSON.stringify 报错 const safeValue = value === undefined || value === null ? null : value; wx.setStorageSync(key, JSON.stringify(safeValue)); } catch (e) { console.error('Storage set error:', e); } } export function get(key, defaultValue = null) { try { const str = wx.getStorageSync(key); return str ? JSON.parse(str) : defaultValue; } catch (e) { console.error('Storage get error:', e); return defaultValue; } }参数说明:
request()的noRetry参数用于 POST/PUT 等非幂等请求,避免重复提交;storage.get()的defaultValue是防御性编程标配,防止首次读取时因 key 不存在返回undefined导致后续逻辑崩溃。
2.3 models/ 目录:数据模型不是空 class,而是带校验和默认值的实体
models/下的Goods.js、Order.js不是摆设。以Goods.js为例,它定义了商品必须有的字段、默认值、以及前端校验规则:
// models/Goods.js class Goods { constructor(data = {}) { this.id = data.id || 0; this.name = data.name || '未知商品'; this.price = parseFloat(data.price) || 0; this.originalPrice = parseFloat(data.originalPrice) || this.price; this.stock = parseInt(data.stock) || 0; this.image = data.image || '/images/default.png'; this.sales = parseInt(data.sales) || 0; } // 前端价格格式化(避免 99.99999999) get displayPrice() { return this.price.toFixed(2); } // 是否有库存 get inStock() { return this.stock > 0; } // 是否特价(原价比现价高 10% 以上) get isOnSale() { return this.originalPrice > this.price * 1.1; } // 校验商品是否可加入购物车 canAddToCart(count = 1) { return this.inStock && count <= this.stock; } } export default Goods;逻辑说明:
canAddToCart()方法被cartService.add()直接调用,而不是在页面里写if (goods.stock > 0)—— 这样把业务规则收口,改规则只改 model,不碰页面逻辑。
2.4 app.js 全局配置:App.onLaunch 里埋了三个关键初始化动作
app.js的onLaunch不只是打个日志。它做了三件必须做的事:
- 检查登录态:读取本地 token,若存在则静默刷新用户信息(调用
/api/user/info); - 初始化全局配置:从
config.js加载 API 基础地址、图片 CDN 域名、是否开启 debug 模式; - 预加载常用数据:如商品分类列表(
/api/category/list),存入getApp().globalData.categories,供多个页面复用,避免重复请求。
// app.js App({ globalData: { userInfo: null, categories: [], apiBase: '', cdnDomain: '' }, onLaunch() { const config = require('./config'); this.globalData.apiBase = config.apiBase; this.globalData.cdnDomain = config.cdnDomain; // 静默登录检查 const token = wx.getStorageSync('auth_token'); if (token) { this.checkLoginStatus(); } // 预加载分类 this.loadCategories(); }, checkLoginStatus() { // 调用 /api/user/info,成功则更新 globalData.userInfo }, loadCategories() { // 调用 /api/category/list,结果存入 this.globalData.categories } });注意:
config.js是独立文件,内容如下,你部署时只需改这里:// config.js module.exports = { apiBase: 'https://your-api-domain.com/api', cdnDomain: 'https://cdn.your-domain.com', debug: true };
3. 后端参考实现:Express + SQLite,30 行代码跑通商品接口,不是假数据
很多人以为“完整代码”只指前端,但这份资源的后端部分才是真正让它脱离 demo 级别的关键。它没用 MongoDB 或 MySQL,而是选了SQLite—— 因为小程序开发阶段,你不需要运维数据库,npm start就能跑,且.sqlite文件可直接拖进 DB Browser 查看数据。后端代码在server/目录下,核心就两个文件:server.js(入口)和db.js(数据库操作)。
3.1 db.js:用 sqlite3 原生驱动,建表语句直接写死,不依赖 ORM
db.js只做三件事:连接数据库、执行建表 SQL、封装增删改查。没有用 Sequelize 或 TypeORM,因为会增加新手理解成本。建表语句直接内联,一目了然:
// server/db.js const sqlite3 = require('sqlite3').verbose(); const path = require('path'); const dbPath = path.join(__dirname, 'data', 'shop.db'); const db = new sqlite3.Database(dbPath); // 创建商品表 db.run(` CREATE TABLE IF NOT EXISTS goods ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, price REAL NOT NULL, original_price REAL DEFAULT 0, stock INTEGER DEFAULT 0, image TEXT DEFAULT '', sales INTEGER DEFAULT 0, category_id INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `); // 创建购物车表(模拟用户购物车,实际应关联用户ID) db.run(` CREATE TABLE IF NOT EXISTS cart ( id INTEGER PRIMARY KEY AUTOINCREMENT, goods_id INTEGER NOT NULL, count INTEGER DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) `);参数说明:
sqlite3.Database的第二个参数sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE被省略,因为new sqlite3.Database()默认就是读写+创建;CURRENT_TIMESTAMP是 SQLite 内置函数,无需手动传时间。
3.2 server.js:路由精简到只剩 4 个,每个都带真实返回结构
server.js只暴露/api/goods/list、/api/goods/detail、/api/cart/add、/api/order/create四个接口,全部返回符合小程序前端network.js期望的{ code: 200, data: ..., message: 'success' }结构:
// server/server.js const express = require('express'); const db = require('./db'); const app = express(); app.use(express.json()); app.use(express.urlencoded({ extended: true })); // 商品列表:支持分页(page=1, size=10) app.get('/api/goods/list', (req, res) => { const page = parseInt(req.query.page) || 1; const size = parseInt(req.query.size) || 10; const offset = (page - 1) * size; db.all( `SELECT * FROM goods ORDER BY sales DESC LIMIT ? OFFSET ?`, [size, offset], (err, rows) => { if (err) { return res.status(500).json({ code: 500, message: err.message }); } res.json({ code: 200, data: { list: rows, total: rows.length }, message: 'success' }); } ); }); // 商品详情:根据 ID 查询 app.get('/api/goods/detail', (req, res) => { const id = parseInt(req.query.id); if (!id) { return res.status(400).json({ code: 400, message: 'id required' }); } db.get(`SELECT * FROM goods WHERE id = ?`, [id], (err, row) => { if (err) { return res.status(500).json({ code: 500, message: err.message }); } res.json({ code: 200, data: row, message: 'success' }); }); }); // 加入购物车:插入 cart 表 app.post('/api/cart/add', (req, res) => { const { goods_id, count = 1 } = req.body; if (!goods_id) { return res.status(400).json({ code: 400, message: 'goods_id required' }); } db.run( `INSERT INTO cart (goods_id, count) VALUES (?, ?)`, [goods_id, count], function (err) { if (err) { return res.status(500).json({ code: 500, message: err.message }); } res.json({ code: 200, data: { id: this.lastID }, message: 'added' }); } ); }); // 创建订单:从 cart 表读取,插入 order 表(简化版) app.post('/api/order/create', (req, res) => { // 实际应先查 cart,再扣库存,再清 cart,此处简化为返回 mock 订单号 const orderNo = `ORD${Date.now()}${Math.floor(Math.random() * 1000)}`; res.json({ code: 200, data: { order_no: orderNo, amount: 99.99 }, message: 'success' }); }); app.listen(3000, () => { console.log('Server running on http://localhost:3000'); });逻辑说明:
/api/order/create没真扣库存,是因为库存一致性需分布式事务,对学习项目过重;它返回order_no和amount,足够前端跳转到“支付成功页”。你上线前,把这里换成真实扣减逻辑即可。
3.3 初始化数据:seed.sql 一行命令注入 20 条测试商品
server/data/seed.sql是个纯 SQL 文件,内容就是 20 条INSERT INTO goods (...) VALUES (...);。运行npm run seed(脚本在package.json里定义)就能一键填充:
# package.json "scripts": { "start": "node server/server.js", "seed": "sqlite3 server/data/shop.db < server/data/seed.sql" }血泪经验:别手敲 INSERT!我第一次用
INSERT INTO goods VALUES (1,'iPhone','5999',...);,结果'iPhone'里有单引号,SQL 报错。后来全改成双引号包裹字符串,或用printf生成,才一次成功。
4. 常见问题与避坑指南:这 4 个坑,我替你踩过了
这份代码能跑通,不等于你 clone 下来就一帆风顺。我在某高校实训课带学生实操时,90% 的卡点都集中在这几个地方。下面不是泛泛而谈“注意配置”,而是每条都按「现象 → 原因 → 解决」写清楚,你遇到哪个,Ctrl+F 找就行。
4.1 现象:小程序开发工具报错 “Cannot find module ‘utils/network’”,但文件明明存在
原因:utils/network.js文件末尾少了module.exports = { request };,或者pages/index/index.js里写了const network = require('../../utils/network');但路径多了一层../。
解决:打开utils/network.js,确认最后一行是module.exports = { request };;再打开任意页面的require语句,用开发工具右键“在资源管理器中显示”,看路径是否精准指向utils/network.js。常见错误是require('../../../utils/network')(多了一个..),应改为require('../../utils/network')。
4.2 现象:购物车页面空白,控制台无报错,this.data.cartItems是空数组
原因:cartService.getCartList()发起的请求,后端返回了code: 200但data字段是null,而前端cart.js的loadCartData()没做空值判断,直接res.data.list报Cannot read property 'list' of null。
解决:在cartService.getCartList()的.then()里加一层保护:
// utils/cartService.js getCartList() { return network.request({ url: '/api/cart/list' }).then(res => { // 关键:res.data 可能为 null,必须兜底 const data = res.data || {}; return { list: data.list || [], totalPrice: parseFloat(data.totalPrice) || 0, totalNum: parseInt(data.totalNum) || 0 }; }); }4.3 现象:点击“立即购买”跳转到订单页,但订单页显示“暂无商品”,getApp().globalData.cartItems是空
原因:app.js的onLaunch里loadCategories()是异步的,但pages/order/order.js的onLoad里直接读getApp().globalData.cartItems,此时cartItems还没从后端拉回来(因为cartService.getCartList()在cart.js的onLoad里才调)。
解决:订单页不依赖全局cartItems,而是自己调cartService.getCartList()。把pages/order/order.js的onLoad改成:
onLoad() { cartService.getCartList().then(res => { this.setData({ cartItems: res.data.list || [] }); }); }玄学提醒:别试图在
app.js里用Promise.all([loadCategories(), loadCart()]),小程序启动时getApp()还未完全初始化,容易引发竞态。
4.4 现象:后端npm start启动成功,但小程序调用/api/goods/list返回 404
原因:开发工具默认请求http://localhost:3000,但你的后端监听的是127.0.0.1:3000,而 macOS 或某些 Windows 系统的localhost解析可能被 hosts 劫持,或防火墙拦截。
解决:
- 先在浏览器访问
http://127.0.0.1:3000/api/goods/list,确认能返回 JSON; - 如果浏览器能通,开发工具不通,改小程序
config.js的apiBase为http://127.0.0.1:3000/api; - 若仍不行,在终端执行
sudo lsof -i :3000查看端口占用,或换端口(如app.listen(3001))。
5. 进阶技巧:用 mock.js 拦截请求,不启后端也能调试支付流程
你可能遇到这种情况:后端同学还没写完/api/order/create,但你需要验证“用户点击支付按钮后,页面如何跳转、如何展示订单号、如何清空购物车”。这时候,与其等后端,不如用mock.js在前端拦截请求,返回模拟数据。这份代码已经预留了 mock 开关,你只需三步启用。
5.1 启用 mock:改 config.js 一行,加 mock 目录两文件
打开config.js,把mock: false改成true:
// config.js module.exports = { apiBase: 'https://your-api-domain.com/api', cdnDomain: 'https://cdn.your-domain.com', debug: true, mock: true // ← 就是这行 };然后在项目根目录新建mock/目录,放入两个文件:
// mock/index.js const Mock = require('mockjs'); // 模拟商品列表 Mock.mock(/\/api\/goods\/list/, 'get', { 'code': 200, 'data|20': [{ 'id|+1': 1, 'name': '@ctitle(8,15)', 'price|100-9999.99': 1, 'original_price|100-9999.99': 1, 'stock|1-100': 1, 'image': '@image(200x200, #4A9FF5, #FFF, png, Mock)', 'sales|0-1000': 1 }], 'message': 'success' }); // 模拟创建订单 Mock.mock(/\/api\/order\/create/, 'post', { 'code': 200, 'data': { 'order_no': function() { return 'ORD' + Date.now() + Math.floor(Math.random() * 1000); }, 'amount|99.99-999.99': 1, 'pay_status': 'unpaid' }, 'message': 'success' });// mock/init.js // 只在 config.mock 为 true 时加载 const config = require('../config'); if (config.mock) { require('./index'); }5.2 修改 network.js:在 request 前加 mock 拦截逻辑
打开utils/network.js,在request()函数开头插入 mock 判断:
// utils/network.js const config = require('../config'); if (config.mock) { // 引入 mock 初始化 require('../mock/init'); } function request(options) { // 如果是 mock 模式,且 URL 匹配 mock 规则,则走 mock if (config.mock && options.url.includes('/api/')) { // mock.js 会自动拦截,此处无需额外处理 return new Promise((resolve) => { // 模拟网络延迟 setTimeout(() => { // mock 已注册,直接触发 const mockRes = Mock.mock(options.url, options.method || 'GET'); resolve({ data: mockRes }); }, 300); }); } // 原有 wx.request 逻辑... }参数说明:
setTimeout的300ms是模拟真实网络延迟,避免响应太快导致 loading 状态一闪而过;Mock.mock()的第一个参数是 URL 正则,第二个是方法,第三个是返回模板,完全匹配mock/index.js的写法。
5.3 验证 mock 是否生效:三步断点法
- 在
pages/order/order.js的onLoad里加console.log('order page loaded'); - 在
utils/network.js的request()开头加console.log('request to:', options.url); - 点击“去结算”,看控制台是否先打印
request to: /api/order/create,再打印mock response(如果 mock 生效,wx.request不会执行,mock.js 会直接返回)。
后悔药:如果 mock 导致其他接口异常,立刻把
config.js的mock改回false,重启开发工具——mock 是开关式设计,不影响原有逻辑。
从那以后我每次要验证一个新流程(比如优惠券选择逻辑),都强制走一遍 mock 流程:先写 mock 规则,再改前端调用,最后用真后端替换。这样既不卡进度,又保证了前后端契约清晰。希望帮到你。
本文还有配套的精品资源,点击获取