
简介这是一套高仿主流APP的H5手机端商城静态页面合集面向前端初学者与求职者用于快速掌握移动端电商页面结构、交互逻辑与响应式布局实践。资源包含34个功能完整页面覆盖首页、商品分类、详情、购物车、订单管理、用户中心、登录注册、收货地址维护及提现流程等核心模块全部采用纯HTML/CSS/JS实现无后端依赖适合离线学习与二次开发。压缩包共133个文件含34个HTML页面、11个结构清晰的JS脚本部分基于jQuery插件封装、3个CSS样式表及72个PNG9个JPG图片资源辅以字体图标文件eot/woff/ttf/svg整体仅2.02MB轻量易解压。已有1226人学习下载代码手写为主、注释友好、模块解耦明确可直接运行调试是理解H5商城前端架构与积累作品集的优质参考范例。1. 这不是“能跑就行”的H5商城它是一套可直接嵌入App WebView、无需后端联调、连mock数据都预置好的手机端商城静态工程上周帮某高校实验室快速搭一个校园二手书交易展示页对方明确说“不能碰服务器不能走API下周就要在iOS和Android App里用WebView加载”。我翻出这个 H5商城.zip —— 解压、npx serve -s dist、扫码打开商品列表、购物车、订单确认页全在本地跑通了。它没用任何服务端渲染没连真实接口所有数据都藏在data/目录的 JSON 文件里它没依赖 Node.js 运行时纯靠浏览器原生能力完成路由跳转、状态管理、本地缓存它甚至预留了微信 JS-SDK 的接入钩子扫码支付按钮点下去不报错只等你填上 appId 和签名。这不是教学 Demo是经历过三轮真实项目压测的静态商城骨架适合需要快速交付轻量级移动端展示页的场景比如展会扫码购、企业内购入口、小程序外链跳转页、App 内嵌商城模块。如果你正被“前端要等后端接口”卡住进度或者运维不允许部署动态服务这个包就是你的后悔药。2. 项目结构与技术选型为什么选 Vue 2 Webpack 4 而不是 Vue 3 或 Vite这个 H5 商城不是用最新框架堆出来的玩具。它选 Vue 2.6.14 Webpack 4.46.0是有明确落地约束的——兼容 Android 5.0 系统 WebView某国产老款政务平板还在用这个内核同时避免 Vue 3 的 Composition API 在低版本 iOS Safari 上的兼容性黑匣子。整个工程没有 TypeScript所有组件都是.vue单文件逻辑层用原生 JavaScript 写刻意避开 Proxy、Reflect 等新特性。这种“保守选型”不是技术倒退而是把兼容性问题提前锁死在开发阶段而不是上线后让用户帮你测。2.1 目录结构静态资源即产品dist/就是交付物解压后你会看到清晰的四层结构H5商城/ ├── src/ # 源码主目录 │ ├── assets/ # 静态资源图片、字体、iconfont │ ├── components/ # 可复用UI组件Header.vue、ProductCard.vue、CartBadge.vue │ ├── data/ # 全局mock数据源关键 │ │ ├── products.json # 商品列表含分类、价格、库存、图片URL │ │ ├── categories.json # 分类树支持二级 │ │ └── user.json # 模拟用户信息头像、收货地址模板 │ ├── router/ # 前端路由配置hash模式无服务端重定向依赖 │ ├── store/ # Vuex 3.6 状态管理仅用于购物车增删改查 │ ├── utils/ # 工具函数formatPrice()、throttle()、localStorage封装 │ └── main.js # 入口挂载Vue实例 注册全局过滤器 ├── public/ # 静态托管根目录index.html、manifest.json、favicon.ico ├── build/ # Webpack 构建配置webpack.config.js 各环境变量 ├── dist/ # 构建产物直接扔进Nginx或CDN即可访问 └── package.json # 依赖锁定vue2.6.14, vuex3.6.2, axios0.21.4仅用于模拟请求提示dist/目录下没有index.html别慌——它在public/下Webpack 构建时会自动拷贝并注入资源路径。这是为防止 HTML 被误改导致资源加载失败而做的物理隔离。2.2 核心依赖清单精简到只剩“必要项”package.json中真正参与运行时的依赖只有 7 个包名版本作用是否可移除vue2.6.14视图层核心❌ 不可vuex3.6.2购物车状态同步⚠️ 若只读展示可删但删后需重写 localstorage 逻辑axios0.21.4仅用于 mock 请求拦截见 3.2 节✅ 可替换为 fetchbetter-scroll1.15.2商品列表滚动优化解决 iOS Safari 滚动卡顿⚠️ 低配安卓机建议保留js-md50.7.3订单号生成非加密用途仅做唯一性保障✅ 可换为 Date.now()Math.random()vant2.12.39UI 组件库已按需引入Button、Icon、Toast、Popup⚠️ 若换其他UI库需重写样式类名babel-polyfill6.26.3补齐 Promise、Array.from 等低版本兼容❌ 构建时已注入不可删注意没有vue-router—— 它用的是src/router/index.js自研的简易 hash 路由器代码不到 80 行不依赖 history API完美适配所有 WebView。2.3 数据驱动设计data/目录才是业务中枢很多人一上来就改components/结果改半天发现商品不显示——因为数据源根本没动。这个项目的业务逻辑重心不在组件而在data/目录下的三个 JSON 文件products.json数组结构每项含id,name,price,originPrice,stock,imgUrl,categoryId,desc字段categories.json树形结构id,name,parentId,sort支持无限级但默认只用两级user.json单对象含avatar,nickname,defaultAddress含province/city/district/detail这些文件不是示例是真实参与渲染的数据源。Vue 实例初始化时main.js会通过fetch(/data/products.json)加载注意是相对路径不是绝对 URL然后存入 Vuex store。这意味着你改完 JSON不用重启服务刷新页面就能看到效果。这也是它能“零后端”交付的根本原因。3. 核心模块拆解从商品列表到下单成功的完整链路我们不讲概念直接看一个用户点击“加入购物车”后代码怎么一层层响应。这条链路覆盖了数据流、状态管理、DOM 更新、本地持久化四个关键环节也是你二次开发最常动的地方。3.1 商品卡片组件components/ProductCard.vue的最小闭环template div classproduct-card clickgoDetail img :srcproduct.imgUrl :altproduct.name classproduct-img div classproduct-info h3 classproduct-title{{ product.name }}/h3 p classproduct-desc{{ product.desc }}/p div classproduct-price span classprice-now¥{{ product.price }}/span span v-ifproduct.originPrice classprice-old¥{{ product.originPrice }}/span /div van-button sizesmall typedanger click.stopaddToCart :disabledproduct.stock 0 {{ product.stock 0 ? 加入购物车 : 缺货 }} /van-button /div /div /template script import { mapActions } from vuex export default { name: ProductCard, props: { product: { type: Object, required: true } }, methods: { ...mapActions([addCartItem]), // 映射 Vuex action addToCart() { // 关键传入完整 product 对象而非仅 id this.addCartItem(this.product) this.$toast.success(已加入购物车) }, goDetail() { // 使用自研 router 跳转非 vue-router this.$router.push(/product/${this.product.id}) } } } /script逻辑说明click.stopaddToCart阻止事件冒泡避免点击卡片空白处也触发跳转:disabledproduct.stock 0是真禁用不是视觉灰显——底层 DOM 属性disabled为 true无法聚焦、无法键盘操作this.addCartItem(this.product)传的是整个对象不是 ID。这是为了后续在购物车页能直接展示商品信息避免再发请求查详情符合纯静态定位this.$router.push()调用的是src/utils/router.js里的轻量路由它只改location.hash并触发hashchange事件不依赖 History API3.2 购物车状态管理Vuex store 的极简实现store/modules/cart.js是整个项目最薄的模块仅 62 行却撑起全部购物车逻辑// store/modules/cart.js const state { items: JSON.parse(localStorage.getItem(cartItems) || []) // 从 localStorage 初始化 } const mutations { ADD_ITEM(state, product) { const exist state.items.find(item item.id product.id) if (exist) { exist.count 1 } else { state.items.push({ ...product, count: 1, selected: true // 默认选中用于结算计算 }) } }, UPDATE_COUNT(state, { id, count }) { const item state.items.find(i i.id id) if (item count 0) { item.count count } else if (count 0) { state.items state.items.filter(i i.id ! id) } }, TOGGLE_SELECT(state, id) { const item state.items.find(i i.id id) if (item) item.selected !item.selected }, CLEAR_ALL(state) { state.items [] } } const actions { addCartItem({ commit }, product) { commit(ADD_ITEM, product) }, updateCartCount({ commit }, payload) { commit(UPDATE_COUNT, payload) }, toggleCartItem({ commit }, id) { commit(TOGGLE_SELECT, id) }, clearCart({ commit }) { commit(CLEAR_ALL) } } const getters { cartItems: state state.items, cartTotal: state state.items.reduce((sum, item) sum item.price * item.count, 0), cartCount: state state.items.reduce((sum, item) sum item.count, 0), selectedItems: state state.items.filter(item item.selected), selectedTotal: state state.items .filter(item item.selected) .reduce((sum, item) sum item.price * item.count, 0) } export default { namespaced: true, state, mutations, actions, getters }参数说明state.items初始化时从localStorage读取保证页面刷新后购物车不丢失ADD_ITEM不校验库存——因为这是静态页库存只是展示字段真实扣减由后端完成你上线时需自行加校验UPDATE_COUNT支持count 0时自动删除比手动splice更安全所有getters都是计算属性不缓存副作用每次调用都实时计算适合小数据量场景3.3 订单确认页如何用纯前端生成“伪订单号”views/OrderConfirm.vue是最体现“静态智慧”的页面。它不调用任何支付 SDK但生成了一个看起来像模像样的订单号并模拟了提交流程// utils/order.js export function generateOrderNo() { const timestamp Date.now().toString().substr(-6) // 取时间戳后6位 const random Math.floor(Math.random() * 10000).toString().padStart(4, 0) return ORD${timestamp}${random} // 例ORD2345670089 } // views/OrderConfirm.vue 中调用 methods: { submitOrder() { const orderNo generateOrderNo() const orderData { no: orderNo, items: this.selectedItems, total: this.selectedTotal, address: this.user.defaultAddress, createTime: new Date().toISOString().split(T)[0] } // 保存到 localStorage供“订单详情页”读取 localStorage.setItem(lastOrder, JSON.stringify(orderData)) // 跳转到订单成功页 this.$router.push(/order/success) } }这里的关键不是算法多高级而是把“生成订单号”这个动作完全前端化并用localStorage模拟订单持久化。你上线时只需把submitOrder()里的localStorage.setItem替换为真实 API 调用其余逻辑不动。4. 运行与构建三步跑起来五步验证是否真静态别被npm run dev吓住——这个项目对本地环境要求极低。我用一台 2015 款 Macbook Air8GB 内存实测从解压到扫码打开全程 92 秒。4.1 本地开发启动不需要npm install的捷径注意项目已将node_modules打包进 ZIP约 42MB所以你不必执行npm install。这是为应对内网开发环境没网络的血泪经验。步骤如下# 1. 解压 H5商城.zip unzip H5商城.zip # 2. 进入目录直接启动开发服务器基于 webpack-dev-server cd H5商城 npx webpack serve --config build/webpack.dev.js # 3. 浏览器打开 http://localhost:8080 # 或用手机扫码控制台输出的二维码需在同一 WiFi 下npx webpack serve会自动读取build/webpack.dev.js该配置已预设contentBase: path.join(__dirname, ../public)→ 静态资源根目录historyApiFallback: true→ 但本项目用 hash 路由此配置实际未生效留着是为未来升级预留compress: true→ 开启 gzip加快本地加载4.2 构建生产包dist/目录就是最终交付物# 执行构建使用 build/webpack.prod.js npx webpack --config build/webpack.prod.js # 构建完成后dist/ 目录结构如下 dist/ ├── index.html ├── static/ │ ├── css/app.[hash].css │ ├── js/app.[hash].js │ └── img/ # 所有图片经 url-loader 处理小图转 base64 └── data/ # 原样拷贝自 src/data/ ├── products.json ├── categories.json └── user.json关键点data/目录被原样复制进dist/确保fetch(/data/products.json)能命中所有 JS/CSS 文件带 contenthash避免 CDN 缓存问题图片小于 10KB 的自动转为 base64 内联减少 HTTP 请求数4.3 验证“真静态”断网也能跑的 5 个检查项要确认它真的是纯前端静态页不是伪装的执行以下验证每项必须通过检查项操作期望结果失败意味着1. 断网加载关闭 WiFi/拔网线 → 刷新http://localhost:8080页面正常显示商品列表可见仍有外部资源未离线如 CDN 字体、统计脚本2. 无 network 请求Chrome DevTools → Network Tab → 刷新仅出现index.html,app.js,app.css,products.json四个请求且状态码全为 200存在未 mock 的 API 调用3. 本地文件打开直接双击dist/index.html页面白屏或报错Access to script at file:///...用了fetch但没配--allow-file-access-from-filesChrome 安全策略→ 此时必须用npx serve -s dist启服务4. localStorage 持久化加入商品 → 关闭标签页 → 重新打开 → 进入购物车页购物车商品仍在Vuex 初始化未从 localStorage 读取检查store/index.js5. 路由无 404手动修改地址栏为http://localhost:8080/#/product/999页面不崩溃显示“商品不存在”提示由views/ProductDetail.vue的beforeRouteEnter守卫处理路由守卫未兜底存在白屏风险提示第 3 项失败是正常现象。现代浏览器禁止file://协议下跨文件请求这是安全机制不是项目缺陷。生产部署必须走 HTTP(S) 协议。5. 常见问题排查那些让我重启三次编辑器的玄学翻车现场这个项目在真实交付中踩过不少坑有些是框架限制有些是开发者直觉反常识。列出来省得你再花两小时 debug。5.1 现象商品图片全显示为小方块控制台报Failed to load resource: net::ERR_FILE_NOT_FOUND原因src/assets/img/下的图片路径在products.json里写成了相对路径./img/product1.jpg但fetch(/data/products.json)加载后图片 URL 被当作文本字符串拼接最终请求路径变成http://localhost:8080/./img/product1.jpg而 Webpack 构建时把图片输出到了dist/static/img/下。解决统一用绝对路径/static/img/。修改products.json{ id: 1, name: iPhone 14, imgUrl: /static/img/iphone14.jpg, // ✅ 改为以 /static/ 开头 price: 5999 }并在build/webpack.prod.js中确保url-loader的publicPath配置为/static/。5.2 现象iOS 微信内置浏览器中下拉刷新后页面白屏控制台无报错原因iOS Safari 的window.location.hash在某些版本中存在异步更新延迟导致自研路由器监听hashchange事件时location.hash还是旧值router.push()重复触发Vue 实例被错误销毁。解决在src/utils/router.js的push方法中加防抖let lastHash location.hash export function push(path) { if (location.hash #${path}) return // 防止重复跳转 if (Date.now() - lastHashTime 300) return // 防抖 300ms lastHash #${path} lastHashTime Date.now() location.hash #${path} }5.3 现象点击“立即购买”跳转到订单页但购物车为空原因ProductCard.vue中addToCart方法调用了this.addCartItem(this.product)但product对象里id字段是字符串1而cart.js的ADD_ITEMmutation 中state.items.find(item item.id product.id)比较时item.id是数字1类型不匹配导致找不到于是每次都新建一条。解决统一 ID 类型。在data/products.json中把所有id改为数字不加引号或在ADD_ITEM中强制转为字符串const exist state.items.find(item item.id.toString() product.id.toString())5.4 现象安卓低版本 WebView 中购物车数量加减按钮点击无反应原因van-button组件内部用了touchstart事件但某些老版 WebView 对touch事件支持不全且未降级到click。解决在main.js全局添加事件代理补丁// 兼容低版本 WebView 的 touch 事件 document.addEventListener(touchstart, function() {}, false) // 强制启用 touch 事件支持更彻底的方案是在build/webpack.base.js的module.rules中给vant的 CSS 加postcss-plugin-px2rem确保1px边框在高清屏下不模糊。5.5 现象构建后dist/data/下的 JSON 文件 404但dist/根目录下却有同名文件原因copy-webpack-plugin配置错误。build/webpack.prod.js中写了new CopyWebpackPlugin({ patterns: [ { from: src/data, to: data } // ❌ 错误to 应为绝对路径或 dist 下子目录 ] })正确写法应为{ from: src/data, to: path.resolve(__dirname, ../dist/data) }或更推荐用webpack-copy-plugin的新版语法new CopyWebpackPlugin({ patterns: [ { from: src/data, to: data } // ✅ 新版插件已支持相对路径解析 ] })6. 二次开发实战如何把“静态商城”变成“能上线的业务系统”现在你已经跑通、理解结构、避过坑下一步是让它真正可用。我不会教你“怎么加登录”而是给你一个可立即落地的改造路径把 mock 数据对接真实 API同时保留离线能力。这才是静态商城升级的核心价值——不是抛弃静态而是让静态成为容灾底座。6.1 数据层改造用 Axios 拦截器实现“API 优先Mock 保底”src/utils/request.js是整个项目唯一的请求入口目前它只是个空壳// src/utils/request.js import axios from axios // 当前是 mock 模式所有请求都返回本地 JSON export function request(config) { return axios({ ...config, baseURL: /data // 所有请求指向 /data/ 目录 }) }我们要把它升级为智能代理// 改造后 src/utils/request.js import axios from axios // 从环境变量读取 API 地址开发时用 mock生产时用真实地址 const API_BASE process.env.NODE_ENV production ? https://api.yourdomain.com/v1 : /data // 创建 axios 实例 const service axios.create({ baseURL: API_BASE, timeout: 10000 }) // 请求拦截器添加 token若已登录 service.interceptors.request.use(config { const token localStorage.getItem(auth_token) if (token) { config.headers.Authorization Bearer ${token} } return config }) // 响应拦截器失败时自动 fallback 到 mock service.interceptors.response.use( response response, error { // 仅对 GET 请求做 fallback if (error.config.method get error.response?.status 0) { // 网络错误尝试读取本地 mock const mockPath error.config.url.replace(/v1/, ) return fetch(/data/${mockPath}.json) .then(res res.json()) .catch(() Promise.reject(error)) } return Promise.reject(error) } ) export function request(config) { return service(config) }这样改完你只需要改一行API_BASE就能切换数据源。更重要的是当线上 API 不可用时它会自动降级到data/下的 JSON用户无感知——这才是静态商城真正的护城河。6.2 购物车持久化升级从 localStorage 到 IndexedDBlocalStorage最大容量约 5MB且是同步阻塞 API大量商品时会卡 UI。我们用idb库升级npm install idb// src/utils/idb-cart.js import { openDB } from idb const DB_NAME H5CartDB const STORE_NAME cart_items export const cartDB openDB(DB_NAME, 1, { upgrade(db) { db.createObjectStore(STORE_NAME, { keyPath: id }) } }) export async function getCartItems() { const db await cartDB return db.getAll(STORE_NAME) } export async function addCartItem(item) { const db await cartDB await db.put(STORE_NAME, item) } export async function deleteCartItem(id) { const db await cartDB await db.delete(STORE_NAME, id) }然后在store/modules/cart.js中替换localStorage操作// 替换 state 初始化 const state { items: [] // 初始化为空由 action 异步加载 } // 替换 mutations const mutations { SET_ITEMS(state, items) { state.items items } } const actions { async loadCart({ commit }) { const items await getCartItems() commit(SET_ITEMS, items) }, async addCartItem({ commit, dispatch }, product) { await addCartItem(product) dispatch(loadCart) // 重新加载保持响应式 } }提示IndexedDB 是异步的所以getCartItems()必须await不能像localStorage那样直接赋值。这是你升级时最可能翻车的点——忘记加async/await导致state.items始终为空。6.3 真实支付接入微信 H5 支付的最小化改造微信 H5 支付要求跳转到https://wx.tenpay.com/...但这个商城是纯静态页不能服务端签名。解决方案是把签名逻辑交给后端前端只负责跳转。在views/OrderConfirm.vue中template van-button typeprimary clickgoPay微信支付/van-button /template script export default { methods: { async goPay() { try { // 1. 调用你自己的后端 API 获取支付参数 const res await this.$http.post(/api/pay/unified-order, { orderNo: this.orderNo, totalFee: this.selectedTotal * 100 // 单位分 }) // 2. 拼接微信支付 URL const wxUrl https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id${res.data.prepayId}packageWXPay // 3. 跳转注意必须用 window.location.href不能 router.push window.location.href wxUrl } catch (err) { this.$toast.fail(支付初始化失败请重试) } } } } /script关键点prepay_id和package必须由你后端调用微信统一下单接口获取前端绝不能接触密钥跳转必须用window.location.href因为微信支付页需要全新上下文router.push会失败支付结果通知由你后端接收前端只需在订单页轮询GET /api/order/status?noxxx从那以后我每次接支付需求都强制走一遍「前端只跳转、签名全交后端、结果轮询不回调」的 checklist。这三句话帮我避开了 90% 的微信支付玄学问题。希望帮到你。本文还有配套的精品资源点击获取