ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Mongoose 插件机制完全指南:用 Schema 插件复用逻辑、扩展模型能力

Mongoose 插件机制完全指南:用 Schema 插件复用逻辑、扩展模型能力 Mongoose 插件机制完全指南用 Schema 插件复用逻辑、扩展模型能力【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongooseSchema 是可插拔的——Mongoose 允许通过插件Plugin把预置的能力批量应用到 Schema 上从而在多套模型之间复用逻辑、统一行为。本文将基于 Mongoose 官方插件文档结合仓库内 lib/schema.js、lib/mongoose.js、lib/connection.js 与 lib/helpers/schema/applyPlugins.js 等源码实现完整讲解插件的编写方式、作用域Schema 级 / 全局 / 连接级、应用时机与底层执行原理并覆盖内置插件与官方维护插件让你读完即可写出可复用的生产级插件。插件是什么Schema 级能力扩展在 Mongoose 中插件本质上是一个接收 Schema 与选项参数的函数。在插件函数内部你可以通过schema.virtual()增加虚拟属性详见 虚拟属性文档通过schema.pre()/schema.post()注册中间件middleware通过schema.methods、schema.statics、schema.index()、schema.path()等 API 扩展模型能力读取并基于options参数做条件化配置。插件机制的入口是 Schema.prototype.plugin()其实现要点如下Schema.prototype.plugin function(fn, opts) { if (typeof fn ! function) { throw new MongooseError(First param to schema.plugin() must be a function, got (typeof fn) ); } if (opts?.deduplicate) { for (const plugin of this.plugins) { if (plugin.fn fn) { return this; } } } this.plugins.push({ fn: fn, opts: opts }); fn(this, opts); return this; };从源码可以确认三点关键事实第一个参数必须是函数否则立即抛出MongooseError插件函数会被立即同步调用fn(this, opts)因此插件内部注册的虚拟属性、方法、索引在调用返回后即刻生效插件会被记录到schema.plugins数组且支持deduplicate: true选项——当同一函数已注册过时直接跳过避免重复应用。基础示例为多套模型统一添加loadedAt行为假设数据库中有多个模型希望每个文档被查询出来时都带有一个loadedAt加载时间属性。只需编写一次插件再分别应用到每个 Schema// loadedAt.js module.exports function loadedAtPlugin(schema, options) { schema.virtual(loadedAt). get(function() { return this._loadedAt; }). set(function(v) { this._loadedAt v; }); schema.post([find, findOne], function(docs) { if (!Array.isArray(docs)) { docs [docs]; } const now new Date(); for (const doc of docs) { doc.loadedAt now; } }); }; // game-schema.js const loadedAtPlugin require(./loadedAt); const gameSchema new Schema({ /* ... */ }); gameSchema.plugin(loadedAtPlugin); // player-schema.js const loadedAtPlugin require(./loadedAt); const playerSchema new Schema({ /* ... */ }); playerSchema.plugin(loadedAtPlugin);这个插件做了两件事定义loadedAt虚拟属性内部读写_loadedAt字段不会污染数据库存储在find/findOne查询之后通过post钩子批量写入加载时间。注意find返回文档数组、findOne返回单个文档因此用Array.isArray(docs)做了归一化处理。几行代码就让Game与Player两个模型同时具备了加载时间行为。若插件逻辑随选项变化只需传入第二个参数即可gameSchema.plugin(loadedAtPlugin, { fieldName: queriedAt });插件函数内部通过options.fieldName动态决定路径名就能让同一插件在不同场景下产生不同行为。全局插件一次注册所有 Schema 生效如果希望某个插件作用于所有Schema可以使用 Mongoose 单例上的.plugin()方法const mongoose require(mongoose); mongoose.plugin(require(./loadedAt)); const gameSchema new Schema({ /* ... */ }); const playerSchema new Schema({ /* ... */ }); // loadedAtPlugin gets attached to both schemas const Game mongoose.model(Game, gameSchema); const Player mongoose.model(Player, playerSchema);其底层实现位于 Mongoose.prototype.plugin()插件被追加到_mongoose.plugins数组随后在mongoose.model()编译模型时通过 Mongoose.prototype._applyPlugins() 统一应用到传入的 Schema 上。全局插件并不止于顶层 Schema从 lib/helpers/schema/applyPlugins.js 的实现可以看出applyPlugins会默认递归应用到子 Schemachild schemas并且可选地应用到discriminator Schema。也就是说一个全局插件通常也会自动覆盖内嵌文档的 Schema 以及鉴别器派生出的 Schema。相关行为可以通过 Mongoose 全局配置项控制配置项默认值含义applyPluginsToChildSchemastrue全局插件是否应用到内嵌子文档/数组子文档的 SchemaapplyPluginsToDiscriminatorsfalse全局插件是否应用到 discriminator 的 Schemamongoose.set(applyPluginsToDiscriminators, true);连接级插件仅作用于某个连接下的模型除了全局与 Schema 级Mongoose 还提供连接级Connection级插件。通过createConnection()创建的连接实例同样具备.plugin()方法见 Connection.prototype.plugin()const db mongoose.createConnection(mongodb://127.0.0.1:27017/mydb); db.plugin(() console.log(Applied)); db.plugins.length; // 1 db.model(Test, new Schema({})); // Prints Applied当通过conn.model()编译模型时lib/connection.js 会先调用applyPlugins(schema, this.plugins, null, $connectionPluginsApplied)应用连接级插件再编译模型。这意味着全局插件作用于所有连接、所有模型连接级插件只作用于该连接下创建的模型Schema 级插件只作用于调用schema.plugin()的那个 Schema。三者叠加时Schema 级插件最早应用创建 Schema 时即生效连接级与全局插件则在模型编译阶段应用。关键时机务必在编译模型之前应用插件插件常用于注册中间件middleware而中间件是在模型编译compile阶段被固化的。因此官方文档强调必须在调用mongoose.model()或conn.model()之前应用插件否则插件注册的中间件不会生效。// game-schema.js const loadedAtPlugin require(./loadedAt); const gameSchema new Schema({ /* ... */ }); const Game mongoose.model(Game, gameSchema); // find() and findOne() hooks from loadedAtPlugin() wont get applied // because mongoose.model() was already called! gameSchema.plugin(loadedAtPlugin);源码层面可以印证这一点lib/mongoose.js 的模型编译流程中_applyPlugins(schema)发生在模型编译调用之前——如果插件是在mongoose.model()返回之后才通过gameSchema.plugin()补注册中间件队列已经构建完成新注册的pre/post钩子自然无法进入模型实例的执行链。同理虚拟属性、实例方法等注册越晚越容易出现行为缺失且难以排查的问题。最佳实践是创建 Schema 后立刻、并且在编译模型前完成所有插件注册。内置插件Mongoose 自身的插件化架构插件机制并非仅为用户扩展而存在——Mongoose 内部也大量使用插件架构。仓库中内置插件定义在 lib/plugins/index.jsexports.saveSubdocs require(./saveSubdocs); exports.sharding require(./sharding); exports.trackTransaction require(./trackTransaction);三个内置插件分别为saveSubdocslib/plugins/saveSubdocs.js在save与deleteOne前后编排子文档subdocument的 pre/post 钩子执行确保save()一个顶层文档时内嵌子文档的中间件也能被正确触发是子文档保存链路的核心shardinglib/plugins/sharding.js为分片集群场景注入必要的查询与写入行为trackTransactionlib/plugins/trackTransaction.js追踪事务相关状态支撑事务 API 的正常工作。这些内置插件通过 lib/helpers/schema/applyBuiltinPlugins.js 在 Schema 创建/编译时以deduplicate: true自动应用保证不会被重复注册。由此可见插件的本质就是给 Schema 挂载能力的一段函数无论是框架内置还是用户自定义走的是同一条路径。编写插件的最佳实践与进阶技巧综合官方文档与源码编写高质量插件时建议遵循以下原则以接收 schema、options 的函数为唯一契约保持插件为纯函数形态不要引入模块级共享可变状态便于复用与测试善用 options 参数做差异化配置路径名、开关、默认值等一律从options读取并通过默认值兜底注意插件注册顺序与时机在mongoose.model()之前完成注册避免中间件缺失合理使用deduplicate当插件可能被多处重复加载时传入{ deduplicate: true }lib/schema.js#L2270-L2276 的实现正是用比较函数引用去重使用pluginTags做条件化应用从 lib/helpers/schema/applyPlugins.js 的源码可以看到全局插件数组中的条目若带tags字段会与schema.options.pluginTags取交集命中才应用——这为只对特定 Schema 生效的全局插件提供了官方机制考虑子 Schema 与 discriminator 的传播范围全局插件默认递归到子 Schema但对 discriminator 默认不传播需要显式开启applyPluginsToDiscriminators。官方维护插件与社区生态除了自定义插件Mongoose 团队还维护了一批官方插件为 Schema 增加开箱即用的能力例如mongoose-autopopulate让 Schema 中的某些字段在查询时自动执行populate()省去每次手动填充关联文档的样板代码mongoose-lean-virtuals在使用.lean()查询结果时也能附加虚拟属性弥补 lean 模式下无虚拟属性的短板mongoose-cast-aggregation为聚合管道补充字段类型转换casting能力。Mongoose 官方维护了一个插件检索站点所有发布到 npm 且带有mongoose关键词的插件都会收录其中方便查找现成的解决方案。与此同时社区生态极大丰富了插件机制的收益你既可以在自己的项目中复用 Schema 功能也可以将成熟的插件模式发布出去供他人使用。任何以mongoose作为 npm keywords 发布到 npm 的包都有机会被检索到从而形成编写一次、全社区复用的良性循环。小结Mongoose 的插件机制是在多个 Schema 中复用逻辑的官方首选方案其核心是一个接收(schema, options)的普通函数Schema 级schema.plugin(fn, opts)创建 Schema 后立即调用全局级mongoose.plugin(fn, opts)作用于所有 Schema默认递归子 Schema连接级conn.plugin(fn, opts)仅作用于该连接下创建的模型内置插件saveSubdocs、sharding、trackTransaction 等展示了插件架构在框架内部的应用范本。牢记编译模型前应用插件这一铁律并善用options、deduplicate与pluginTags机制你就能像官方维护的 autopopulate、lean-virtuals 一样把重复的模式沉淀为一行schema.plugin()调用。【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongoose创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表