了解mongoose的插件机制

Schemas是可以插拔的,也就是说,它们提供在应用预先打包能力来扩展它们的功能。这是非常强大的特性。

插件写法

基本结构

  1. module.exports = exports = function (schema, options) {
  2. }

参数说明

  • schema是schema定义
  • options为配置项

用法

  1. var some_plugin = require('./some_plugin');
  2. var Game = new Schema({ ... });
  3. Game.plugin(some_plugin, { index: true });

说明

  • Game是用户自己定义的Schema
  • Game.plugin是给Game增加plugin
  • plugin方法有2个参数
    • 插件对象:some_plugin
    • 配置项: { index: true }

比如在数据库里想给所有的collection增加last-modified功能。这时候使用插件是非常简单。仅仅需要创建一个插件,在每个Schema里应用它即可:

示例

给所有的collection增加last-modified功能

定义

  1. // lastMod.js
  2. module.exports = exports = function lastModifiedPlugin (schema, options) {
  3. schema.add({ lastMod: Date })
  4. schema.pre('save', function (next) {
  5. this.lastMod = new Date
  6. next()
  7. })
  8. if (options && options.index) {
  9. schema.path('lastMod').index(options.index)
  10. }
  11. }

调用

  1. // game-schema.js
  2. var lastMod = require('./lastMod');
  3. var Game = new Schema({ ... });
  4. Game.plugin(lastMod, { index: true });

我们可以看出,整体来说,mongoose的插件机制比较简单,就是通过约定参数(schema, options),然后在函数内部来对schema进行扩展。简单点说,就是在Game.plugin(lastMod, { index: true });时,把schema里的变化应用到Game的Schema里。

schema操作

这不是mongoose里对schema变动的唯一方式。其实都是schema.add的功劳。

  1. var ToySchema = new Schema;
  2. ToySchema.add({ name: 'string', color: 'string', price: 'number' });

我们获得某个Schema,然后自己手动add也是可以的,但要保证的是在定义model之前即可。

配置项

配置项也非常有意思,可以结合配置项给schema里的字段增加一些特性,比如增加索引,做一些更复杂的判断等。

  1. if (options && options.index) {
  2. schema.path('lastMod').index(options.index)
  3. }

base-user-plugin

我们再举个例子,用户登录和注册的功能在hook一节已经讲过了,对密码的加密和解密可以说是最常见的需求,那么我们能不能够里有plugin机制完善一下呢?

答案是可以,首先我们观察一下它们的共同点,然后给出可行性分析。

可行性分析

hook的核心

  1. UserSchema.statics.login = function(username, password, cb) {
  2. this.findOne({
  3. username: username
  4. }, function (err, user) {
  5. if (err || !user) {
  6. if (err)
  7. console.log(err);
  8. return cb(err, {
  9. code: -1,
  10. msg : username + ' is not exist!'
  11. });
  12. }
  13. bcrypt.compare(password, user.password, function(error, res) {
  14. if (error) {
  15. console.log(error);
  16. return cb(err, {
  17. code: -2,
  18. msg : 'password is incorrect, please check it again!'
  19. });
  20. }
  21. return cb(null, user);
  22. });
  23. });
  24. };
  25. UserSchema.pre('save', function (next) {
  26. var that = this;
  27. bcrypt.genSalt(this._salt_bounds, function(err, salt) {
  28. if (err) {
  29. console.log(err);
  30. return next();
  31. }
  32. bcrypt.hash(that.password, salt, function(error, hash) {
  33. if (error) {
  34. console.log(error);
  35. }
  36. // console.log(this.password + ' \n ' + hash);
  37. //生成密文
  38. that.password = hash;
  39. return next();
  40. });
  41. });
  42. })

它们的共同点,无论是pre还是statics都是对UserSchema进行操作。

插件定义

而插件定义

  1. // lastMod.js
  2. module.exports = exports = function (schema, options) {
  3. ...
  4. }

可以看出,插件的第一个参数就是schema,很明显,它们都是对schema操作,基于这个思考,我们可以确定这种改造是可行的。

mongoose-base-user-plugin核心代码

  1. const mongoose = require('mongoose');
  2. const bcrypt = require('bcrypt');
  3. module.exports = exports = function baseUserPlugin (schema, options) {
  4. schema.add({
  5. username : {// 真实姓名
  6. type: String,
  7. required: true
  8. },
  9. password : { // 密码
  10. type: String,
  11. required: true
  12. },
  13. _salt_bounds: {
  14. type: Number,
  15. required: false,
  16. default : 10
  17. },
  18. created_at : {
  19. type: Date,
  20. "default": Date.now
  21. }
  22. });
  23. schema.statics.login = function(username, password, cb) {
  24. this.findOne({
  25. username: username
  26. }, function (err, user) {
  27. if (err || !user) {
  28. if (err)
  29. console.log(err);
  30. return cb(err, {
  31. code: -1,
  32. msg : username + ' is not exist!'
  33. });
  34. }
  35. bcrypt.compare(password, user.password, function(error, res) {
  36. if (error) {
  37. console.log(error);
  38. return cb(err, {
  39. code: -2,
  40. msg : 'password is incorrect, please check it again!'
  41. });
  42. }
  43. return cb(null, user);
  44. });
  45. });
  46. };
  47. schema.pre('save', function (next) {
  48. var that = this;
  49. bcrypt.genSalt(this._salt_bounds, function(err, salt) {
  50. if (err) {
  51. console.log(err);
  52. return next();
  53. }
  54. bcrypt.hash(that.password, salt, function(error, hash) {
  55. if (error) {
  56. console.log(error);
  57. }
  58. // console.log(this.password + ' \n ' + hash);
  59. //生成密文
  60. that.password = hash;
  61. return next();
  62. });
  63. });
  64. });
  65. }

这其实和之前讲的的没啥区别,简单封装而已。

定义MyUser模型

  1. var mongoose = require('mongoose');
  2. var base_user = require('.')
  3. var MyUserSchema = new mongoose.Schema({
  4. invite_code : String, // 邀请码
  5. phone_number : Number, // 电话号码
  6. address : String, // 地址
  7. unionid : String,
  8. nickname : String,// from weixin 昵称
  9. sex : String,// from weixin 性别 0->女 1->男
  10. language : String,// from weixin 语言
  11. city : String,// from weixin 城市
  12. province : String,// from weixin
  13. country : String,// from weixin
  14. headimgurl : String,// from weixin 头像路径
  15. privilege : [], // from weixin
  16. created_at : {
  17. type: Date,
  18. "default": Date.now
  19. }
  20. });
  21. MyUserSchema.plugin(base_user);
  22. // 定义Model
  23. var MyUserModel = mongoose.model('MyUser', MyUserSchema);
  24. // 暴露接口
  25. module.exports = MyUserModel;

测试代码

剩下和之前的测试一样了,我们简单的看一下测试代码

  1. import test from 'ava';
  2. // 1、引入`mongoose connect`
  3. require('../connect');
  4. // 2、引入`User` Model
  5. const User = require('../user');
  6. // 3、定义`user` Entity
  7. const user = new User({
  8. username: 'i5ting',
  9. password: '0123456789'
  10. });
  11. test.before.cb( t => {
  12. User.remove({}, (err, u) => {
  13. t.end()
  14. })
  15. });
  16. test.cb('#register()', t => {
  17. user.save((err, u) => {
  18. t.true(u.password.length > 50)
  19. t.end()
  20. })
  21. });
  22. test.cb('#User.login(username, password) sucess', t => {
  23. let _user = new User({
  24. username: 'i5ting for is_login_valid',
  25. password: '0123456789'
  26. });
  27. _user.save((err, u) => {
  28. User.login('i5ting for is_login_valid', '0123456789', function (err, result) {
  29. if (!err) {
  30. t.pass()
  31. t.end()
  32. }
  33. })
  34. })
  35. });
  36. test.cb('#User.login(username, password) fail with username is not exist', t => {
  37. let _user = new User({
  38. username: 'i5ting for is_login_valid2',
  39. password: '0123456789'
  40. });
  41. _user.save((err, u) => {
  42. User.login('i5ting for is_login_valid not exist', '0123456789', function (err, result) {
  43. if (err) {
  44. // console.log(err)
  45. t.pass()
  46. t.end()
  47. }
  48. if (result.code === -1) {
  49. t.pass()
  50. t.end()
  51. }
  52. })
  53. })
  54. });
  55. test.cb('#User.login(username, password) fail with password is incorrect', t => {
  56. let _user = new User({
  57. username: 'i5ting 2',
  58. password: '0123456789'
  59. });
  60. _user.save((err, u) => {
  61. // console.log(err)
  62. User.login('i5ting 2', '0123456', function (err, result) {
  63. if (err) {
  64. console.log(err)
  65. t.fail()
  66. t.end()
  67. }
  68. if (result) {
  69. t.is(result.username, _user.username)
  70. t.end()
  71. }
  72. })
  73. })
  74. });

执行

  1. $ npm test
  2. > mongoose-base-user-plugin@1.0.0 test /Users/sang/workspace/github/mongoose-base-user-plugin
  3. > ava -v
  4. 数据库连接成功
  5. #register() (261ms)
  6. #User.login(username, password) fail with username is not exist (269ms)
  7. #User.login(username, password) sucess (359ms)
  8. #User.login(username, password) fail with password is incorrect (356ms)
  9. 4 tests passed

集成travis-ci

在根目录创建.travis.yml文件

  1. sudo: false
  2. addons:
  3. apt:
  4. sources:
  5. - ubuntu-toolchain-r-test
  6. packages:
  7. - g++-4.8
  8. env:
  9. - CXX=g++-4.8
  10. language: node_js
  11. node_js:
  12. - 6
  13. - 5
  14. - 4
  15. script:
  16. - npm test
  17. services:
  18. - mongodb
  19. before_script:
  20. - mongo mongoose-base-user-plugin --eval 'db.addUser("travis", "test");

说明

  • services指定mongodb连接
  • before_script 创建数据库和用户西你想
  • addons 使用apt安装g++依赖
  • env 定义CXX环境变量

这是因为bcrypt是Node.js的c/c++ addons,需要g++来编译,所以需要g++依赖。如果mac也需要先安装Xcode。

所以如果我们在项目代码里是bcrypt,部署的时候也需要安装g++的。

源码

https://github.com/i5ting/mongoose-base-user-plugin

总结

mongoose插件机制功能非常强大,又非常简单,对于我们优化代码,复用等有非常明显的帮助,在真实项目里,可以更好的组织代码,可以说是必会的一个特性。