ARTICLE DETAIL

资讯详情

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

Node.js + Vue全栈实战:从零构建原神游戏商城

Node.js + Vue全栈实战:从零构建原神游戏商城 作为一个常年写前后端项目的老手看到“nodejs基于Vue的原神游戏商城”这个标题第一反应就是又一个典型的全栈练手项目。但它恰好是前端入门到进阶最值得走完的一条完整链路——从Node.js的安装环境到Vue组件化开发再到前后端联调、打包部署里头的坑和细节够写一整篇实战笔记。这篇就围绕我做完这个项目后的实际经验来聊从环境搭建、技术选型到后端接口设计、前端页面实现再到联调部署中那些让人抓狂的报错一次性说清楚。想拿Vue和Node.js练手、或者正在做类似商城项目的朋友可以直接照着走。1. 项目整体设计与技术选型1.1 为什么是Node.js Vue这对组合在做这个项目之前我先明确了一个原则既然是游戏商城核心就是“商品展示-加入购物车-下单结算-订单管理”这条业务线再加上用户登录和管理后台。选Node.js Vue不是因为它俩是“网红组合”而是因为它们各自承担的角色太清晰了。前半段用Node.js跑后端服务只需要提供JSON格式的数据接口简单直接不需要像Spring Boot那样配一堆东西。项目的主要目标是打通全栈链路。Node.js的事件驱动模型处理商城这种高并发I/O场景本身就占优势更重要的是前后端都用JavaScript语言统一调试成本低。写完后端接口前端同事或者说另一个自己可以直接对着接口文档写Vue页面不用关心Java、Python那套类型转换。Vue负责的是页面交互层。原神游戏商城这个定位决定了页面要有时尚感、有游戏氛围Vue的组件化开发非常适合把一个页面拆成“商品卡片组件”“购物车列表组件”“订单状态组件”来独立维护。而且Vue 3的Composition API写起来比Vue 2的Options API顺手得多——逻辑可以按功能聚合比如购物车相关的响应式数据、计算方法、操作函数能放在一起不用再拆到data、methods、computed三个区域来回跳。这个项目的架构定位是“轻后端、重前端”。后端不需要做微服务、不需要消息队列把接口写好、把数据存好就行前端的交互体验、页面跳转、状态管理才是用户能直接感受到的。一套Express Vue 3的简单结构比那些动辄上微服务的大厂架构更适合作为学习项目。1.2 整体架构与目录规划我最终采用的目录结构是这样的前后端分离清晰又保证一个仓库就能跑起来genshin-shop/ ├── server/ # Node.js 后端 │ ├── app.js # 入口文件Express配置 │ ├── routes/ # 路由文件auth、products、cart、orders、admin │ ├── models/ # 数据模型User、Product、Order │ ├── middleware/ # 鉴权中间件、错误处理中间件 │ └── config/ # 数据库、密钥配置 ├── client/ # Vue 前端 │ ├── src/ │ │ ├── api/ # axios请求封装 │ │ ├── router/ # Vue Router配置 │ │ ├── stores/ # Pinia状态管理 │ │ ├── views/ # 页面级组件商品列表、详情、购物车、订单、后台 │ │ ├── components/ # 通用组件商品卡片、导航栏、分页 │ │ └── App.vue │ └── package.json └── README.md有个地方我特意做了设计后端的数据存储先用JSON文件模拟。你别觉得low开发阶段用JSON文件存数据方便调试、方便初始化不用每次跑个数据库容器。等基础功能全部跑通了再平滑切换到MySQL或MongoDB。这个思路对新手很友好避免一上来就被数据库配置绊住。前端之所以把api、router、stores拆开是为了后面扩展方便。比如以后要接入真实支付只需要改api目录里对应的方法页面不用动。2. 环境准备与工程搭建2.1 Node.js安装与环境配置——不少人第一步就卡住这个项目的第一个坎坦白说不是写代码而是把Node.js环境装好。我见过太多人卡在这一步包括我自己当初也是。去官网nodejs.org下载LTS版本这个不要犹豫别下载Current版LTS稳定得多坑少。安装的时候有一个关键点经常被忽略安装路径尽量不要带空格和中文更不要装到Program Files (x86)这样的目录。原因很简单后面很多命令行工具对带空格的路径处理不友好你可能遇到各种莫名其妙的问题。我建议直接装到D:\nodejs这样干净的路由。装完之后验证是否成功node -v npm -v这两个命令能输出版本号说明核心装好了。但这时候真正的大坑才出现——在Windows上执行npm命令很多人会看到这样一条报错npm : 无法加载文件 D:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本这个报错的原因Windows PowerShell默认的执行策略是Restricted不允许运行未签名的脚本。npm命令本质是一个.ps1脚本所以被拦住了。解决办法有两条用管理员身份打开PowerShell执行Set-ExecutionPolicy RemoteSigned最后输入Y确认。RemoteSigned的意思是本地脚本可以运行远程脚本必须有数字签名才运行。这是比较平衡的策略推荐。如果你不想动系统策略可以全程用cmd命令提示符代替PowerShell操作npmcmd没有这个限制。注意不要随便把执行策略设成Unrestricted虽然也能解决问题但系统会运行一切脚本安全风险太大。RemoteSigned是够用的。还有一点装完Node.js后npm的全局包路径和缓存路径最好也配置一下。默认情况下全局包会装到C盘用户目录下时间长了C盘容易爆。我习惯这样配置npm config set prefix D:\nodejs\node_global npm config set cache D:\nodejs\node_cache配置完成后之前装过的全局包需要重装一遍。这个步骤不做也不影响项目开发但可能一段时间后你就遇到奇怪的路径问题所以建议一次配到位。2.2 Vue脚手架创建前端工程环境准备好后开始创建Vue项目。这里我遇到了一个有意思的取舍用Vite还是Vue CLI。如果是Vue 3项目我强烈推荐Vite它的启动速度和热更新简直是享受。Vue CLI基于Webpack在Vue 2时代是标配但Webpack的冷启动要等好几秒甚至十几秒而Vite借助原生ES模块秒开。创建命令npm create vuelatest这个命令会进入一个交互式问答问你要不要TypeScript、路由、状态管理等。根据项目需要我推荐做以下选择TypeScript如果你对TS不熟先选No。这个项目本身就是练全栈基本功别让类型把精力消耗掉。Vue Router选Yes商城必然有多个页面路由。Pinia选Yes购物车状态需要全局管理。其他Jest、Cypress等全部No开发阶段用不到。项目创建完之后还需要安装UI组件库。商城系统不可能每个按钮、每个弹窗都自己写样式我用的是Element Plus跟Vue 3配合很默契。安装npm install element-plus考虑到原神主题的视觉风格偏游戏化Element Plus的基础样式我会配合自定义CSS调整——全局主色调改成类似游戏UI的蓝紫色比如#4766ff这种色调按钮圆角做大一点卡片增加边框发光效果。这些通过覆盖CSS变量实现:root { --el-color-primary: #4766ff; --el-border-radius-base: 8px; }C端页面用户看的页面可以做得有游戏感但后台管理页保持Element Plus的简洁风格就好这样管理起来效率高。2.3 后端工程初始化后端我选择了Express框架它是Node.js生态里最老牌、最稳定、资料最多的框架。新建server目录执行npm init -y npm install express cors morgan nodemon几个包的分工expressWeb框架负责路由和中间件cors允许跨域请求。前后端分离开发时前端跑在5173端口后端跑在3000端口不同端口就是跨域不加cors就等着看CORS报错morgan日志中间件控制台会打印每个请求的方法、路径、状态码和耗时调试很方便nodemon监听代码修改自动重启Node服务不用每次都手动重新node app.js入口文件app.js的基本结构const express require(express); const cors require(cors); const logger require(morgan); const app express(); app.use(cors()); app.use(logger(dev)); app.use(express.json()); app.use(/api/auth, require(./routes/auth)); app.use(/api/products, require(./routes/products)); app.use(/api/cart, require(./routes/cart)); app.use(/api/orders, require(./routes/orders)); app.use((err, req, res, next) { res.status(err.status || 500).json({ message: err.message }); }); const PORT process.env.PORT || 3000; app.listen(PORT, () { console.log(Server running on port ${PORT}); });强调一下express.json()。这个是内置的JSON请求体解析中间件没有它你POST过来的JSON数据在req.body里就是undefined。Vue前端用axios发JSON数据这一层非常关键。package.json里设置一下启动脚本scripts: { start: node app.js, dev: nodemon app.js }然后npm run dev启动后端控制台会输出Server running on port 3000这里环境就全部就绪了。3. 后端核心接口设计与数据建模3.1 商城数据模型设计做商城项目数据模型是地基。我建了三个核心模型用户(User)、商品(Product)、订单(Order)。用户模型的核心字段{ id: 唯一标识, username: 用户名唯一, password: 密码用bcrypt加密后存储, avatar: 头像URL, createdAt: 注册时间 }商品模型是重点展示模块要贴合原神主题。我定义的商品字段{ id: 商品ID, name: 商品名称, category: 分类角色周边/武器周边/道具周边/服饰, price: 价格单位分, stock: 库存, imageUrl: 商品图片地址, description: 商品描述, sales: 销量, isOnSale: 是否上架 }这里有个容易犯的错价格字段。别用浮点数存价格JavaScript的浮点运算会有精度问题比如0.1 0.2可能得到0.30000000000000004。我统一的方案是价格用整数“分”存储前端展示时再转换成“元”。比如一件商品的price是12800前端就显示“128.00元”。这样可以彻底规避精度问题也符合电商系统的规范。举个例子商品数据大概是这个感觉{ id: g_001, name: 「风神」巴巴托斯粘土人, category: 角色周边, price: 29900, stock: 50, imageUrl: /images/bard_01.jpg, description: 高约20cm细节精致附赠风琴琴特效件。, sales: 120, isOnSale: true }订单模型相对复杂涉及多商品下单逻辑{ id: 订单号, userId: 下单用户ID, items: [ { productId: 商品ID, name: 商品名, price: 下单时快照单价, quantity: 数量 } ], totalAmount: 订单总金额分, status: 订单状态pending/paid/shipped/completed/cancelled, createdAt: 下单时间, address: 收货地址 }我强调一下items里为什么要存name和price快照。因为商品表的数据是可变的——商品名称可能改、价格可能调但是订单作为历史记录下完单那一刻的信息必须固定。等哪天商品改价了用户查看历史订单看到的还是原来的价格。这是电商系统很基础的设计意识却经常被初学者忽略。3.2 RESTful接口设计与实现后端接口我严格按照RESTful风格来设计一个语义清晰的URL比什么都重要。用户相关接口方法路径功能备注POST/api/auth/register用户注册用户名密码POST/api/auth/login用户登录返回JWT token商品相关接口方法路径功能备注GET/api/products商品列表支持关键字、分类、分页查询GET/api/products/:id商品详情按ID查POST/api/admin/products新增商品管理员PUT/api/admin/products/:id修改商品管理员DELETE/api/admin/products/:id删除商品管理员购物车接口方法路径功能GET/api/cart获取当前用户购物车POST/api/cart添加商品到购物车PUT/api/cart/:id修改购物车商品数量DELETE/api/cart/:id删除购物车商品订单接口方法路径功能POST/api/orders创建订单GET/api/orders获取用户订单列表GET/api/admin/orders管理员获取所有订单PUT/api/admin/orders/:id更新订单状态商品列表接口实现分页和筛选这一段是C端体验的关键。我写了一个支持多条件组合查询的接口// routes/products.js router.get(/, async (req, res) { const { keyword, category, page 1, pageSize 12 } req.query; let result [...productList]; // 关键字匹配在名称和描述里搜 if (keyword) { result result.filter(p p.name.includes(keyword) || p.description.includes(keyword) ); } // 分类筛选 if (category category ! 全部) { result result.filter(p p.category category); } // 只展示上架商品 result result.filter(p p.isOnSale); const total result.length; const start (page - 1) * pageSize; const data result.slice(start, start pageSize); res.json({ data, total, page: Number(page), pageSize: Number(pageSize) }); });这个接口同时满足“搜索栏输入关键字”和“点分类Tab筛选”两个需求前端同一个接口就能覆盖两个场景。订单创建接口相对复杂因为它要处理库存扣减、金额计算、购物车清理三个事务性操作// routes/orders.js router.post(/, authMiddleware, (req, res) { const userId req.userId; const { items, address } req.body; if (!items || items.length 0) { return res.status(400).json({ message: 订单不能为空 }); } let totalAmount 0; for (let item of items) { const product productList.find(p p.id item.productId); if (!product) { return res.status(404).json({ message: 商品 ${item.productId} 不存在 }); } if (product.stock item.quantity) { return res.status(400).json({ message: ${product.name} 库存不足 }); } item.name product.name; item.price product.price; // 快照商品价格 totalAmount product.price * item.quantity; } // 扣减库存 for (let item of items) { const product productList.find(p p.id item.productId); product.stock - item.quantity; } const order { id: ORD Date.now(), userId, items, totalAmount, status: pending, address, createdAt: new Date().toISOString() }; orderList.unshift(order); res.status(201).json(order); });订单号为什么用ORD Date.now()而不是自增数字因为订单号要保证唯一性自增数字在多实例部署或者数据迁移时容易冲突。用时间戳虽然简单但单毫秒内可能生成重复所以实际生产更建议用UUID或时间戳加随机串。这里演示用时间戳就够了但你得知道这个问题。3.3 鉴权中间件的实现逻辑用户登录后要返回一个token后续所有跟用户相关的接口都要带上这个token。选型上我用JWTJSON Web Token配合jsonwebtoken这个包。JWT的好处是服务端不需要存session——token本身携带了用户信息和过期时间服务端只要验签就行。登录接口核心逻辑// routes/auth.js const jwt require(jsonwebtoken); const bcrypt require(bcryptjs); router.post(/login, (req, res) { const { username, password } req.body; const user userList.find(u u.username username); if (!user) { return res.status(401).json({ message: 用户不存在 }); } // bcrypt.compareSync 验证密码 const isMatch bcrypt.compareSync(password, user.password); if (!isMatch) { return res.status(401).json({ message: 密码错误 }); } const token jwt.sign( { userId: user.id, username: user.username }, SECRET_KEY, { expiresIn: 7d } ); res.json({ token, username: user.username }); });中间件authMiddleware就是一个拦截器专门校验请求头里的token// middleware/auth.js function authMiddleware(req, res, next) { const header req.headers.authorization; if (!header) { return res.status(401).json({ message: 未登录 }); } // 前端传的是 Bearer token 格式 const token header.split( )[1]; try { const decoded jwt.verify(token, SECRET_KEY); req.userId decoded.userId; next(); } catch (err) { return res.status(401).json({ message: token无效或过期 }); } }然后把需要登录的接口里加上这个中间件router.get(/cart, authMiddleware, (req, res) { // 这里req.userId就能拿到当前登录用户的ID });注意密码绝不能明文存储。用bcrypt哈希后的密码即使数据库泄漏攻击者也拿不到原始密码。bcrypt是自动加盐的同样的密码两次加密结果都不同安全性有保障。你可以在注册接口里用bcrypt.hashSync(password, 10)生成哈希10代表加密强度太低太弱、太高太慢10是合理默认值。4. 前端页面开发与路由设计4.1 商品列表页与详情页的实现前端的核心页面是商品列表页它决定了用户对商城的第一印象。我用Element Plus的栅格布局将页面拆成两部分上方的分类导航全部、角色周边、武器周边、道具周边、服饰下方是商品卡片网格。商品卡片我抽成了一个独立的组件ProductCard.vuetemplate div classproduct-card clickgoDetail el-image :srcproduct.imageUrl fitcover classproduct-image / div classproduct-info h3 classproduct-name{{ product.name }}/h3 div classproduct-meta span classprice¥ {{ (product.price / 100).toFixed(2) }}/span span classsales已售 {{ product.sales }}/span /div el-button typeprimary sizesmall click.stopaddToCart 加入购物车 /el-button /div /div /template两个细节值得说。第一click.stop必须加——如果不加点击按钮会冒泡触发整个卡片的点击事件导致误跳详情页。这种事件冒泡的坑新手至少踩一次。第二价格字段从接口拿到的是“分”展示时需要除以100并保留两位小数所以我用了(product.price / 100).toFixed(2)这就是后端建模时决定用整数分的直接回馈。列表页的逻辑就清晰了const products ref([]); const total ref(0); const currentPage ref(1); const activeCategory ref(全部); const keyword ref(); async function fetchProducts() { const res await http.get(/api/products, { params: { page: currentPage.value, pageSize: 12, category: activeCategory.value, keyword: keyword.value } }); products.value res.data.data; total.value res.data.total; } watch([activeCategory, currentPage, keyword], fetchProducts);用watch监听筛选条件变化自动重新拉数据。用户点分类、翻页、搜索页面的响应逻辑都是同一条链路。详情页相对简单就是把单个商品的图片、名称、价格、描述、库存拿出来大图展示然后一个“立即购买”按钮——本质是跳到结算流程。4.2 购物车与结算流程购物车是商城项目里最能体现“状态管理”价值的模块。它有几个特点全局共享导航栏里的购物车数量角标、购物车页面、结算页都要访问、频繁修改增删改是常态、实时性要求不高存后端接口本地做一份缓存。我用Pinia来管理购物车的状态// stores/cart.js export const useCartStore defineStore(cart, { state: () ({ items: [], isLoaded: false }), getters: { totalCount: (state) state.items.reduce((sum, item) sum item.quantity, 0), totalAmount: (state) state.items.reduce((sum, item) sum item.price * item.quantity, 0) }, actions: { async fetchCart() { const res await http.get(/api/cart); this.items res.data; this.isLoaded true; }, async addToCart(product, quantity 1) { await http.post(/api/cart, { productId: product.id, quantity }); await this.fetchCart(); } } });导航栏右上角数字角标就读取totalCount不管用户在哪个页面角标都是实时响应的。实际操作中把一个接口的PINIA封装做好购物车逻辑会清爽很多。结算流程我做一个简单的分步购物车页点击“去结算” - 填写收货地址 - 确认订单 - 提交后展示订单成功页。这里没有接入真实支付用一个“模拟支付”替代——点击“立即支付”后订单状态直接变为已支付。如果需要真实支付逻辑只需要在创建订单后加一步调起支付接口的操作架构上是留好位置的。4.3 路由守卫与权限控制前端路由这块我用Vue Router管理所有页面。默认页面是商品列表登录页、购物车页、订单页都有对应路由后台管理部分单独成组。路由定义我分成两块// 用户端路由 const routes [ { path: /, redirect: /products }, { path: /products, component: () import(/views/ProductList.vue) }, { path: /product/:id, component: () import(/views/ProductDetail.vue) }, { path: /cart, component: () import(/views/CartPage.vue) }, { path: /login, component: () import(/views/LoginPage.vue) }, { path: /orders, component: () import(/views/OrderList.vue) } ];路由守卫这里要注意未登录用户如果直接访问购物车或订单页要踢回登录页否则后端接口报401前端就会白屏报错。所以加一个全局前置守卫router.beforeEach((to, from, next) { const token localStorage.getItem(token); // 需要登录才能访问的页面 if (to.path ! /login !token) { if (to.meta.requiresAuth) { next(/login); } else { next(); } } else { next(); } });这个逻辑先把不涉及个人数据、不需要登录也能看的公开页面商品列表、详情放出来需要登录的页面购物车、订单、后台加meta.requiresAuth标识。用户没有token就引导去登录页。关于JWT的存储位置我选择localStorage。其实也有别的方案比如存内存里但社区最常用、对新手最友好的就是localStorage。要注意的是前端的localStorage只是方便携带token真正的安全靠后端验签前端不要过度纠结。4.4 后台管理页面雏形后台管理这一段虽然部分教程会略过但我认为完整商城必须有。我的后台实现了一个独立路由组放在/admin前缀下并单独做了一个AdminLayout.vue布局左侧导航栏 右侧内容区。后台功能包括商品列表增删改查、订单列表更新发货状态、销售统计图表展示。销售统计可视化我用了ECharts效果很直观。这里放一个前端页面调用的核心代码片段展示“近七日销量”柱状图import * as echarts from echarts; const chartDom document.getElementById(salesChart); const myChart echarts.init(chartDom); myChart.setOption({ title: { text: 近七日销量 }, tooltip: {}, xAxis: { data: stats.dates }, yAxis: {}, series: [{ name: 销量, type: bar, data: stats.sales, itemStyle: { color: #4766ff } }] });ECharts的好处是只要把后端传回来的日期数组和销量数组填进去图表就出来了不需要自己写SVG或者Canvas。后台页面让我这个项目的完整度一下子高了不少——前端展示、后台管理、数据统计三个维度都有了。5. 前后端联调、打包部署与常见问题排查5.1 跨域问题与axios统一封装开发时前端运行在Vite的5173端口后端跑在3000端口两者不同源浏览器会拦截跨域请求。解决跨域有两层方案我同时做了第一层后端用cors中间件允许跨域。开发阶段最简单的方式是app.use(cors())不加任何配置放行所有来源。但生产环境建议只允许自己的域名访问比如后台从18房间进来的请求日志只放行一个固定域名。第二层前端用axios发请求时统一带baseURL// client/src/api/http.js import axios from axios; const http axios.create({ baseURL: /api, // 开发环境走Vite代理 timeout: 10000 }); // 请求拦截器自动带token http.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }); // 响应拦截器统一处理401状态码 http.interceptors.response.use( response response, error { if (error.response error.response.status 401) { localStorage.removeItem(token); window.location.href /login; } return Promise.reject(error); } );这个封装解决了两件事一是写接口时不用每次手写baseURL和token二是一旦token过期后端返回401前端自动跳登录页用户不用手动清理。拦截器是axios最实用的功能没有之一。Vite开发时的代理配置在vite.config.js里加上export default defineConfig({ plugins: [vue()], server: { proxy: { /api: { target: http://localhost:3000, changeOrigin: true } } } });这样前端写的http.get(/api/products)会被Vite代理转发到后端3000端口。注意一旦配了代理前端就不要再写绝对地址http://localhost:3000/api/products了统一用/api开头后面部署时可以只改代理指向不用改代码。5.2 打包与部署实战前端开发完毕执行打包命令npm run buildVite编译完成后会生成dist目录里面是纯静态文件HTML、CSS、JS。这个dist目录就是需要部署的静态资源。我选Nginx作为Web服务器它的配置要点是把dist目录作为站点根目录同时把/api开头的请求反向代理到Node服务。Nginx配置大概长这样server { listen 80; server_name your-domain.com; root /var/www/genshin-shop/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:3000/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }关键点在这个try_files $uri $uri/ /index.html;。Vue是单页应用路由由前端控制。用户访问/products/1Nginx会先找有没有这个真实文件找不到就回退到index.html把路由交给Vue处理。如果你漏掉这一行用户刷新详情页就白屏404。后端要部署到生产环境不能直接在Node服务里老挂着一个终端窗口。用pm2管理进程它能在Node服务崩溃后自动重启也能开机自启npm install -g pm2 pm2 start app.js --name genshin-shop pm2 save pm2 startup # 设置为开机自启pm2的几个基础命令记一下就够了pm2 logs看日志、pm2 restart genshin-shop重启、pm2 stop genshin-shop停止。部署端口冲突的问题我遇到过好多次检查一下3000端口有没有被占用netstat -ano | findstr :3000有进程占用的话要么释放端口要么给Node应用换个端口配置。5.3 高频报错排查速查表做了十几个前后端分离的项目以下这些错误几乎每个人都遇到过至少一次。我整理成一个速查表遇到问题直接对照着排报错信息原因分析处理方法npm : 无法加载文件 ...npm.ps1...禁止运行脚本PowerShell执行策略限制Set-ExecutionPolicy RemoteSignedUnable to resolve dependency treenpm依赖版本冲突先删node_modules和package-lock.json再npm installModule not found: Error: Cant resolve element-plus组件库未安装或未注册npm install element-plus并按需引入或全局注册Cannot find module express在项目目录下没装Express确认在server目录下执行了npm install expressAccess to XMLHttpRequest...CORS policy跨域未处理后端加cors中间件前端配Vite代理404 (Not Found)刷新后报错Nginx没配置try_files回退加上try_files $uri $uri/ /index.html;UnauthorizedError: jwt expiredtoken过期前端响应拦截器统一跳登录页或续签tokenlisten EADDRINUSE: address already in use端口被占用netstat -ano查PID杀掉进程或换端口接口返回中文乱码响应头charset不对后端设置res.setHeader(Content-Type, application/json; charsetutf-8)GET /api/products 500后端代码报错看日志用pm2 logs或nodemon终端打印的报错堆栈排查这里最想强调的还是第一行——npm.ps1的报错。我在网上看到大量提问不少人在这一步就放弃了。其实说白了就是Windows安全策略的问题不是你的代码有问题。排除问题时先怀疑环境再怀疑配置最后怀疑代码本身按这个顺序排查效率最高。5.4 从开发到上线的完整配置最后说下我在实际部署过程中反复优化的几个配置点。先说后端的package.json{ name: genshin-shop-server, version: 1.0.0, main: app.js, scripts: { start: node app.js, dev: nodemon app.js }, dependencies: { bcryptjs: ^2.4.3, cors: ^2.8.5, express: ^4.19.2, jsonwebtoken: ^9.0.2, morgan: ^1.10.0 } }生产环境启动一定用npm start而不是npm run dev可避免nodemon这种开发依赖介入生产环境。前端的dist目录和Node服务我之前说的是两台独立服务。如果你想省事也可以把dist目录放到Node服务的public目录下让Express直接托管静态文件app.use(express.static(public));这样直接访问Node的3000端口就能看到前端页面同时/api接口也在同一个端口不需要Nginx做反向代理。但这种方案只适合个人项目或临时Demo真正的生产环境还是配上Nginx做静态托管反向代理更好——性能、缓存、HTTPS配置能力都更强。还有一个容易出现的问题把SECRET_KEY硬编码在代码里。我的做法是把它放到config/目录下的环境变量文件里用dotenv加载。比如新建.env文件PORT3000 SECRET_KEYyour-secret-key-change-me代码中读取require(dotenv).config(); const SECRET_KEY process.env.SECRET_KEY;这样做的好处不同环境开发、测试、生产用不同的密钥不泄露在代码仓库里。我把.env加进.gitignore提交到仓库的只有.env.example模板里面是空的密钥和配置说明。这个习惯给团队协作、代码保护省了大麻烦。6. 项目扩充方向与实战心得总结6.1 这个项目还能怎么扩展做完了商城的基础版本我的真实感受是它“麻雀虽小五脏俱全”。以后还能往这几个方向快速扩展商品新增多规格比如手办分“普通版/典藏版”、颜色、大小等维度需要把商品模型从“单SKU”升级成“多SKU体系”接口的查询和购物车逻辑同步改造接入真实支付微信支付、支付宝在创建订单后增加支付回调接口订单状态从“待支付”变为“已支付”的逻辑要能对得上账增加评价系统商品详情的口碑评价是电商转化率的一个关键因素后台管理加数据分析面板把用户数、订单数、销售额趋势、品类占比都可视化出来服务端技术可以升级成TypeScript加上类型系统重构时的问题少一些数据库换MySQL把数据持久化下来6.2 踩过坑之后我自己的实测体会一个全栈项目最容易出问题的不是某个页面怎么写而是“前后端接口约定是否一致”。比如前端传参数时直接传字符串后端期望数字结果库里存了NaN看上去数据还在但计算全错。我个人的习惯是把接口参数写成文档哪怕只是简单的一个清单前端照着写后端照着验联调时能少一大半问题。编码环节里有一个容易被忽视的坑图片资源路径。开发时我用的图片是本地路径/images/xxx.jpg但打包后dist目录里没有这堆图片。所以商品图片我建议直接用对象存储的URL比如阿里云OSS、腾讯云COS这样前端不管怎么打包部署图片路径都是完整可用的不会出现某个页面图裂了。如果你没有云存储也要事先规划好把图片放在一个固定可访问的位置。加JWT鉴权之后Postman测接口时有个小技巧要记住从登录接口拿到token后创建一个环境变量{{token}}然后在Authorization类型选Bearer Token填入{{token}}。之后再测其他必须登录的接口就不用反复重新拷贝token了。6.3 最后的叮嘱如果你拿这个项目当练手我建议你把完整的流程自己走一遍不要只看不写。环境配好、接口写完、页面渲染出来、打包部署成功这一整套走通之后你对“一个网站是怎么做出来的”会有质的理解。之后再去看Vue深入浅出、Node.js源码或者微服务框架都有了落地的锚点。Node.js Vue的商城项目不说什么炫酷技术但它是全栈入门到进阶最值得完整走一次的项目形态。把这篇教程里的每一步跟下来相当于亲手完成了一个真实可运行的全栈系统拿到这个基础后面加功能、换框架、上云部署都不是难事。
返回列表