Nuxt.js自定义服务器中间件

学习目标

通过本章节的学习,你将能够:

  • 了解Nuxt.js服务器中间件的概念和作用
  • 掌握服务器中间件的创建和配置方法
  • 学会使用服务器中间件实现API路由
  • 了解如何在服务器中间件中集成数据库
  • 掌握认证和授权的实现方式
  • 了解服务器中间件的最佳实践

核心知识点

服务器中间件的基本概念

服务器中间件是运行在Nuxt.js服务器端的函数,用于处理HTTP请求和响应。它可以:

  1. 处理API请求:创建RESTful API端点
  2. 修改请求和响应:添加头部、修改数据等
  3. 执行认证和授权:验证用户身份和权限
  4. 集成数据库:连接和操作数据库
  5. 处理文件上传:处理文件上传请求

创建服务器中间件

基本结构

在Nuxt.js 3中,服务器中间件存放在 server/middleware 目录中:

// server/middleware/cors.js
export default defineEventHandler((event) => {
  // 设置CORS头部
  event.node.res.setHeader('Access-Control-Allow-Origin', '*')
  event.node.res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
  event.node.res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
  
  // 处理OPTIONS请求
  if (event.node.req.method === 'OPTIONS') {
    event.node.res.statusCode = 204
    event.node.res.end()
  }
})

执行顺序

服务器中间件按照文件名的字母顺序执行,你可以通过在文件名前添加数字来控制执行顺序:

server/middleware/
├── 01-cors.js        # 首先执行
├── 02-auth.js        # 其次执行
└── 03-logger.js      # 最后执行

API路由实现

创建API路由

在Nuxt.js 3中,API路由存放在 server/api 目录中:

// server/api/users.js
export default defineEventHandler(async (event) => {
  // 获取查询参数
  const { page = 1, limit = 10 } = getQuery(event)
  
  // 模拟用户数据
  const users = [
    { id: 1, name: 'John Doe', email: 'john@example.com' },
    { id: 2, name: 'Jane Smith', email: 'jane@example.com' },
    { id: 3, name: 'Bob Johnson', email: 'bob@example.com' }
  ]
  
  // 返回用户数据
  return {
    page: parseInt(page),
    limit: parseInt(limit),
    total: users.length,
    data: users
  }
})

动态路由

使用下划线前缀创建动态路由:

// server/api/users/_id.js
export default defineEventHandler(async (event) => {
  // 获取路由参数
  const { id } = event.context.params
  
  // 模拟用户数据
  const users = {
    1: { id: 1, name: 'John Doe', email: 'john@example.com' },
    2: { id: 2, name: 'Jane Smith', email: 'jane@example.com' },
    3: { id: 3, name: 'Bob Johnson', email: 'bob@example.com' }
  }
  
  // 检查用户是否存在
  if (!users[id]) {
    throw createError({
      statusCode: 404,
      statusMessage: 'User not found'
    })
  }
  
  // 返回用户数据
  return users[id]
})

不同HTTP方法

使用 readBody 函数处理POST请求:

// server/api/users.js
export default defineEventHandler(async (event) => {
  // 处理GET请求
  if (event.node.req.method === 'GET') {
    // 返回用户列表
    return {
      data: [
        { id: 1, name: 'John Doe' },
        { id: 2, name: 'Jane Smith' }
      ]
    }
  }
  
  // 处理POST请求
  if (event.node.req.method === 'POST') {
    // 获取请求体
    const body = await readBody(event)
    
    // 验证数据
    if (!body.name || !body.email) {
      throw createError({
        statusCode: 400,
        statusMessage: 'Name and email are required'
      })
    }
    
    // 创建新用户
    return {
      id: Date.now(),
      ...body
    }
  }
  
  // 处理其他请求方法
  throw createError({
    statusCode: 405,
    statusMessage: 'Method not allowed'
  })
})

数据库集成

使用SQLite

  1. 安装依赖
npm install sqlite3 better-sqlite3
  1. 创建数据库连接
// server/utils/db.js
import Database from 'better-sqlite3'

// 创建数据库连接
const db = new Database('./data.db')

// 初始化数据库
db.exec(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL,
    password TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  );
  
  CREATE TABLE IF NOT EXISTS posts (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    content TEXT NOT NULL,
    user_id INTEGER NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users (id)
  );
`)

export default db
  1. 在API路由中使用数据库
// server/api/users.js
import db from '../utils/db'

export default defineEventHandler(async (event) => {
  // 处理GET请求
  if (event.node.req.method === 'GET') {
    // 查询所有用户
    const users = db.prepare('SELECT id, name, email, created_at FROM users').all()
    return { data: users }
  }
  
  // 处理POST请求
  if (event.node.req.method === 'POST') {
    // 获取请求体
    const body = await readBody(event)
    
    // 验证数据
    if (!body.name || !body.email || !body.password) {
      throw createError({
        statusCode: 400,
        statusMessage: 'Name, email and password are required'
      })
    }
    
    // 插入用户
    const stmt = db.prepare('INSERT INTO users (name, email, password) VALUES (?, ?, ?)')
    const result = stmt.run(body.name, body.email, body.password)
    
    // 返回新用户
    const user = db.prepare('SELECT id, name, email, created_at FROM users WHERE id = ?').get(result.lastInsertRowid)
    return user
  }
})

使用MongoDB

  1. 安装依赖
npm install mongodb
  1. 创建数据库连接
// server/utils/db.js
import { MongoClient } from 'mongodb'

// 数据库连接字符串
const uri = process.env.MONGODB_URI || 'mongodb://localhost:27017/nuxt-app'

// 创建客户端
const client = new MongoClient(uri)

// 连接数据库
let db

async function connectDB() {
  if (!db) {
    await client.connect()
    db = client.db()
  }
  return db
}

export default connectDB
  1. 在API路由中使用数据库
// server/api/posts.js
import connectDB from '../utils/db'

export default defineEventHandler(async (event) => {
  // 连接数据库
  const db = await connectDB()
  const postsCollection = db.collection('posts')
  
  // 处理GET请求
  if (event.node.req.method === 'GET') {
    // 查询所有帖子
    const posts = await postsCollection.find({}).toArray()
    return { data: posts }
  }
  
  // 处理POST请求
  if (event.node.req.method === 'POST') {
    // 获取请求体
    const body = await readBody(event)
    
    // 验证数据
    if (!body.title || !body.content) {
      throw createError({
        statusCode: 400,
        statusMessage: 'Title and content are required'
      })
    }
    
    // 创建新帖子
    const result = await postsCollection.insertOne({
      ...body,
      createdAt: new Date()
    })
    
    // 返回新帖子
    const post = await postsCollection.findOne({ _id: result.insertedId })
    return post
  }
})

认证和授权

实现JWT认证

  1. 安装依赖
npm install jsonwebtoken bcrypt
  1. 创建认证工具
// server/utils/auth.js
import jwt from 'jsonwebtoken'
import bcrypt from 'bcrypt'

// 密钥
const secret = process.env.JWT_SECRET || 'your-secret-key'

// 生成JWT令牌
export function generateToken(payload) {
  return jwt.sign(payload, secret, { expiresIn: '7d' })
}

// 验证JWT令牌
export function verifyToken(token) {
  try {
    return jwt.verify(token, secret)
  } catch (error) {
    return null
  }
}

// 哈希密码
export async function hashPassword(password) {
  const salt = await bcrypt.genSalt(10)
  return await bcrypt.hash(password, salt)
}

// 验证密码
export async function verifyPassword(password, hashedPassword) {
  return await bcrypt.compare(password, hashedPassword)
}
  1. 创建认证中间件
// server/middleware/auth.js
import { verifyToken } from '../utils/auth'

export default defineEventHandler((event) => {
  // 获取Authorization头部
  const authHeader = event.node.req.headers.authorization
  
  // 检查是否有token
  if (!authHeader) {
    return
  }
  
  // 提取token
  const token = authHeader.replace('Bearer ', '')
  
  // 验证token
  const payload = verifyToken(token)
  
  // 如果token有效,将用户信息添加到上下文
  if (payload) {
    event.context.user = payload
  }
})
  1. 实现登录和注册API
// server/api/auth/login.js
import db from '../../utils/db'
import { verifyPassword, generateToken } from '../../utils/auth'

export default defineEventHandler(async (event) => {
  // 获取请求体
  const body = await readBody(event)
  
  // 验证数据
  if (!body.email || !body.password) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Email and password are required'
    })
  }
  
  // 查询用户
  const user = db.prepare('SELECT * FROM users WHERE email = ?').get(body.email)
  
  // 检查用户是否存在
  if (!user) {
    throw createError({
      statusCode: 401,
      statusMessage: 'Invalid email or password'
    })
  }
  
  // 验证密码
  const isValid = await verifyPassword(body.password, user.password)
  
  // 检查密码是否正确
  if (!isValid) {
    throw createError({
      statusCode: 401,
      statusMessage: 'Invalid email or password'
    })
  }
  
  // 生成token
  const token = generateToken({ id: user.id, email: user.email })
  
  // 返回token和用户信息
  return {
    token,
    user: {
      id: user.id,
      name: user.name,
      email: user.email
    }
  }
})
// server/api/auth/register.js
import db from '../../utils/db'
import { hashPassword } from '../../utils/auth'

export default defineEventHandler(async (event) => {
  // 获取请求体
  const body = await readBody(event)
  
  // 验证数据
  if (!body.name || !body.email || !body.password) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Name, email and password are required'
    })
  }
  
  // 检查用户是否已存在
  const existingUser = db.prepare('SELECT * FROM users WHERE email = ?').get(body.email)
  if (existingUser) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Email already exists'
    })
  }
  
  // 哈希密码
  const hashedPassword = await hashPassword(body.password)
  
  // 插入用户
  const stmt = db.prepare('INSERT INTO users (name, email, password) VALUES (?, ?, ?)')
  const result = stmt.run(body.name, body.email, hashedPassword)
  
  // 返回新用户
  const user = db.prepare('SELECT id, name, email, created_at FROM users WHERE id = ?').get(result.lastInsertRowid)
  return user
})
  1. 保护需要认证的API
// server/api/posts.js
import db from '../utils/db'

export default defineEventHandler(async (event) => {
  // 检查用户是否已认证
  if (!event.context.user) {
    throw createError({
      statusCode: 401,
      statusMessage: 'Unauthorized'
    })
  }
  
  // 处理GET请求
  if (event.node.req.method === 'GET') {
    // 查询用户的帖子
    const posts = db.prepare('SELECT * FROM posts WHERE user_id = ?').all(event.context.user.id)
    return { data: posts }
  }
  
  // 处理POST请求
  if (event.node.req.method === 'POST') {
    // 获取请求体
    const body = await readBody(event)
    
    // 验证数据
    if (!body.title || !body.content) {
      throw createError({
        statusCode: 400,
        statusMessage: 'Title and content are required'
      })
    }
    
    // 插入帖子
    const stmt = db.prepare('INSERT INTO posts (title, content, user_id) VALUES (?, ?, ?)')
    const result = stmt.run(body.title, body.content, event.context.user.id)
    
    // 返回新帖子
    const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(result.lastInsertRowid)
    return post
  }
})

服务器中间件的最佳实践

  1. 模块化设计:将相关功能组织到不同的文件中
  2. 错误处理:统一处理错误,返回一致的错误格式
  3. 输入验证:验证所有用户输入,防止恶意数据
  4. 日志记录:记录重要的请求和错误信息
  5. 性能优化:避免重复数据库查询,使用缓存
  6. 安全措施:使用HTTPS,防止SQL注入,加密敏感数据
  7. 测试:为API端点编写测试用例

实用案例分析

案例一:博客系统API

功能需求

实现一个博客系统的API,包括:

  • 用户注册和登录
  • 帖子的创建、读取、更新和删除
  • 评论的创建和读取

实现步骤

  1. 创建数据库表
// server/utils/db.js
import Database from 'better-sqlite3'

// 创建数据库连接
const db = new Database('./blog.db')

// 初始化数据库
db.exec(`
  -- 用户表
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL,
    password TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  );
  
  -- 帖子表
  CREATE TABLE IF NOT EXISTS posts (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    content TEXT NOT NULL,
    user_id INTEGER NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users (id)
  );
  
  -- 评论表
  CREATE TABLE IF NOT EXISTS comments (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    content TEXT NOT NULL,
    user_id INTEGER NOT NULL,
    post_id INTEGER NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users (id),
    FOREIGN KEY (post_id) REFERENCES posts (id)
  );
`)

export default db
  1. 实现认证API
// server/api/auth/register.js
import db from '../../utils/db'
import { hashPassword } from '../../utils/auth'

export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  
  if (!body.name || !body.email || !body.password) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Name, email and password are required'
    })
  }
  
  // 检查用户是否已存在
  const existingUser = db.prepare('SELECT * FROM users WHERE email = ?').get(body.email)
  if (existingUser) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Email already exists'
    })
  }
  
  // 哈希密码
  const hashedPassword = await hashPassword(body.password)
  
  // 插入用户
  const stmt = db.prepare('INSERT INTO users (name, email, password) VALUES (?, ?, ?)')
  const result = stmt.run(body.name, body.email, hashedPassword)
  
  // 返回新用户
  const user = db.prepare('SELECT id, name, email, created_at FROM users WHERE id = ?').get(result.lastInsertRowid)
  return user
})
// server/api/auth/login.js
import db from '../../utils/db'
import { verifyPassword, generateToken } from '../../utils/auth'

export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  
  if (!body.email || !body.password) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Email and password are required'
    })
  }
  
  // 查询用户
  const user = db.prepare('SELECT * FROM users WHERE email = ?').get(body.email)
  
  // 检查用户是否存在
  if (!user) {
    throw createError({
      statusCode: 401,
      statusMessage: 'Invalid email or password'
    })
  }
  
  // 验证密码
  const isValid = await verifyPassword(body.password, user.password)
  
  // 检查密码是否正确
  if (!isValid) {
    throw createError({
      statusCode: 401,
      statusMessage: 'Invalid email or password'
    })
  }
  
  // 生成token
  const token = generateToken({ id: user.id, email: user.email })
  
  // 返回token和用户信息
  return {
    token,
    user: {
      id: user.id,
      name: user.name,
      email: user.email
    }
  }
})
  1. 实现帖子API
// server/api/posts.js
import db from '../utils/db'

export default defineEventHandler(async (event) => {
  // 处理GET请求
  if (event.node.req.method === 'GET') {
    // 查询所有帖子
    const posts = db.prepare(`
      SELECT p.*, u.name as user_name 
      FROM posts p 
      JOIN users u ON p.user_id = u.id 
      ORDER BY p.created_at DESC
    `).all()
    return { data: posts }
  }
  
  // 处理POST请求
  if (event.node.req.method === 'POST') {
    // 检查用户是否已认证
    if (!event.context.user) {
      throw createError({
        statusCode: 401,
        statusMessage: 'Unauthorized'
      })
    }
    
    const body = await readBody(event)
    
    if (!body.title || !body.content) {
      throw createError({
        statusCode: 400,
        statusMessage: 'Title and content are required'
      })
    }
    
    // 插入帖子
    const stmt = db.prepare('INSERT INTO posts (title, content, user_id) VALUES (?, ?, ?)')
    const result = stmt.run(body.title, body.content, event.context.user.id)
    
    // 返回新帖子
    const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(result.lastInsertRowid)
    return post
  }
})
// server/api/posts/_id.js
import db from '../../utils/db'

export default defineEventHandler(async (event) => {
  const { id } = event.context.params
  
  // 处理GET请求
  if (event.node.req.method === 'GET') {
    // 查询帖子
    const post = db.prepare(`
      SELECT p.*, u.name as user_name 
      FROM posts p 
      JOIN users u ON p.user_id = u.id 
      WHERE p.id = ?
    `).get(id)
    
    if (!post) {
      throw createError({
        statusCode: 404,
        statusMessage: 'Post not found'
      })
    }
    
    // 查询评论
    const comments = db.prepare(`
      SELECT c.*, u.name as user_name 
      FROM comments c 
      JOIN users u ON c.user_id = u.id 
      WHERE c.post_id = ? 
      ORDER BY c.created_at DESC
    `).all(id)
    
    return { ...post, comments }
  }
  
  // 处理PUT请求
  if (event.node.req.method === 'PUT') {
    // 检查用户是否已认证
    if (!event.context.user) {
      throw createError({
        statusCode: 401,
        statusMessage: 'Unauthorized'
      })
    }
    
    // 查询帖子
    const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(id)
    
    if (!post) {
      throw createError({
        statusCode: 404,
        statusMessage: 'Post not found'
      })
    }
    
    // 检查用户是否是帖子的作者
    if (post.user_id !== event.context.user.id) {
      throw createError({
        statusCode: 403,
        statusMessage: 'Forbidden'
      })
    }
    
    const body = await readBody(event)
    
    if (!body.title || !body.content) {
      throw createError({
        statusCode: 400,
        statusMessage: 'Title and content are required'
      })
    }
    
    // 更新帖子
    const stmt = db.prepare('UPDATE posts SET title = ?, content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
    stmt.run(body.title, body.content, id)
    
    // 返回更新后的帖子
    const updatedPost = db.prepare('SELECT * FROM posts WHERE id = ?').get(id)
    return updatedPost
  }
  
  // 处理DELETE请求
  if (event.node.req.method === 'DELETE') {
    // 检查用户是否已认证
    if (!event.context.user) {
      throw createError({
        statusCode: 401,
        statusMessage: 'Unauthorized'
      })
    }
    
    // 查询帖子
    const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(id)
    
    if (!post) {
      throw createError({
        statusCode: 404,
        statusMessage: 'Post not found'
      })
    }
    
    // 检查用户是否是帖子的作者
    if (post.user_id !== event.context.user.id) {
      throw createError({
        statusCode: 403,
        statusMessage: 'Forbidden'
      })
    }
    
    // 删除帖子的评论
    db.prepare('DELETE FROM comments WHERE post_id = ?').run(id)
    
    // 删除帖子
    db.prepare('DELETE FROM posts WHERE id = ?').run(id)
    
    return { message: 'Post deleted successfully' }
  }
})
  1. 实现评论API
// server/api/comments.js
import db from '../utils/db'

export default defineEventHandler(async (event) => {
  // 处理POST请求
  if (event.node.req.method === 'POST') {
    // 检查用户是否已认证
    if (!event.context.user) {
      throw createError({
        statusCode: 401,
        statusMessage: 'Unauthorized'
      })
    }
    
    const body = await readBody(event)
    
    if (!body.content || !body.post_id) {
      throw createError({
        statusCode: 400,
        statusMessage: 'Content and post_id are required'
      })
    }
    
    // 检查帖子是否存在
    const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(body.post_id)
    if (!post) {
      throw createError({
        statusCode: 404,
        statusMessage: 'Post not found'
      })
    }
    
    // 插入评论
    const stmt = db.prepare('INSERT INTO comments (content, user_id, post_id) VALUES (?, ?, ?)')
    const result = stmt.run(body.content, event.context.user.id, body.post_id)
    
    // 返回新评论
    const comment = db.prepare('SELECT * FROM comments WHERE id = ?').get(result.lastInsertRowid)
    return comment
  }
})

案例二:文件上传

功能需求

实现文件上传功能,允许用户上传图片。

实现步骤

  1. 安装依赖
npm install formidable
  1. 创建文件上传中间件
// server/middleware/upload.js
import formidable from 'formidable'
import fs from 'fs'
import path from 'path'

// 确保上传目录存在
const uploadDir = './public/uploads'
if (!fs.existsSync(uploadDir)) {
  fs.mkdirSync(uploadDir, { recursive: true })
}

export default defineEventHandler((event) => {
  // 只有POST请求才处理文件上传
  if (event.node.req.method !== 'POST') {
    return
  }
  
  // 检查是否是文件上传请求
  const contentType = event.node.req.headers['content-type']
  if (!contentType || !contentType.includes('multipart/form-data')) {
    return
  }
  
  // 创建formidable实例
  const form = new formidable.IncomingForm({
    uploadDir,
    keepExtensions: true,
    maxFileSize: 5 * 1024 * 1024, // 5MB
    filename: (name, ext, part) => {
      return `${Date.now()}-${part.originalFilename}`
    }
  })
  
  // 解析表单
  return new Promise((resolve, reject) => {
    form.parse(event.node.req, (err, fields, files) => {
      if (err) {
        reject(err)
        return
      }
      
      // 将文件信息添加到上下文
      event.context.files = files
      event.context.fields = fields
      
      resolve()
    })
  })
})
  1. 创建文件上传API
// server/api/upload.js
export default defineEventHandler(async (event) => {
  // 检查用户是否已认证
  if (!event.context.user) {
    throw createError({
      statusCode: 401,
      statusMessage: 'Unauthorized'
    })
  }
  
  // 检查是否有文件
  if (!event.context.files || !event.context.files.file) {
    throw createError({
      statusCode: 400,
      statusMessage: 'No file uploaded'
    })
  }
  
  const file = event.context.files.file
  
  // 返回文件信息
  return {
    filename: file.newFilename,
    originalFilename: file.originalFilename,
    path: file.filepath,
    size: file.size,
    url: `/uploads/${file.newFilename}`
  }
})

总结

本章节介绍了Nuxt.js的自定义服务器中间件,包括:

  1. 服务器中间件的基本概念:了解了服务器中间件的作用和使用场景
  2. 创建服务器中间件:掌握了服务器中间件的创建和配置方法
  3. API路由实现:学会了使用服务器中间件实现RESTful API
  4. 数据库集成:了解了如何在服务器中间件中集成SQLite和MongoDB
  5. 认证和授权:掌握了JWT认证和授权的实现方式
  6. 服务器中间件的最佳实践:了解了模块化设计、错误处理、输入验证等最佳实践

通过本章节的学习,你应该能够使用服务器中间件创建完整的后端功能,包括API路由、数据库集成、认证和授权等。这使得Nuxt.js成为一个真正的全栈框架,允许你在同一个项目中同时开发前端和后端代码。

« 上一篇 组合式API在Nuxt.js中的应用 下一篇 » Nuxt.js模块系统和扩展