与Express.js的集成
与Express.js的集成
Mongoose与Express.js的集成是构建Node.js后端服务的常见组合。通过Mongoose管理MongoDB数据,Express处理HTTP请求,可以快速搭建RESTful API或服务端渲染应用。
基本集成方式
在Express项目中集成Mongoose通常从连接数据库开始。以下是最基础的集成示例:
const express = require('express');
const mongoose = require('mongoose');
const app = express();
// 连接MongoDB
mongoose.connect('mongodb://localhost:27017/myapp', {
useNewUrlParser: true,
useUnifiedTopology: true
});
// 定义模型
const User = mongoose.model('User', new mongoose.Schema({
name: String,
email: String
}));
// Express路由
app.get('/users', async (req, res) => {
const users = await User.find();
res.json(users);
});
app.listen(3000);
项目结构组织
实际项目中,推荐采用更清晰的结构组织代码:
project/
├── models/
│ └── User.js
├── routes/
│ └── userRoutes.js
└── app.js
模型定义示例 (models/User.js
):
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: true
},
email: {
type: String,
required: true,
unique: true
},
createdAt: {
type: Date,
default: Date.now
}
});
module.exports = mongoose.model('User', userSchema);
路由定义示例 (routes/userRoutes.js
):
const express = require('express');
const router = express.Router();
const User = require('../models/User');
router.get('/', async (req, res) => {
try {
const users = await User.find().limit(10);
res.json(users);
} catch (err) {
res.status(500).json({ message: err.message });
}
});
router.post('/', async (req, res) => {
const user = new User({
name: req.body.name,
email: req.body.email
});
try {
const newUser = await user.save();
res.status(201).json(newUser);
} catch (err) {
res.status(400).json({ message: err.message });
}
});
module.exports = router;
中间件集成
Mongoose可以与Express中间件深度集成,例如实现数据验证:
// 验证ObjectId的中间件
async function validateUser(req, res, next) {
if (!mongoose.Types.ObjectId.isValid(req.params.id)) {
return res.status(400).json({ message: 'Invalid ID format' });
}
try {
req.user = await User.findById(req.params.id);
if (!req.user) {
return res.status(404).json({ message: 'User not found' });
}
next();
} catch (err) {
res.status(500).json({ message: err.message });
}
}
// 在路由中使用
router.get('/:id', validateUser, (req, res) => {
res.json(req.user);
});
错误处理
统一的错误处理机制能更好地管理Mongoose操作中的异常:
// 错误处理中间件
function errorHandler(err, req, res, next) {
if (err instanceof mongoose.Error.ValidationError) {
return res.status(400).json({
message: 'Validation Error',
details: err.errors
});
}
if (err.code === 11000) { // MongoDB重复键错误
return res.status(409).json({
message: 'Duplicate key error',
field: Object.keys(err.keyPattern)[0]
});
}
console.error(err);
res.status(500).json({ message: 'Server error' });
}
// 在app.js中注册
app.use(errorHandler);
高级查询集成
Express路由中可以充分利用Mongoose的查询能力:
// 复杂查询示例
router.get('/search', async (req, res) => {
const { name, email, page = 1, limit = 10 } = req.query;
const query = {};
if (name) query.name = new RegExp(name, 'i');
if (email) query.email = email;
try {
const users = await User.find(query)
.skip((page - 1) * limit)
.limit(Number(limit))
.sort({ createdAt: -1 })
.select('name email createdAt');
const count = await User.countDocuments(query);
res.json({
data: users,
pagination: {
page: Number(page),
limit: Number(limit),
total: count
}
});
} catch (err) {
res.status(500).json({ message: err.message });
}
});
事务支持
在需要多个操作原子性执行的场景,可以使用Mongoose事务:
router.post('/transfer', async (req, res) => {
const session = await mongoose.startSession();
session.startTransaction();
try {
const { fromUserId, toUserId, amount } = req.body;
const fromUser = await User.findById(fromUserId).session(session);
if (fromUser.balance < amount) {
throw new Error('Insufficient balance');
}
const toUser = await User.findById(toUserId).session(session);
fromUser.balance -= amount;
toUser.balance += amount;
await fromUser.save();
await toUser.save();
await session.commitTransaction();
res.json({ message: 'Transfer successful' });
} catch (err) {
await session.abortTransaction();
res.status(400).json({ message: err.message });
} finally {
session.endSession();
}
});
性能优化
集成时可以采取一些性能优化措施:
- 索引优化:
// 在模型定义中添加索引
userSchema.index({ email: 1 }, { unique: true });
userSchema.index({ name: 'text' });
- 查询优化:
// 只选择需要的字段
router.get('/minimal', async (req, res) => {
const users = await User.find().select('name email -_id');
res.json(users);
});
// 使用lean()获取纯JavaScript对象
router.get('/fast', async (req, res) => {
const users = await User.find().lean();
res.json(users);
});
- 批量操作:
router.post('/bulk', async (req, res) => {
try {
const result = await User.insertMany(req.body);
res.status(201).json(result);
} catch (err) {
res.status(400).json({ message: err.message });
}
});
实时应用集成
结合Socket.io实现实时数据更新:
const http = require('http');
const socketio = require('socket.io');
const server = http.createServer(app);
const io = socketio(server);
// 监听Mongoose变化
User.watch().on('change', (change) => {
io.emit('user_change', change);
});
// 客户端连接处理
io.on('connection', (socket) => {
socket.on('get_users', async () => {
const users = await User.find();
socket.emit('users_list', users);
});
});
server.listen(3000);
测试策略
集成测试示例(使用Jest和SuperTest):
const request = require('supertest');
const app = require('../app');
const User = require('../models/User');
beforeAll(async () => {
await mongoose.connect('mongodb://localhost:27017/testdb', {
useNewUrlParser: true,
useUnifiedTopology: true
});
});
afterAll(async () => {
await mongoose.connection.close();
});
describe('User API', () => {
beforeEach(async () => {
await User.deleteMany();
});
test('POST /users - create new user', async () => {
const response = await request(app)
.post('/users')
.send({ name: 'Test', email: 'test@example.com' });
expect(response.statusCode).toBe(201);
expect(response.body).toHaveProperty('_id');
expect(response.body.name).toBe('Test');
});
test('GET /users - retrieve all users', async () => {
await User.create([
{ name: 'User1', email: 'user1@example.com' },
{ name: 'User2', email: 'user2@example.com' }
]);
const response = await request(app).get('/users');
expect(response.statusCode).toBe(200);
expect(response.body.length).toBe(2);
});
});
安全考虑
集成时需要注意的安全问题:
- 数据验证:
// 使用express-validator进行输入验证
const { body, validationResult } = require('express-validator');
router.post(
'/',
[
body('name').trim().isLength({ min: 2 }),
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 6 })
],
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// 处理请求...
}
);
- 查询注入防护:
// 不安全的做法
router.get('/unsafe', async (req, res) => {
const users = await User.find(req.query); // 直接使用用户输入
res.json(users);
});
// 安全的做法
router.get('/safe', async (req, res) => {
const { name, email } = req.query;
const query = {};
if (name) query.name = { $regex: new RegExp(`^${name}$`, 'i') };
if (email) query.email = email;
const users = await User.find(query);
res.json(users);
});
- 速率限制:
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100 // 每个IP限制100个请求
});
app.use('/api/', limiter);
部署考虑
生产环境部署时的注意事项:
- 连接池配置:
mongoose.connect(process.env.MONGODB_URI, {
poolSize: 10, // 连接池大小
bufferMaxEntries: 0, // 连接错误时不缓冲操作
connectTimeoutMS: 10000, // 连接超时
socketTimeoutMS: 45000 // socket超时
});
- 健康检查端点:
router.get('/health', (req, res) => {
const dbStatus = mongoose.connection.readyState;
const status = dbStatus === 1 ? 'healthy' : 'unhealthy';
res.json({
status,
db: {
state: ['disconnected', 'connected', 'connecting', 'disconnecting'][dbStatus],
dbName: mongoose.connection.name
},
uptime: process.uptime()
});
});
- 环境配置:
// 使用dotenv管理环境变量
require('dotenv').config();
const env = process.env.NODE_ENV || 'development';
const configs = {
development: {
db: 'mongodb://localhost:27017/devdb'
},
test: {
db: 'mongodb://localhost:27017/testdb'
},
production: {
db: process.env.MONGODB_URI
}
};
mongoose.connect(configs[env].db);
调试技巧
开发过程中的调试方法:
- 启用调试日志:
// 启用Mongoose调试
mongoose.set('debug', true);
// 或者自定义调试函数
mongoose.set('debug', (collectionName, method, query, doc) => {
console.log(`${collectionName}.${method}`, JSON.stringify(query), doc);
});
- 查询钩子:
// 记录所有查询耗时
mongoose.plugin((schema) => {
schema.pre('find', function() {
this._startTime = Date.now();
});
schema.post('find', function(result) {
console.log(`Query ${this.op} took ${Date.now() - this._startTime}ms`);
});
});
- 性能分析:
// 使用Mongoose的profile功能
mongoose.set('profile', 2); // 记录所有操作
// 或者针对特定操作
const user = await User.findOne().explain('executionStats');
console.log(user);
版本兼容性
不同版本间的兼容性处理:
- Express 4.x vs 5.x:
// Express 5.x处理异步错误的方式不同
// 在Express 4.x中需要显式捕获错误
router.get('/async', async (req, res, next) => {
try {
const data = await User.find();
res.json(data);
} catch (err) {
next(err);
}
});
// Express 5.x会自动捕获
router.get('/async', async (req, res) => {
const data = await User.find();
res.json(data);
});
- Mongoose 6.x的变化:
// 不再需要useNewUrlParser和useUnifiedTopology选项
mongoose.connect('mongodb://localhost:27017/myapp');
// 默认严格查询
// 旧版需要设置strictQuery: false来允许非模式字段
mongoose.set('strictQuery', false);
自定义中间件
创建可重用的Mongoose相关中间件:
- 分页中间件:
function paginate(model) {
return async (req, res, next) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const startIndex = (page - 1) * limit;
const results = {};
try {
results.total = await model.countDocuments();
results.data = await model.find()
.limit(limit)
.skip(startIndex);
res.paginatedResults = results;
next();
} catch (err) {
res.status(500).json({ message: err.message });
}
};
}
// 使用示例
router.get('/', paginate(User), (req, res) => {
res.json(res.paginatedResults);
});
- 缓存中间件:
const cache = require('memory-cache');
function cacheMiddleware(duration) {
return (req, res, next) => {
const key = '__express__' + req.originalUrl;
const cachedBody = cache.get(key);
if (cachedBody) {
return res.json(cachedBody);
} else {
res.sendResponse = res.json;
res.json = (body) => {
cache.put(key, body, duration * 1000);
res.sendResponse(body);
};
next();
}
};
}
// 使用示例
router.get('/popular', cacheMiddleware(60), async (req, res) => {
const users = await User.find().sort({ views: -1 }).limit(5);
res.json(users);
});
高级模式设计
复杂场景下的模式设计示例:
- 多态关联:
// 评论可以关联到文章或产品
const commentSchema = new mongoose.Schema({
content: String,
targetType: {
type: String,
enum: ['Post', 'Product'],
required: true
},
targetId: {
type: mongoose.Schema.Types.ObjectId,
required: true,
refPath: 'targetType'
}
});
// 查询时动态填充
Comment.find().populate('targetId');
- 树形结构:
// 使用materialized path模式实现分类树
const categorySchema = new mongoose.Schema({
name: String,
path: String
});
categorySchema.pre('save', function(next) {
if (this.isNew) {
if (this.parent) {
this.path = `${this.parent.path}/${this._id}`;
} else {
this.path = this._id;
}
}
next();
});
// 查询子树
Category.find({ path: new RegExp(`^${parent.path}/`) });
- 版本控制:
// 文档版本历史
const docSchema = new mongoose.Schema({
title: String,
content: String,
version: {
number: Number,
timestamp: Date
},
previousVersions: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'DocHistory'
}]
});
const docHistorySchema = new mongoose.Schema({
_originalId: mongoose.Schema.Types.ObjectId,
title: String,
content: String,
version: {
number: Number,
timestamp: Date
}
});
// 保存历史钩子
docSchema.pre('save', function(next) {
if (this.isModified()) {
const history = new DocHistory({
_originalId: this._id,
title: this.title,
content: this.content,
version: this.version
});
history.save();
this.previousVersions.push(history._id);
this.version.number += 1;
this.version.timestamp = new Date();
}
next();
});
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:RESTful API开发示例
下一篇:用户认证与权限管理