Node.js+Vue构建种子商城:从环境配置到部署上线的完整实践

发布时间:2026/10/7 11:52:40
Node.js+Vue构建种子商城:从环境配置到部署上线的完整实践
去年下半年接了一个农资店的线上商城需求店主想把自己线下经营了几年的种子生意搬到网上去。需求不复杂展示种子商品、按品类筛选、加入购物车、下单、后台管库存但要求上线快、成本低、一个人能维护。我最后定的技术方案就是 Node.js Vue这也是目前中小型电商系统里非常成熟的一套组合。这篇文章把这些天踩过的坑、做过的取舍、最终跑通的完整设计过程全部写下来包括环境配置、数据库设计、前后端实现逻辑、部署上线以及对那些高频报错比如 npm.ps1 脚本无法加载、端口号怎么隐藏的处理办法。准备做类似商城系统的人不管你是学生做课程设计还是小团队接私单都可以直接参考这套思路。1. 一个真实场景为什么小型农业电商要选 Node.js Vue1.1 种子商店业务与普通商品电商的差异点种子商城看起来是个标准的B2C电商但它和卖衣服、卖数码产品的系统有很明显的区别。种子这种商品用户在下单前需要看的信息远比普通商品多而且大多和“种植行为”强相关。我梳理需求时发现种子商品详情页至少要包含这些维度信息维度具体字段为什么需要品种信息名称、品种编号、品系用户需要准确区分不同批次作物类别蔬菜、花卉、粮食、牧草商城首页的核心筛选维度种植属性适宜季节、生长周期、亩用种量、适宜温度决定用户买回去能不能种活产地与资质产地、检测报告、商家备注农业商品的信任关键交易属性价格、单位、库存、上下架状态支撑购物车和订单流程这意味着数据库表结构不能拿通用电商模板直接套字段要按农业场景单独设计。我后面在第三节会详细讲建表逻辑。另一个差异点是用户群体。种子商城的用户里既有大田种植户也有家庭园艺爱好者。前者关注亩用种量和价格后者关注品种好不好养、生长周期多长。所以前端页面设计不能只做单一的信息陈列筛选和搜索要足够顺手。1.2 技术选型的对比和最终取舍做技术选型时身边有人建议用 Spring Boot有人建议直接 Wordpress 加 WooCommerce。我最终选 Node.js Vue核心原因是这个项目的实际约束第一开发人员少。整个系统只有我一个人负责前后端如果前后端各用一种语言等于要维护两套心智模型。Node.js 用 JavaScriptVue 也是 JavaScript切换上下文几乎没有成本。第二上线周期短。店主希望在两周内看到能用的版本。Node.js 生态里Express 写接口很快Vue 配合 Vite 的开发服务器热更新效率极高改完代码浏览器立刻能看到效果这个反馈速度对电商类页面迭代非常关键。第三业务量级匹配。一个区域性的种子店日订单量峰值也就几十到几百单。这个量级下Node.js 的单线程事件循环完全扛得住没必要引入更重的 Java 体系。很多开发者会担心 Node.js 的 CPU 密集场景性能但商城系统的核心操作是数据库读写和接口转发属于 I/O 密集这恰恰是 Node.js 的强项。第四部署运维简单。后端一个进程前端打包成静态文件后由同一个 Node 服务托管服务器上只需要装一个 Node.js 运行时对只有一台云服务器的农业小店来说非常友好。对比之下Spring Boot 的优势是生态规范和事务控制更强大但前提是你愿意接受更重的开发节奏和更高的服务器资源占用。PHP 方案虽然也能快速实现但前后端语言不一致团队协作时要多一道沟通成本。所以这个场景下的结论很明确Node.js Vue 是最务实的组合而不是技术上的炫耀。2. 环境准备阶段Node.js 安装、npm 脚本权限与项目初始化2.1 Windows 和 Mac 下 Node.js 安装及环境配置的正确打开方式很多新手在环境准备阶段就会被安装问题浇一盆冷水这部分热搜词出现频率高是有原因的。我先按不同系统把正确的安装路径讲清楚。Windows 系统去 Node.js 官网下载 LTS 版本的 Windows Installer不要下载 Current 尝鲜版LTS 稳定性更高第三方包兼容性最好。安装时一路 Next但注意安装路径不要选带中文或空格的目录比如D:\Program Files\Nodejs这种路径在后续配置环境变量时很容易出幺蛾子建议直接用D:\nodejs。安装完成后验证是否成功打开命令提示符cmd执行node -v和npm -v能看到版本号说明核心环境没问题。Mac 系统推荐用 Homebrew 安装命令是brew install node它会自动把环境变量配好比去官网下 pkg 安装包省心。验证命令和 Windows 一样同样执行node -v和npm -v。这里要注意一个细节环境变量的作用是让操作系统在任意目录下都能找到 node.exe。Windows 安装器一般会自动写入 PATH但如果你用的是解压版 Node.js需要手动把解压目录添加到系统环境变量里。添加完之后要重新打开终端窗口才能生效这个问题经常被忽略导致明明装了却提示找不到命令。2.2 npm.ps1 执行策略报错卡住无数新手的典型排查接下来是高频问题也是这个项目里最常见的启动事故。在 PowerShell 里执行 npm 命令时报错信息往往是这样的npm : 无法加载文件 c:\program files\nodejs\npm.ps1因为在此系统上禁止运行脚本。 有关详细信息请参阅 about_Execution_Policies。甚至有人是d:\program files\nodejs\npm.ps1本质都一样。这不是 Node.js 装坏了而是 PowerShell 的执行策略Execution Policy默认限制了脚本运行。Windows 下 npm 命令实际是npm.ps1脚本而 PowerShell 默认的 Restricted 策略不允许执行任何本地脚本所以直接拒绝。排查和修复步骤打开 PowerShell执行Get-ExecutionPolicy查看当前策略如果显示Restricted那就是问题所在。执行Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser。再次执行Get-ExecutionPolicy确认已经变成RemoteSigned。RemoteSigned的含义是本地创建的脚本可以运行从互联网下载的脚本必须包含可信数字签名。npm 脚本是你本地安装的属于本地脚本所以可以正常执行。这个策略比Unrestricted安全得多是官方推荐的折中方案。需要说明的是-Scope CurrentUser只对当前用户生效不需要管理员权限也不影响系统安全策略。如果实在不想改策略还有一个临时办法在 cmd 里使用 npm不要用 PowerShell。cmd 不会读取 PowerShell 的执行策略同样能正常运行。但既然以后每天都要敲 npm 命令建议还是直接改策略一劳永逸。2.3 从空目录到可运行商城雏形的初始化步骤环境就绪后你就可以初始化项目了。我这里把整体项目目录规划好前后端分开管理seed-shop/ ├── server/ # Node.js 后端 │ ├── app.js │ ├── routes/ │ ├── controllers/ │ ├── db.js │ └── uploads/ └── client/ # Vue 前端 ├── src/ ├── vite.config.js └── package.json后端初始化mkdir seed-shop cd seed-shop mkdir server cd server npm init -y npm install express mysql2 cors jsonwebtoken multer前端初始化我用 Vite 创建 Vue 3 项目mkdir client cd client npm create vuelatest创建过程中会询问是否启用 TypeScript、Vue Router、Pinia 等选项。做商城类项目建议开启 Vue Router 和 Pinia这两个是后面做页面跳转和购物车状态管理必需的。TypeScript 按你们团队习惯来如果对类型系统不熟先选 JavaScript 也完全能跑。npm create vuelatest这个命令本身就是对 npm 生态的依赖执行失败时优先检查上一节说的执行策略问题不要第一时间怀疑网络或安装源。3. 数据库建模种子商品和订单的数据结构怎么设计才够用3.1 种子商品表字段设计把种植属性独立拆出来种子商品和其他电商商品最大的区别在于它有一大堆农业属性。这些属性不只是详情页展示用还会承担搜索和筛选功能比如用户会问“有没有春天种的叶菜”“哪个品种生长周期短”。如果这些字段全塞在一个通用product表里逻辑会非常混乱。我给种子单独建了一张seeds表CREATE TABLE seeds ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT 种子ID, name VARCHAR(100) NOT NULL COMMENT 品种名称, category VARCHAR(50) NOT NULL COMMENT 作物类别蔬菜/花卉/粮食/牧草, variety VARCHAR(100) COMMENT 品种编号或品系, origin VARCHAR(100) COMMENT 产地省份, season VARCHAR(50) COMMENT 适宜种植季节如春季/秋季, growth_cycle INT COMMENT 生长周期单位天, temperature VARCHAR(50) COMMENT 适宜温度范围如15-25℃, sowing_rate VARCHAR(50) COMMENT 亩用种量, price DECIMAL(10,2) NOT NULL COMMENT 销售单价, unit VARCHAR(20) DEFAULT 袋 COMMENT 销售单位袋/克/株, stock INT NOT NULL DEFAULT 0 COMMENT 库存数量, image_url VARCHAR(255) COMMENT 商品封面图, detail TEXT COMMENT 详情介绍, status TINYINT DEFAULT 1 COMMENT 状态1上架0下架, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_category (category), INDEX idx_season (season) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有几个设计决策值得解释一下。category和season加索引是因为商城首页最常用的操作就是按分类和季节筛选不加索引的数据表在数据量上来后会明显变慢。unit字段单独存在而不是直接拼在价格字段里是因为种子的单位很乱有的按袋卖有的按克卖同一个品种可能两种包装都有。growth_cycle用整数天数而不是字符串是为了以后做种植日历、生长提醒这类扩展功能时有数值可以计算。3.2 用户、订单、订单明细与库存扣减的关联关系种子商城需要用户系统但不需要搞太复杂。我设计了四张核心表users、seeds、orders、order_items。关系模型是一个用户可以有多个订单一个订单包含多个订单明细每个细项对应一个种子商品。用户表CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, phone VARCHAR(20), role TINYINT DEFAULT 0 COMMENT 角色0普通用户1管理员, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );密码字段存的是password_hash不是明文密码这是安全底线。实际开发中我用 bcrypt 加密注册时加密存入登录时比对哈希值即使数据库泄露用户密码也不会直接暴露。订单表CREATE TABLE orders ( id INT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) UNIQUE NOT NULL COMMENT 订单号, user_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT 状态0待付款1待发货2已完成3已取消, receiver_name VARCHAR(50), receiver_phone VARCHAR(20), receiver_address VARCHAR(255), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES users(id) );订单明细表CREATE TABLE order_items ( id INT AUTO_INCREMENT PRIMARY KEY, order_id INT NOT NULL, seed_id INT NOT NULL, seed_name VARCHAR(100) COMMENT 下单时的商品名称快照, price DECIMAL(10,2) COMMENT 下单时的单价快照, quantity INT NOT NULL COMMENT 购买数量, subtotal DECIMAL(10,2) COMMENT 小计金额, CONSTRAINT fk_items_order FOREIGN KEY (order_id) REFERENCES orders(id) );为什么订单明细里要冗余一份seed_name和price因为商品名称和价格是会变的如果商家改了价格历史订单里的价格也要能查得到。把下单时刻的快照存下来才能保证订单数据的可追溯性。这个设计在电商订单系统里是基本功但在课程设计项目里经常被省略等到运营时才发现账单对不上非常被动。3.3 库存扣减在事务里完成避免超卖再说一个容易被忽略的库存问题。商城下单流程不能只是“前端把数量减一下”因为多人同时下单时会发生超卖库存只剩10袋两个人各买6袋如果不加限制两个订单都能成功库存变成负数。正确做法是在数据库层面做原子操作和事务START TRANSACTION; UPDATE seeds SET stock stock - 6 WHERE id 1 AND stock 6; SELECT ROW_COUNT(); -- 如果影响行数为1说明扣减成功插入订单和明细 -- 如果影响行数为0说明库存不足回滚事务 COMMIT;这段 SQL 的关键在于WHERE id 1 AND stock 6它把“判断库存够不够”和“扣减库存”合并成了一个原子操作。只有满足条件时才会更新行ROW_COUNT()返回1说明成功返回0说明库存不足此时直接回滚事务即可。我在后端接口里是这样实现的const connection await pool.getConnection(); try { await connection.beginTransaction(); const [result] await connection.execute( UPDATE seeds SET stock stock - ? WHERE id ? AND stock ?, [quantity, seedId, quantity] ); if (result.affectedRows 0) { await connection.rollback(); return res.json({ code: 1, message: 库存不足 }); } // 插入订单主表和明细表... await connection.commit(); } catch (err) { await connection.rollback(); throw err; } finally { connection.release(); }这种写法是专门为商城系统准备的初学者如果直接写“先 SELECT 查库存再 UPDATE”在高并发下一定会出问题建议在一开始就养成分事务处理库存的习惯。4. Node.js 后端实现接口设计、鉴权与图片上传4.1 RESTful 接口清单与请求响应约定后端设计先从接口清单入手。我习惯先把接口和数据模型定下来这样写前端的时候心里有数不用边写边猜。种子商城核心接口如下方法路径功能说明访问权限GET/api/seeds种子列表支持分类、季节、关键词筛选公开GET/api/seeds/:id种子详情公开POST/api/users/register用户注册公开POST/api/users/login用户登录返回 JWT Token公开GET/api/users/profile获取当前用户信息登录用户POST/api/cart/add添加购物车登录用户GET/api/cart查看购物车登录用户POST/api/orders创建订单登录用户GET/api/orders查询我的订单登录用户POST/api/seeds新增种子商品管理员PUT/api/seeds/:id修改种子商品管理员POST/api/upload上传商品图片管理员接口统一返回 JSON我沿用了一个简单的约定{ code: 0, data: ... }表示成功{ code: 1, message: ... }表示业务失败。HTTP 状态码就用 200 表示请求到达业务错误不再额外堆砌状态码这样前后端联调时看一眼返回结构就知道成功还是失败。4.2 Express 主服务搭建与路由中间件Express 是 Node.js 社区最经典的 Web 框架配置非常轻量。主入口文件app.js大致长这样const express require(express); const cors require(cors); const path require(path); const app express(); // 中间件配置 app.use(cors()); // 解决跨域 app.use(express.json()); // 解析 JSON 请求体 app.use(/uploads, express.static(path.join(__dirname, uploads))); // 静态文件目录 // 路由注册 app.use(/api/seeds, require(./routes/seeds)); app.use(/api/users, require(./routes/users)); app.use(/api/orders, require(./routes/orders)); app.use(/api/cart, require(./routes/cart)); // 部署时托管 Vue 构建产物 app.use(express.static(path.join(__dirname, ../client/dist))); const PORT process.env.PORT || 3000; app.listen(PORT, () { console.log(种子商城服务已启动http://localhost:${PORT}); });路由文件里再拆具体的业务逻辑比如种子列表接口// routes/seeds.js const express require(express); const router express.Router(); const db require(../db); router.get(/, async (req, res) { const { category, season, keyword } req.query; let sql SELECT * FROM seeds WHERE status 1; const params []; if (category) { sql AND category ?; params.push(category); } if (season) { sql AND season LIKE ?; params.push(%${season}%); } if (keyword) { sql AND name LIKE ?; params.push(%${keyword}%); } sql ORDER BY created_at DESC; const [rows] await db.query(sql, params); res.json({ code: 0, data: rows }); }); module.exports router;这里用db.query的参数占位符而不是把用户输入直接拼进 SQL 字符串是为了防范 SQL 注入。初学者很容易在这里图省事用字符串拼接一旦有人传一个恶意参数进去整张表都可能被删掉。使用?占位符配合 MySQL 驱动自动转义是最基本的防线。4.3 图片上传、端口隐藏与本地启动脚本的实操细节种子商品需要上传图片我用 Multer 处理。Multer 是 Express 生态里最常用的文件上传中间件配置一个上传目录和文件命名规则就行const multer require(multer); const path require(path); const storage multer.diskStorage({ destination: function (req, file, cb) { cb(null, path.join(__dirname, ../uploads)); }, filename: function (req, file, cb) { const uniqueSuffix Date.now() - Math.round(Math.random() * 1e9); const ext path.extname(file.originalname); cb(null, file.fieldname - uniqueSuffix ext); } }); const upload multer({ storage, limits: { fileSize: 2 * 1024 * 1024 } // 限制 2MB });上传处理函数里接收文件后把它在数据库里的image_url字段设置为/uploads/图片文件名前端直接拼上后端域名就能访问。关于热搜词里提到的“nodejs怎样隐藏端口号”这里一起说清楚。开发阶段端口号可用环境变量管理不硬编码。做法是在项目根目录创建.env文件PORT3000 DB_HOSTlocalhost DB_USERroot DB_PASSWORDyourpassword DB_NAMEseed_shop然后在代码里读取require(dotenv).config(); const PORT process.env.PORT || 3000;部署到服务器之后隐藏端口最重要的手段是前置一层 Nginx 反向代理。用户访问域名 80 端口或 443 端口Nginx 把请求转发给局域网内部或本机的 3000 端口这样外部根本接触不到 Node.js 的真实端口。Nginx 配置的核心片段server { listen 80; server_name yourdomain.com; location / { proxy_pass http://127.0.0.1:3000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这样的好处有两个第一安全攻击者无法直接针对 Node 服务的漏洞端口发起攻击第二以后可以很自然地加 HTTPS443 端口 SSL 证书不用改应用代码。最后说一个很多人问过的本地启动问题。热搜词里有一条“建一个.bat文件在桌面运行nodejs打开服务”这件事的完整做法是在桌面新建启动种子商城.bat内容写echo off chcp 65001 nul echo 正在启动种子商城后端服务... cd /d D:\seed-shop\server node app.js pause注意chcp 65001是为了避免 bat 文件里中文乱码cd /d表示切换驱动器并进入目录。双击这个文件就能拉起服务适合给不熟悉命令行的店主使用。但生产环境不要用 bat应该用 PM2 或 systemd 做进程守护这部分放在第六节讲。5. Vue 前端实现商城页面、购物车状态与路由守卫5.1 前端项目结构与商城页面组件划分Vue 3 项目创建好之后我习惯把src目录按功能模块组织src/ ├── api/ # 所有接口请求封装 │ ├── seeds.js │ ├── users.js │ └── orders.js ├── components/ # 通用组件 │ ├── SeedCard.vue │ └── HeaderBar.vue ├── views/ # 页面组件 │ ├── HomeView.vue │ ├── SeedListView.vue │ ├── SeedDetailView.vue │ ├── CartView.vue │ ├── CheckoutView.vue │ └── AdminView.vue ├── stores/ # Pinia 状态管理 │ └── cart.js ├── router/ # Vue Router 配置 │ └── index.js └── App.vue这里强调一个组件划分原则一个组件只干一件事。SeedCard.vue负责单张种子卡片的展示SeedListView.vue负责列表页的数据请求和筛选交互HomeView.vue只放首页框架。这样写的好处是后面改样式、接接口、加新页面都不会牵一发动全身。5.2 种子列表筛选、详情与购物车状态管理前端最核心的交互之一是种子列表页。用户从首页点进“蔬菜种子”分类页面需要马上筛选出对应数据。我的实现是让列表页监听路由参数变化参数变了就重新请求接口script setup import { ref, watch } from vue; import { useRoute, useRouter } from vue-router; import { getSeeds } from ../api/seeds; import SeedCard from ../components/SeedCard.vue; const route useRoute(); const router useRouter(); const seedList ref([]); const loading ref(false); async function loadSeeds() { loading.value true; const category route.query.category || ; const keyword route.query.keyword || ; const res await getSeeds({ category, keyword }); seedList.value res.data; loading.value false; } watch(() route.query, loadSeeds, { immediate: true }); function changeCategory(category) { router.push({ path: /seeds, query: { category } }); } /script如果把筛选状态放在组件内部刷新页面或者复制链接给别人时筛选条件就会丢。放在 URL 的 query 参数里页面刷新后依然能根据 URL 恢复筛选状态这是电商列表页的推荐做法。购物车状态我用 Pinia 管理。为什么不用组件本地 state因为购物车涉及好几个页面的数据共享列表页加购、购物车页修改数量、结算页读取总价。Vue 组件之间的通信跨多层会很痛苦Pinia 是官方推荐的集中状态管理方案写起来也简单import { defineStore } from pinia; export const useCartStore defineStore(cart, { state: () ({ items: [], totalCount: 0, totalAmount: 0 }), actions: { addItem(seed, quantity 1) { const existed this.items.find(item item.seedId seed.id); if (existed) { existed.quantity quantity; } else { this.items.push({ seedId: seed.id, name: seed.name, price: seed.price, image_url: seed.image_url, quantity }); } this.recalc(); }, removeItem(seedId) { this.items this.items.filter(item item.seedId ! seedId); this.recalc(); }, recalc() { this.totalCount this.items.reduce((sum, item) sum item.quantity, 0); this.totalAmount this.items.reduce((sum, item) sum item.price * item.quantity, 0); }, clear() { this.items []; this.totalCount 0; this.totalAmount 0; } } });购物车数据按说应该在后端也存一份这样用户换设备后还能看到购物车。不过对于小型种子商城我建议先做前端持久化——把购物车数据写进 localStorage刷新页面不丢而且不用为购物车单独建表和接口成本最低。等业务真到了需要跨设备同步的规模再迁移到后端不迟。5.3 登录鉴权与后台管理的路由守卫商城后台只能管理员进普通用户可以访问商品和下单但看到后台管理页面时会被拦下来。这个控制要用 Vue Router 的全局前置守卫实现import { createRouter, createWebHistory } from vue-router; import { useUserStore } from ../stores/user; const router createRouter({ history: createWebHistory(), routes: [ { path: /, component: () import(../views/HomeView.vue) }, { path: /seeds, component: () import(../views/SeedListView.vue) }, { path: /seed/:id, component: () import(../views/SeedDetailView.vue) }, { path: /cart, component: () import(../views/CartView.vue) }, { path: /checkout, component: () import(../views/CheckoutView.vue) }, { path: /admin, component: () import(../views/AdminView.vue), meta: { requiresAdmin: true } } ] }); router.beforeEach((to) { const userStore useUserStore(); if (to.meta.requiresAdmin userStore.role ! 1) { return { path: /, query: { redirect: to.fullPath } }; } }); export default router;登录态我用 JWT Token 实现。用户登录成功后后端返回 Token前端把它存在 localStorage 里。每次请求 API 时在请求头里带上Authorization: Bearer token后端通过中间件校验用户身份和角色。// 前端 api/request.js 的请求拦截器 import axios from axios; const request axios.create({ baseURL: /api, timeout: 10000 }); request.interceptors.request.use((config) { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; });有人会问为什么不直接用 Cookie 而要用 JWT原因主要有两点一是前端跨域场景下 Cookie 配置比较复杂二是 JWT 是无状态的后端不用存 Session对后续水平扩展更友好。但 JWT 也有个需要注意的地方它一旦签发在过期之前是没法主动失效的所以 Token 有效期不要设置太长我通常设成 24 小时并要求用户密码不能太简单。6. 前后端联调与上线的坑跨域、代理、打包和进程守护6.1 开发环境跨域问题的两种处理方式前后端分开跑时Vue 默认在 5173 端口Express 在 3000 端口浏览器会拦截不同端口之间的请求这就是跨域问题。常见处理方式有两种我根据自己的经验把优缺点列出来方式实现难度适用场景备注后端开启 CORS 中间件一行代码开发环境联调、前后端分离部署需要额外处理预检请求Vite 代理转发配置简单开发环境最推荐请求路径不变无跨域问题我在后端已经加了app.use(cors())这是最基础的方案。但在实际开发中我更喜欢用 Vite 代理因为代理之后前端代码里的接口路径可以写成/api/seeds这种相对路径部署时不需要改任何请求地址。Vite 代理配置如下写在client/vite.config.jsimport { defineConfig } from vite; import vue from vitejs/plugin-vue; export default defineConfig({ plugins: [vue()], server: { proxy: { /api: { target: http://localhost:3000, changeOrigin: true } } } });配置完成后浏览器访问 Vite 开发服务器时所有/api开头的请求会被自动转发到 3000 端口。浏览器看见的始终是同源请求完全绕开了跨域。这是前端开发者最容易踩的坑之一我见过太多人明明接口没问题却因为跨域报错卡了一整天。6.2 打包部署Express 托管静态文件 PM2 守护开发完成后上线流程分三步。第一步前端打包cd client npm run build打包完成后dist目录里就是压缩和优化后的静态文件。第二步让 Express 托管这个目录上一节app.js里已经写过这段代码app.use(express.static(path.join(__dirname, ../client/dist)));这个配置很关键它让前端页面和后端接口在同一个端口下对外提供服务用户访问一次部署域名就能同时拿到页面和接口不需要单独为前端配置静态服务器。第三步用 PM2 把 Node.js 服务变成守护进程。直接node app.js启动的进程只要终端一关或者服务器意外重启服务就没了。PM2 是 Node.js 生态里最常用的进程管理工具安装和启动命令很简单npm install -g pm2 cd /root/seed-shop/server pm2 start app.js --name seed-shop pm2 savepm2 save会把当前进程列表保存下来配合pm2 startup命令服务器重启后 PM2 会自动拉起服务。这样基本做到了无人值守。PM2 有几个常用命令要记一下pm2 logs查看日志、pm2 restart seed-shop重启服务、pm2 status查看进程状态。排查线上问题时先pm2 logs看报错比瞎猜快得多。6.3 上线后最容易忽略的数据与安全细节上线不是终点我在这里把几个容易忽略的点集中提醒一下。第一数据库备份。种子商城虽然业务量小但订单数据不可丢失。最简单的方式是在服务器上写一个定时任务每天凌晨用 mysqldump 把数据库导出成 SQL 文件并保留最近 7 天的备份mysqldump -u root -p yourpassword seed_shop /backup/seed_shop_$(date \%Y\%m\%d).sql再用 crontab 设置每天执行一次。这套方案对于小型项目完全够用。第二上传文件的类型校验。Multer 里我限制了文件大小为 2MB但还应该限制文件类型防止用户上传可执行文件。在 Multer 的 fileFilter 里检查文件扩展名和 MIME type只允许 jpg、png、webp 格式。第三Nginx 层面的安全加固。Nginx 上配置请求体大小限制和超时时间避免有人通过超大的请求体攻击后端client_max_body_size 5m; proxy_read_timeout 60s;第四日志。Express 默认不打请求日志联调时出了问题很难排查。建议引入 morgan 中间件一行代码就能把每个请求的方法、路径、状态码、耗时打到控制台或日志文件里const morgan require(morgan); app.use(morgan(combined));我在实际项目里被这个问题坑过一次用户反馈下单老是失败但我反复测试都没复现后来打开日志才发现是某个请求带着特殊字符导致 JSON 解析失败。没有日志的话这种情况非常难定位。最后再回到种子商城这个项目本身。说实话这套系统从技术结构上看并不复杂核心就是 Express 提供接口、Vue 展示页面、MySQL 存数据但它完整覆盖了一个真实线上商城从零到上线的所有环节。我在开发过程中最有价值的体会是好的系统设计不是堆砌新技术而是让每个模块都恰好在它该在的位置上前端把筛选条件和购物车数据流转管清楚后端把库存扣减和订单状态管严谨部署环节把端口隐藏和进程守护做好这套系统就足够稳定地支撑一家种子店的线上生意了。如果你们也在做同类项目我的建议是先把环境问题处理干净再动手写业务代码。环境配置看着不起眼但它决定了你后面的开发心情和效率。npm.ps1 执行策略这些问题一分钟就能修好但不知道原理的人能卡上半天。把这些基础问题一次性搞定剩下的开发过程会比想象中顺畅得多。