news 2026/9/13 8:20:35

Vue3构建购物商城网站源码全流程实战解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue3构建购物商城网站源码全流程实战解析

简介:这是一套基于Vue.js开发的完整购物商城网站源码,面向前端初学者与中小型项目开发者,旨在解决电商类Web应用快速搭建与功能复用问题。资源包含登录注册、首页、商品列表、商品详情、购物车等核心模块,代码结构清晰、功能独立、上手门槛低,可直接运行预览或按需抽取组件集成到自有项目中。压缩包共2000个文件,主体为1596个JavaScript逻辑文件、190个Markdown说明文档、181个JSON配置与数据文件,辅以少量CSS样式、HTML模板及XML资源,整体体积27.59MB,便于本地部署与学习调试。已有26466人下载学习,配套博文提供效果演示链接,涵盖页面交互逻辑、路由配置、状态管理实践及常见样式处理方案,特别适合巩固Vue基础语法、理解单页应用架构与电商场景下的工程化组织方式。 说个挺有意思的现象:前端圈子里拿来练手的实战项目,十个里有八个是商城。不管是找工作写简历,还是公司内部带新人,几乎都绕不开商品列表、购物车、下单结算这一整套逻辑。我自己这几年带着团队写 Vue 项目,商城源码前后至少搭过三套,每次重写都能发现以前没注意到的细节。这篇就以“VUE实现购物商城网站源码”为线索,把从技术选型、项目骨架、交易链路,到前后端联调和部署上线的完整思路理一遍,顺便把开发过程中真正踩过、也在各种热搜关键词里反复出现的坑一起讲透。

这篇文章适合谁?适合刚学完 Vue 基础、想找一个完整项目练手的前端开发者,也适合准备做毕设或者给公司搭内部商城原型的朋友。你在里面能看到的不只是代码,还有每一个设计决策背后的理由。比如为什么很多人一上来就卡在“购物车状态不知道放哪”“keep-alive 缓存后页面乱跳”“el-table 滚动位置回不去”这类问题上,其实都是没把 Vue 的运行机制真正吃透。

1. 为什么拿 Vue 写商城,值得每个前端认真做一遍

先说一个反直觉的结论:商城看起来很简单,无非是展示商品、加入购物车、下单,但真正动手写的时候,你会发现自己被迫把 Vue 的绝大部分核心知识点都用了一遍。这正是它适合做练手项目的原因。

1.1 一个商城页面背后藏了多少基本功

做个不完整的清单你就明白了:

  • 组件通信:商品卡片、数量选择器、购物车角标之间需要通信,props、emit、provide/inject、状态管理全会用到。
  • 路由设计:首页、列表页、详情页、购物车页、结算页、订单页,动态路由传参是基础,路由守卫做登录鉴权。
  • 生命周期:详情页进入时拉数据、离开时清理定时器、用 keep-alive 缓存列表页状态。
  • 计算属性与侦听器:购物车总价计算、搜索防抖、库存变化监听。
  • 状态管理:用户信息、购物车列表、收货地址,这些跨页面共享的数据不能放在组件内部。
  • 前后端交互:Axios 封装、拦截器、Token 鉴权、接口错误处理。
  • 构建部署:路由懒加载、打包优化、Nginx 刷新 404 处理。

这些东西分开看每一个都不难,但放到一个项目里串起来,你就会发现“会写 demo”和“能写出一个可上线的商城源码”之间隔着一条很深的沟。我在带新人的时候经常说:能把商城从零写到能跑通下单流程,Vue 就算入门了。

1.2 技术栈选型:Vue2 还是 Vue3,要不要上 TypeScript

很多读者拿到一个“VUE实现购物商城网站源码”的标题,第一反应是问:我用 Vue2 还是 Vue3?

我的建议是直接上 Vue 3。这不是追新,而是现实:Vue 2 在 2023 年 12 月 31 日已经正式停止维护,新项目再用 Vue2 等于给自己埋坑。而且 Vue 3 的 Composition API 在处理商城这种“多业务逻辑组合”的场景下,比 Options API 舒服太多。比如购物车的商品列表、选中状态、总价计算,用 setup 函数组织逻辑,数据和操作内聚在一起,代码可读性会高很多。

配套的技术栈,我列了一个常用组合,也是这几年来团队新建商城项目的标准配置:

模块选型说明
构建工具Vite开发启动快,热更新体验远好于 Webpack
语言JavaScript / TypeScript小项目用 JS,多人协作建议 TS
路由Vue Router 4适配 Vue3 的正式版本
状态管理Pinia官方推荐,比 Vuex 更轻量,TS 支持更好
UI 组件库Element Plus / VantPC 端选 Element Plus,移动端选 Vant
HTTPAxios拦截器做鉴权和错误处理最方便

这里要特别说一句:如果是从网上找的开源商城源码,看到还是 Vue2 + Vuex + Webpack 的老组合,不是说不能参考,但建议自己动手升到 Vue3。升级过程本身就是一次很好的学习。

2. 从零搭源码骨架:路由、状态管理与目录设计

很多新手拿到“购物商城网站源码”之后第一件事是找功能代码,但我建议先看目录结构。目录结构反映的是项目的组织思路。一个商城源码如果目录乱,后面加需求、修 bug 都会非常痛苦。

2.1 一个清晰的 src 目录应该长什么样

这是我在商城项目里长期使用的一套结构,也直接体现在源码里:

src/ ├── api/ # 接口请求 │ ├── request.js # axios 实例封装 │ ├── product.js # 商品相关接口 │ ├── cart.js # 购物车相关接口 │ └── order.js # 订单相关接口 ├── assets/ # 静态资源 ├── components/ # 公共组件 │ ├── ProductCard.vue # 商品卡片 │ ├── SkuSelector.vue # 规格选择器 │ └── CartBar.vue # 底部购物车栏 ├── router/ │ └── index.js # 路由配置 ├── stores/ │ ├── cart.js # 购物车状态 │ ├── user.js # 用户状态 │ └── app.js # 全局 UI 状态 ├── utils/ # 工具函数 ├── views/ # 页面组件 │ ├── Home.vue │ ├── ProductList.vue │ ├── ProductDetail.vue │ ├── Cart.vue │ ├── Checkout.vue │ └── OrderList.vue ├── App.vue └── main.js

api 目录集中管理接口,组件里不直接写请求路径。这个习惯特别重要。我以前见过一个项目,接口地址散落在各个页面组件里,后来后端改了路径前缀,差点把开发者逼疯。集中管理之后,改一个文件就完事。

2.2 路由设计:从首页到订单的全链路

商城页面的路由之间有一条清晰的业务链路:用户逛首页 → 进入列表页筛选 → 点进详情页 → 加入购物车 → 去结算 → 登录 → 下单 → 查看订单。设计路由时要让这条链路顺畅。

// src/router/index.js import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/', name: 'Home', component: () => import('@/views/Home.vue'), meta: { title: '首页' } }, { path: '/products', name: 'ProductList', component: () => import('@/views/ProductList.vue'), meta: { title: '商品列表', keepAlive: true } }, { path: '/products/:id', name: 'ProductDetail', component: () => import('@/views/ProductDetail.vue'), meta: { title: '商品详情' } }, { path: '/cart', name: 'Cart', component: () => import('@/views/Cart.vue'), meta: { title: '购物车' } }, { path: '/checkout', name: 'Checkout', component: () => import('@/views/Checkout.vue'), meta: { title: '确认订单', requiresAuth: true } }, { path: '/orders', name: 'OrderList', component: () => import('@/views/OrderList.vue'), meta: { title: '我的订单', requiresAuth: true } }, { path: '/login', name: 'Login', component: () => import('@/views/Login.vue'), meta: { title: '登录' } } ] const router = createRouter({ history: createWebHistory(), routes }) // 全局前置守卫:登录鉴权 router.beforeEach((to, from, next) => { document.title = to.meta.title ? `${to.meta.title} - 商城` : '商城' const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next({ name: 'Login', query: { redirect: to.fullPath } }) } else { next() } }) export default router

这里有两个细节值得展开讲。

第一,keepAlive: true这个 meta 标记。商品列表页用户往往会来回筛选、翻页,如果每次离开再回来都重新请求数据,体验很差。结合<keep-alive>可以把列表页状态缓存下来,但缓存也带来了滚动位置无法自动恢复的问题,这个坑我在第 5 章专门展开。

第二,登录守卫的redirect参数。用户没登录就点结算,应该跳转到登录页,登录成功后自动跳回原来想去的页面,而不是固定跳回首页。这个体验细节很多开源商城源码都没做好。

2.3 Pinia 状态管理:购物车为什么不能放在组件里

购物车数据必须在多个页面共享。你在商品详情页点“加入购物车”,跳转到购物车页要能看到;在首页也要显示购物车角标数量。如果每个组件自己各自维护一份,数据会不一致。

用 Pinia 管理购物车,核心逻辑其实很清晰:

// src/stores/cart.js import { defineStore } from 'pinia' export const useCartStore = defineStore('cart', { state: () => ({ items: [], // 购物车列表 selectedIds: [] // 选中的商品项 id }), getters: { totalCount: (state) => state.items.reduce((sum, item) => sum + item.count, 0), selectedItems: (state) => state.items.filter(item => state.selectedIds.includes(item.id)), totalPrice: (state) => state.selectedItems.reduce( (sum, item) => sum + item.price * item.count, 0 ) }, actions: { addItem(product) { const existing = this.items.find(item => item.id === product.id) if (existing) { existing.count++ } else { this.items.push({ ...product, count: 1 }) } }, removeItem(id) { this.items = this.items.filter(item => item.id !== id) this.selectedIds = this.selectedIds.filter(sid => sid !== id) }, updateCount(id, count) { const item = this.items.find(item => item.id === id) if (item) item.count = count } } })

计算总价用 getter 而不是在组件里各自算,好处是所有用到总价的组件拿到的都是同一份计算结果。选中商品和总价联动是商城购物车的核心交互,把选中状态也放进 store 管理,避免在组件里传得晕头转向。

3. 交易核心链路:商品浏览、购物车、下单结算的实现细节

商城源码的价值核心在交易链路。商品展示做得再漂亮,购物车和结算逻辑写不清楚,上线就会被用户骂。

3.1 商品列表的加载、搜索与排序

商品列表页是最容易暴露前端基本功的地方,因为这里集中了数据请求、状态切换、筛选排序和性能优化。

一个常见的实现思路是用ref管理列表数据和加载状态,用watch监听筛选条件变化后重新请求:

// src/views/ProductList.vue <template> <div class="product-list"> <el-input v-model="keyword" placeholder="搜索商品" clearable /> <el-radio-group v-model="sortOrder"> <el-radio-button value="default">综合</el-radio-button> <el-radio-button value="sales">销量</el-radio-button> <el-radio-button value="priceAsc">价格从低到高</el-radio-button> <el-radio-button value="priceDesc">价格从高到低</el-radio-button> </el-radio-group> <el-skeleton v-if="loading" :rows="6" animated /> <el-empty v-else-if="productList.length === 0" description="没有找到相关商品" /> <div class="product-grid"> <ProductCard v-for="product in productList" :key="product.id" :product="product" @add-cart="handleAddCart" /> </div> <el-pagination v-model:current-page="page" :total="total" :page-size="pageSize" layout="prev, pager, next" @current-change="fetchList" /> </div> </template> <script setup> import { ref, watch, onActivated } from 'vue' import { fetchProductList } from '@/api/product' import { useCartStore } from '@/stores/cart' const cartStore = useCartStore() const keyword = ref('') const sortOrder = ref('default') const page = ref(1) const pageSize = ref(12) const total = ref(0) const productList = ref([]) const loading = ref(false) async function fetchList() { loading.value = true try { const res = await fetchProductList({ keyword: keyword.value, sort: sortOrder.value, page: page.value, pageSize: pageSize.value }) productList.value = res.list total.value = res.total } finally { loading.value = false } } // 关键点:防抖处理搜索 let timer = null watch(keyword, () => { clearTimeout(timer) timer = setTimeout(() => { page.value = 1 fetchList() }, 300) }) // 排序、翻页都走同一套请求 watch(sortOrder, () => { page.value = 1 fetchList() }) fetchList() </script>

这里最容易翻车的细节是搜索防抖。如果你在每个input事件里直接请求接口,用户输入“手机”两个字会产生至少两次请求:输入“手”一次、输入“机”一次。并发请求如果响应顺序不一致,先发的请求后返回,就会把后发请求的结果覆盖掉。防抖 + 取消过期请求(或者在请求里带上请求序号做丢弃)是必须要处理的。

3.2 购物车的状态设计与金额计算

购物车的核心交互有三块:选中/取消选中、修改数量、删除。这三块逻辑在 UI 上看很简单,但状态设计一旦混乱,后面加需求就会疯。

我的建议是购物车条目在 store 里维护,但“选中状态”一定要单独管理,不要塞在商品数据里。

为什么要单独管理?因为在真实商城业务里,加入购物车和“本次结算选中哪些商品”是两个概念。用户可能购物车里有 10 件商品,这次只勾选 3 件结算。如果你在商品条目上改了selected字段,下次加入同款商品时这个字段的初始值会很难处理,全选/取消全选的状态同步也容易出 bug。

总价的计算放在 getter 里,我在 2.3 已经给出代码,这里不再重复。但要注意金额计算的精度问题。JavaScript 的浮点数运算有精度隐患,比如0.1 + 0.2 = 0.30000000000000004。商城项目里金额计算必须用“分”作为单位,也就是在后端返回价格时通常是“1980”表示 19.80 元,前端计算时用整数,只在展示时除以 100 并保留两位小数。

// src/utils/format.js export function formatPrice(cents) { return (cents / 100).toFixed(2) }

这个习惯是从真实商城项目里学来的。我之前见过一个开源项目直接用浮点数计算总价,当用户买了 3 件 19.99 元的商品时,总价显示59.970000000000006,很尴尬。

3.3 订单结算与登录鉴权的配合

结算页是整条链路里最容易出问题的地方,因为这里要同时处理:用户是否登录、收货地址选择、商品清单确认、优惠信息、提交订单后的状态流转。

先说登录鉴权。在 2.2 的路由守卫里,我给结算页打上了requiresAuth标记。当用户未登录时点击“去结算”,路由守卫会先拦截并跳转到登录页,登录成功后再用redirect参数跳回来。这个流程的关键在于登录组件里成功后要读取路由参数:

// src/views/Login.vue const router = useRouter() const route = useRoute() async function handleLogin() { const res = await loginApi({ username, password }) localStorage.setItem('token', res.token) localStorage.setItem('userInfo', JSON.stringify(res.userInfo)) // 登录后跳回原来要去的页面 const redirect = route.query.redirect if (redirect) { router.replace(redirect) } else { router.replace('/') } }

下单提交的时候还要注意“防止重复提交”。用户在结算页点击“提交订单”,如果网络慢,往往习惯性地再点一次,就会生成两笔订单。解决思路是提交后立即把按钮置为 loading 状态,同时用一个submitting标志位做保护:

const submitting = ref(false) async function submitOrder() { if (submitting.value) return submitting.value = true try { await createOrder({ items: selectedItems, addressId }) ElMessage.success('下单成功') router.push('/orders') } finally { submitting.value = false } }

4. 前后端分离实战:Axios 封装、鉴权拦截与 Mock 联调

商城项目现在基本都是前后端分离开发。“SpringBoot + Vue”这个词在热搜里出现了很多次,说明这是当前国内企业级项目的典型组合。前端源码能不能顺利跑起来,很大程度上取决于 API 层设计得是否合理。

4.1 Axios 封装与拦截器

axios 封装要解决几个痛点:baseURL 统一管理、request 里自动带 Token、response 里统一拆包、全局错误提示、网络超时处理。

// src/api/request.js import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || '/api', timeout: 10000 }) // 请求拦截器:自动携带 Token request.interceptors.request.use( (config) => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }, (error) => Promise.reject(error) ) // 响应拦截器:统一处理业务码与异常 request.interceptors.response.use( (response) => { const res = response.data // 和后端约定好,code === 0 表示成功 if (res.code === 0) { return res.data } // 业务失败 ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) }, (error) => { if (error.response && error.response.status === 401) { // token 过期,清除登录态,跳转登录页 localStorage.removeItem('token') localStorage.removeItem('userInfo') router.push({ name: 'Login', query: { redirect: router.currentRoute.value.fullPath } }) } else { ElMessage.error(error.message || '网络异常') } return Promise.reject(error) } ) export default request

为什么要把 token 放在请求拦截器而不是每个接口单独传?因为商城项目里除了登录注册,几乎每个接口都需要鉴权。如果每个接口都手动传一遍,一旦后端改 token 的 header 字段名,你得改几十个地方。拦截器统一处理是唯一合理的选择。

4.2 Mock 数据与接口约定

如果你拿到的是一套纯前端商城源码,通常后端接口还没就绪,或者需要自己用 Node 写后端。这时候有两个选择:

第一,用vite-plugin-mock在本地启动一个 Mock 服务,模拟后端接口。这样前后端可以并行开发,前端先按约定的接口文档写好调用,后端实现后再通过环境变量切换 baseURL。

第二,如果源码本身是 SpringBoot + Vue 的完整项目,需要检查后端的端口和接口路径。我见过很多人在本地启动 Vue 后发现请求 404,常见原因就是baseURL/api,而 SpringBoot 的context-path配的是别的值,或者跨域没配置好。

开发环境解决跨域最简单的方式是利用 Vite 的 proxy:

// vite.config.js import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })

这样前端请求/api/products会被代理到http://localhost:8080/api/products。注意后端接口如果有带/api前缀,那直接这样用就行;如果后端接口本身没有这个前缀,代理时需要加上rewrite: (path) => path.replace(/^\/api/, '')

5. 热词里藏着的高频坑:keep-alive 滚动、el-table 回顶、draggable 拖不动

很多人觉得看源码只要看懂逻辑就行,但真正动手跑起来时,被拦住的往往是一些奇奇怪怪的小问题。我去翻了一下和这个项目相关的热搜词,下面这几个出现频率极高,也都是我在实际开发中确确实实踩过的坑。

5.1 keep-alive 缓存后,滚动位置回不去

项目里为了让商品列表页保留浏览状态,通常会配合<keep-alive>使用。但这样会带来一个新问题:用户从列表页滑到比较深的位置,点进详情页再返回列表页,页面内容被缓存了,但滚动位置也在原来的深度,有时候体验恰恰是反的——我们希望返回时恢复原来的位置,但如果你是从 A 入口跳转到详情页再回来,可能希望回到顶部。

这里的关键是区分场景。如果是通过“返回”回到列表页,恢复原位置是对的;如果是通过切换 Tab 重新进入,通常应该回到顶部。

我的处理方案是利用 Vue 的onActivated钩子:

<!-- ProductList.vue --> <script setup> import { ref, onActivated, onDeactivated } from 'vue' const scrollContainer = ref(null) let scrollTop = 0 onActivated(() => { // 重新进入页面时恢复滚动位置 if (scrollContainer.value) { scrollContainer.value.scrollTop = scrollTop } }) onDeactivated(() => { // 离开页面时记录滚动位置 if (scrollContainer.value) { scrollTop = scrollContainer.value.scrollTop } }) </script>

如果页面滚动发生在 window 上,就记录window.pageYOffsetdocument.documentElement.scrollTop。这里有个容易踩的细节:onActivated触发的时候页面的 DOM 可能还未完全渲染完,特别是列表里有图片时。稳妥的做法是在onActivated里用nextTick包裹恢复逻辑:

import { nextTick } from 'vue' onActivated(async () => { await nextTick() if (scrollContainer.value) { scrollContainer.value.scrollTop = scrollTop } })

5.2 el-table 切换路由后滚回到表头

热搜里那句“vue keep-alive 切换路由子组件 el-table 滚回头部”说的问题其实很具体:列表页用 Element Plus 的el-table,表格内容比较多,表格容器内部会出现滚动条。当路由切换再回来时,表格的滚动位置仍停留在切换前的位置,用户会觉得“为什么打开表格直接停在中间”。

原因和 5.1 一样,是 keep-alive 缓存导致的。但和普通滚动条不同,el-table的滚动容器在.el-table__body-wrapper这个元素上,单纯恢复scrollTop不够,还得找到正确的容器。

常见的两种思路:

  1. 在组件离开时把表格滚动位置重置为零,保证下次进入永远从头部开始。
  2. 如果不希望重置,而是保存原位置,则需要给el-table加 ref,并在onActivated里手动恢复。
<template> <el-table ref="tableRef" :data="tableData"> <!-- columns --> </el-table> </template> <script setup> import { ref, onActivated } from 'vue' const tableRef = ref(null) // 方式一:返回时回到头部 onActivated(() => { if (tableRef.value) { const bodyWrapper = tableRef.value.$el.querySelector('.el-table__body-wrapper') if (bodyWrapper) { bodyWrapper.scrollTop = 0 } } }) // 方式二:如果想要保存原位置,onDeactivated 里记录,onActivated 里恢复 </script>

实际项目中我更多用的是“记录并恢复”,但要注意如果是路由切换到了详情页再回来,用户通常是希望回到原来浏览位置的;如果是从 Tab 切换出去再回来,最好是回顶部。所以具体用哪种,要先想清楚你项目的导航结构。

5.3 vue-draggable-plus 拖不动,问题出在哪

热搜里“vue draggable plus拖不动”也上榜了,这个我太有感触了。新版vue-draggable-plus是专门给 Vue3 用的拖拽库,和 Vue2 时代的vuedraggableAPI 有些差别。很多人把 Vue2 的写法搬过来,发现拖不动或者拖了没反应。

先说最常见的错误:同时绑定v-model:list。在 vue-draggable-plus 里,v-model 是推荐用法,list 属性在部分版本里会和 v-model 冲突,导致拖拽结束后数据没有被更新,看起来就像“拖不动”。

正确写法:

<template> <VueDraggable v-model="list" class="drag-list"> <div v-for="item in list" :key="item.id" class="drag-item"> {{ item.name }} </div> </VueDraggable> </template> <script setup> import { ref } from 'vue' import { VueDraggable } from 'vue-draggable-plus' const list = ref([ { id: 1, name: '商品A' }, { id: 2, name: '商品B' }, { id: 3, name: '商品C' } ]) </script>

另外一个坑是拖拽的 handle 选择器写错。如果你只允许按住某个区域才能拖动,要写:handle="'.drag-handle'",并且确保被拖拽的子元素里确实存在这个 class。最常见的问题是 handle 选择器匹配到了元素,但该元素被其他元素遮住了,导致拖拽事件根本触发不了。

还有一点容易被忽略:拖拽列表项之间必须设置不重复的key。如果你用的是数组下标做 key,拖拽后元素交换位置,Vue 复用组件时状态会错乱,表现就是拖一下整个列表顺序乱了或者拖不动。

5.4 搜索防抖、路由参数与联动刷新

商城源码里的商品搜索也有很多细节。如果搜索条件放在路由 query 上,搜索框输入后跳转路由,这样分享链接给朋友时,好友也能直接看到搜索后的结果,但代价是必须处理路由变化和组件内状态的双向同步。

具体实现思路是:用useRoute()读取 query 作为响应式来源,监听 query 变化后重新请求数据。但这会遇到一个问题:如果用户在同一个列表页调整排序方式,路由 query 变了,组件被复用不会重新创建,就必须在 watch 回调里重新拉数据。这一点很容易和“首次进入页面拉数据”的逻辑重复,所以在封装商品列表组件时,我习惯把“数据加载”抽成一个独立函数,路由变化时统一调用,避免重复请求。

我之前还碰到过一个 bug:搜索关键词里有特殊字符,比如用户搜“手机+壳”,+号在 URL query 里会被解析成空格,导致搜索结果错误。解决办法是用encodeURIComponent对参数编码,或者传给后端时做处理。这个细节很小,但确实能卡住人。

6. 构建上线:从源码到跑在服务器上

源码在本机能跑通,距离上线还有一段路要做。商城类项目对首屏加载速度和稳定性要求都很高,打包优化和部署配置不能马虎。

6.1 打包体积优化的三个关键动作

Vue3 + Vite 项目默认打包体积在包含 Element Plus 全家桶时会比较大。我在实际项目里做了三件事,效果非常明显。

第一,组件按需引入。用unplugin-vue-componentsunplugin-auto-import这两个插件,可以做到按需自动引入 Element Plus 组件,而不是在 main.js 里整体app.use(ElementPlus)。打包体积能少一半以上。

第二,路由懒加载。2.2 的路由配置里我已经用了() => import(),这会自动按路由分包。用户访问首页时只加载首页的 js,不会把商品详情、结算、订单这些页面全都下载下来。

第三,手动分包。把 node_modules 里体积大且不常变化的库拆出来。以 Element Plus 为例,可以放到单独的 vendor chunk:

// vite.config.js build: { rollupOptions: { output: { manualChunks: { 'vue-vendor': ['vue', 'vue-router', 'pinia'], 'element-plus': ['element-plus'], 'axios': ['axios'] } } } }

第三方库拆分后,浏览器可以长期缓存这些文件,用户再次访问时不用重复下载。

这里还要提醒一句:Element Plus 的图标如果按需引入,不要用全量注册的方式,比如for (const icon in icons) app.component(...),这会把几百个图标全打进去。正确做法是在组件里按需导入用到的图标。

6.2 Nginx 部署与刷新 404 问题

商城的构建产物是一堆静态文件,部署到 Nginx 非常常见。但 Vue Router 如果用的是createWebHistory(history 模式),直接部署会出现一个经典问题:用户在/products/123页面刷新,Nginx 找不到对应的物理文件,返回 404。

解决办法是配置try_files,让所有路径都回退到index.html

server { listen 80; server_name yourdomain.com; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } # 静态资源缓存 location /assets/ { expires 30d; add_header Cache-Control "public, no-transform"; } # 开启 gzip gzip on; gzip_types text/plain text/css application/json application/javascript; }

刷新 404 是 history 模式部署最常见的坑。如果你用的是哈希模式(createWebHashHistory),不会有这个问题,但 URL 会带#,看起来不够正式。项目上线我一般推荐 history 模式 + Nginx 回退配置。

还有一个容易忽略的细节:把构建产物放到服务器上之后,修改了 API 的baseURL要确认是不是指向了正确的后端地址。我在线下部署时吃过一次亏,前端构建时没有把环境变量切换成生产环境的 API 地址,结果线上页面所有请求都指向了localhost,白屏加一串 403。

建议在源码里用环境变量区分场景:

# .env.development VITE_API_BASE_URL=/api # .env.production VITE_API_BASE_URL=https://api.yourdomain.com/api

构建时 Vite 会自动加载对应环境的变量,不需要改代码。

从选型到上线,这套商城源码的脉络基本就清晰了。回头再想想,很多人问“Vue 商城应该怎么做”,答案其实不是某个页面怎么写,而是你有没有把状态管理、路由守卫、接口封装、性能优化这四块地基打好。写代码的时候多问自己一句为什么,比照着网上源码抄一遍有用得多。这几年我每次重新搭商城,都能对自己之前的设计不满意,这大概就是进步的过程。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 8:18:36

游戏攻略网站毕设项目全解析:从Vue前端到Spring Boot后端

简介&#xff1a;这是一套面向计算机专业本科生的毕业设计与期末大作业实战资源&#xff0c;聚焦游戏攻略网站的全流程开发实践&#xff0c;解决学生缺乏可运行、可扩展、文档完备的SSMVue全栈项目参考的痛点。资源包共774个文件&#xff0c;24.08MB&#xff0c;涵盖156个JavaS…

作者头像 李华
网站建设 2026/9/13 8:19:36

MATLAB印刷品缺陷检测系统实战:图像处理与机器视觉全流程解析

简介&#xff1a;本资源是一个基于MATLAB开发的印刷品缺陷检测系统&#xff0c;面向计算机、自动化、人工智能及通信等专业的学生、教师与工程实践者&#xff0c;解决印刷质量控制中污点、刮痕、色差等常见缺陷的自动识别与定位问题&#xff0c;适用于课程设计、大作业及毕业设…

作者头像 李华
网站建设 2026/9/13 8:18:22

大模型应用开发:普通程序员也能掌握的收藏必备技能!

本文详细解释了大模型应用开发的概念&#xff0c;强调其与算法岗的区别&#xff0c;指出普通程序员也能参与其中。文章还介绍了应用开发者的日常工作内容&#xff0c;包括业务流程建模、模型交互调优等&#xff0c;并深入探讨了RAG、Agent、调用和部署等关键模块。最后&#xf…

作者头像 李华
网站建设 2026/9/12 10:10:27

Nucleo-WBA25CE1开发板BLE Direct Test Mode(DTM)射频测试实战解析

手里拿到一块 Nucleo-WBA25CE1 开发板&#xff0c;第一件事不是点灯&#xff0c;而是把它切到 Direct Test Mode&#xff08;DTM&#xff09;里跑一趟射频指标。这个标题看着像是蓝牙协议栈里的一个冷门模式&#xff0c;实际上凡是做 BLE 产品的硬件工程师、射频调试工程师&…

作者头像 李华
网站建设 2026/9/2 3:32:18

STM32U5G9ZJT6Q Video Stop停止顺序详解与低功耗实现

做车载显示项目这几年&#xff0c;我有个习惯&#xff1a;一听到需求方提“Video Stop”&#xff0c;先追问一句“停到什么程度”。大多数时候&#xff0c;对方说的“停止播放视频”&#xff0c;心里真正想的其实只是“屏幕别再动画了”&#xff0c;但在 STM32U5G9ZJT6Q 这类集…

作者头像 李华
网站建设 2026/9/3 12:59:59

微信小程序+云开发:校园跑步社交系统的设计与实现

简介&#xff1a;这是一套面向计算机类本科毕业生的完整毕业设计资源&#xff0c;聚焦校园场景下的运动社交需求&#xff0c;提供从开发到答辩的全流程支撑材料。项目以微信小程序为载体&#xff0c;实现跑步轨迹记录、实时配速里程、整公里语音提醒、周/月排行榜、打卡分享、线…

作者头像 李华