Mongoose 插件(Plugins)是一种可重用的机制,用于扩展 Mongoose Schema 的功能。插件允许你将通用功能封装起来,并在多个 Schema 中复用。
基本插件
创建简单插件
javascript// timestamp.js function timestampPlugin(schema) { schema.add({ createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }); schema.pre('save', function(next) { this.updatedAt = new Date(); next(); }); } module.exports = timestampPlugin;
使用插件
javascriptconst timestampPlugin = require('./plugins/timestamp'); const userSchema = new Schema({ name: String, email: String }); userSchema.plugin(timestampPlugin); const User = mongoose.model('User', userSchema);
插件选项
带选项的插件
javascript// softDelete.js function softDeletePlugin(schema, options = {}) { const deletedAtField = options.deletedAtField || 'deletedAt'; const isDeletedField = options.isDeletedField || 'isDeleted'; schema.add({ [deletedAtField]: Date, [isDeletedField]: { type: Boolean, default: false } }); schema.pre('find', function() { this.where({ [isDeletedField]: { $ne: true } }); }); schema.pre('findOne', function() { this.where({ [isDeletedField]: { $ne: true } }); }); schema.methods.softDelete = function() { this[isDeletedField] = true; this[deletedAtField] = new Date(); return this.save(); }; } module.exports = softDeletePlugin; // 使用带选项的插件 userSchema.plugin(softDeletePlugin, { deletedAtField: 'deletedAt', isDeletedField: 'isDeleted' });
常用插件类型
1. 时间戳插件
javascriptfunction timestampPlugin(schema) { schema.add({ createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }); schema.pre('save', function(next) { this.updatedAt = new Date(); next(); }); }
2. 软删除插件
javascriptfunction softDeletePlugin(schema) { schema.add({ deletedAt: Date, isDeleted: { type: Boolean, default: false } }); schema.pre('find', function() { this.where({ isDeleted: { $ne: true } }); }); schema.pre('findOne', function() { this.where({ isDeleted: { $ne: true } }); }); schema.methods.softDelete = function() { this.isDeleted = true; this.deletedAt = new Date(); return this.save(); }; schema.statics.findDeleted = function() { return this.find({ isDeleted: true }); }; }
3. 分页插件
javascriptfunction paginatePlugin(schema) { schema.statics.paginate = function(query = {}, options = {}) { const { page = 1, limit = 10, sort = {}, populate = [] } = options; const skip = (page - 1) * limit; return Promise.all([ this.countDocuments(query), this.find(query) .sort(sort) .skip(skip) .limit(limit) .populate(populate) ]).then(([total, docs]) => ({ docs, total, page, pages: Math.ceil(total / limit) })); }; } // 使用分页插件 const result = await User.paginate( { status: 'active' }, { page: 1, limit: 10, sort: { name: 1 } } );
4. 自动填充插件
javascriptfunction autoPopulatePlugin(schema, options) { const fields = options.fields || []; schema.pre('find', function() { fields.forEach(field => { this.populate(field); }); }); schema.pre('findOne', function() { fields.forEach(field => { this.populate(field); }); }); } // 使用自动填充插件 userSchema.plugin(autoPopulatePlugin, { fields: ['profile', 'settings'] });
5. 加密插件
javascriptconst bcrypt = require('bcrypt'); function encryptionPlugin(schema, options) { const fields = options.fields || ['password']; schema.pre('save', async function(next) { for (const field of fields) { if (this.isModified(field)) { this[field] = await bcrypt.hash(this[field], 10); } } next(); }); schema.methods.comparePassword = async function(candidatePassword) { return bcrypt.compare(candidatePassword, this.password); }; } // 使用加密插件 userSchema.plugin(encryptionPlugin, { fields: ['password'] });
插件组合
多个插件
javascriptconst timestampPlugin = require('./plugins/timestamp'); const softDeletePlugin = require('./plugins/softDelete'); const paginatePlugin = require('./plugins/paginate'); userSchema.plugin(timestampPlugin); userSchema.plugin(softDeletePlugin); userSchema.plugin(paginatePlugin);
插件依赖
javascriptfunction advancedPlugin(schema, options) { // 依赖其他插件的功能 if (!schema.path('createdAt')) { throw new Error('timestampPlugin must be applied before advancedPlugin'); } // 使用其他插件添加的字段 schema.virtual('age').get(function() { return Date.now() - this.createdAt.getTime(); }); }
全局插件
应用到所有 Schema
javascript// 全局应用插件 mongoose.plugin(timestampPlugin); // 之后创建的所有 Schema 都会自动应用该插件 const userSchema = new Schema({ name: String }); const postSchema = new Schema({ title: String }); // 两个 Schema 都会有 createdAt 和 updatedAt 字段
条件全局插件
javascript// 只对特定模型应用插件 mongoose.plugin(function(schema) { if (schema.options.enableTimestamps) { schema.add({ createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now } }); } }); // 在 Schema 选项中启用 const userSchema = new Schema({ name: String }, { enableTimestamps: true });
插件最佳实践
1. 插件命名
javascript// 使用清晰的命名 const timestampPlugin = require('./plugins/timestamp'); const softDeletePlugin = require('./plugins/softDelete');
2. 插件文档
javascript/** * 软删除插件 * * 功能: * - 添加 deletedAt 和 isDeleted 字段 * - 自动过滤已删除的文档 * - 提供 softDelete() 方法 * * 选项: * - deletedAtField: 删除时间字段名(默认:deletedAt) * - isDeletedField: 删除标记字段名(默认:isDeleted) */ function softDeletePlugin(schema, options = {}) { // 插件实现 }
3. 插件测试
javascript// plugins/softDelete.test.js const mongoose = require('mongoose'); const softDeletePlugin = require('./softDelete'); describe('softDeletePlugin', () => { let User; beforeAll(async () => { await mongoose.connect('mongodb://localhost/test'); const userSchema = new Schema({ name: String }); userSchema.plugin(softDeletePlugin); User = mongoose.model('User', userSchema); }); it('should add soft delete fields', async () => { const user = await User.create({ name: 'John' }); expect(user.isDeleted).toBe(false); expect(user.deletedAt).toBeUndefined(); }); it('should filter deleted documents', async () => { const user = await User.create({ name: 'Jane' }); await user.softDelete(); const users = await User.find(); expect(users.length).toBe(0); }); });
发布插件
NPM 包结构
shellmongoose-plugin-softdelete/ ├── package.json ├── README.md ├── index.js └── test/ └── softDelete.test.js
package.json
json{ "name": "mongoose-plugin-softdelete", "version": "1.0.0", "description": "Mongoose plugin for soft delete functionality", "main": "index.js", "keywords": ["mongoose", "plugin", "soft-delete"], "peerDependencies": { "mongoose": ">=6.0.0" } }
最佳实践
- 保持插件简单:每个插件只做一件事
- 提供选项:允许用户自定义插件行为
- 文档完善:提供清晰的文档和示例
- 测试覆盖:为插件编写完整的测试
- 版本管理:使用语义化版本控制
- 错误处理:妥善处理错误情况
- 性能考虑:避免插件影响性能
- 向后兼容:保持 API 的向后兼容性