Nuxt.js自定义服务器中间件
学习目标
通过本章节的学习,你将能够:
- 了解Nuxt.js服务器中间件的概念和作用
- 掌握服务器中间件的创建和配置方法
- 学会使用服务器中间件实现API路由
- 了解如何在服务器中间件中集成数据库
- 掌握认证和授权的实现方式
- 了解服务器中间件的最佳实践
核心知识点
服务器中间件的基本概念
服务器中间件是运行在Nuxt.js服务器端的函数,用于处理HTTP请求和响应。它可以:
- 处理API请求:创建RESTful API端点
- 修改请求和响应:添加头部、修改数据等
- 执行认证和授权:验证用户身份和权限
- 集成数据库:连接和操作数据库
- 处理文件上传:处理文件上传请求
创建服务器中间件
基本结构
在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
- 安装依赖
npm install sqlite3 better-sqlite3- 创建数据库连接
// 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- 在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
- 安装依赖
npm install mongodb- 创建数据库连接
// 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- 在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认证
- 安装依赖
npm install jsonwebtoken bcrypt- 创建认证工具
// 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)
}- 创建认证中间件
// 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
}
})- 实现登录和注册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
})- 保护需要认证的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
}
})服务器中间件的最佳实践
- 模块化设计:将相关功能组织到不同的文件中
- 错误处理:统一处理错误,返回一致的错误格式
- 输入验证:验证所有用户输入,防止恶意数据
- 日志记录:记录重要的请求和错误信息
- 性能优化:避免重复数据库查询,使用缓存
- 安全措施:使用HTTPS,防止SQL注入,加密敏感数据
- 测试:为API端点编写测试用例
实用案例分析
案例一:博客系统API
功能需求
实现一个博客系统的API,包括:
- 用户注册和登录
- 帖子的创建、读取、更新和删除
- 评论的创建和读取
实现步骤
- 创建数据库表
// 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- 实现认证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
}
}
})- 实现帖子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' }
}
})- 实现评论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
}
})案例二:文件上传
功能需求
实现文件上传功能,允许用户上传图片。
实现步骤
- 安装依赖
npm install formidable- 创建文件上传中间件
// 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()
})
})
})- 创建文件上传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的自定义服务器中间件,包括:
- 服务器中间件的基本概念:了解了服务器中间件的作用和使用场景
- 创建服务器中间件:掌握了服务器中间件的创建和配置方法
- API路由实现:学会了使用服务器中间件实现RESTful API
- 数据库集成:了解了如何在服务器中间件中集成SQLite和MongoDB
- 认证和授权:掌握了JWT认证和授权的实现方式
- 服务器中间件的最佳实践:了解了模块化设计、错误处理、输入验证等最佳实践
通过本章节的学习,你应该能够使用服务器中间件创建完整的后端功能,包括API路由、数据库集成、认证和授权等。这使得Nuxt.js成为一个真正的全栈框架,允许你在同一个项目中同时开发前端和后端代码。