十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

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移动端选 VantHTTPAxios拦截器做鉴权和错误处理最方便这里要特别说一句如果是从网上找的开源商城源码看到还是 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.jsapi 目录集中管理接口组件里不直接写请求路径。这个习惯特别重要。我以前见过一个项目接口地址散落在各个页面组件里后来后端改了路径前缀差点把开发者逼疯。集中管理之后改一个文件就完事。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 classproduct-list el-input v-modelkeyword placeholder搜索商品 clearable / el-radio-group v-modelsortOrder el-radio-button valuedefault综合/el-radio-button el-radio-button valuesales销量/el-radio-button el-radio-button valuepriceAsc价格从低到高/el-radio-button el-radio-button valuepriceDesc价格从高到低/el-radio-button /el-radio-group el-skeleton v-ifloading :rows6 animated / el-empty v-else-ifproductList.length 0 description没有找到相关商品 / div classproduct-grid ProductCard v-forproduct in productList :keyproduct.id :productproduct add-carthandleAddCart / /div el-pagination v-model:current-pagepage :totaltotal :page-sizepageSize layoutprev, pager, next current-changefetchList / /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.pageYOffset或document.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不够还得找到正确的容器。常见的两种思路在组件离开时把表格滚动位置重置为零保证下次进入永远从头部开始。如果不希望重置而是保存原位置则需要给el-table加 ref并在onActivated里手动恢复。template el-table reftableRef :datatableData !-- 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-modellist classdrag-list div v-foritem in list :keyitem.id classdrag-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-components和unplugin-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 如果用的是createWebHistoryhistory 模式直接部署会出现一个经典问题用户在/products/123页面刷新Nginx 找不到对应的物理文件返回 404。解决办法是配置try_files让所有路径都回退到index.htmlserver { 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_URLhttps://api.yourdomain.com/api构建时 Vite 会自动加载对应环境的变量不需要改代码。从选型到上线这套商城源码的脉络基本就清晰了。回头再想想很多人问“Vue 商城应该怎么做”答案其实不是某个页面怎么写而是你有没有把状态管理、路由守卫、接口封装、性能优化这四块地基打好。写代码的时候多问自己一句为什么比照着网上源码抄一遍有用得多。这几年我每次重新搭商城都能对自己之前的设计不满意这大概就是进步的过程。本文还有配套的精品资源点击获取
返回列表