10.1 项目需求分析
10.1.1 项目概述
我们将开发一个完整的电商平台,包含以下核心功能:
- 商品展示与搜索
- 用户注册与登录
- 购物车管理
- 订单处理
- 支付集成
- 管理后台
10.1.2 技术栈选择
// 技术栈配置
const techStack = {
frontend: {
framework: 'Vue.js 2.x',
ssr: 'Vue Server Renderer',
router: 'Vue Router',
state: 'Vuex',
ui: 'Element UI',
build: 'Webpack 4',
bundler: 'Vue CLI'
},
backend: {
runtime: 'Node.js',
framework: 'Express.js',
database: 'MongoDB',
orm: 'Mongoose',
auth: 'JWT',
cache: 'Redis',
search: 'Elasticsearch'
},
devops: {
containerization: 'Docker',
orchestration: 'Docker Compose',
ci_cd: 'GitHub Actions',
monitoring: 'PM2',
logging: 'Winston'
}
}
10.1.3 项目架构设计
ecommerce-ssr/
├── client/ # 客户端代码
│ ├── components/ # Vue组件
│ ├── pages/ # 页面组件
│ ├── store/ # Vuex状态管理
│ ├── router/ # 路由配置
│ ├── plugins/ # 插件
│ ├── utils/ # 工具函数
│ └── assets/ # 静态资源
├── server/ # 服务端代码
│ ├── api/ # API路由
│ ├── models/ # 数据模型
│ ├── middleware/ # 中间件
│ ├── services/ # 业务服务
│ ├── utils/ # 工具函数
│ └── config/ # 配置文件
├── shared/ # 共享代码
│ ├── constants/ # 常量定义
│ ├── utils/ # 通用工具
│ └── types/ # 类型定义
├── build/ # 构建配置
├── static/ # 静态文件
├── tests/ # 测试文件
└── docs/ # 项目文档
10.2 项目初始化
10.2.1 项目脚手架搭建
# 创建项目目录
mkdir ecommerce-ssr
cd ecommerce-ssr
# 初始化package.json
npm init -y
# 安装核心依赖
npm install vue vue-server-renderer vue-router vuex express
# 安装开发依赖
npm install -D webpack webpack-cli webpack-dev-server
npm install -D vue-loader vue-template-compiler css-loader
npm install -D babel-loader @babel/core @babel/preset-env
npm install -D webpack-merge webpack-node-externals
npm install -D cross-env rimraf
# 安装UI库和工具
npm install element-ui axios lodash moment
npm install bcryptjs jsonwebtoken mongoose redis
npm install winston morgan helmet cors
# 安装测试工具
npm install -D jest @vue/test-utils supertest
10.2.2 Webpack配置
// build/webpack.base.config.js
const path = require('path')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const isProd = process.env.NODE_ENV === 'production'
module.exports = {
devtool: isProd ? false : '#cheap-module-source-map',
output: {
path: path.resolve(__dirname, '../dist'),
publicPath: '/dist/',
filename: '[name].[chunkhash].js'
},
resolve: {
alias: {
'public': path.resolve(__dirname, '../public'),
'@': path.resolve(__dirname, '../client'),
'~': path.resolve(__dirname, '../server'),
'shared': path.resolve(__dirname, '../shared')
},
extensions: ['.js', '.vue', '.json']
},
module: {
noParse: /es6-promise\.js$/, // avoid webpack shimming process
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
compilerOptions: {
preserveWhitespace: false
}
}
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.(png|jpg|gif|svg)$/,
loader: 'url-loader',
options: {
limit: 10000,
name: '[name].[ext]?[hash]'
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'fonts/[name].[hash:7].[ext]'
}
},
{
test: /\.css$/,
use: ['vue-style-loader', 'css-loader']
},
{
test: /\.scss$/,
use: ['vue-style-loader', 'css-loader', 'sass-loader']
}
]
},
performance: {
maxEntrypointSize: 300000,
hints: isProd ? 'warning' : false
},
plugins: isProd ? [
new VueLoaderPlugin(),
new webpack.optimize.UglifyJsPlugin({
compress: { warnings: false }
}),
new webpack.optimize.ModuleConcatenationPlugin()
] : [
new VueLoaderPlugin(),
new FriendlyErrorsPlugin()
]
}
// build/webpack.client.config.js
const webpack = require('webpack')
const merge = require('webpack-merge')
const base = require('./webpack.base.config')
const SWPrecachePlugin = require('sw-precache-webpack-plugin')
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')
const config = merge(base, {
entry: {
app: './client/entry-client.js'
},
resolve: {
alias: {
'create-api': './create-api-client.js'
}
},
plugins: [
// strip dev-only code in Vue source
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
'process.env.VUE_ENV': '"client"'
}),
// extract vendor chunks for better caching
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function (module) {
// a module is extracted into the vendor chunk if...
return (
// it's inside node_modules
/node_modules/.test(module.context) &&
// and not a CSS file (due to extract-text-webpack-plugin limitation)
!/\.css$/.test(module.request)
)
}
}),
// extract webpack runtime & manifest to avoid vendor chunk hash changing
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest'
}),
new VueSSRClientPlugin()
]
})
if (process.env.NODE_ENV === 'production') {
config.plugins.push(
// auto generate service worker
new SWPrecachePlugin({
cacheId: 'ecommerce-ssr',
filename: 'service-worker.js',
minify: true,
dontCacheBustUrlsMatching: /./,
staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/]
})
)
}
module.exports = config
// build/webpack.server.config.js
const webpack = require('webpack')
const merge = require('webpack-merge')
const nodeExternals = require('webpack-node-externals')
const base = require('./webpack.base.config')
const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')
module.exports = merge(base, {
entry: './client/entry-server.js',
target: 'node',
devtool: '#source-map',
output: {
filename: 'server-bundle.js',
libraryTarget: 'commonjs2'
},
resolve: {
alias: {
'create-api': './create-api-server.js'
}
},
// https://webpack.js.org/configuration/externals/#externals
// do not externalize CSS files in case we need to import it from a dep
externals: nodeExternals({
allowlist: /\.(css|vue)$/
}),
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
'process.env.VUE_ENV': '"server"'
}),
new VueSSRServerPlugin()
]
})
10.2.3 项目配置文件
// server/config/index.js
const path = require('path')
module.exports = {
// 服务器配置
server: {
port: process.env.PORT || 3000,
host: process.env.HOST || 'localhost'
},
// 数据库配置
database: {
mongodb: {
uri: process.env.MONGODB_URI || 'mongodb://localhost:27017/ecommerce',
options: {
useNewUrlParser: true,
useUnifiedTopology: true,
useCreateIndex: true,
useFindAndModify: false
}
},
redis: {
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASSWORD || '',
db: process.env.REDIS_DB || 0
}
},
// JWT配置
jwt: {
secret: process.env.JWT_SECRET || 'your-secret-key',
expiresIn: process.env.JWT_EXPIRES_IN || '7d'
},
// 文件上传配置
upload: {
path: path.join(__dirname, '../../static/uploads'),
maxSize: 5 * 1024 * 1024, // 5MB
allowedTypes: ['image/jpeg', 'image/png', 'image/gif']
},
// 邮件配置
email: {
service: process.env.EMAIL_SERVICE || 'gmail',
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS
},
// 支付配置
payment: {
stripe: {
publicKey: process.env.STRIPE_PUBLIC_KEY,
secretKey: process.env.STRIPE_SECRET_KEY
},
paypal: {
clientId: process.env.PAYPAL_CLIENT_ID,
clientSecret: process.env.PAYPAL_CLIENT_SECRET,
mode: process.env.PAYPAL_MODE || 'sandbox'
}
},
// 缓存配置
cache: {
ttl: {
page: 15 * 60, // 15分钟
api: 5 * 60, // 5分钟
static: 24 * 60 * 60 // 24小时
}
},
// 日志配置
logging: {
level: process.env.LOG_LEVEL || 'info',
file: process.env.LOG_FILE || 'logs/app.log'
}
}
10.3 核心功能实现
10.3.1 用户认证系统
// server/models/User.js
const mongoose = require('mongoose')
const bcrypt = require('bcryptjs')
const jwt = require('jsonwebtoken')
const config = require('../config')
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
trim: true,
minlength: 3,
maxlength: 30
},
email: {
type: String,
required: true,
unique: true,
trim: true,
lowercase: true
},
password: {
type: String,
required: true,
minlength: 6
},
profile: {
firstName: String,
lastName: String,
avatar: String,
phone: String,
dateOfBirth: Date
},
addresses: [{
type: {
type: String,
enum: ['home', 'work', 'other'],
default: 'home'
},
street: String,
city: String,
state: String,
zipCode: String,
country: String,
isDefault: {
type: Boolean,
default: false
}
}],
role: {
type: String,
enum: ['user', 'admin', 'moderator'],
default: 'user'
},
isActive: {
type: Boolean,
default: true
},
emailVerified: {
type: Boolean,
default: false
},
lastLogin: Date,
preferences: {
language: {
type: String,
default: 'en'
},
currency: {
type: String,
default: 'USD'
},
notifications: {
email: {
type: Boolean,
default: true
},
sms: {
type: Boolean,
default: false
}
}
}
}, {
timestamps: true
})
// 密码加密中间件
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next()
try {
const salt = await bcrypt.genSalt(12)
this.password = await bcrypt.hash(this.password, salt)
next()
} catch (error) {
next(error)
}
})
// 密码验证方法
userSchema.methods.comparePassword = async function(candidatePassword) {
return bcrypt.compare(candidatePassword, this.password)
}
// 生成JWT令牌
userSchema.methods.generateAuthToken = function() {
const payload = {
id: this._id,
username: this.username,
email: this.email,
role: this.role
}
return jwt.sign(payload, config.jwt.secret, {
expiresIn: config.jwt.expiresIn
})
}
// 获取公开信息
userSchema.methods.toPublicJSON = function() {
return {
id: this._id,
username: this.username,
email: this.email,
profile: this.profile,
role: this.role,
isActive: this.isActive,
emailVerified: this.emailVerified,
preferences: this.preferences,
createdAt: this.createdAt
}
}
// 索引
userSchema.index({ email: 1 })
userSchema.index({ username: 1 })
userSchema.index({ 'addresses.isDefault': 1 })
module.exports = mongoose.model('User', userSchema)
// server/services/AuthService.js
const User = require('../models/User')
const jwt = require('jsonwebtoken')
const config = require('../config')
const { AppError } = require('../utils/errors')
class AuthService {
// 用户注册
async register(userData) {
const { username, email, password } = userData
// 检查用户是否已存在
const existingUser = await User.findOne({
$or: [{ email }, { username }]
})
if (existingUser) {
throw new AppError('用户已存在', 400)
}
// 创建新用户
const user = new User({
username,
email,
password,
profile: userData.profile || {}
})
await user.save()
// 生成令牌
const token = user.generateAuthToken()
return {
user: user.toPublicJSON(),
token
}
}
// 用户登录
async login(credentials) {
const { email, password } = credentials
// 查找用户
const user = await User.findOne({ email })
if (!user) {
throw new AppError('邮箱或密码错误', 401)
}
// 验证密码
const isPasswordValid = await user.comparePassword(password)
if (!isPasswordValid) {
throw new AppError('邮箱或密码错误', 401)
}
// 检查账户状态
if (!user.isActive) {
throw new AppError('账户已被禁用', 401)
}
// 更新最后登录时间
user.lastLogin = new Date()
await user.save()
// 生成令牌
const token = user.generateAuthToken()
return {
user: user.toPublicJSON(),
token
}
}
// 验证令牌
async verifyToken(token) {
try {
const decoded = jwt.verify(token, config.jwt.secret)
const user = await User.findById(decoded.id)
if (!user || !user.isActive) {
throw new AppError('无效的令牌', 401)
}
return user
} catch (error) {
if (error.name === 'JsonWebTokenError') {
throw new AppError('无效的令牌', 401)
}
if (error.name === 'TokenExpiredError') {
throw new AppError('令牌已过期', 401)
}
throw error
}
}
// 刷新令牌
async refreshToken(token) {
const user = await this.verifyToken(token)
return user.generateAuthToken()
}
// 修改密码
async changePassword(userId, oldPassword, newPassword) {
const user = await User.findById(userId)
if (!user) {
throw new AppError('用户不存在', 404)
}
// 验证旧密码
const isOldPasswordValid = await user.comparePassword(oldPassword)
if (!isOldPasswordValid) {
throw new AppError('原密码错误', 400)
}
// 更新密码
user.password = newPassword
await user.save()
return { message: '密码修改成功' }
}
// 重置密码
async resetPassword(email) {
const user = await User.findOne({ email })
if (!user) {
throw new AppError('用户不存在', 404)
}
// 生成重置令牌
const resetToken = jwt.sign(
{ id: user._id, type: 'password_reset' },
config.jwt.secret,
{ expiresIn: '1h' }
)
// 这里应该发送邮件
// await emailService.sendPasswordResetEmail(user.email, resetToken)
return { message: '重置密码邮件已发送' }
}
// 确认重置密码
async confirmPasswordReset(token, newPassword) {
try {
const decoded = jwt.verify(token, config.jwt.secret)
if (decoded.type !== 'password_reset') {
throw new AppError('无效的重置令牌', 400)
}
const user = await User.findById(decoded.id)
if (!user) {
throw new AppError('用户不存在', 404)
}
user.password = newPassword
await user.save()
return { message: '密码重置成功' }
} catch (error) {
if (error.name === 'JsonWebTokenError' || error.name === 'TokenExpiredError') {
throw new AppError('无效或过期的重置令牌', 400)
}
throw error
}
}
}
module.exports = new AuthService()
10.3.2 商品管理系统
// server/models/Product.js
const mongoose = require('mongoose')
const productSchema = new mongoose.Schema({
name: {
type: String,
required: true,
trim: true,
maxlength: 200
},
slug: {
type: String,
required: true,
unique: true,
lowercase: true
},
description: {
type: String,
required: true,
maxlength: 2000
},
shortDescription: {
type: String,
maxlength: 500
},
sku: {
type: String,
required: true,
unique: true
},
price: {
regular: {
type: Number,
required: true,
min: 0
},
sale: {
type: Number,
min: 0
},
currency: {
type: String,
default: 'USD'
}
},
inventory: {
quantity: {
type: Number,
required: true,
min: 0,
default: 0
},
trackQuantity: {
type: Boolean,
default: true
},
allowBackorder: {
type: Boolean,
default: false
}
},
images: [{
url: {
type: String,
required: true
},
alt: String,
isPrimary: {
type: Boolean,
default: false
}
}],
categories: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Category'
}],
tags: [String],
attributes: [{
name: {
type: String,
required: true
},
value: {
type: String,
required: true
},
type: {
type: String,
enum: ['text', 'number', 'boolean', 'select'],
default: 'text'
}
}],
variants: [{
name: String,
sku: String,
price: Number,
inventory: Number,
attributes: [{
name: String,
value: String
}],
images: [String]
}],
seo: {
title: String,
description: String,
keywords: [String]
},
status: {
type: String,
enum: ['draft', 'published', 'archived'],
default: 'draft'
},
featured: {
type: Boolean,
default: false
},
weight: {
value: Number,
unit: {
type: String,
enum: ['g', 'kg', 'lb', 'oz'],
default: 'kg'
}
},
dimensions: {
length: Number,
width: Number,
height: Number,
unit: {
type: String,
enum: ['cm', 'm', 'in', 'ft'],
default: 'cm'
}
},
shipping: {
free: {
type: Boolean,
default: false
},
weight: Number,
class: String
},
reviews: {
count: {
type: Number,
default: 0
},
average: {
type: Number,
default: 0,
min: 0,
max: 5
}
},
sales: {
count: {
type: Number,
default: 0
},
revenue: {
type: Number,
default: 0
}
},
createdBy: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
updatedBy: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
}
}, {
timestamps: true
})
// 虚拟字段:当前价格
productSchema.virtual('currentPrice').get(function() {
return this.price.sale || this.price.regular
})
// 虚拟字段:是否有折扣
productSchema.virtual('onSale').get(function() {
return this.price.sale && this.price.sale < this.price.regular
})
// 虚拟字段:折扣百分比
productSchema.virtual('discountPercentage').get(function() {
if (!this.onSale) return 0
return Math.round(((this.price.regular - this.price.sale) / this.price.regular) * 100)
})
// 虚拟字段:是否有库存
productSchema.virtual('inStock').get(function() {
if (!this.inventory.trackQuantity) return true
return this.inventory.quantity > 0 || this.inventory.allowBackorder
})
// 中间件:保存前生成slug
productSchema.pre('save', function(next) {
if (this.isModified('name') && !this.slug) {
this.slug = this.name
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '')
}
next()
})
// 中间件:确保只有一个主图片
productSchema.pre('save', function(next) {
if (this.images && this.images.length > 0) {
let primaryCount = 0
this.images.forEach((image, index) => {
if (image.isPrimary) {
primaryCount++
if (primaryCount > 1) {
this.images[index].isPrimary = false
}
}
})
// 如果没有主图片,设置第一张为主图片
if (primaryCount === 0) {
this.images[0].isPrimary = true
}
}
next()
})
// 静态方法:搜索产品
productSchema.statics.search = function(query, options = {}) {
const {
page = 1,
limit = 20,
sort = '-createdAt',
category,
minPrice,
maxPrice,
inStock,
featured
} = options
const filter = {
status: 'published'
}
// 文本搜索
if (query) {
filter.$or = [
{ name: { $regex: query, $options: 'i' } },
{ description: { $regex: query, $options: 'i' } },
{ tags: { $in: [new RegExp(query, 'i')] } }
]
}
// 分类过滤
if (category) {
filter.categories = category
}
// 价格过滤
if (minPrice || maxPrice) {
filter['price.regular'] = {}
if (minPrice) filter['price.regular'].$gte = minPrice
if (maxPrice) filter['price.regular'].$lte = maxPrice
}
// 库存过滤
if (inStock) {
filter.$or = [
{ 'inventory.trackQuantity': false },
{ 'inventory.quantity': { $gt: 0 } },
{ 'inventory.allowBackorder': true }
]
}
// 特色产品过滤
if (featured) {
filter.featured = true
}
const skip = (page - 1) * limit
return this.find(filter)
.populate('categories', 'name slug')
.sort(sort)
.skip(skip)
.limit(limit)
}
// 实例方法:更新库存
productSchema.methods.updateInventory = function(quantity, operation = 'subtract') {
if (!this.inventory.trackQuantity) return this
if (operation === 'subtract') {
this.inventory.quantity = Math.max(0, this.inventory.quantity - quantity)
} else if (operation === 'add') {
this.inventory.quantity += quantity
}
return this.save()
}
// 实例方法:添加评论统计
productSchema.methods.updateReviewStats = function(rating) {
const totalRating = (this.reviews.average * this.reviews.count) + rating
this.reviews.count += 1
this.reviews.average = totalRating / this.reviews.count
return this.save()
}
// 索引
productSchema.index({ name: 'text', description: 'text', tags: 'text' })
productSchema.index({ slug: 1 })
productSchema.index({ sku: 1 })
productSchema.index({ categories: 1 })
productSchema.index({ status: 1 })
productSchema.index({ featured: 1 })
productSchema.index({ 'price.regular': 1 })
productSchema.index({ createdAt: -1 })
productSchema.index({ 'reviews.average': -1 })
productSchema.index({ 'sales.count': -1 })
module.exports = mongoose.model('Product', productSchema)
10.3.3 购物车系统
// client/store/modules/cart.js
const state = {
items: [],
loading: false,
error: null
}
const getters = {
cartItems: state => state.items,
cartTotal: state => {
return state.items.reduce((total, item) => {
return total + (item.price * item.quantity)
}, 0)
},
cartCount: state => {
return state.items.reduce((count, item) => count + item.quantity, 0)
},
isInCart: state => productId => {
return state.items.some(item => item.productId === productId)
},
getCartItem: state => productId => {
return state.items.find(item => item.productId === productId)
}
}
const mutations = {
SET_LOADING(state, loading) {
state.loading = loading
},
SET_ERROR(state, error) {
state.error = error
},
SET_CART_ITEMS(state, items) {
state.items = items
},
ADD_TO_CART(state, item) {
const existingItem = state.items.find(i => i.productId === item.productId)
if (existingItem) {
existingItem.quantity += item.quantity
} else {
state.items.push({
...item,
id: Date.now() + Math.random() // 临时ID
})
}
},
UPDATE_CART_ITEM(state, { productId, quantity }) {
const item = state.items.find(i => i.productId === productId)
if (item) {
if (quantity <= 0) {
state.items = state.items.filter(i => i.productId !== productId)
} else {
item.quantity = quantity
}
}
},
REMOVE_FROM_CART(state, productId) {
state.items = state.items.filter(item => item.productId !== productId)
},
CLEAR_CART(state) {
state.items = []
}
}
const actions = {
// 加载购物车
async loadCart({ commit }) {
commit('SET_LOADING', true)
commit('SET_ERROR', null)
try {
// 从localStorage加载(客户端)
if (process.client) {
const cartData = localStorage.getItem('cart')
if (cartData) {
const items = JSON.parse(cartData)
commit('SET_CART_ITEMS', items)
}
}
// 如果用户已登录,从服务器同步
if (this.getters['auth/isAuthenticated']) {
const response = await this.$api.get('/cart')
commit('SET_CART_ITEMS', response.data.items)
// 同步到localStorage
if (process.client) {
localStorage.setItem('cart', JSON.stringify(response.data.items))
}
}
} catch (error) {
commit('SET_ERROR', error.message)
} finally {
commit('SET_LOADING', false)
}
},
// 添加到购物车
async addToCart({ commit, dispatch, getters }, { product, quantity = 1, variant = null }) {
commit('SET_LOADING', true)
commit('SET_ERROR', null)
try {
const cartItem = {
productId: product.id,
name: product.name,
slug: product.slug,
price: product.currentPrice,
image: product.images[0]?.url,
quantity,
variant
}
// 检查库存
if (product.inventory.trackQuantity) {
const currentItem = getters.getCartItem(product.id)
const totalQuantity = (currentItem?.quantity || 0) + quantity
if (totalQuantity > product.inventory.quantity && !product.inventory.allowBackorder) {
throw new Error('库存不足')
}
}
commit('ADD_TO_CART', cartItem)
// 保存到localStorage
if (process.client) {
localStorage.setItem('cart', JSON.stringify(getters.cartItems))
}
// 如果用户已登录,同步到服务器
if (this.getters['auth/isAuthenticated']) {
await this.$api.post('/cart/add', cartItem)
}
// 显示成功消息
this.$toast.success('商品已添加到购物车')
} catch (error) {
commit('SET_ERROR', error.message)
this.$toast.error(error.message)
} finally {
commit('SET_LOADING', false)
}
},
// 更新购物车项目
async updateCartItem({ commit, getters }, { productId, quantity }) {
commit('SET_LOADING', true)
commit('SET_ERROR', null)
try {
commit('UPDATE_CART_ITEM', { productId, quantity })
// 保存到localStorage
if (process.client) {
localStorage.setItem('cart', JSON.stringify(getters.cartItems))
}
// 如果用户已登录,同步到服务器
if (this.getters['auth/isAuthenticated']) {
await this.$api.put(`/cart/items/${productId}`, { quantity })
}
} catch (error) {
commit('SET_ERROR', error.message)
this.$toast.error(error.message)
} finally {
commit('SET_LOADING', false)
}
},
// 从购物车移除
async removeFromCart({ commit, getters }, productId) {
commit('SET_LOADING', true)
commit('SET_ERROR', null)
try {
commit('REMOVE_FROM_CART', productId)
// 保存到localStorage
if (process.client) {
localStorage.setItem('cart', JSON.stringify(getters.cartItems))
}
// 如果用户已登录,同步到服务器
if (this.getters['auth/isAuthenticated']) {
await this.$api.delete(`/cart/items/${productId}`)
}
this.$toast.success('商品已从购物车移除')
} catch (error) {
commit('SET_ERROR', error.message)
this.$toast.error(error.message)
} finally {
commit('SET_LOADING', false)
}
},
// 清空购物车
async clearCart({ commit }) {
commit('SET_LOADING', true)
commit('SET_ERROR', null)
try {
commit('CLEAR_CART')
// 清除localStorage
if (process.client) {
localStorage.removeItem('cart')
}
// 如果用户已登录,清除服务器购物车
if (this.getters['auth/isAuthenticated']) {
await this.$api.delete('/cart')
}
this.$toast.success('购物车已清空')
} catch (error) {
commit('SET_ERROR', error.message)
this.$toast.error(error.message)
} finally {
commit('SET_LOADING', false)
}
},
// 同步购物车(登录后)
async syncCart({ commit, getters }) {
if (!this.getters['auth/isAuthenticated']) return
try {
const localItems = getters.cartItems
if (localItems.length > 0) {
// 将本地购物车同步到服务器
await this.$api.post('/cart/sync', { items: localItems })
}
// 从服务器获取最新购物车
const response = await this.$api.get('/cart')
commit('SET_CART_ITEMS', response.data.items)
// 更新localStorage
if (process.client) {
localStorage.setItem('cart', JSON.stringify(response.data.items))
}
} catch (error) {
console.error('Cart sync error:', error)
}
}
}
export default {
namespaced: true,
state,
getters,
mutations,
actions
}
10.4 本章小结
本章介绍了Vue SSR实战项目的开发过程,包括:
核心要点
项目架构设计
- 技术栈选择与配置
- 项目结构规划
- 开发环境搭建
核心功能实现
- 用户认证系统
- 商品管理系统
- 购物车功能
数据模型设计
- MongoDB数据建模
- 索引优化
- 关联关系处理
最佳实践
代码组织
- 模块化设计
- 分层架构
- 代码复用
状态管理
- Vuex模块化
- 客户端与服务端同步
- 本地存储集成
错误处理
- 统一错误处理
- 用户友好提示
- 日志记录
练习作业
- 完善用户认证功能
- 实现商品搜索与过滤
- 开发订单管理系统
- 集成支付功能
下一章我们将学习Vue SSR的生态系统与工具链。