做家装商城这件事,和我以前做的普通电商完全不是一个套路。普通电商把商品挂上去、购物车结算、发货就完事了,家装平台得把"效果图展示、主材选购、设计师预约、施工订单跟踪"这一整条链条在同一个站点里跑通。这个项目严格来说是Node.js + Vue的全栈项目,后端用Node.js提供API,前端用Vue 3搭单页应用,面向的是一站式的家装在线服务。这篇文章我会把整个项目的设计思路、核心业务建模、前后端实现、以及踩过的那些坑都过一遍,基本按照我实际开发的顺序来写,如果你手头也打算做类似的行业商城,可以直接对着这套方案抄作业。

1. 项目定位与整体设计思路

1.1 家装商城不是普通电商,先想清楚业务模型

家装行业和快消品电商最大的区别在于:客单价高、消费频次极低、决策周期长、参与角色多。用户买一件衣服可能三分钟下单,装修一套房子从浏览案例、预约设计、选材料到最终签施工合同,可能要花一到两个月。这就决定了平台的业务模型不能是"一件商品卖出去就结束",而是要从内容种草到交易履约,全程都在平台上形成闭环。

我最初设计这个项目时,和团队反复对过需求,最后把核心流程收敛成五条主线:

  • 装修案例浏览:用户通过真实完工案例了解平台水平,案例以图文和视频形式展示。
  • 设计预约:用户在线预约设计师,填写房屋信息、风格偏好,设计师后台接单。
  • 主材与套餐选购:卫浴、瓷砖、地板、全屋套餐等以标准SKU形式在线售卖。
  • 订单履约:用户下单施工服务或材料商品,后端生成订单并跟踪状态。
  • 会员与营销:积分、优惠券、限时折扣辅助转化,这个对家装类平台非常有效。

把这五条主线拆清楚以后,我再去设计数据库、接口和前端页面,整个项目的边界就非常明确。很多新手做项目最忌讳一上来就写代码,结果业务边界模糊,改到最后代码和需求互相打架。家装平台尤其要注意,因为它牵扯线下服务,模型没理清楚,后面订单、财务、结算全部都要返工。

1.2 技术选型:为什么是Node.js + Vue,而不是Java + React?

我在这套项目里选择Node.js + Vue,不是跟风,而是从实际约束条件出发做的一个理性选择。

先说后端。Node.js最大的优势是开发效率高,JS采用异步非阻塞的I/O模型,在处理高并发I/O密集场景时非常擅长。商城系统80%的接口本质上是数据库读写和文件访问,比如商品列表、订单查询、图片加载,这些都是典型的I/O密集型操作。虽然Java在后端生态上更成熟,但对于一个中小型家装商城项目来说,Spring Boot的初始化成本和部署成本都更高,Node.js配合Express框架可以用更短的代码量实现同样的接口。

再说前端。Vue在国内的普及率相当高,上手曲线比React平缓,模板语法和原生HTML高度接近,配合Element Plus组件库做后台管理页面非常顺手。Vue的响应式原理使得页面状态管理更简单,性能上也足够满足商城前端的需求。如果你是个人开发者或者小团队接活,Vue + Node.js这套组合几乎是效率最优解,前后端都用JS,还能省掉语言切换的认知负载。

选型的时候也考虑过其他方案,我把当时的对比整理成一张表:

维度 Node.js + Vue Java + Spring Boot + Vue Python + Django + Vue
开发效率 高,前后端同语言 中,样板代码较多 高,内置功能丰富
性能水平 高并发I/O表现优秀 稳定,适合大型系统 中等,适合中小型项目
学习成本 低,JS门槛低 高,Java体系庞大 低,Python易上手
部署运维 轻量,单进程即可 较重,依赖JVM和容器 中等,依赖Python环境
适合场景 中小型电商、全栈应用 企业级复杂系统 内容站、工具站、后台

考虑到项目定位是"能跑起来的完整商城"且要求快速交付,Node.js + Vue是性价比最高的选择。如果你追求极致性能或者公司有Java技术栈沉淀,那选Spring Boot也没问题,但本文后面的设计思路和数据结构仍然可以复用。

1.3 工程目录与前后端架构规划

这个项目我采用了前后端分离架构,仓库分为两个工程目录: mall-server (后端API服务)和 mall-web (前端单页应用)。后端负责业务逻辑、数据库操作和文件服务,前端负责页面渲染和用户交互,两者通过RESTful API通信。

先看后端目录结构:

mall-server/
├─ app.js                  // 服务入口文件
├─ config/
│  ├─ db.js                // 数据库连接配置
│  └─ env.js               // 环境变量配置
├─ models/                 // Sequelize数据模型
│  ├─ user.js
│  ├─ product.js
│  ├─ sku.js
│  ├─ order.js
│  └─ case.js
├─ routes/                 // 路由模块
│  ├─ user.routes.js
│  ├─ product.routes.js
│  ├─ order.routes.js
│  └─ case.routes.js
├─ middleware/             // 中间件
│  ├─ auth.js              // JWT鉴权
│  └─ upload.js            // 文件上传
└─ utils/
   ├─ response.js          // 统一响应格式
   └─ logger.js            // 日志工具

前端目录结构:

mall-web/
├─ public/
└─ src/
   ├─ api/                 // 接口请求封装
   ├─ assets/
   ├─ components/          // 公共组件
   ├─ router/              // 路由配置
   ├─ store/               // Pinia状态库
   ├─ views/               // 页面
   │  ├─ home/
   │  ├─ product/
   │  ├─ cart/
   │  ├─ order/
   │  ├─ user/
   │  └─ admin/
   └─ utils/

这套结构的核心逻辑是"模块按业务划分,而不是按技术类型划分"。比如商品相关的页面都放在 views/product 下面,组件就放在 components/product 下面,这样做的好处是业务范围清晰,后期扩展新功能时找文件非常快。我见过很多项目按 views/page1 、 views/page2 这样命名,项目一大就完全失控,改一个需求要好几个文件来回跳。

2. 核心功能模块设计与数据建模

2.1 商品中心:普通电商的商品模型对装修套餐不够用

做家装商城,最核心的模块是商品中心,但它的商品模型和普通电商有本质差异。普通电商只需要SPU(标准商品单元)+ SKU(库存量单位)两级模型,比如一件T恤,SPU是"白色圆领T恤",SKU是"白色-XXL"。家装平台除了卖标准材料,还卖装修套餐,一个套餐可能包含地板、墙面、卫浴、主灯等几十个SKU,而且每个SKU有品牌、型号、面积单价这些属性。

我在这套项目里设计了三张基础表:

表名 字段示例 说明
products id, title, category_id, type 商品主表,type区分单商品还是套餐
skus id, product_id, name, price, stock 商品规格表,套餐SKU可以关联多个子项
product_items id, package_id, sku_id, quantity 套餐明细,存一个套餐包含哪些商品

商品主表的 type 字段很关键,它把"单商品"和"套餐"分成两类。普通商品点开直接下单购买,套餐商品则展示套餐明细,购买后平台需要安排配送或施工服务。这个设计让后台可以灵活配置,商家上架单个卫浴产品,或者上架一个"厨房焕新套餐"都走同一套逻辑。

装修商品还有一个特点是规格属性复杂,比如瓷砖要有规格、材质、风格、适用空间四个维度,卫浴要有安装方式、排水类型等。我额外建了一张 product_attrs 表,用JSON字段存动态属性,这样不用为每种商品类型建一张属性表,后端接口返回时直接透传给前端渲染。实际跑下来挺稳的,即便是后期加了智能家居这个新品类,也只是改JSON结构,不用动表结构。

2.2 预约设计与服务流程闭环

家装商城和普通电商另一个显著的差异,是有线下服务环节。用户不会直接在一个页面上下单"装修一套房子",他更希望能先约设计师聊一聊,了解报价和方案,然后才决定是否合作。所以平台必须有一套预约管理系统。

我的做法是建一张 appointments 表,字段包括:用户ID、设计师ID、预约时间、房屋地址、面积、风格偏好、描述信息、状态(待接单/已确认/已完成/已取消)。前端在设计师详情页展示一个预约表单,后端收到请求后,在设计师的后台面板生成一条待接单记录。设计师接单之后,双方可以在平台消息里沟通,这个流程就把"线上浏览"和"线下量房"串起来了。

接口设计上我提供了两个视角的查询接口:用户端按手机号或用户ID查自己的预约记录,设计师端按设计师ID查待办列表。核心是后端的 status 状态机要设计好,因为预约状态直接影响后续订单生成, 待确认 -> 已确认 -> 已生成订单 -> 已完成 这条链路要保证数据一致性。

2.3 购物车、订单与支付状态机

购物车模块我采用的是"前端本地存储 + 后端校验"方案。用户未登录时,购物车数据存在浏览器localStorage里,登录后前端会把本地购物车同步到后端表 cart_items 。这个方案实施起来比纯后端购物车简单很多,而且用户不会因为换设备就丢失购物车,体验很好。后端校验的作用是防止用户篡改价格,每次提交购物车时,后端都会重新查询数据库中的最新价格和库存。

订单模块是整个系统的重点,我设计了一张订单主表 orders ,字段包括:订单编号、用户ID、总金额、支付方式、收货地址、订单状态、创建时间。另外有一张 order_items 子表,存订单包含的商品明细,这样订单生成后即便商品价格变动,订单详情里的快照数据也不会变,这是电商系统很基础但很多人会忽略的设计。

订单状态变化我整理为:

状态值 含义 触发动作
0 待支付 用户提交订单,等待付款
1 已支付/待发货 支付成功,商家后台可看到
2 已发货/施工中 商品出库或施工进场
3 已签收/已完成 用户确认收货或验收完成
4 已取消 用户或后台取消订单

支付环节我接入了微信和支付宝的沙箱环境,生产环境替换成正式商户号即可。注意支付回调要幂等处理,同一个支付回调通知可能因为网络原因发送多次,后端必须判断该订单是否已经是"已支付"状态,否则会产生重复发货的问题。我当时还额外写了一个定时任务,超过30分钟未支付的订单自动取消并释放库存,避免资源被无效占用。

2.4 会员、优惠券与装修进度跟踪

营销模块对家装平台很重要,因为用户决策周期长,需要用优惠手段持续触达。我实现了三类基本玩法:

  • 会员积分:注册、下单、评价都可获得积分,积分可以在商城兑换小礼品或抵扣现金。
  • 优惠券:后台创建满减券,比如满5000减200,领取后存在 coupons 表,下单时校验是否满足使用条件。
  • 限时秒杀:针对热门主材设置秒杀活动,秒杀接口用Redis做库存预扣减,防止超卖。

装修进度跟踪是这个项目的亮点功能。用户签订施工订单后,后台可以按阶段更新工程状态,如"拆改阶段""水电阶段""泥瓦阶段""木工阶段""油漆阶段""竣工阶段",每个阶段可以上传现场照片和文字说明。用户在个人中心可以看到一个进度时间轴,直观了解装修进展。这个功能对平台的价值在于,它大幅增强了用户对平台的信任感,也降低了客服沟通成本。

数据表设计上, renovation_progress 表包含订单ID、阶段名、描述、现场图片URL、更新时间。每次更新相当于往表里插入一条新记录,前端按时间倒序渲染,天然形成一条时间轴,实现起来非常简单但效果很好。

3. 环境搭建到前后端联调全流程

3.1 Node.js安装与npm配置,先把开发环境踩平

先聊环境。很多新人在第一个环节就卡住了,而且卡的点特别集中。Node.js安装本身不难,去官网下载LTS版本,Windows用户直接双击安装包即可,一路下一步。需要注意两点:

第一,建议下载LTS(长期支持版)而不是Current(最新版)。LTS版本经过了更长时间的稳定性验证,兼容性更好,很多第三方依赖对最新版本的支持是滞后的,用LTS能省掉大量莫名其妙的兼容性问题。我当时用的Node 18 LTS,到现在项目跑得非常稳定。

第二,安装路径最好不要带空格和中文,否则后续一些工具链会出问题。推荐直接装在 D:\nodejs 或者 C:\nodejs 这种干净路径下,避免踩坑。

装完以后,在命令行里执行:

node -v
npm -v

能看到版本号就说明安装成功。如果用Windows PowerShell,很大概率会遇到下面这个报错:

npm.ps1无法加载文件...因为在此系统上禁止运行脚本

这个问题的原因我简单说一下:PowerShell的安全策略默认是Restricted,禁止执行任何.ps1脚本,而npm在Windows上的可执行入口恰好是一个 npm.ps1 文件。解决办法是修改PowerShell的执行策略,用管理员身份打开PowerShell,执行:

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

然后输入Y确认即可。 RemoteSigned 的含义是:本地脚本可以运行,从网上下载的脚本必须有信任签名。这个是微软官方推荐的策略,不会降低安全性,日常开发完全够用。如果你不想改策略,另外一个简单粗暴的办法是换CMD来执行npm命令,CMD不检查执行策略,完全绕开这个问题。

npm安装依赖慢的问题,推荐切换淘宝镜像源:

npm config set registry https://registry.npmmirror.com

切换完用 npm config get registry 验证一下,以后下载依赖的速度会明显提升。

3.2 Vue项目初始化与UI组件库集成

Vue项目我推荐用Vite作为构建工具,它比Webpack冷启动快很多,几十毫秒就能起服务,开发体验完全不是一个档次。创建项目的命令:

npm create vue@latest

按提示选择Vue Router、Pinia、ESLint等选项,对你需要的功能都选Yes,其他可以No。新版脚手架会生成一个干净的Vue 3项目,然后安装Element Plus作为UI组件库:

npm install element-plus
npm install axios vue-router@4 pinia

在 main.js 里全局注册Element Plus:

import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'

const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')

这里我多说一句:Element Plus的组件按需引入和全量引入选择上,项目初期直接用全量引入就够,因为Element Plus的包体积虽然不小,但通过Vite构建的Tree Shaking机制,实际生产包的体积是可接受的。等业务增长到需要关心首屏加载速度时,再切换成按需自动导入方案。全量引入的优点是省心,不需要额外配置插件,对中小型项目来说,少一个配置项就少一个踩坑点。

3.3 基于Express搭建后端接口服务

后端我用Express框架。初始化也很简单:

mkdir mall-server
cd mall-server
npm init -y
npm install express mysql2 sequelize cors jsonwebtoken bcryptjs

数据库选用MySQL,我用Sequelize作为ORM。选Sequelize的原因是我懒得手写SQL,而且它可以做数据库表结构迁移,团队协作时每个人在本地跑一次迁移就能生成同样的表结构,非常省心。

创建连接:

const { Sequelize } = require('sequelize')

const sequelize = new Sequelize('mall', 'root', 'password', {
  host: 'localhost',
  dialect: 'mysql',
  logging: false
})

module.exports = sequelize

定义一个简单的商品模型:

const { DataTypes } = require('sequelize')
const sequelize = require('../config/db')

const Product = sequelize.define('Product', {
  id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
  title: { type: DataTypes.STRING, allowNull: false },
  categoryId: { type: DataTypes.INTEGER, allowNull: false },
  type: { type: DataTypes.INTEGER, defaultValue: 1 }, // 1单商品 2套餐
  price: { type: DataTypes.DECIMAL(10, 2), allowNull: false },
  stock: { type: DataTypes.INTEGER, defaultValue: 0 },
  cover: { type: DataTypes.STRING },
  status: { type: DataTypes.INTEGER, defaultValue: 1 }
}, {
  tableName: 'products',
  timestamps: true
})

module.exports = Product

商品查询接口是一个典型的分页列表接口,实现如下:

const { Op } = require('sequelize')
const Product = require('../models/product')

router.get('/list', async (req, res) => {
  try {
    const page = parseInt(req.query.page) || 1
    const pageSize = parseInt(req.query.pageSize) || 20
    const { list, count } = await Product.findAndCountAll({
      where: { status: 1 },
      order: [['id', 'DESC']],
      offset: (page - 1) * pageSize,
      limit: pageSize
    })
    res.json({ code: 0, data: { list, total: count } })
  } catch (err) {
    res.status(500).json({ code: 500, message: err.message })
  }
})

findAndCountAll 会同时返回数据列表和总条数,一次查询就满足前端分页组件的需求,不需要额外发请求。这里注意,所有查询条件最好都放在 where 里,不要在前端取全量数据再过滤,数据量一大页面必卡。

3.4 JWT鉴权和Axios请求封装,把登录体系串起来

登录模块是整个商城系统的基础,前后端联调时最先要打通的就是登录接口。我用JWT(JSON Web Token)实现无状态认证,这个方案的好处是后端不用维护Session,服务器扩展时不需要共享会话状态,非常适合前后端分离架构。

登录成功后端返回一个签名后的Token:

const jwt = require('jsonwebtoken')

const token = jwt.sign(
  { userId: user.id, username: user.username },
  process.env.JWT_SECRET,
  { expiresIn: '7d' }
)

前端的Axios请求拦截器统一在请求头上加上Token:

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
})

request.interceptors.response.use(
  response => {
    return response.data
  },
  error => {
    if (error.response && error.response.status === 401) {
      localStorage.removeItem('token')
      window.location.href = '/login'
    }
    return Promise.reject(error)
  }
)

export default request

后端写一个鉴权中间件:

const jwt = require('jsonwebtoken')

module.exports = function auth(req, res, next) {
  const authHeader = req.headers.authorization || ''
  const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : ''

  if (!token) {
    return res.status(401).json({ code: 401, message: '未登录' })
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET)
    req.userId = decoded.userId
    next()
  } catch (err) {
    return res.status(401).json({ code: 401, message: '登录已过期' })
  }
}

跨域问题上,前端开发环境通过Vite的proxy代理绕开跨域;生产环境用Nginx将 /api 反向代理到后端服务,也不存在跨域问题。开发环境的 vite.config.js 里这样配置:

export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true
      }
    }
  }
})

联调时最容易出错的就是接口路径不一致。建议前后端把接口文档放在一起维护,哪怕是简单的Excel表格,也要保证"路径、入参、出参"三块完全对齐,否则传输层永远在报错。

4. 装修案例视频:m3u8点播方案落地

4.1 为什么视频案例要用m3u8切片播放

家装平台的内容展示,图片只能展示静态效果,视频才能把空间感、采光、材质质感完整体现出来。我当时在装修案例详情页集成了视频播放能力,方案选的是HLS协议对应的m3u8格式。

m3u8是Apple推出的HTTP Live Streaming(HLS)协议里的索引文件,它把一段完整视频切成了很多个小的.ts分片文件,通过一个.m3u8索引文件按顺序播放。相比直接播放一个完整MP4文件,m3u8有几个明显的优势:

第一,播放器可以边下边播,用户拖动进度条时只需下载对应的分片,首屏加载更快。第二,服务端方便做码率自适应,网络好的时候播高清分片,网络差自动切低码率分片。第三,防盗链能力更强,因为分片文件是动态请求的,可以在服务端做校验。

实测下来,对于一个几百MB的装修案例视频,切成10秒一个的分片,用户在4G网络下开播只要1到2秒,体验比直出MP4好很多。后来我还用同一套方案做了工地直播回放,把监控摄像头录制的视频直接切片存档,代码几乎可以复用。

4.2 前端用video.js接入HLS播放

前端播放m3u8,我用的方案是video.js。Vue 3项目里先安装依赖:

npm install video.js

在组件里引入样式和播放器初始化逻辑:

<template>
  <div>
    <video
      id="caseVideo"
      ref="videoElement"
      class="video-js vjs-big-play-centered"
      controls
      preload="auto"
      style="width: 100%; height: 100%"
    ></video>
  </div>
</template>

<script setup>
import { onMounted, ref } from 'vue'
import videojs from 'video.js'
import 'video.js/dist/video-js.css'

const videoElement = ref(null)
let player = null

onMounted(() => {
  player = videojs(videoElement.value, {
    controls: true,
    autoplay: false,
    preload: 'auto',
    sources: [
      {
        src: 'https://your-cdn.com/video/2025/demo.m3u8',
        type: 'application/x-mpegURL'
      }
    ]
  })
})

onBeforeUnmount(() => {
  if (player) {
    player.dispose()
  }
})
</script>

关键点是 sources 里的 type 必须设置为 application/x-mpegURL ,video.js才能识别并调用HLS引擎进行分片请求。如果不设置这个type,有些版本的video.js会尝试按MP4格式解析,结果视频黑屏不出画面。

实际开发中我还遇到了一个情况:点击切换不同案例视频时,直接修改 sources 数组不够稳健,正确做法是先调用 player.src() 设置新地址,再调用 player.load() 重新加载:

function playNewVideo(url) {
  player.src({ src: url, type: 'application/x-mpegURL' })
  player.load()
  player.play()
}

不重新加载的话,播放器可能残留上一个视频的分片缓存,导致画面与标题不匹配,这个坑我在联调时排查了半天,最后加了 load() 就解决了。

4.3 视频处理与防盗链思路

服务端要生成m3u8索引和ts分片,我推荐直接用FFmpeg做转码切分,命令如下:

ffmpeg -i demo.mp4 -profile:v baseline -level 3.0 -start_number 0 -hls_time 10 -hls_list_size 0 -f hls demo.m3u8

几个参数简单解释一下:

  • -profile:v baseline :使用H.264 Baseline画质档位,兼容性最广,老手机也能播。
  • -hls_time 10 :每10秒切一个分片,这个值根据视频时长调整,短一些加载更快,长一些请求数更少,我建议10到15秒。
  • -hls_list_size 0 :生成的m3u8索引文件包含所有分片,默认值只会保留最近几个分片的记录,不加这个参数会出现视频播到一半就停止的问题。

转码完成后,把生成的.m3u8文件和.ts分片文件上传到对象存储服务并开启CDN分发即可。防盗链方面,我在生产环境做了两重措施:一是设置CDN的Referer白名单,只允许官网域名来源的请求访问视频资源;二是对部分付费内容的视频做签名URL校验,URL里带一个有时效的token参数,过期就返回403,这样即使别人拿到视频链接也无法直接播放。

5. 问题排查与避坑记录

5.1 npm.ps1无法加载脚本的系列问题

这个问题出现的频率太高了,几乎每天都能在各种开发群里看到。前面说过核心原因是PowerShell执行策略限制,但很多人改了策略以后还是遇到问题,这里我再补充几个排查方向:

第一,确认当前使用的是哪个shell。如果你在VS Code里用的是PowerShell终端,即使之前用CMD装好了Node,依然会触发这个报错。VS Code默认终端可能是PowerShell,可以在 Ctrl + Shift + P 里搜"Terminal: Select Default Profile",把默认切换成"Command Prompt"。

第二,执行策略修改后要重新打开终端窗口才会生效,不要在一个已经打开的终端里测试。第三,如果公司电脑有域策略锁死了执行策略,可以试试用 npx 代替 npm 运行某些命令,比如 npx vite ,因为npx走的执行链路不太一样,有时能绕过去。

我再提供一个终极方案:安装多版本管理工具 nvm-windows ,用它来安装Node。nvm安装的Node会把npm的软链接放到自己的目录,而且nvm自带的命令窗口不会触发PowerShell的脚本限制,治标又治本。更重要的是,nvm可以让你在多个Node版本之间自由切换,项目A需要Node 14、项目B需要Node 20的时候,一条命令就能搞定。

5.2 Vue打包后布局异常

开发环境一切正常, npm run build 之后打包上传到服务器,打开页面发现布局乱了、图片不显示、样式丢失,这是Vue新手最常遇到的问题。我结合自己的排查经验,把原因和解决方式整理了一下。

最常见的原因是静态资源路径配置不对。Vite默认的 base 是 / ,也就是说打包后的资源引用路径是 /assets/index-xxx.js ,但如果你把前端文件部署在服务器的子目录下,比如 https://example.com/mall/ ,这个绝对路径就会指向错误的位置。解决方法是在 vite.config.js 里设置:

export default defineConfig({
  base: './'   // 使用相对路径
})

这样构建出的 index.html 里资源引用会是相对路径,部署到任意子目录都不怕。

另一个常见原因是路由使用history模式后部署到Nginx时,刷新子页面出现404。这是SPA应用的通病——Nginx找不到 /mall/product/123 这个物理文件,需要配置try_files回退到index.html:

location /mall/ {
  try_files $uri $uri/ /mall/index.html;
}

这样无论用户刷新哪个路由,Nginx都会把请求转发到前端入口,再由Vue Router接管路由解析。

还有一类布局异常是CSS样式错乱,通常是Element Plus按需引入配置不正确,组件样式没有全量加载导致。检查 main.js 里是否完整引入了 element-plus/dist/index.css ,如果只引用了部分组件的样式,页面就会出现"有结构没样式"的诡异效果。

5.3 路由参数与页面数据刷新问题

开发过程中还遇到过路由参数跳转后页面数据不更新的情况。比如从装修案例列表页进入详情页,第一次打开数据正常,返回列表再点进另一个案例,详情页显示的还是上一个案例的内容。

这个问题的根源是Vue组件复用了:当路由参数变化但组件实例没有销毁时,Vue不会重新执行组件的 onMounted 钩子,所以数据不会重新拉取。解决方式有两个:

第一种,在组件内监听路由变化,通过 watch 触发数据请求:

import { watch } from 'vue'
import { useRoute } from 'vue-router'

const route = useRoute()

watch(() => route.params.id, (newId) => {
  if (newId) {
    fetchData(newId)
  }
}, { immediate: true })

第二种,给路由跳转加上 :key 强制组件重建:

<router-view :key="$route.fullPath" />

两方案对比,我推荐第一种。因为第二种会销毁重建整个页面,虽然能解决数据刷新问题,但页面会闪一下,而且组件内部的状态(比如滚动位置)都会丢失,体验不太好。

5.4 项目实战的延伸价值

这个项目做完以后,我最大的感受是它把前端和后端知识整个串起来了。之前学Vue可能停留在组件怎么写、路由怎么配,学Node可能停留在回调函数和模块化,但当你要做一个真实可运行的商城系统时,必须自己打通数据库设计、接口签名、鉴权、跨域、部署这一整条链路。

我后来面试也经常拿这套项目做案例来聊。面试官问Vue生命周期,我就说商品列表页从 onMounted 里拉数据到组件销毁时取消请求的完整流程;面试官问Node的事件循环,我就说高并发下订单接口的I/O阻塞怎么影响性能。有了真实项目背书,面试题不再是无源之水,答起来会很扎实。

写在最后的个人经验

这套项目从立项到跑通核心流程,我前前后后用了大概两个月,其中数据库设计和业务建模花的时间比写代码多得多。我最想分享的一个经验是:做全栈项目,一定要先花时间把业务模型画清楚,再去写代码。很多人在数据库建表阶段偷懒,表结构缺字段,后期就要靠加冗余字段或者拆表来补,改起来痛苦指数翻倍。

另外,Node.js做后端不是只能做Demo,配合Express、Sequelize和Redis,它完全有能力支撑一个日活几万的小型电商平台。选技术栈不用盲从大厂,符合自己团队能力和项目规模的就是最好的。

如果后续要继续扩展这个平台,我个人觉得有两个方向很值得投入:一个是接入即时通讯,让用户和设计师能在站内直接沟通;另一个是工地施工现场直播,用之前说的m3u8切片方案把监控画面实时推给业主。这两个功能对家装平台的信任提升比任何营销活动都管用。

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐