Node.js课程知识讲解大全(五)

5.3.5 mongoose操作数据库

Node.js中连接使用mongoose库来操作MongoDB数据库。这里查看mongoose中文版文档。

安装mongoose(前提是已经安装好了mongoDB数据库)。

npm install --save mongoose

Express入口文件index.js中通过代码连接数据库。

注意,不需要等待数据库连接完成,就可以继续后续操作,这些操作在连接完成之前会被缓存起来,一旦连接上就会调用。

var mongoose = require('mongoose');

mongoose.connect('mongodb://localhost/test');

var db = mongoose.connection;

db.on('error', console.error.bind(console, 'connection error:'));

db.once('open', function() {

  // we're connected!

});

基本使用

mongoose中创建集合的方式基于Schema,通过它来声明集合每一个文档的数据结构(包括属性和行为)。

var kittySchema = mongoose.Schema({

  name: String

});

// kittySchema也可以创建方法,方法给文档实例使用

kittySchema.methods.speak = function () {

  var greeting = this.name

    ? "Meow name is " + this.name

    : "I don't have a name";

  console.log(greeting);

}

接下来,利用Schema生成用来构造document的model,model表示一个文档的构造体。

var Kitten = mongoose.model('Kitten', kittySchema);

model方法的第一个参数最好使用负数的名字,如果使用单数的名字,它会自动转成复数名称。

通过Kitten来创建一个文档实例

var felyne = new Kitten({ name: 'Felyne' });

console.log(felyne.name);

var fluffy = new Kitten({ name: 'fluffy' });

fluffy.speak(); // "Meow name is fluffy"

保存这两个文档到数据库

felyne.save(function (err, felyne) {

  if (err) return console.error(err);

});

fluffy.save(function (err, fluffy) {

  if (err) return console.error(err);

});

查找我们保存的数据

Kitten.find(function (err, kittens) {

  if (err) return console.error(err);

  console.log(kittens);

})

Schema详解

Schema相当于文档字段的定义,或者可以理解成表格数据的表头信息。

以下是 mongoose 的所有合法 SchemaTypes:

String:字符串。

Number:数字,包括整数、小数、十进制、十六进制等。

Date:标准的ISO格式日期。

Buffer:用来存储字节。

Boolean:布尔。

Mixed:可以理解成任意类型,定义时传入Object对象也会被当做Mixed类型处理。

ObjectId:标记用的唯一ID,他不是简单的数字或字符串,由24位Hash字符串组成。

Array:数组。

Decimal128:使用128位二进制表示的十进制超大值整数类型,可用于科学计数法。

Schema:属性也支持使用另一个Schema。

创建Schema字段的方式有两种:

var schema = new Schema({

  name: String,

  age: { type: Number, min: 18, max:65, default: 20},

  any:{}

})

字段配置支持的选项

required: 布尔值或函数 如果值为真,为此属性添加 required 验证器

default: 任何值或函数 设置此路径默认值。如果是函数,函数返回值为默认值

select: 布尔值 指定 query 的默认 projections

validate: 函数 adds a validator function for this property

其他特定字段的配置项请参考这里。

Schema也支持自定义Type。

Model详解

Model是Schema编译而来,表示的是从数据库保存和读取的 documents。

var schema = new mongoose.Schema({ name: String, age: Number });

var Tank = mongoose.model('Child', schema);

尤其注意

第一个参数是跟 model 对应的集合( collection )名字的 单数 形式。 Mongoose 会自动找到名称是 model 名字 *复数* 形式的 collection 。 对于上例,Child 这个 model 就对应数据库中 Children 这个 collection。.model() 这个函数是对 schema 做了拷贝(生成了 model)。 你要确保在调用 .model() 之前把所有需要的东西都加进 schema 里了!

以下两种方式都可以保存单条数据,但推荐第二种:

var Tank = mongoose.model('Tank', yourSchema);

var small = new Tank({ size: 'small' });

small.save(function (err) {

  if (err) return handleError(err);

  // saved!

})

// or

Tank.create({ size: 'small' }, function (err, small) {

  if (err) return handleError(err);

  // saved!

})

如果 model 是通过调用 mongoose.model() 生成的,它将使用 mongoose 的默认连接。如果自行创建了连接,就需要使用 connection 的 model() 函数代替 mongoose 的 model() 函数。

var connection = mongoose.createConnection('mongodb://localhost:27017/test');

var Tank = connection.model('Tank', yourSchema);

Document

通过Model操作得到的结构都是Document对象(或数组)。Document对象的大多数类型属性都可以直接操作,但是Date类型和Mixed类型则不行,操作之后,需要调用markModified()方法。因此操作属性的底层使用的是set,所以不支持对值直接进行操作。

Tank.findById(id, function (err, tank) {

  if (err) return handleError(err);

  tank.size = 'large';

  // or

  tank.set({ size: 'large' });

  // 因为Date内部不支持直接调用方法修改自身。

  tank.bornDate.setMonth(3);

  tank.makModified('bornDate');

  tank.save(function (err, updatedTank) {

    if (err) return handleError(err);

    res.send(updatedTank);

  });

});

查询器

查询是mongoose最主要的功能,支持单个和多个查询,支持ID和任何条件查询,同时还支持聚合查询和排序等。

Model 的方法中包含查询条件参数的( find findById count update )都可以按以下两种方式执行:

传入 callback 参数,操作会被立即执行,查询结果被传给回调函数( callback )。

如果查询时发生错误,error 参数即是错误文档, result 参数会是 null。

如果查询成功,error 参数是 null,result 即是查询的结果。

不传 callback 参数,会返回一个Query实例。

Query 实例本质上是一个封装过的Promise对象。

它支持链式调用。

查询结果的格式取决于做什么操作:

findOne() 是单个文档(有可能是 null )

find() 是文档列表

count() 是文档数量

update() 是被修改的文档数量。

Models API 文档中有详细描述被传给回调函数的值。

var Person = mongoose.model('Person', yourSchema);

Person.

  find({

    occupation: /host/,

    'name.last': 'Ghost',

    age: { $gt: 17, $lt: 66 },

    likes: { $in: ['vaporizing', 'talking'] }

  }).

  limit(10).

  sort({ occupation: -1 }).

  select({ name: 1, occupation: 1 }).

  exec(callback);

填充(populate)

mongoose里的populate类似SQL中的join操作,也就是可以进行级联查询。

创建了两个 Model。Person model 的 stories 字段设为 ObjectId数组。 ref 选项告诉 Mongoose 在填充的时候使用哪个 model,本例中为 Story model。 所有储存在此的 _id 都必须是 Story model 中 document 的 _id。

var mongoose = require('mongoose');

var Schema = mongoose.Schema;

var personSchema = Schema({

  _id: Schema.Types.ObjectId,

  name: String,

  age: Number,

  stories: [{ type: Schema.Types.ObjectId, ref: 'Story' }]

});

var storySchema = Schema({

  author: { type: Schema.Types.ObjectId, ref: 'Person' },

  title: String,

  fans: [{ type: Schema.Types.ObjectId, ref: 'Person' }]

});

var Story = mongoose.model('Story', storySchema);

var Person = mongoose.model('Person', personSchema);

var author = new Person({

  _id: new mongoose.Types.ObjectId(),

  name: 'Ian Fleming',

  age: 50

});

author.save(function (err) {

  if (err) return handleError(err);

  var story1 = new Story({

    title: 'Casino Royale',

    author: author._id    // 把Person的Id赋给story1

  });

  story1.save(function (err) {

    if (err) return handleError(err);

    // thats it!

  });

});

查询Story的方式也很简单,需要在find后面链式调用populate。

被填充的字段已经不是原来的 _id,而是被指定的 document 代替,这个 document 由另一条 query 从数据库返回。

Story.

  findOne({ title: 'Casino Royale' }).

  populate('author').

  exec(function (err, story) {

    if (err) return handleError(err);

    console.log('The author is %s', story.author.name);

    // prints "The author is Ian Fleming"

  });

如果我们只需要填充的 document 其中一部分字段怎么办? 第二参数传入 field name syntax 就能实现。

Story.

  findOne({ title: /casino royale/i }).

  populate('author', 'name'). // only return the Persons name

  exec(callback);

填充多个document可以使用多个populate。

验证

验证可以保证在数据被存储在mongoDB之前对数据进行校验。

如果你要使用验证,请注意一下几点:

验证定义于 SchemaType

验证是一个中间件。它默认作为 pre('save')` 钩子注册在 schema 上

你可以使用 doc.validate(callback) 或 doc.validateSync() 手动验证

验证器不对未定义的值进行验证,唯一例外是 required 验证器,所有 SchemaTypes 都有内建 required 验证器。required 验证器使用 checkRequired() 函数 判定这个值是否满足 required 验证器

验证是异步递归的。当你调用 Model#save,子文档验证也会执行,出错的话 Model#save 回调会接收错误

验证是可定制的

内建验证器处理required:

Numbers 有 min 和 max 验证器.

Strings 有 enum、 match、 maxlength 和 minlength 验证器

内建验证器不够用可以使用validate自建验证器,内建验证器支持返回Promise

var schema = new Schema({

  name: {

    type: String,

    required: true

  },

  phone: {

    type: String,

    validate: {

      validator: function(v) {

        return /\d{3}-\d{3}-\d{4}/.test(v);

      },

      message: '{VALUE} is not a valid phone number!'

    },

    required: [true, 'User phone number required']

  },

  name: {

    type: String,

    // You can also make a validator async by returning a promise. If you

    // return a promise, do **not** specify the `isAsync` option.

    validate: function(v) {

      return new Promise(function(resolve, reject) {

        setTimeout(function() {

          resolve(false);

        }, 5);

      });

    }

  }

});

var Cat = db.model('Cat', schema);

// This cat has no name :(

var cat = new Cat();

cat.save(function(error) {

  assert.equal(error.errors['name'].message,

              'Path `name` is required.');

  error = cat.validateSync();

  assert.equal(error.errors['name'].message,

              'Path `name` is required.');

});

中间件

  mongoose也提供了和Express类似的中间件机制(或者叫钩子机制)。

  根据调用发生的位置,分为pre和post两种钩子,pre是在mongoose指定方法(如save)之前运行,而post是在之后。

  var schema = new Schema(..);

  schema.pre('save', function(next) {

    // do stuff

    next();

  });

  schema.pre('save', async function() {

    await doStuff();

    await doMoreStuff();

  });

  // 这个save pre钩子会和前面连个实现并行机制运行

  schema.pre('save', true, function(next, done) {

    // calling next kicks off the next middleware in parallel

    next();

    setTimeout(done, 100);

  });

save() 函数触发 validate() 钩子,mongoose validate() 其实就是 pre('save') 钩子, 这意味着所有 pre('validate') 和 post('validate') 都会在 pre('save') 钩子之前调用。

  Mongoose 4.x 有四种中间件: document 中间件,model 中间件,aggregate 中间件,和 query 中间件。

Document 中间件支持以下 document 操作, this 指向当前 document:

init

validate

save

remove

Query 中间件,this 指向当前 query。 Query 中间件支持以下 Model 和 Query 操作:

count

find

findOne

findOneAndRemove

findOneAndUpdate

update

Query 中间件 不同于 document 中间件:document 中间件中, this 指向被更新 document,query 中间件中, this 指向 query 对象而不是被更新 document。

例如,如果你要在每次 update之前更新 updatedAt 时间戳, 你可以使用 pre 钩子。

schema.pre('update', function() {

this.update({},{ $set: { updatedAt: new Date() } });

});

Model 中间件,this 指向当前 model。 Model 中间件支持以下 Model 操作:

insertMany

Aggregate(聚合) 中间件作用于 MyModel.aggregate(), 它会在你对 aggregate 对象调用 exec() 时执行。 对于 aggregate 中间件,this 指向当前aggregation 对象。

aggregate

mongoose中最重要的API是Model API,需要多加参考。

六. 案例:实现一个后台管理系统

6.1 创建一个git项目,发布到github

在码云上面注册一个账号。

创建一个项目songs-manager

接下来,在你电脑的本地找到一个可以存放工程的目录,比如我们就叫它:~/workspace,把命令行环境切换到这个目录中。

cd ~/workspace

通过git clone 把项目克隆到本地,克隆完毕修改下README.md。整个项目的架构规范就按照这个来即可。

# Songs-Manager

#### 介绍

管理曲库和歌手库的Nodejs练习项目

#### 软件架构

├── README.md - 项目文档

├── app.js - 初始化应用

├── bin - 命令脚本

├── controllers - 定义路由处理的实现逻辑

├── helpers - 可以被项目各部分所调用的功能函数和代码

├── middlewares - Express 中间件,将要处理在进入路由之前的请求

├── models - 表示数据,实现业务逻辑和处理存储

├── package.json - 项目配置及其依赖的包

├── public - 包含所有的静态文件,像图片、样式和脚本

├── routes - 定义路由,这里的路由仅用于转发

├── tests - 测试在其他文件夹的的代码

└── views - 提供模板文件,模板文件将会在你路由中进行渲染和使用

在目录下配置git邮件信息(码云需要绑定了邮箱才可以)

$ git config user.email

express songs-manager创建项目,允许在原项目创建

$ express songs-manager --view=ejs

提示:目标路径已存在,是否继续?输入:y

把工作目录切换到项目目录:

cd ./songs-manager

后续操作就是在整个环境中安装依赖包和开发了

项目中安装nodemon

npm install --save nodemon

或者

yarn add -D nodemon

修改package.json

"license": "Apache Licence 2.0",

"scripts": {

  "start": "node ./bin/www",

  "debug": "set DEBUG=myapp:* && nodemon --inspect ./bin/www"

}

express生成的项目,默认么有安装依赖,需要用命令安装依赖

npm install

或者

yarn install

启动项目:

npm run start

或者

yarn run start

注意,如果启动不成功很有可能是端口3000已经被占用,需要关闭进程:

windows:

netstat -ano | findstr 端口号

参数为LISTENING 时,就需要手动关闭这个进程,最后一个参数是这个进程的进程号。

taskkill -PID 进程号 -F

mac:

$ lsof -i:端口号

$ kill 进程号

6.2 实现注册登录

接下来,我们利用Express实现一个具备基本功能的曲库管理网站,我们需要使用一个叫Hui的框架。

Hui是一个基于Bootstrap的轻量级前端框架,它的设计风格很美观,而且简单免费,兼容性好。同时提供了一个H-ui.admin,这个是基于Hui的组件库构建的一整套后台管理页面,可以快速开发轻量级网站后台模版。

首先,我们利用这个框架实现一个基本的登录注册功能。

6.2.1 登录页面改造

先进入官网体验一下体验框架,然后下载框架。注意,我们下载H-ui.Admin.page v3.1,它是基于普通页面,而非iframe的。

下载完成后,把它放到songs-manager项目中,留待使用。

把H-ui.Admin.page v3.1的公共的static和lib目录放到public目录中,以便访问HTML页面时可以直接访问到这些静态资源文件。

选中其中的login.html放到views目录下面,并把文件名改为login.ejs,这样该文件就能转成一个ejs文件可以拼接数据了,并把所有script和link标签引用的资源文件路径,指向可访问的根路径下的对应目录。

如static/h-ui/css/H-ui.min.css改成/static/h-ui/css/H-ui.min.css

(可选)删除无用代码,例如:百度统计的代码:

增加routes/index.js的路由,渲染login页面:

/* 登录页面 */

router.get('/login', function (req, res, next) {

  res.render('login', { title: '登录管理系统' });

});

查看效果。

6.2.2 后端登录逻辑实现

1. 解决HTTP无状态

上文讲解网络协议的时候,最后我们提到HTTP是无状态协议,这样就导致我们无法光通过HTTP请求就能判断客户端的身份来源(你只知道IP地址,但你不知道对方操作者是人还是机器)。

2. session和cookie

cookie解决了服务器存储和获取客户端身份的问题,它的基本原理是:

当用户第一次浏览某个使用Cookie的网站时,该网站的服务器为该用户生成一个唯一的识别码(Cookie id),同时创建一个Cookie对象;

Cookie是什么?

Cookie是网站为了辨别用户身份、进行session跟踪而储存在用户本地终端上的数据。

Cookie有时候是由服务端生成的,发送给客户端(通常是浏览器)的,当然客户端也可以自己生成Cookie。

Cookie总是保存在客户端中,按在客户端中的存储位置,可分为内存Cookie和硬盘Cookie:

服务器如何通知客户端保存Cookie?

服务器返回给客户端的响应报文可以将Cookie值插入到一个 Set-Cookie HTTP请求报头中。

默认情况下它是一个会话级别的cookie,存储在浏览器的内存中,用户退出浏览器之后被删除。

如何区分会话内存Cookie和持久硬盘Cookie?

如果网站希望浏览器将该Cookie存储在磁盘上,则需要设置最大时效(maxAge),并给出一个以秒为单位的时间(将最大时效设为0则是命令浏览器删除该Cookie);

如果存储时没有设置失效则是内存Cookie。

浏览器收到该响应报文之后,根据报文头里的Set-Cookie生成相应的Cookie,保存在客户端。该Cookie里面记录着用户当前的信息。

当用户再次访问该网站时,浏览器首先检查所有存储的Cookie,如果找到了该网站的Cookie,则把该Cookie附在请求资源的HTTP请求头上发送给服务器。

服务器接收到用户的HTTP请求报文之后,从报文头获取到该用户的Cookie,从里面找到所需要的东西。

Cookie的主要作用就是在客户端存储用户访问网站的一些信息,以便下次访问时可以携带这些数据进行验证。常用于:

记住密码,下次自动登录。

电商网站保存购物车数据。

记录用户浏览的足迹,定制化广告推荐。

Cookie有哪些缺点?

同域的HTTP请求默认会携带Cookie,而且HTTP请求中的Cookie是明文传递的,所以安全性成问题,不过用HTTPS协议可以解决这个问题。

Cookie的大小限制在4KB左右。对于复杂的存储需求来说是不够用的。

如果想解决上面的两个问题,就应该考虑SessionStorage或者LocalStorage了,这两个都是本地存储API,不仅解决了容量问题,而且存储的数据不会自动被HTTP请求携带。

session解决了HTTP无状态的问题,它利用了Cookie的存储和请求携带的特点,可以判断多次用户连接是否是来源一个用户。

Session是一种服务器端的机制,Session 对象用来存储特定用户会话所需的信息。因此Session是由服务端生成,保存在服务器的内存、缓存、硬盘或数据库中。

它的基本原理是:

用户首次访问服务器,服务器就为该客户端创建一个Session并生成一个与此Session相关的Session id。这个Session id是唯一的、不重复的、不容易找到规律的字符串。

服务器生成Session id之后,这个Session id在本次响应中通过HTTP的响应报文返回到客户端保存,而保存这个Session id的正是Cookie。

浏览器在下次请求时,自动的按照规则把这个Session id发送给服务器。

服务器通过Session id找到存储在服务器上的Session在信息。

Session和Cookie看似很相似,但他们完全不是一个东西,Cookie是服务器用来让客户端存储和携带的数据容器,而Session是服务器判断客户端身份的一种验证方式。

3. 保存会话身份

session是服务器的机制,因此可以用在Express中。

接下来我们就利用session机制来实现一个用户是否登录的过程。

安装相关中间件

npm install --save express-session

express-session:用来方便的创建和操作session,在服务端默认会使用MemoryStore存储 Session,这样在进程重启时会导致Session丢失,且不能多进程环境中传递。在生产环境中,应该使用外部存储,以确保 Session 的持久性。

脚手架生成的项目,cookie-parser已经默认安装好了

改造app.js文件,加入如下代码:

//require others...

var session = require('express-session')

//

app.use(session({

  secret:'fddfsjfkl',// 加密session信息的私钥

  cookie:{maxAge:60*1000},// Cookie的过期时间

  resave:true,// 强制将 session 保存回session存储区,即使在请求期间session从不被修改。

  saveUninitialized:true,// 强制将未初始的session保存到存储中。Session在新创建而未修改时,是未初始化状态。

}))

在routes目录中创建一个api.js文件,该文件对应的是/api/v1的路由:

api.js

var express = require('express');

var router = express.Router();

/* 登录页面 */

router.post('/login', function (req, res, next) {

  const body = req.body

  console.log(body)

  if (

    body.username === '小明' &&

    body.password === '123456' &&

    body.email && body.email.toLowerCase() === '[email protected]' &&

    body.verifyCode && body.verifyCode.toLowerCase() === '1234'

  ) {

    req.session.isLogin = true

    if (body.online === 'online') {

      // 以后处理

    }

    res.send({

      username: '小明',

      age: 34,

      school: '清华大学'

    });

  } else {

    res.status(401).send('登录失败')

  }

});

/* 退出登录页面 */

router.post('/logout', function (req, res, next) {

  // 销毁当前用户的session

  if (req.session && req.session.isLogin) {

    req.session.destroy();

    res.send('登出成功');

  } else {

    res.send('当前没有用户登录');

  }

});

module.exports = router;

在app.js中,引入api.js:

var apiRouter = require('./routes/api')

// 加在app.use('/users', usersRouter);语句后面

app.use('/api/v1', apiRouter);

通过PostMan测试下接口。

测试登录

测试退出登录

接下来改造前端登录页面login.ejs,使之点击登录按钮可以自动请求接口并处理返回数据。

修改body的主体内容:

主要是添加表单元素的name属性,请求地址,绑定监听器,调整样式。

   

       

           

               

               

                   

                   

               

           

           

               

               

                   

                   

               

           

           

               

               

                   

                   

                          name="email" style="width:200px;">

                   

                          onclick="countDown(this,5)"

                          value=" 获取邮箱验证码 ">

               

           

           

               

               

                   

                   

               

           

           

               

                   

                       

                        使我保持登录状态

               

           

           

               

                   

                          value=" 登    录 ">

                   

                          value=" 取    消 ">

               

           

       

   

这里我们使用一个jquery.form.js的库

它可以将一个表单的submit转换成ajax提交,而无须我们做任何多余处理。中文文档在这里,点击查看。

修改下login.ejs中的样式,防止容器内部的表单元素超出容器。

.loginBox{ position:absolute; width:617px; height:370px; background:transparent url("../images/admin-loginform-bg.png") center no-repeat fixed / cover; left:50%; top:50%; margin-left:-309px; margin-top:-244px; padding-top:38px}

验证页面看看,填写验证信息,是否能够登录成功,目前我们用的都是假数据:

{username: "小明", password: "123456", email: "[email protected]", verifyCode: "1234"}

你可能感兴趣的:(Node.js课程知识讲解大全(五))