简介:面向移动端开发学习者的一份 Vue2 电商实战资源,以智慧商城为完整业务场景,覆盖组件化开发、响应式布局、接口封装与状态管理等多个核心议题,适合有一定前端基础、希望提升工程化能力的读者。资源压缩包约四十点八九兆,共二千零二个文件,其中包含一千一百一十一个 Markdown 说明文档、七百零八个 JavaScript 逻辑文件、一百七十二个 JSON 配置文件,并附有少量 HTML 与文本辅助材料,目录按功能模块划分,便于逐层阅读和代码复现。已有三百七十九人学习下载,具备不错的参考热度。项目重点展现了组件库的按需引入、视口单位自适应方案、请求与本地存储的二次封装、嵌套路由与导航守卫、路由跳转传参以及分模块管理等实用技巧;这些内容既可用来对照练习,也能作为移动端课程设计或毕业设计的基础模板。 做移动端电商项目,和做PC端商城完全是两种体验。这句话我重复了无数次,但真到自己带着团队从零搭一个智慧商城移动端项目的时候,才发现很多坑不踩一遍根本写不出经验。这篇博文就是对这个项目从技术选型、适配方案、核心业务模块到性能优化、线上排错的完整复盘。全部是实际跑过的流程,不是PPT级别的规划。如果你正在做移动端商城H5、Web App,或者打算把PC商城往移动端迁移,这篇可以直接对照着用。
1. 智慧商城的技术选型:不追最新,只追最稳
1.1 技术组合的取舍逻辑
项目启动时,团队里争论最多的就是技术栈。有同事提议用uni-app,一套代码同时产出H5、小程序和App;也有同事建议上React Native,理由是性能接近原生。最后我们锁定的方案是:Vue 3 + Vite + Pinia + Vant 4,配axios做请求层,配postcss-px-to-viewport做适配。
为什么这么选?核心原因是"智慧商城"这类项目的本质是重业务、轻交互创新。它的主战场是商品浏览、搜索、购物车、订单结算这些标准链路,UI组件库的成熟度比跨端能力更重要。Vant本身就是移动端组件库的头部选手,弹层、轮播、地址选择、支付密码框这些电商高频组件开箱即用,比uni-app的生态更聚焦。React Native则卡在排期上——团队没有人写过RN,现学现卖的成本比多打包一个H5要贵得多。
另外,我们用Vite替代了vue-cli。开发环境下热更新速度是碾压级的,项目启动从原来的十几秒降到两三秒,这对日常联调体验的提升非常明显。Vite在Webpack之后成了社区主流,生态已经足够成熟,不是冒险。
1.2 目录结构与模块解耦
工程化不是为了好看,是为了让不同模块的改动互不踩踏。我们按业务域划分目录,核心结构长这样:
src/ ├── api/ # 接口层,按模块拆分 │ ├── goods.js │ ├── cart.js │ └── order.js ├── assets/ # 静态资源 ├── components/ # 通用组件 │ ├── GoodsCard/ │ ├── Skeleton/ │ └── PriceText/ ├── composables/ # 组合式函数 ├── router/ ├── stores/ # Pinia状态 ├── styles/ # 全局样式与适配变量 ├── utils/ # 工具函数 └── views/ # 页面 ├── home/ ├── goods/ ├── cart/ └── order/一个容易被忽略的点是composables目录。我们把"加购物车的飞入动画""倒计时抢购""列表分页加载"这类有状态逻辑的代码抽成hooks,页面组件只负责组装,可测试性高很多。后期需求迭代,商品详情页要加"加入购物车后弹出推荐商品",只改一个composable就能全局生效,不用在十几个页面里翻来翻去。
2. 移动端适配方案:从rem到vw的进化实录
2.1 适配方案的横向对比
移动端适配是每个做移动端项目的人绕不开的第一课。现在市面上主流方案就三种:rem(配合flexible.js)、vw/vh、以及PostCSS插件自动转换px到vw。
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| flexible.js + rem | 根据屏幕宽度动态设置html字号 | 兼容性好,老项目用得多 | 需要引入JS,依赖window.onresize,过高频重绘 |
| 纯vw方案 | 以视口宽度的1%为单位 | 纯CSS解决,无JS依赖 | 边界case多,1px边框和字体大小需特殊处理 |
| postcss-px-to-viewport | 编译期把px转vw | 开发者写px,构建自动转换 | 第三方库内部样式可能被误转换,需配置忽略 |
我最终选的是第三种。理由很朴素:团队写代码时不需要考虑设计稿除以多少的问题,UI给750的设计稿,我们直接按标注写px,构建工具自动处理。转换配置在postcss.config.js里:
module.exports = { plugins: { 'postcss-px-to-viewport': { viewportWidth: 750, // 设计稿宽度 unitPrecision: 5, // 转换后保留位数 viewportUnit: 'vw', selectorBlackList: ['.ignore-'], // 忽略指定类名 minPixelValue: 1, // 小于1px不转换 mediaQuery: false } } }2.2 iOS与安卓的差异化边界处理
适配方案选好只是开始,真正的坑在真机差异。这里分享三个我们实际处理过的case。
第一个是100vh的问题。iOS Safari的地址栏会随着滚动收起和展开,导致100vh不等于可视区高度,页面底部footer要么被地址栏挡住,要么空出一大截。我们的对策是给底部TabBar和结算栏使用env(safe-area-inset-bottom),页面容器用100dvh兜底,兼容写法是:
.container { height: 100vh; /* 兜底 */ height: 100dvh; /* 支持动态视口的浏览器用这个 */ }第二个是1px边框问题。在Retina屏幕上,CSS的1px实际渲染成2px或3px,商品卡片之间的分隔线会显得粗笨。我们封装了一个hairline工具类,用伪元素配合transform: scaleY(0.5)实现真正的物理1px。
第三个是设计稿标注与真机视觉偏差。750设计稿在iPhone SE这类小屏设备上,所有元素会等比缩小,阅读起来偏吃力。后来我们在全局样式中给最小字号做了兜底:正文不低于12px,价格不低于14px,避免在窄屏上出现蚂蚁字。
3. 商城核心业务模块的落地细节
3.1 商品列表:无限滚动、骨架屏与懒加载三件套
商品列表是商城流量最集中的页面,任何卡顿都会被放大。我们用了"初始骨架屏 + 滚动分页 + 图片懒加载"的组合。
骨架屏不是简单的转圈loading,而是根据商品卡片的真实布局画出来的灰色占位块。Vant提供了Skeleton组件,我们基于它封装了双列商品卡片骨架。用户在等待首屏数据的时候看到的是和真实页面几乎一样的轮廓,体感上会快很多。实测数据上,加了骨架屏后首屏可交互时间虽然没变,但用户跳出率降了约11%,这就是感知性能的价值。
无限滚动用的是IntersectionObserver而不是scroll事件监听。scroll事件在移动端触发频率极高,每次触发都要计算是否到底部,容易造成掉帧。IntersectionObserver是浏览器原生API,可以异步观察元素是否进入视口,性能消耗小一个量级。底部放一个占位元素,进入视口就拉下一页:
const loadMoreObserver = new IntersectionObserver((entries) => { if (entries[0].isIntersecting && !loading.value) { page.value += 1 fetchGoods(page.value) } }) loadMoreObserver.observe(loadMoreEl.value)图片懒加载用的Vant内置的Lazyload指令,它会自动处理图片进入视口前的占位和进入后的加载。需要注意设置loading占位图,避免图片未加载时页面布局塌陷。
3.2 购物车状态:从组件各自为政到Pinia统一管理
购物车是电商项目里状态管理最复杂的模块。它的特点是:多个页面会读写购物车数据(商品列表页加购、详情页加购、购物车页修改数量、结算页读取),而且数据需要持久化,刷新不能丢。
我们早期用的是ref + localStorage各页面自己管理,很快就出了问题:A页面改了购物车,B页面拿到的是旧数据。后来全部收归Pinia,用一个cartstore统一维护:
export const useCartStore = defineStore('cart', { state: () => ({ items: JSON.parse(localStorage.getItem('cart') || '[]') }), getters: { totalCount: (state) => state.items.reduce((sum, item) => sum + item.count, 0) }, actions: { addItem(goods) { const exist = this.items.find(item => item.id === goods.id) if (exist) { exist.count += 1 } else { this.items.push({ ...goods, count: 1 }) } this.persist() }, persist() { localStorage.setItem('cart', JSON.stringify(this.items)) } } })登录状态下,购物车还要和服务端同步,防止用户换设备后购物车丢失。我们的策略是:未登录时本地保存,登录成功后调用mergeCart接口把本地数据合并到服务端,合并完成再拉取服务端最新列表覆盖本地。这个顺序不能反,否则容易把用户之前的服务端购物车覆盖成空数据。
3.3 订单结算与支付交互中的安全细节
支付环节我们是模拟支付(走的是沙箱环境),但交互层完全按真实场景来做。这里有两个容易被忽视的细节。
一是支付弹层的滚动穿透问题。iOS上弹层弹出后,底层页面依然可以滚动,体验极差。解决方式是给body加overflow: hidden,同时在弹层关闭时移除。但Android上部分WebView对此不敏感,还得在弹层内容区捕获touchmove事件并preventDefault。
二是支付结果的轮询逻辑。用户支付成功后,服务端回调可能延迟几秒,前端不能等回调才跳转订单页。我们的处理是支付成功后立即跳转订单详情,同时前端每2秒轮询一次订单状态,页面上显示"支付确认中"的动画。轮询最多进行5次,超过后提示用户查询订单列表。这个方案的体验远好于让用户干等回调。
4. 移动端性能优化:首屏提速与滚动流畅度
4.1 首屏加载的组合拳
智慧商城的首页包含轮播图、分类导航、推荐商品流、活动入口等多个模块,接口数量多。如果不干预,首屏会变成"串行请求——全部返回——渲染"的长流程,用户等得心焦。
我们的第一招是路由级代码分割。Vite天然支持动态import,把首页、详情页、购物车页拆成独立chunk,只有访问对应路由才加载对应JS。首页初始JS体积从480KB降到了260KB左右。
第二招是接口并行与关键请求优先。首页所有模块的接口并发请求,不做串行依赖。但轮播图和推荐商品接口是优先级的,用Promise.allSettled等待这些核心数据到位就渲染第一帧,其他模块(如活动入口、排行榜)数据到位后二次渲染,用Vant的Skeleton占位。
第三招是图片压缩和CDN分发。电商项目的图片占流量大头,我们统一接入了图片处理服务,压缩质量参数设为75%,格式优先返回WebP。实测首屏图片总大小从1.8MB降到680KB。对于不支持的浏览器,服务端根据Accept头自动降级返回JPEG。
4.2 滚动流畅度的调试心得
列表滚动卡顿不是JS的问题,大部分是"强制同步布局"在作祟。移动端每滚动一帧,浏览器都要重新计算布局,如果JS在滚动过程中频繁读取offsetTop、offsetHeight这些会强制刷新的属性,就会造成额外开销。
我们的优化原则是:动画只动transform和opacity,不动top、left、width、height。加购的飞入动画就是用transform实现的,用requestAnimationFrame驱动,避免在滚动监听里同步改样式。
另外一个常被忽略的点是列表项的数量。首页推荐流做了无限滚动,如果不加限制,DOM节点会无限增长。我们在滚动到底部时做了分页上限,最多渲染50条,超出部分用虚拟滚动裁掉视口外的节点。虚拟滚动不复杂,核心是计算可视区高度、每行高度和滚动偏移量,然后只渲染可视区对应的若干行。Vant的List组件支持offset参数控制加载时机,配合固定高度卡片,基本够用。
5. 上线后的排错链路与经验沉淀
5.1 一次真机白屏的完整排查过程
项目上线第二天,运营反馈有一部分安卓用户打开首页直接白屏。奇怪的是我们测试机全部正常,iOS也正常,报错的设备集中在安卓7.0以下的低版本机型。
排查链路是这样的:第一步,通过友盟的JS错误监控看到报错信息是SyntaxError: Unexpected token ?。第二步,定位到是可选链操作符?.在低版本WebView中不被支持。理论上Vite默认的build.target是'modules',也就是只兼容支持ES模块的浏览器,低版本安卓WebView直接被排除在此范围之外。
我当时的处理方案有两个,选了组合拳:
// vite.config.js export default defineConfig({ build: { target: ['es2015'] } })同时把@vitejs/plugin-legacy加上,它会自动为不支持ES模块的浏览器生成降级包。重新构建后,这个方案的兼容包大约增加了40KB,但换来了安卓5.0以上所有机型正常访问。
第一步定位最重要:如果当时没有错误监控,或者错误监控没有自动记录SyntaxError,光靠真机人工复现会浪费一整天。
5.2 接口层防抖、去重与Token刷新
商城页面交互频繁,搜索框输入、筛选条件切换都会触发接口请求。不做处理的话,用户每敲一个字就发一个请求,后端扛不住,前端也会因为响应顺序错乱而展示旧数据。
我们在axios拦截器里做了两层防护:请求去重和响应竞态处理。同一个接口在短时间内重复发起时,直接复用第一次的Promise,丢弃多余请求。响应阶段,每次请求带上时间戳,如果响应返回时这个请求已经不是最新的,则丢弃结果。
const pendingMap = new Map() function getRequestKey(config) { return `${config.method}:${config.url}:${JSON.stringify(config.params || {})}` } axios.interceptors.request.use((config) => { const key = getRequestKey(config) if (pendingMap.has(key)) { config.cancelToken = new axios.CancelToken((cancel) => cancel('重复请求已拦截')) } else { pendingMap.set(key, true) } return config })Token刷新是另一个经典场景。移动端用户长时间停留在App内,access_token过期后要自动用refresh_token换取新token,而中途并发请求的多个接口如果同时遇到401,会触发多次刷新,造成token互相覆盖。我们在拦截器里对刷新操作做了单例处理:第一个401触发刷新,后续401等待同一个刷新Promise完成后重放请求。这个小细节能避免掉线上大面积的登录态丢包问题。
6. 最后说点实际的话
这个智慧商城项目从立项到上线,前后整整三个月。最大的体会是移动端项目的成败不取决于炫酷的技术,而取决于对细节的把控——适配边界、滚动体验、网络异常兜底、低端机兼容,每一个都直接影响用户留存。
再分享一个我个人很后悔没有早做的事:从项目第一天就接入错误监控和性能监控。白屏问题、接口报错、首屏加载耗时,这些数据在开发和测试阶段很难系统性暴露,只有真实的线上用户能告诉你答案。有了监控数据,每次优化都能用数字说话,而不是靠"感觉快了"。
如果你也在做类似的移动端电商项目,希望这篇能帮你少走点弯路。适配方案、购物车状态管理、列表优化这几块,照着我上面写的思路落地,基本能覆盖掉80%的常见问题。剩下的20%,就靠你在真实用户环境里去踩、去补了。
本文还有配套的精品资源,点击获取