简介:从零到一完成一个可面试的Vue实战项目,核心不在于像素级还原UI,而在于理解前端工程化的关键环节。组件拆分决定了代码的可维护性,状态管理则直接影响复杂交互的可控性。以仿美团外卖APP为例,整个项目覆盖了组件通信、路由懒加载、SKU规格选择、购物车持久化、接口Mock与axios拦截器封装等高频技术点。通过合理设计目录结构、统一分页接口返回格式、使用Pinia管理跨组件数据同步,你能在开发过程中逐步建立工程化思维。这类C端项目业务链路完整、面试官认可度高,适合作为前端求职作品。本文从项目选型到调试优化,拆解每个模块的设计依据与踩坑记录,帮助你不仅会写代码,更能讲清楚背后的原理,真正在面试中脱颖而出。 很多前端新手都会卡在同一个问题上:视频看了不少,教程跟了好多套,但让独立做一个完整项目时依然不知道从哪下手。我这两年面试新人,简历里十有八九写着“仿美团外卖”“仿饿了么”,但能扛住追问的不到三分之一。原因很简单——只是照着别人源码敲了一遍,没搞懂背后的模块拆分、状态设计、接口约定这些工程问题。
所以这次我把自己的Vue仿美团外卖APP实战项目从头到尾复盘一遍,不贴全量源码(那没有意义),而是把项目拆成“为什么要这么做、核心链路怎么设计、实际操作中踩了哪些坑、面试时怎么讲”这四个维度。无论你是刚学完Vue基础想找第一个完整项目,还是准备春招秋招需要拿得出手的简历项目,这篇都能直接当参考。
1. 为什么拿“仿美团外卖”开刀:项目定位与选型逻辑
1.1 一个练手项目值多少钱
技术圈有个常见误区:以为项目越“高级”越好,于是去仿后台管理系统、仿电商中台,结果做完发现全是CRUD表格,核心价值没体现出来,简历也写不出亮点。
仿美团外卖这种C端应用恰恰相反。它麻雀虽小五脏俱全,覆盖了前端日常开发里最常遇到的几类场景:定位与城市选择、搜索防抖、无限滚动列表、Tab栏切换、购物车状态共享、规格弹窗、订单流程、用户登录态管理。把这些串起来,等于把Vue的组件通信、路由、状态管理、生命周期、插槽、指令全部实战了一遍。
更重要的是,这类项目面试官都熟。你一说“仿美团外卖”,对方脑子里立刻有一个预期模型,知道应该问你什么,也方便你主动引导话题——这是其他冷门项目比不了的。
1.2 技术栈选型的真实依据
这个项目我用的是Vue 3 + Vite + Vue Router + Pinia + Axios + Sass。选这套组合的原因很简单:
- Vue 3 + Composition API:现在企业新项目基本都切到Vue 3了,组合式API在逻辑复用上比Options API强太多,比如购物车逻辑抽成一个useCart()方法,多个组件共用,代码量直接少一半。
- Pinia而不是Vuex:Pinia对TypeScript支持更好,API更简洁,写起来几乎没有模板代码。Vuex 4虽然也能用,但从学习成本角度讲,Pinia更适合作业周期短的项目。
- Vite而不是Webpack:Vite冷启动速度是秒级的,开发时改代码热更新几乎无感。你要是用Webpack,启动一次等半天,心态容易崩。
移动端样式处理上,我用了rem + flexible方案,视觉稿按750px设计,直接写px,编译时自动转rem。如果你想把项目更快跑起来,也可以直接用viewport单位。
1.3 整体目录结构怎么搭
目录结构不一定要多花哨,但要保证“页面、组件、状态、接口、工具”五层清晰分离。我实际用的结构是这样的:
src/ api/ // 接口请求封装 assets/ // 静态资源 components/ // 公共组件 router/ // 路由配置 store/ // Pinia状态 utils/ // 工具函数 views/ // 页面级组件 App.vue main.js这里有个小建议:不要在views里直接写请求逻辑。很多新人习惯在页面里直接import axios然后调接口,短demo没问题,但页面一多就乱了。统一走api/目录,每个接口函数名字起得清晰一点(如getShopDetail(shopId)),后期维护和面试讲起来都舒服。
2. 首页与店铺列表:组件拆分决定开发效率
2.1 首页的楼层结构
首页是整个App的门面,也是最容易做成“一坨”的地方。我的做法是把它拆成四层,每一层独立成组件:
- 顶部搜索栏:固定在页面顶部,点击跳转搜索页,中间加了一个防抖的实时搜索建议。
- 轮播图:用swiper实现,请求接口拿banner列表渲染。
- 金刚区导航:就是那种一排排图标的快捷入口,比如美食、外卖、超市、药房。这里用grid布局两行,数据从接口返回,图标用字体图标。
- 推荐店铺列表:一个垂直滚动列表,拉到接近底部时自动加载下一页。
轮播图有个特别容易踩的坑:swiper版本不同,初始化方式差异很大。老项目用swiper 4/5是new Swiper('.swiper-container', {...}),新版swiper 8/9是模块化引入,还要单独引入Pagination等模块。如果你直接复制网上旧代码,大概率白屏半天才发现是版本问题。
2.2 店铺卡片组件的取舍
店铺卡片是首页高频复用的组件,我做成了ShopCard.vue,接收一个shop对象,展示店名、评分、月售、起送价、配送费、距离、活动标签。这里有三种实现思路,我实际对比下来差异很大:
| 方案 | 优点 | 缺点 | 结论 |
|---|---|---|---|
| 写死在首页组件里 | 简单直接 | 无法复用,代码爆炸 | 不推荐 |
| 抽取ShopCard组件,props传值 | 复用性好,逻辑清晰 | 需要预先设计props结构 | 推荐 |
| 用render函数动态生成 | 灵活,适合复杂场景 | 难维护,模板可读性差 | 不推荐 |
我在实际项目中采用第二种。props传值时有个细节:美团接口返回的店铺活动是个数组,每条有type(枚举值:满减、折扣、新客立减等)和content(描述文案)。页面上需要用不同的背景色标识不同类型。这个映射逻辑放在ShopCard内部用computed处理,而不是在父组件里拼好再传,这样卡片保持高内聚,其他页面拿过来直接用。
2.3 骨架屏和滚动加载的体验优化
首页接口慢的情况下,直接白屏会让人觉得App卡死。我在店铺列表和商品列表里加了骨架屏,不是UI框架那种复杂库,就是自己写个简单Skeleton组件:放几张灰色的占位块,等接口返回后给一个v-if切换。
列表滚动我用的是onMounted里绑定window的scroll事件,计算scrollTop + clientHeight >= scrollHeight - 200时触发加载更多。这里有一个很多人忽略的性能问题:scroll事件触发频率极高,必须做节流。我用throttle(fn, 200)处理了一下,下拉加载瞬间的重复请求问题也顺手解决了。
如果不做节流,快速滑动时可能一次性发出七八个重复请求,接口被刷爆不说,页面还会出现重复数据。
3. 点餐页与购物车:状态管理才是这个项目的心脏
3.1 双栏联动的页面设计
点餐页是仿美团外卖里最有技术含量的一页,也是面试官最爱深挖的一页。它分成左栏(分类列表)和右栏(商品列表),左右滚动需要联动。经典做法是:
- 左侧分类列表:监听右侧滚动位置,根据当前滚动到的商品所属分类,高亮左侧对应的分类项。
- 右侧商品列表:点击左侧某个分类,右侧滚动到对应分类的锚点位置。
右侧滚动定位我用的是scrollIntoView,简单直接。但这里有个坑——scrollIntoView会滚动到浏览器窗口的最近滚动祖先,如果你的外层容器不是真正的滚动容器,它会带着整个页面一起滚,表现很诡异。
我当时排查了半天才定位到是滚动容器嵌套问题。解决方案是给右侧列表设overflow-y: auto,并且给要滚动的商品分组元素加id,然后:
document.getElementById(`category-${index}`).scrollIntoView({ behavior: 'smooth', block: 'start' })如果是复杂项目,建议用better-scroll这类库处理,它内部对滚动容器做了很完善的兼容处理,省去不少踩坑时间。但要注意,使用库也要想清楚它的滚动原理,否则出了问题更不好排查。
3.2 购物车状态的数据结构设计
购物车是这个项目最核心的状态,我的做法是用Pinia维护一个cart模块,核心数据结构长这样:
{ shopId: '12345', // 当前购物车所属店铺 items: { 'sku_001': { // 商品SKU作为key,方便查找 productId: 'p001', skuId: 'sku_001', name: '原味奶茶', price: 12, count: 2, specs: { '温度': '少冰', '糖度': '五分甜' }, image: 'xxx.jpg' } }, totalCount: 2, totalPrice: 24 }这里有几个需要认真思考的问题:
为什么用对象而不是数组?购物车变动非常频繁(加、减、改规格),如果用数组,每次要find去遍历,复杂度O(n);用对象以skuId为key,增删改都是O(1),性能更好,代码也简洁。渲染时再用Object.values(cart.items)转成数组,完全够用。
加购逻辑怎么写?每次加购时先判断skuId是否存在,存在就count+1,不存在就新建一条。同时要判断当前商品的库存上限,加到上限后Toast提示。
购物车携带店铺信息做什么?美团这类App的购物车是有“店铺归属”的,切换店铺后原购物车清空。用shopId字段做隔离,后续加购物车商品时先判断shopId是否一致,不一致就先清空再添加。这个逻辑经常被初学者漏掉,但它在真实业务里非常重要。
要不要存localStorage?我的答案是:要。用户加了几样商品,切到别的页面再回来,购物车状态还在,体验会好很多。用store.$subscribe监听变化,自动写入localStorage,刷新后重新hydrate,核心代码就几行,收益却很大。
3.3 规格弹窗与SKU选择的细节
点餐时选规格(份量、温度、糖度)是一个典型的SKU场景。我做了一个半屏弹窗组件,点商品时先判断hasSpecs字段,没有规格就直接加购,有规格才弹出选规格。
规格选择里的坑主要在两个地方:
一是规格联动。比如选了“大杯”,糖度选项里可能有几个不可选(因为大杯默认无糖);选了“热饮”,冰度选项直接置灰。这种联动的核心是配置规则,我在项目里用了一个简化方案——每个商品配置一个disables字段,描述哪些SKU组合不可选,组件内部维护一个selectedSpecs对象,每次选择后遍历规则校验,不可选的置灰。
二是弹窗滚动穿透。蒙层弹窗打开后,背后的页面依然可以滚动,体验很差。解决方法是给body加overflow:hidden,关闭时去掉。要在onMounted和onUnmounted里分别处理,别写成全局一次性就完事。
4. 接口请求与Mock数据:没有后端也能跑通完整链路
4.1 为什么用json-server + Mock.js组合
真实面试里很多项目死在“没有后端接口”上,前端只能写死数据。我建议的做法是:本地起一个json-server模拟RESTful接口,再用Mock.js生成随机数据。
json-server:读取一个db.json文件,自动生成/api/shops、/api/shops/:id、/api/products这类RESTful接口,支持分页参数_page和_limit。Mock.js:负责随机生成店名、评分、月售、商品图片等数据,避免你手动造几十条假数据。
这样做的好处是,你的项目从开发第一天起就是“走接口”的,换到真实后端时只需要改axios的baseURL,其他业务代码一行不用动。很多小公司前端就是这么跟后端联调的,你提前熟悉这套流程,入职就能直接上手。
4.2 axios封装与拦截器的必要性
如果只用axios默认配置,请求/响应处理逻辑会散落在各个页面。我封装了一个request.js,核心做了三件事:
- baseURL统一配置:开发环境指向JSON Server,生产环境指向真实域名。
- 请求拦截器:自动从localStorage读取token,加到请求头
Authorization字段。 - 响应拦截器:统一解包数据,只返回
res.data.data;捕获403、500等状态码,统一弹Toast提示并跳转登录页。
有人会觉得“这点东西封装个啥”,但这不是炫技,而是工程习惯。你的项目如果每个页面都自己处理错误提示、自己存token,面试官问“并发请求里一个token失效,其他请求怎么办”时,你只能说“不知道”——因为你在真实项目里没踩过这个坑。
4.3 接口数据结构设计的经验谈
前端做Mock时最容易忽略的就是数据结构设计。我一开始犯过懒,直接把商品列表返回成数组,后面加规格、加活动时发现没法扩展,被迫重构了一次。
建议所有列表接口统一返回分页结构:
{ code: 0, message: 'success', data: { list: [], total: 100, page: 1, pageSize: 10 } }单个实体接口统一返回:
{ code: 0, message: 'success', data: { ... } }这样做的价值是,无论ShopCard还是订单列表、搜索结果,拿到数据后的处理逻辑完全一致,代码高度统一,不容易出bug。结构设计的功夫花在前面,后面能帮你省两三天的联调时间。
5. 从能跑到能面试:调试、性能与项目包装的差距
5.1 开发调试中的几个必查项
项目能跑起来只是第一步,开发调试过程中的规范性决定了你上线后会不会被线上bug折磨。我从这个项目里学到最痛的一课是移动端点击延迟和300ms点击穿透。PC端预览时一切正常,一放到手机真机预览,点店铺卡片经常没反应,要么点一下弹了两次。
解决方案是引入fastclick(或使用touchstart事件配合处理)。现在是2026年了,大部分现代浏览器已经修复了这个300ms延迟问题,但如果你还在用老版本内核的WebView,那就必须处理。
另一个必查项是iOS安全区。iPhone X以后的机型底部有Home Indicator,页面底部如果放了一个“去结算”固定按钮,就会被Home Indicator挡住一块。处理方式是用viewport-fit=cover + env(safe-area-inset-bottom):
.settle-bar { padding-bottom: env(safe-area-inset-bottom); }这个细节在PC上根本看不出来,但真机一打开就非常明显,面试时主动提这个,很能体现你做过移动端适配。
5.2 顺手能做掉的性能优化
我在这个项目里做了几处不复杂但很加分的性能优化:
- 路由懒加载:
component: () => import('@/views/Home.vue'),首屏只加载首页需要的JS,其他页面按需加载。Vite天然支持,收益很大,一行代码的事。 - 图片懒加载:店铺列表和商品列表的图片,用
v-lazy指令,滚动进入视口才加载。用VueUse的useIntersectionObserver也行,本质都是IntersectionObserver。 - 列表项复用:
v-for渲染时一定要带:key,且key要用唯一稳定的值,不要用index。用了index做key,列表中间插一条数据时,Vue复用了错误的DOM节点,会出现输入框内容串位、图片闪烁等诡异问题。 - 计算属性缓存:购物车总价、总数量这类派生数据用
computed而不是在模板里调方法。模板里调方法每次渲染都会重新计算,computed有缓存,性能好得多。
最后一个组件级缓存:首页和点餐页之间来回切换时,希望页面滚动位置不丢失,我给这两个路由页面包了一层<keep-alive>。但注意加缓存后,onMounted只在第一次进入时触发,后续进入需要监听onActivated来刷新数据。这里踩过一次坑——缓存了详情页,结果每次进去都是旧数据,后来在onActivated里重新拉接口才解决。
5.3 面试时项目经验怎么讲
项目做完了,简历也写了“Vue仿美团外卖APP”,但面试时不能只扔出去一句“我用Vue写了一个仿美团的外卖项目”。面试官追问的第一个问题八成是“这个项目的难点是什么?你怎么解决的?”
我的建议是准备两到三个深度技术点,每个都能讲两分钟以上。这个项目里我挑的点是:
- 购物车模块的状态设计:为什么用对象不用数组、怎么处理店铺隔离、怎么做localStorage持久化。这个点能体现你数据结构设计和状态管理能力。
- 双栏滚动联动:滚动容器怎么定位、scrollIntoView的坑、为什么不用复杂库。这个点能体现你对DOM和布局的理解。
- 移动端适配处理:rem/flexible、安全区适配、click 300ms延迟。这个点能体现你做过真实移动端兼容。
还有一个实战建议:把项目的Git提交记录留好。面试官问“你项目开发了多久”时,如果你说“两周”,但Git记录只有五六条提交,对方很容易怀疑项目含金量。养成按功能点拆分提交的习惯,每次提交写清楚message,比如“feat: 完成点餐页双栏联动”“fix: 修复购物车店铺切换数据残留”,这本身就是工程素养的体现,简历和面试都会用到。
最后再分享一个小技巧:仿项目的精髓不是复制像素级还原,而是把业务链路跑完整。美团外卖里最难的不是UI,是“点餐页加了购物车,购物车角标要同步更新;商品详情页改了规格,列表页的SKU信息也要跟着变;下单成功后购物车自动清空”——这些跨组件状态同步的思路,才是你做完这个项目后真正沉淀下来的核心资产。能把这些说清楚,这个仿项目的价值就完全体现出来了。
本文还有配套的精品资源,点击获取