尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Node.js+Vue构建云上水果商城:全栈开发与部署实践

Node.js+Vue构建云上水果商城:全栈开发与部署实践 最近在做一个基于Node.js和Vue的云上新鲜水果超市商城系统内部代号g0a71。这个项目不是一个什么重量级电商平台而是面向中小型生鲜商家的一条线上化方案。从用户端的水果浏览、购物车到管理端的订单处理、商品上下架再到云服务器部署整条链路都走了一遍。过程中踩了不少坑尤其是Node.js环境配置和npm的PowerShell权限问题。今天这篇文章我就把项目的整体拆解、核心功能实现、环境搭建和部署上线一次性讲清楚给打算用Node.js和Vue做电商类项目的朋友做一份参考。1. 项目概述与业务定位1.1 为什么做这样一个“云上水果超市”水果这个品类和数码产品、服装完全不一样。最容易变质的商品之一就是水果库存放在那里不管每天都有损耗实体店半径通常只有两三公里靠自然流量很难撑起大生意。把水果超市搬到“云上”本质上是让销售范围从门店周边扩展到整个城区用户线上下单商家统一配货甚至配送到家。这样做最直接的价值是降低损耗、提升周转效率。我之前帮朋友设计过一个小程序生鲜商城当时对生鲜行业的配送和库存印象很深。后来决定用Node.js和Vue自己从零做一个完整的Web版水果商城原因很简单想让一套代码同时覆盖用户端和后台管理端并且把前后端分离的流程走通。这个项目虽然叫“云上新鲜水果超市”但业务并不复杂核心是几件事卖水果、管订单、控库存、做营销。难的是把这些流程通过代码稳定地串起来。这个项目适合谁参考如果你是一个想练手全栈的学生或者准备转行做开发的初学者又或者是一名独立开发者想给线下水果店做一套线上商城那这篇文章的路子基本够用了。Node.js负责提供API接口Vue负责页面展示和交互云服务器负责最终上线这是一个非常典型的现代前后端分离项目。1.2 核心需求拆解与功能清单做项目最忌讳上来就写代码需求不清会导致后面反复返工。我一开始就把用户端和管理端拆开列了一张角色功能清单。角色核心功能普通用户注册登录、浏览商品、分类筛选、关键词搜索、商品详情、加购、下单、模拟支付、订单查询、确认收货、商品评价管理员商品上架/下架、库存调整、分类管理、订单发货、订单状态修改、用户列表、销售数据查看用户端重点是购物体验要流畅。水果商城有很多分类场景比如时令水果、进口水果、水果礼盒还有应季水果的推荐位。所以商品模块不能只是一个列表还需要支持分类、价格排序、销量排序、搜索。购物车要能增删改查下单时要有收货地址订单状态要清晰可追踪。管理端重点是控制库存和处理订单。水果是生鲜品库存数量直接关系到损耗系统必须保证库存不能超额售卖。另外还有一个隐藏需求订单状态必须有序流转不能从待支付直接跳到已完成中间要经过配货、配送这些环节。所以我在设计的时候加了一个订单状态机用枚举值锁死合法状态转换。这个需求清单看起来功能不少但拆到后端接口其实也就三四十个。前端页面大概十个左右用户端覆盖首页、商品列表、详情、购物车、订单结算、个人中心管理端覆盖商品管理、订单管理、数据看板。整体工程量完全在一个人可以控制的范围内这也是选型Node.js和Vue的一个重要原因。2. 技术选型与架构设计2.1 为什么选 Node.js Vue 这套组合技术选型没有绝对的对错关键看团队和场景。我现在带领的这个项目本身是前端团队主导所以选Node.js是顺理成章的所有人都写JavaScript前后端通用一套语言和数据结构思维沟通成本为零。前端用Vue也不只是因为生态成熟更因为组件化开发非常适合商城这类重复结构很多的系统。Node.js自身适合处理I/O密集型请求。电商场景里很多操作例如打开商品详情、加载图片、提交订单本质上是高频率、轻计算量的I/O操作Node.js的事件循环机制在这种情况下表现很好。再加上Express中间件生态非常成熟写一个RESTful接口非常快。后端如果选Spring Boot也不是不行但对于一个小型商城项目来说Java本身的部署成本和团队学习成本会直接把开发周期拉长。Vue这边我最看重的是它的渐进式特点。项目初期可以用Vue Router加快单页应用后续如果要做复杂状态管理就引入Pinia。生态里还有Element Plus、Vant这些现成的UI库后台管理页面几乎就是拖组件。对比ReactVue单文件组件的模板写法对新手更友好上手速度确实快很多。2.2 整体架构与数据流设计整体架构我用文字描述一下方便理解。用户通过浏览器访问域名Nginx接收请求。所有前端静态资源也就是Vue工程npm run build之后生成的dist目录直接交给Nginx托管。浏览器向/api开头的路径发送请求Nginx将这些请求反向代理到后端Node.js服务Node.js服务再连接MySQL数据库和Redis缓存。图片文件统一放到云存储OSS上浏览器通过直链访问。项目目录结构上也做了前后端分离fruit-shop/ ├── server/ # Node.js 后端 │ ├── app.js │ ├── config/ │ ├── routes/ │ ├── controllers/ │ ├── models/ │ └── middlewares/ ├── web/ # Vue 前端 │ ├── src/ │ │ ├── api/ │ │ ├── router/ │ │ ├── stores/ │ │ ├── views/ │ │ └── components/ │ └── package.json └── docs/后端采用MVC的分层方式routes定义路由controllers处理业务逻辑models负责数据库映射。前端按模块划分目录api目录统一管理所有请求stores管理全局状态。接口统一使用/api/v1前缀比如登录接口是/api/v1/auth/login商品列表是/api/v1/goods/list。返回格式固定在{ code, message, data }三层前端axios拦截器统一处理不用每个页面各自判断错误码。数据库设计我是花了心思的。核心表包括用户表、商品分类表、商品表、购物车表、订单表、订单明细表和评价表。商品表中有几个字段特别关键stock库存、sales销量、price价格、status上下架状态。订单表和订单明细表是主从结构一个订单对应多个商品这样方便统计每个商品卖了多少。这个模型设计看起来简单但发货、退款、库存扣减都能在上面展开。3. 核心功能模块实现3.1 用户登录与JWT鉴权用户模块是所有系统的入口方式我选的是JWT。之所以不用Session是因为前后端分离以后后端服务可能多实例部署Session共享比较麻烦JWT天然无状态便于扩展。注册的密码处理绝对不能明文存储。我在后端用bcrypt库对密码做加盐哈希比对的时候用bcrypt.compare()判断。登录成功以后签发JWTpayload里只放userId和role过期时间设置为24小时。服务端有一个鉴权中间件读取请求头里的Authorization: Bearer token校验签名成功以后把用户信息放进req.user后续接口通过req.user直接拿当前用户。// 登录接口核心逻辑 router.post(/auth/login, async (req, res) { const { username, password } req.body; const user await User.findOne({ where: { username } }); if (!user || !bcrypt.compareSync(password, user.password)) { return res.json({ code: 400, message: 用户名或密码错误 }); } const token jwt.sign( { userId: user.id, role: user.role }, process.env.JWT_SECRET, { expiresIn: 24h } ); res.json({ code: 200, data: { token, userInfo: { username, role } } }); });前端Vue这边登录页拿到token以后存到localStorageaxios请求拦截器在每个请求头上加上token。如果接口返回401说明token过期或者非法就清除本地身份信息并跳回登录页。这里有一个小提示把token存localStorage在安全上有XSS风险但对于内部小项目来说这是最方便的做法如果追求更安全可以存内存里刷新页面时再通过一个刷新接口续签项目复杂度会高一些。管理员权限是另外处理的。我写了一个requireAdmin中间件在鉴权中间件之后判断req.user.role是否为admin不是就返回403。这样商品管理、订单发货这些接口就只对管理员开放前端也会根据角色隐藏管理入口。3.2 商品模块与图片上传商品模块是整个商城的信息基础。列表接口我支持了分页、分类ID、关键词、价格排序和销量排序。查询用Sequelize的where和order动态拼接避免自己拼SQL语句。图片上传是最容易忽略实际问题的环节。开发环境可以用multer把图片存到本地public/uploads目录但上线以后图片一定要放云存储OSS。原因很简单应用服务器磁盘有限图片多了会影响磁盘空间而且云存储自带CDN加速全国访问都会快很多。我的做法是后端接收上传的图片然后调用云存储SDK转存到OSS桶再把返回的URL保存到数据库浏览器直接用这个URL访问图片。商品列表页因为图片多前端全部加了懒加载。Vue里用自定义指令或者现成的懒加载库都能实现这个优化很重要不然几百个商品在一个页面上页面会非常卡。商品详情页则用了Swiper轮播展示多张图片这些细节在真实商城里都是用户直接体验到的部分。库存处理是商品模块里最容易出错的地方。我之前踩过并发超卖的问题后台把库存改成0了还在卖。后来把扣库存操作改成一条原子SQLUPDATE goods SET stock stock - 1, sales sales 1 WHERE id ? AND stock 0执行以后判断受影响行数如果为0就说明库存不足下单失败。这样做的好处是扣库存和检查库存一次性完成没有“先查再改”的中间窗口从根上避免了超卖。3.3 购物车与订单流转购物车我选择了服务端存储而不是只存在浏览器本地。这样用户在手机和电脑之间切换时购物车数据不会丢失。购物车表结构比较简单user_id、goods_id、goods_num、selected加购和修改数量都是标准的增删改查。订单流转是整系统的核心我把它设计成了状态机。状态枚举有这些pending待支付、paid待配货、shipping配送中、completed已完成、cancelled已取消、refunding退货中。每个状态都有合法的下一个状态不允许跳变。比如待支付只能去已支付或取消已支付只能去配送中配送中才能到已完成。创建订单涉及两个数据表事务必须处理好。用户从购物车把选中的商品提交过来后端先生成订单主记录再循环生成订单明细同时扣减库存。这三步必须放到同一个数据库事务里否则会出现订单创建了但库存没扣或者库存扣了但订单失败。我用的Sequelize事务const transaction await sequelize.transaction(); try { const order await Order.create({ ... }, { transaction }); for (const item of items) { await OrderItem.create({ ... }, { transaction }); await Goods.decrement({ stock: item.num }, { where: { id: item.goodsId }, transaction }); } await transaction.commit(); } catch (e) { await transaction.rollback(); }订单金额计算要格外小心。我在后端重新计算了一次商品总价而不是直接信任前端传来的金额防止有人篡改请求数据。订单号我用时间戳加随机数生成并且加唯一索引保证并发时不会重复。支付环节由于是演示项目我做了模拟支付接口调用后直接把状态从待支付改成已支付真实生产环境可以对接微信支付或支付宝逻辑都是一样的只是在支付回调里更新状态。4. 开发环境搭建与高频报错4.1 Node.js 安装与环境变量配置很多人在环境这步就会被卡住。Node.js安装建议去官网下LTS版本LTS是长期维护版稳定可靠。安装时一路Next即可注意安装目录不要有中文和空格。安装完以后在命令行输入node -v和npm -v能显示版本号就是成功。有些用户下载的是免安装版解压后还需要手动配置环境变量。我的做法是新建一个系统变量NODE_HOME指向Node.js的解压目录然后在Path变量里追加%NODE_HOME%。当前窗口不生效需要重新打开命令行。如果出现npm 不是内部或外部命令多半就是环境变量没配置正确。npm用官方源在国内速度慢一定要设置镜像源npm config set registry https://registry.npmmirror.com设置完以后npm config get registry检查一下。更方便的做法是安装pnpmnpm install -g pnpm安装依赖会快很多还能节省磁盘空间。我后面创建Vue项目用的就是ViteVite对Node.js版本要求比较高一点尽量用16.0以上。初学者学Node.js不要一开始就去啃Express应该先把这个顺序理清楚JS基础语法模块系统npm包管理http模块再上手Express框架。有了这个基础后面写接口就不会觉得都是“魔法”。4.2 前端 Vue 环境初始化与 npm.ps1 报错创建Vue项目我用的Vite模板npm create vitelatest web -- --template vue cd web npm install这里要说一个高频坑Windows用户在命令行执行npm命令的时候很可能会看到这样的报错npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本这是因为Windows PowerShell的脚本执行策略默认是Restricted不允许运行.ps1脚本。解决办法有两个第一个是以管理员身份打开PowerShell执行Set-ExecutionPolicy RemoteSigned然后输入Y确认。第二个方法是改用CMD或者GitBash来运行npm命令绕开PowerShell的限制。如果不想动系统全局策略也可以在PowerShell里临时绕过PowerShell -ExecutionPolicy Bypass这个报错太常出现了我在不止一台机器上遇到过每次都先检查执行策略。Vue项目初始化以后还需要安装Vue Router和Pinianpm install vue-router pinia element-plus axiosElement Plus在后台管理页面上非常好用表格、表单、弹窗都有现成组件。开发时后端服务和前端Vite默认不在同一个端口所以跨域问题也需要处理。我在vite.config.js里配置了devServer的proxy把/api请求代理到本机3000端口export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:3000, changeOrigin: true } } } });这样开发环境就不会有CORS报错生产环境因为Nginx做了同源也没有跨域问题。Vue调试建议装Vue Devtools插件可以实时看组件树和Pinia状态排查数据流问题会方便很多。Vue Router如果是history模式还要注意部署时后端Nginx要配合配置后面我会讲到。5. 云上部署与项目复盘5.1 云服务器部署流程项目开发完不能只停留在本地上线部署才是完整闭环。我用的是云厂商的一台Linux服务器2核4G配置跑这个项目绰绰有余。部署流程大致是先安装Node.js和PM2然后把前后端代码拉下来分别安装依赖构建前端最后配置Nginx。后端启动我推荐PM2它自带进程守护、日志管理和负载均衡pm2 start server/app.js --name fruit-shop没有安装PM2的话先执行npm install -g pm2。PM2的好处是进程意外退出会自动重启服务器重启也能通过pm2 startup设置开机自启。前端构建cd web npm run build构建完以后dist目录里的静态文件复制到Nginx的工作目录Nginx配置大致长这样server { listen 80; server_name yourdomain.com; root /var/www/fruit-shop/web/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }location /里边的try_files非常关键如果少了Vue Router的history模式在刷新非首页路由时会直接404。location /api/负责把接口请求转发到后端Node服务。数据库建议用云数据库MySQL自带备份和安全组比在服务器上自己装省心很多。云存储OSS存放商品图片部署配置完成后跑一遍全流程测试注册、登录、加购、下单、发货都要验证通过。5.2 遇到的一些问题与排查技巧开发过程中踩过不少坑我按频率高低整理了一份排查清单。第一个高频问题是Nginx配置完成后刷新页面404。原因就是上面说的history路由没有配置try_files。解决办法是在配置里加上那三行让前端路由匹配不到文件时统一返回index.html由Vue Router自己去解析路径。第二个问题是接口测试正确前端调用却报跨域。开发环境的跨域用了proxy解决生产环境如果后端接口和前端静态资源不在同一个域名也需要处理。最好的办法是保持同源也就是Nginx同时托管前端资源并代理API。如果确实要分开域名那后端就要开启CORS并设置Access-Control-Allow-Origin。第三个问题是后端接口参数校验不够严谨导致一些脏数据进入数据库。比如商品价格被提交成负数用户手机号格式不对。后来我引入Joi做参数校验每个接口在进入业务逻辑前先校验请求体不符合规定的直接返回参数错误。这个小改造省了很多后面查数据的麻烦。第四个问题是MySQL连接数被打满应用不时报ETIMEDOUT。原因是最初创建数据库连接池时没有限制最大连接数请求一多连接就被耗尽。改成固定连接池以后问题消失。下面是Sequelize连接池的推荐配置new Sequelize(database, username, password, { dialect: mysql, pool: { max: 10, min: 0, idle: 30000 } });另外在云服务器部署时第一次上线经常出现接口能通但是浏览器访问还是白屏一定要先查看Vue的构建日志确认dist目录是不是真的生成了以及Nginx的root路径是不是指向了正确的目录。这些小问题排查起来不难但第一次做会找很久经验就在于先看Nginx错误日志和前端构建日志不要瞎猜。5.3 做这个项目的几点心得这个项目做完我个人的体会是Node.js和Vue的组合做中小型商城确实舒服但前提是要有工程规范。我早期写接口时命名混乱有的叫getGoodsList有的叫listGoods前端还要配合记忆。后端接口务必统一命名规则例如GET /goods、POST /orders、PUT /orders/:id/status按资源划分语义清晰前端可以少走很多弯路。数据库表结构也要提前设计。我在评审时把用户、商品、订单的主外键关系画成简单的关联图确认无误后才开始写代码。中间因为购物车和订单关联关系不清晰调整过一次表结构成本不低。一定要在开发前把表字段、枚举值、状态流转都列出来落到文档上。还有一个心得不要忽略测试。整个项目手工测试了好几轮特别是订单状态乱序操作比如用户下单后强制刷新、后台重复发货这些边界情况如果不写状态机系统肯定会出问题。我在状态转换处加了合法性校验不符合流程的操作一律拒绝。如果后续还要扩展我会做这几件事一是用Redis缓存热点商品数据降低数据库压力二是接入微信小程序端复用现有接口三是增加限时秒杀功能进一步压测Node.js在高并发下的表现。这个项目本身还有很多可以打磨的地方但核心链路已经是完整闭环对整个开发流程的价值非常大。
返回列表