Practical Node.js (2018版) 第5章:数据库 使用MongoDB和Mongoose,或者node.js的native驱动。

Persistence with MongoDB and Mongoose

https://github.com/azat-co/practicalnode/blob/master/chapter5/chapter5.mdjavascript

学习mongodb的官方网站:css

https://university.mongodb.com/ (免费课程,-> study guide,-> exam)html

https://docs.mongodb.com/manual/tutorial/getting-started/前端

 

Mongodb node.js driver3.x版本的 guide:java

http://mongodb.github.io/node-mongodb-native/3.1/upgrade-migration/main/node

⚠️:本书上的案例使用的是2.2版本的代码,如open命令,在3.0驱动版已经删除。jquery

 

Mongoose :https://mongoosejs.com/ (17000✨)ios

写MongoDb Validation, casting和business logic boilerplate是很烦人的drag/boring。git

所以使用mongoose代替。github

 


 

我真的喜欢使用MongoDB with Node。由于这个数据库有JavaScript interface, 并使用JSON-like data structure。

MongoDB是一种NoSQL database。

 

NoSQL databases (DBs), also called non-relational databases非关系型数据库。

more horizontally scalable,在横向上有更多的扩展性

better suited for distributed systems than traditional SQL,比传统的SQL更适合分布式的系统。

NoSQL DBs内置方法让数据复制和自定义查询语法。这被叫作反规范化denormalization.

NoSQL databases deal routinely with larger data sizes than traditional ones.

 一般非关系数据库处理比传统关系数据库更大的数据。

 

关键的区别:

NoSQL DBs是没有schema的

没有table,仅仅是一个有IDS的简单store。

大量数据类型是不储存在数据库(没有更多的ALTER table 查询);  它们被移动到app,或者object-relational mapping (ORM) levels--咱们的案例,移动到Node.js代码。

做者说:

这些NoSQL最棒的优势!我能够快速地prototype prototyping和iterate (more git pushes!)
一旦我或多或少的完成,我能执行 implement schema和validation in Node.
This workflow allows me to not waste time early in the project lifecycle 
while still having the security at a more mature stage. 这种工做流程,让我不会在工程的早期浪费时间,同时在更成熟的阶段仍有安全。?

 

 

MongoDB

文档储存的非关系型数据库。

与之对比的是key-value类型的数据库如Redis, 以及wide-column store NoSQL databases。

NoSQL DBs汇总:http://nosql-database.org/

MongoDB是最成熟mature,dependable的NoSQL数据库。

 

另外,MongoDB有一个JavaScript交互interface!这很是棒!

由于如今无需在前端(browser JavaScript)和后端(Node.js)之间来回切换switch context,和数据库。

这是我最喜欢的功能!

 

开发MongoDB的公司是一个工业的领导者,提供了学习MongoDB的在线网站   (https://university.mongodb.com).

https://docs.mongodb.com/manual/tutorial/getting-started/

 

准备开始MongoBD和Node.js , 本章将讲以下章节:

  • Easy and proper installation of MongoDB
  • How to run the Mongo server
  • Data manipulation from the Mongo console
  • MongoDB shell in detail
  • Minimalistic native MongoDB driver for Node.js example
  • Main Mongoskin methods
  • Project: Storing Blog data in MongoDB with Mongoskin

 

 

Easy and Proper Installation of MongoDB

macOS使用HomeBrew安装。✅

$ brew install mongodb

 

或者从官网下载文件并配置它。http://www.mongodb.org/downloads

其余安装方式见本文后续说明和链接。

 

可选:

若是想在你的系统的任何位置使用MongoDB命令, 须要把mongoDB path增长到$PATH变量。

对应macOS,你须要open-system path file, 它在/etc/paths:

$ sudo vi /etc/paths

而后,在/etc/paths文件内增长下面的代码:

/usr/local/mongodb/bin

 

建立一个data文件夹;默认 MongoDB使用根目录的/data/db。 

//建立文件夹
$ sudo mkdir -p /data/db
//改变group和owner $ sudo chown `id
-u` /data/db

 

这个数据文件夹是你的本地数据库实例的存放位置~,它存放全部databases, documents,和on-all data.

若是你想储存数据在其余地方,能够指定path, 当你登录你的database实例时,使用--dbpath选项给mongod命令.

 

各类安装方法官网:

"Install MongoDB on OS X" 


 

 

How to Run the Mongo Server

使用mongod命令来启动Mongo server

若是你手动安装,并无链接位置到PATH, 去到你解包MongoDB的这个folder。那里有个bin文件夹。

输入:

$ ./bin/mongod

 

若是你像大多数开发者同样,喜欢在你的电脑任意位置输入mongod, 我猜想你把MongoDB bin文件夹放入了你的PATH环境变量中。因此若是你为MongoDB location增长$PATH, 就直接输入:

//任意位置
$ mongod

 ⚠️注意,在增长一个新的path给$PATH变量须要重启terminal window。

 

当teminal上出现

waiting for connections on port 27017

意味着MongoDB数据库server正在运行!Congratulations!

默认它监听  http://localhost:27017

This is the host and port for the scripts and applications to access MongoDB.

In our Node.js code, we use 27017 for for the database and port 3000 for the server.

 


 

Data Manipulation from the Mongo Console

和Node.js REPL相似,MongoDB也有一个console/shell,做为database server实例的客户端。

这意味着,保持一个terminal window跑server,再开启一个tab/window用于console。

 

开启console的命令:

$ ./bin/mongo  //或mongo

 

当你成功地链接到数据库实例,以后你应该看到这些:

MongoDB shell version: 4.0.5
connecting to: test

 

看到光标>, 如今你在一个不一样的环境内而不是zsh或bash。

你不能再执行shell命令了,因此不要使用node server.js或者mkdir。

可是你可使用JavaScript,Node.js和一些特定的MongoDB代码。

例如,执行下面的命令,来保存一个document{a: 1}, 而后查询这个集合,来看看这个新建立的document:

> db.test.save({a: 1})
WriteResult({ "nInserted" : 1 })
> db.test.find()
{ "_id" : ObjectId("5c3c68cb8126284dea64ff02"), "a" : 1 }

 命令find(), save()就是字面的意思。

你须要在它们前面加上db.COLLECTION_NAME,  用你本身的名字替代COLLECTION_NAME

⚠️在masOS,使用control + C关闭process,

 

下章讨论最重要的MongoDB console commands:


  

MongoDB Console in Detail

MongoDB控制台语法是JavaScript。这很是棒!

最后一件咱们想要的是学习一个新的复制的语言,如SQL。

db.test.find()

//db是类名

//test是collection name

//find是方法名

 

 

经常使用的MongoDB console(shell) commands:

  • help: 打印可使用的命令
  • show dbs: 打印数据库名字, prints the names of the databases on the database server to which the console is connected。默认是localhost:27017; 可是若是传递参数到mongo, 咱们能够链接到任何远程实例remote instance。
  • admin   0.000GB
    config  0.000GB
    local   0.000GB
    test    0.000GB
  • use db_name:  switches to db_name
  • show collections: 打印所选的数据库中的collections的名单。
  • db.collection_name.find(query), 找到全部匹配查询的items。
  • db.collection_name.findOne(query)
  • ⚠️:find方法是Read Operations(CURD)
  • db.collection_name.insert(document)  除此以外还有insertOne, insertMany方法。
  • ⚠️:insert是create operations建立操做符
  • db.collection_name.save(docuemnt)
  • .
  • db.collection_name.update(query, {$set: data}): 先找到匹配query条件的items,而后更新。
  • ⚠️:update是Update Operations
  • db.collection_name.remove(query): 移除全部匹配query的items。
  • deleteOne(query), deleteMany(query)是3.0之后的版本的方法。
  • printjson(document); 打印变量document的值。
  • > db.messages.save({a: 1})
    > var a = db.messages.findOne()
    //a的值是{ "_id" : ObjectId("5c3c8112cdb3fb6e147fb614"), "a" : 1 }
    > printjson(a) > a.text = "hi" > printjson(a) > db.messages.save(a)

     

save()方法有2种工做形式:

若是有_id, 这个document会被更新,不管什么新属性被传入到save()。

> db.messages.save(a)
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.messages.find()
{ "_id" : ObjectId("5c3c8112cdb3fb6e147fb614"), "a" : 1, "text" : "hi" }

 

若是没有_id,将插入一个新document并建立一个新的document ID🆔(ObjectId)in _id

> db.messages.save({b: 2})
WriteResult({ "nInserted" : 1 })
> db.messages.find()
{ "_id" : ObjectId("5c3c8112cdb3fb6e147fb614"), "a" : 1, "text" : "hi" }
{ "_id" : ObjectId("5c3c81e6cdb3fb6e147fb615"), "b" : 2 }

 

可见save(),相似upsert(update or insert)

 

这里只列出了最少的API。详细见:

"Overview—The MongoDB Interactive Shell" (http://www.mongodb.org/display/DOCS/Overview+-+The+MongoDB+Interactive+Shell).

 

使用MongoUI(git上是3年前的版本,比较旧)

一个基于web数据库的管理员界面,能够编辑,搜索,移除MongoUI documents,无需手输入命令了。

它会在默认的浏览器打开app,并链接到你的本地DB实例。

 

更好的桌面工具是Compass,下载地址:https://www.mongodb.com/products/compass

 

mongoimport命令

把一个JSON文件引进到一个数据库。就是把种子数据,存入到一个数据库。也可使用(CSV,TSV文件)

mongoimport --db dbName --collection collectionName --file fileName.json --jsonArray

进入控制台后,显示默认的这些数据库:

> show dbs
admin   0.000GB
config  0.000GB
local   0.000GB
test    0.000GB

当前数据库是test, 显示这个数据库的collections:

> show collections
messages
test 

显示messages中的documents:

> db.messages.find()
{ "_id" : ObjectId("5c3c8112cdb3fb6e147fb614"), "a" : 1, "text" : "hi" }
{ "_id" : ObjectId("5c3c81e6cdb3fb6e147fb615"), "b" : 2 }

为了让Node和MongoDB一块儿工做,须要驱动driver。

 


 

Minimalistic Native MongoDB Driver for Node.js Example

为了演示Mongoskin的优点,我会展现如何使用 Node.js native driver for MongoDB,它比用Mongoskin多一些work。下面我建立一个基本的script,存储database.

 

注意MongoDB的语法在不一样驱动driver环境,不一样: (点击查看文档)

  • Mongo Shell: 控制台语言
  • Compass: 是一个GUI图形界面工具
  • Python
  • Node.js: 使用MongoDB Node.js Driver
  • Ruby

 

了解Node.js下的方法:

Collection.insertMany(docs, options, callback)  (点击查看Node.js MongoDB Driver API)

//insertMany方法会返回promise对象,用于then的链接。
db.collection('name').insertMany([
   { item: "journal", qty: 25, status: "A",
       size: { h: 14, w: 21, uom: "cm" }, tags: [ "blank", "red" ] },
   { item: "postcard", qty: 45, status: "A",
       size: { h: 10, w: 15.25, uom: "cm" }, tags: [ "blue" ] }
])
.then(function(result) {
  //处理结果
}) 

 

首先, 

npm init -y

而后,保存一个指定的版本为dependency。

$ npm install mongodb@2.2.33 -SE

(⚠️:下面的代码是使用node-native-driver的2.2版本,已经被抛弃,下面主要是看一下设计结构,和一些方法的使用,新的3.0版本见下面👇。)

所以,咱们先创建一个小的案例,它测试是否咱们可以从一个Node.js脚本链接到一个本地的MongoDB实例, 并运行一系列的声明相似上个章节:

  1. 声明依赖
  2. 定义数据库host,port
  3. 创建一个数据库链接
  4. 建立一个数据库文档
  5. 输出一个新建立的document/object

文件名是:code/ch5/mongo-native-insert.js

首先引入mongodb。而后咱们使用host,port链接到数据库。这是创建链接到数据库的方法之一,db变量把一个reference在一个具体的host和port, 给这个数据库。

const mongo = require('mongodb')
const dbHost = '127.0.0.1'
cibst dbPort = 27017
const {Db, Server} = mongo
const db = new Db('local', new Server(dbHost, dbPort), {safe: true})

使用db.open来创建链接:(version3.0被抛弃,改用MongoClient类的实例建立链接)

db.open((error, dbConnection) => {
  //写database相关的代码
  //console.log(util.inspect(db))
  console.log(db._state)
  db.close()
})

执行完后,db.close()  (version3.0,也须要关闭数据库, MongoClient类提供实例方法close)

 

为在MongoDB建立一个document,须要使用insert()方法。insert()方法在异步环境下使用,能够传递callback做为参数,不然insert()会返回一个promise对象用于异步的then()。

 

回调函数有error做为第一个参数。这叫作error-first pattern。

第二个参数是回调函数,它新建立一个document.

在控制台,咱们无需有多个客户执行查询,因此在控制台方法是同步的,不是异步的。

可是,使用Node, 由于当咱们等待数据库响应的时侯,咱们想要去处理多个客户,因此用回调函数。

 

下面的完整的代码,是第五步:建立一个document。

⚠️:整个的insert代码是在open()的回调函数内的。由于insert()是异步的。

const mongo = require('mongodb')
const dbHost = '127.0.0.1'
const dbPort = 27017
const {Db, Server} = mongo
const db = new Db('local', new Server(dbHost, dbPort), {safe: true})

//open函数是异步的
db.open((error, dbConnection) => {
  if (error) {
    console.error(error)
    return process.exit(1)
  }
  console.log('db state:', db._state)
  
  const item = {
    name: 'Azat'
  }
  dbConnection.collection('messages').insert(item, (error, document) => {
    if (error) {
      console.error(error)
      return process.exit(1)
    }
    console.info('created/inserted:', document)
    db.close()
    process.exit(0)
  })
})

 


 

process.exit([code]) 

这个方法告诉Node.js关闭进程,同步地提供一个状态码:

  • 默认exit code是0, 表明success。
  • 1表明failure。

调用这个方法会强制的关闭进程,即便还有异步的操做⌛️。

通常无需明确调用process.exit方法。 Node.js会在没有additional work pending in the event loop后退出.


 

除了mongo-native-insert.js脚本,咱们还能够创建更多的方法,如 findOne()。

例如mogo-native.js脚本查询任意对象并修改它:

  1. Get one item from the message collection
  2. Print it
  3. Add a property text ,并赋值
  4. 保存这个item到message collection
const mongo = require('mongodb')
const dbHost = '127.0.0.1'
const dbPort = 27017
const {Db, Server} = mongo
const db = new Db('local', new Server(dbHost, dbPort), {safe: true})

 

而后,打开一个链接,并加上错误的测试:

db.open((error, dbConnection) => {
  if (error) {
    console.error(error)
    process.exit(1)
  }
  console.log('db state: ', db._state)

如今能够进行第一步:

从message collection那里获得一个item

使用findOne(query, options, callback)方法 

第一个参数是query条件, 它的类型是object,

第二个参数是Collection~resultCallback函数,它内部处理返回的document。

(若是没有callback传入,返回Promise)

  dbConnection.collection('messages').findOne({}, (error, item) => {
    if (error) {
      console.error(error)
      process.exit(1)
    }

 

Mongo Shell Method: db.collection.findOne(query, projection)

返回一个document,要知足collection/view的指定的查询标准,并返回符合标准的第一个document,没有则返回null。

参数 projection,用于限制返回的fields。

{ field1: <boolean>, field2: <boolean> ... }

如上面的区别:

methods在console和在Node.js惟一区别是在node,开发者必须使用回调函数(异步)。

 

而后,第二步,打印:

console.info('findONe:', item)

第三,四步骤:增长新的属性并save。

    item.text = 'hi'
    var id = item._id.toString() // we can store ID in a string
    console.info('before saving: ', item)
    dbConnection
      .collection('messages')
      .save(item, (error, document) => {
        if (error) {
          console.error(error)
          return process.exit(1)
        }
        console.info('save: ', document)

解释:

save(doc, options, callback)方法,相似update or insert

  • 若是提供了_id,则document被更新
  • 不然,建立一个新的document.
        dbConnection.collection('messages')
         .find({_id: new mongo.ObjectID(id)})
          .toArray((error, documents) => {
            if (error) {
              console.error(error)
              return process.exit(1)
            }
            console.info('find: ', documents)
            db.close()
            process.exit(0)
          }
        )
    })
  })
})

 

find(query)方法 -> {Cursor}

query参数类型是object. 

{Cursor} 一个Cursor实例(内部的类型,不能直接地实例化。)

建立一个cursor实例,用于迭代从MongoDB查询的结果。

 

toArray(callback) -> {Promise}

返回documents的array格式。Promise if no callback passed  

解释:

.find({_id: new mongo.ObjectID(id)})

为了再次核查保存的对象,咱们使用了document ID和find()方法,这个方法返回一个cursor,而后咱们使用toArray()提取standard JavaScript array。

 

最后,执行, 要先运行mongod service:

$ node mongo-native

 

⚠️???执行node  mongo-native-insert.js命令。提示❌:

TypeError: db.open is not a function

这是版本的问题,须要使用的Node.js驱动的mongoDB版本

$ npm install mongodb@2.2.33 -SE

 若是使用稳定的驱动driver版本:

npm i mongodb@stable

//3.0.1
//执行node xxx.js会报告❌
db.open((error, dbConnection) => {
   ^

TypeError: db.open is not a function
    at Object.<anonymous> (/Users/chent

 

缘由

见2.2api:http://mongodb.github.io/node-mongodb-native/2.2/api/Db.html#open 

和3.0api:   http://mongodb.github.io/node-mongodb-native/3.0/api/Db.html#open

2.2的DB#open方法,在3.0被删除了。

 

另外官网的代码从2.2开始已经使用的是MongoClient创建链接 

🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿🌿重要!

http://mongodb.github.io/node-mongodb-native/3.1/quick-start/quick-start/

下面是创建链接并插入一个document的演示代码:

version3-insert.js

const MongoClient = require('mongodb').MongoClient;
const dbHost = '127.0.0.1'
const dbPort = 27017

const assert = require('assert');
// Database Name
const dbName = 'myproject';
// Connection URL
const url = `mongodb://${dbHost}:${dbPort}`;
// Create a new MongoClient
const client = new MongoClient(url);

// Use connect method to connect to the Server
client.connect(function(err) {
  assert.equal(null, err) //若是有err,就会报告❌
  console.log("Connected successfully to server");
  //MongoClient#db方法,建立一个Db实例
  const db = client.db(dbName);
  // 使用insetMany()方法,若是myproject不存在,则同时建立myproject数据库和documents collection
  // 并插入一个document。 insertMany()方法,至关于create or update方法的判断的整合。
  insertDocuments(db, function() {
    //回调函数用于执行close,关闭db和它的链接。
    client.close()
  })
});

const insertDocuments = function(db, myCallback) {
  // 获得或者建立一个document: (这里命名为documents)
  const collection = db.collection('documents');
  // Insert some documents, 
// insertMany()的回调函数,能够进行err的判断(assert,expect),发出提示信息,并调用传入的回调函数myCallback
collection.insertMany([ {a : 12}, {b : 2}, {b : 3} ], function(err, result) { console.log("Inserted 3 documents into the collection, \n", result); myCallback(); }); }

 

$ node version3-insert.js
//获得结果:
Connected successfully to server
Inserted 3 documents into the collection

除此以外还有find, findOne, 等方法。

 


  

Main Mongoskin Methods(⚠️,先看文章末尾)

文章末尾有新的更新的内容,本章推荐使用mongoose。若是不看的话,前面的白学了,由于不实用!

Mongoskin 提供了更好的API。 一个wrapper,包裹node-mongodb-native。(1600🌟)过期。

除本书外,流行的驱动: 

mongoose:(17800✨)✅。

一个MongoDB对象 模型化工具,用于在异步的环境下工做!

https://github.com/Automattic/mongoose

文档https://mongoosejs.com/docs/index.html

 

数据验证很是重要,大多数MongoDb库都须要开发者建立他们本身的验证validation, 可是mongoose是例外! 

Mongoose有内置的数据验证。因此mongoose被大型的apps所推荐使用✅!

在Express.js上,能够下载验证模块如:node-validator,或者express-validator。

 


mongoose

简介

elegant mongodb object modeling for node.js。

const mongose = rquire('mongose')
mongoose.connect('mongodb://localhost:27017/test', {useNewUrlParser: true});

//声明一个类
const Cat = mongoose.model('Cat', { name: String })
//实例化类
const kitty = new Cat({name: 'Zildjian'});
//save方法保存实例,并返回promise。
kitty.save().then(() => {
  console.log('meow')
})

 

Mongoose提供了直接的,基于schema的解决方案,来构造你的app data。

它包括内置的类型构件, 验证, 查询,业务逻辑勾子和更多的功能,开箱即用out of the box!

 

快速指导开始

创建链接

//先安装好MongoDb和Node.js
$ npm install mongoose
// getting-started.js
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/test');

 

在脚本文件中引入mongoose,而后打开一个链接到test数据库,这是在本地运行的MongoDB实例(本地开启mongod服务器).

如今,咱们有了一个等待链接,它会链接到正在本地运行的test数据库。

如今须要判断链接是成功仍是失败,并给出❌提示:

var db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', function() {
  // we're connected!
});

 

db.once()一旦链接开始,就调用回调函数。为了简化,让咱们假设全部的代码在这个回调函数内。

 

设计一个Schema:

在Mongoose中,一切都从Schema中取用。

var kittySchema = new mongoose.Schema({
  name: String
})

 

如今有了一个schema和一个属性name,类型是string。

 

下一步是把schema编译进一个 Model.

var Kitten = mongoose.model("Kitten", kittySchema)

 

一个Model就是一个类,咱们用它来构建documents。

 

在这个案例,每一个document都是一只kitten, 它的属性和行为会在schema内声明!

如今建立一只kitten:(建立一个实例)

var silence = new Kitten({name: 'Silence'})
console.log(silence.name)  //'Silence'

 

添加行为函数:如speak:

kittySchema.methods.speak = function() {
  var greeting = this.name
    ? "Meow name is " + this.name
    : "I don't have a name"
}

var Kitten = mongoose.model("Kitten", kittySchema)

 

添加到schema的methods属性中的函数,被编译到Model prototype, 并会被exposed 到每一个document实例。

var fluffy = new Kitten({ name: 'fluffy'})
fluffy.speak();
// "Meow name is fluffy"

 

上面的操做是在视图层到control层,最后要存入MongoDB数据库的!使用save方法。

save(callback)

fluffy.save(function(err, fluffy) {
  if (err) return console.error(err)
  fluffy.speak();
})

 

如何显示全部的kittens?

使用find(callback)方法

Kitten.find(function(err, kittens) {
  if (err) return console.error(err);
  console.log(kittens);
})

 

若是想要搜索指定的document,使用:

//db.collection('Kitten').find({}, callback) 
Kitten.find({name: /^fluff/}, callback)

 

这和原生的Node.js mongoDb driver的方法相似。collection就是mongoose中模型构建的“类”。

而且,均可以使用regexp表达式。

 

总结:建立一个mongoose model有2个步骤(部分):

  1. 首先建立 a schema ,
  2. 而后建立 a model .

 

 


 

Project: Storing Blog Data in MongoDB with mongoose(改原文使用mongoskip弃用)

本案例使用第2章创建的hello-world app

步骤:

1.增长种子文件 

2.写Mocha tests

3.增长persistence

 

第一步:添加seed

在程序根目录建立db文件夹,而后在terminal输入:

mongoimport --db blog --collection users --file ./db/users.json --jsonArray
mongoimport --db blog --collection articles --file ./db/articles.json --jsonArray

⚠️参数--jsonArray:  能够引入的文件内是array格式的数据,默认这种格式不能被引进,除非加上这个选项。

或者把👆的代码存入一个seed.js脚本,脚本放在程序根目录文件夹后,执行:

sudo bash seed.sh

 

建立db/users.json

[{
  "email": "hi@azat.co",
  "admin": true,
  "password": "1"
}]

建立 db/articles.json

[ 
  {
    "title": "Node is a movement",
    "slug": "node-movement",
    "published": true,
    "text": "In one random deployment, it is often assumed that the number of scattered sensors are more than that required by the critical sensor density. Otherwise, complete area coverage may not be guaranteed in this deployment, and some coverage holes may exist. Besides using more sensors to improve coverage, mobile sensor nodes can be used to improve network coverage..."
  }, {
    "title": "Express.js Experience",
    "slug": "express-experience",
    "text": "Work in progress",
    "published": false
  }, {
    "title": "Node.js FUNdamentals: A Concise Overview of The Main Concepts",
    "slug": "node-fundamentals",
    "published": true,
    "text": "Node.js is a highly efficient and scalable nonblocking I/O platform that was built on top of a Google Chrome V8 engine and its ECMAScript. This means that most front-end JavaScript (another implementation of ECMAScript) objects, functions, and methods are available in Node.js. Please refer to JavaScript FUNdamentals if you need a refresher on JS-specific basics."
  }
]

 

第2步:用mocha, chai写测试

须要安装mocha, chai。

建立一个测试文件:tests/index.js

const boot = require('../app').boot
const shutdown = require('../app').shutdown
const port = require('../app').port
const axios = require('axios')   
const { expect } = require('chai') 
const seedArticles = require('../db/articles.json')

使用axios发收请求响应。使用chai.expect方法,并引入数据。

首先,补上views的html代码,而后再开始测试。

const boot = require('../app').boot
const shutdown = require('../app').shutdown
const port = require('../app').port
const axios = require('axios')
const expect = require('chai').expect
const boot = require('../app').boot
const shutdown = require('../app').shutdown
const port = require('../app').port
const axios = require('axios')
const expect = require('chai').expect

const seedArticles = require('../db/articles.json')

describe('server', () => {
  before(() => {
    boot()
  })

  describe('homepage', () => {
    it('should respond to GET', (done) => {
      axios
        .get(`http://localhost:${port}`)
        .then(function(response) {
          expect(response.status).to.equal(200)
        })
        .catch((err) => {
          console.log(err)
        })
        .then(done) //promise的写法,告诉mocha这个it的test彻底结束。
    })

    it('should contain posts', (done) => {
      axios
        .get(`http://localhost:${port}`)
        .then((response) => {
          seedArticles.forEach((item, index, list) => {
            // ⚠️,respnse.text目前是undefined!,没有test这个属性,有data属性。
            if (item.published) {
              expect(response.data).to.include(`<h2><a href="/articles/${item.slug}">${item.title}`)
            } else {
              expect(response.data).not.to.include(`<h2><a href="/articles/${item.slug}">${item.title}`)
            }
          })
        })
        .catch((err) => {
          console.log("Throw err: ", err)
        })
        .then(done)
    })
  })

  after(() => {
    shutdown()
  })
})

在第一个describe内嵌套一个describle:

//和上面的describe不能一块儿运行,不然链接不到url。 缘由未找到!!!
  describe('article page', () => {
    it('should display text or 401', (done) => {
      let n = seedArticles.length
      seedArticles.forEach((item, index, list) => {
        // console.log(`http://localhost:${port}/articles/${seedArticles[index].slug}`)
        axios
          .get(`http://localhost:${port}/articles/${seedArticles[index].slug}`)
          .then((response) => {
            console.log("success!!", seedArticles[index].slug)
            if (item.published) {
              expect(response.data).to.include(seedArticles[index].text)
            } else {
              expect(response).to.exist
              expect(response.data).to.be(401)
            }
          })
          .catch((error) => {
            console.log("error!!!", seedArticles[index].slug)
            console.log(error.message)
          })
      })
      done()   //这里⚠️,axios异步是在一个循环内部执行屡次,所以done放在这个it代码最后一行 
}) })

 

 

done()方法是mocha检测异步函数测试结束的回调函数!具体见文档搜索done()

⚠️原文response.text是❌的,应该改为response.data 

 

在完成下一章重写app.js后,执行mocha test

提示❌:最后检查app.js发现

require('./routes')返回一个空对象{}, 这是语法糖的缘由。解决方法:

在routes/index.js文件,引入article.js和user.js

exports.article = require('./article')
exports.user = require('./user')

exports.index = (req, res, next) => {
  req.collections.articles
    .find({published: true}, {sort: {_id: -1}})
    .toArray((error, articles) => {
      if (error) return next(error)
      res.render('index', {articles: articles})
  })
}

 

 cursor.sort(sort)
// 参数sort是一个document对象,格式是{field: value}
// 升降分类: 值为1是升序, -1为降序。
// 复杂的排序方式见MongoDB文档说明。

如此require('./routes')会返回一个对象:

{
  article: {
    show: [Function],
    list: [Function],
    add: [Function],
    edit: [Function],
    del: [Function],
    postArticle: [Function],
    admin: [Function]
  },
  user: {
    list: [Function],
    login: [Function],
    logout: [Function],
    authenticate: [Function]
  },
  index: [Function]
}

 

再次执行mocha test,仍然❌

Error: Route.get() requires callback functions but got a [object Undefined]

执行:

node inspect app.js
//app.js内能够加上debugger

发现:第76行❌

76 app.get('/post', routes.article.post) 

后面都报告错误,注释掉76-86行,则正常。

!!这是由于我没有安装pug视图引擎,也没有写view相关页面代码的缘由。

在完成后续步骤后,再执行分别对2个describe块执行测试,成功。

问题:执行完测试,不能自动回到terminal, 须要ctrl + C返回terminal. 

 

第三步:Adding Persistence

修改app.js:

const express = require('express')
const http = require('http')
const path = require('path')
const ejs = require('ejs')  //使用html模版
const routes = require('./routes')  //引入routes文件下的脚本
const mongoose = require('mongoose')//引入mongoose
const dbUrl = process.env.MONGOHQ_URL || 'mongodb://127.0.0.1:27017/blog' mongoose.connect(dbUrl)
const db = mongoose.connection; //获得默认的链接的数据库数据。
db.on('error', console.error.bind(console, 'connection error:'));
// db.once('open', function() {
//   // we're connected!
// });
//从数据库取出数据,存入一个变量collections
const collections = {
  articles: db.collection('articles'),
  users: db.collection('users')
}

 

添加如下声明,用于Express.js中间件模块:

// 添加中间件:
const logger = require('morgan')  //用于logging
const errorHandler = require('errorhandler') //错误处理
const bodyParser = require('body-parser')    //解析进入的HTTP请求 bodies
const methodOverride = require('method-override') //to support clients that do not have all HTTP methods

 

// 建立Express实例, 
let app = express()
app.locals.appTitle = 'blog-express'

  

这一步🌿:经过req对象,在每一个Express.js route内,引入MongoDB collections:

// decorator pattern:
//经过req对象, expose mongoose/MongoDB collections 在每一个Express.js route内
app.use((req, res, next) => {
  if (!collections.articles || !collections.users) {
    return next(new Error("No collections."))
  }
  req.collections = collections
  return next()
})

 

上面的代码叫作decorator pattern

一种Node的设计模式,能够参考在线课程:Node Patterns: From Callbacks to Observer. (收费!)

这种设计思路是让req.collections在后续的中间件和路径内可使用。

⚠️使用next()在中间件内部,不然请求会停滞。


 

插入的知识点:

app.use([path,] callback [, callback...])

Mounts the specified middleware function(s) at the specified path:

当请求的路径匹配path时,中间件函数被执行

回调函数,能够是:

  • 一个中间件函数
  • 系列的中间件函数
  • 数组形式的中间件函数[callback, callback...]
  • 上面的组合形式

例子,按顺序执行中间件函数:

// this middleware will not allow the request to go beyond it
app.use(function(req, res, next) {
  res.send('Hello World');
});

// requests will never reach this route
app.get('/', function (req, res) {
  res.send('Welcome');
});

 


 

 

继续,下一步:

在第2章已经写好这些代码:

定义Express设置:创建端口,模版的路径位置,使用什么模版引擎。

// 定义Express设置:创建端口,模版的路径位置,使用什么模版引擎。
app.set('appName', "Blog")
app.set('port', process.env.PORT || 3000)
app.set('views', path.join(__dirname, 'views'))

app.engine('html', ejs.__express)
app.set('view engine', 'html')

 

如今,是对你已经熟悉的经常使用功能: 请求登录的中间件,转化JSON input, 使用Stylus for CSS,和serving of static content。

使用app.user()插入这些中间件到Express app。

app.use(logger('dev'))
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({extended: true}))
app.use(methodOverride())
app.use(require('stylus').middleware(path.join(__dirname, 'public')))
app.use(express.static(path.join(__dirname, 'public')))

 

使用标准的Express.js error handler, 以前使用require()引入的。

errorhandler是一个(300✨)的用于开发阶段的错误处理中间件。

if (app.get('env') === 'development') {
  app.use(errorHandler('dev'))
}

 

下一步,app.js会处理服务器路径。路径链接到views:

app.get('/', routes.index)

app.get('/login', routes.user.login)
app.post('/login', routes.user.authenticate)
app.get('/logout', routes.user.logout)

app.get('/admin', routes.article.admin)
app.get('/post', routes.article.post)
app.post('/post', routes.article.postArticle)

app.get('/articles/:slug', routes.article.show)

 

 EEST api routes大多数时候用在admin page: 

// REST API routes
// 用在adimn page。 That's where our fancy AJAX browser JavaScript will need them.
// 他们使用GET,POST, PUT, DELETE方法,不会渲染网易模版,只输出JSON数据。
app.get('/api/articles', routes.article.list)
app.post('/api/articles', routes.article.add)
app.put('/api/articles/:id', routes.article.edit)
app.delete('/api/articles/:id', routes.article.del)

 

加上一个404route, 用于用户键入了错误的URL的提示:》

app.all("*", (req, res) => {
   res.status(404).send()
})

 

建立一个http.Server的实例

// 建立一个http.Server的实例
// 第3章的代码:
// 根据是否在terminal执行node app.js来决定:
//   1.执行server.listen(),
//   2.出口module到缓存。
const server = http.createServer(app)

const boot = () => {
  server.listen(app.get('port'), () => {
    console.info(`Express server listening on port ${app.get('port')}`)
    console.log(`Express server listening on port ${app.get('port')}`)
  })
}
const shutdown = () => {
  server.close()
}

if ( require.main === module ) {
  boot()
} else {
  console.log('Running app as a module')
  exports.boot = boot
  exports.shutdown = shutdown
  exports.port = app.get('port')
}

完成app.js✅

 

Routes module:

下一步添加index.jsarticle.js, and user.js文件到routes文件夹内。 

 

user.js

用于验证(见第6章)

The method for the GET /users route, which should return a list of existing users (which we'll implement later)

export.list = (req, res, next) => {
   res.send('respond with a resource')
}    

 这个为GET /login page route的方法,会渲染login form:(login.html或.pug)

exports.login = (req, res, next) => {
  res.render('login')
}

这个方法最终会destroy the session并返回到home page:

exports.logout = (req, res, next) => {
  res.redirect('/')
} 

这个为POST /authenticate route的方法,会处理验证并返回到admin page:

exports.authenticate = (req, res, next) => {
  res.redirect('/admin')
}

上面是 user.js的彻底代码,一个输出4个方法。

 

article.js

如今最主要的database行为发生在article.js routes。

GET article page (异步函数的2种写法),查询一个article的,进入一个article的详情页面:

传统:推荐✅,由于文档里回调函数的参数已经提供好了。

exports.show = (req, res, next) => {
  if (!req.params.slug) return next(new Error('No article slug.'))
  req.collections.articles.findOne({slug: req.params.slug}, 
    (error, article) => {
      if (error) return next(error)
      if (!article.published) return res.status(401).send()
      res.render('article', article)
  })
}

 使用promise,⚠️,若是使用promise,回调参数只有article

exports.show = (req, res, next) => {
  if (!req.params.slug) {
    return next(new Error('No article slug.'))
  }
  req.collections.articles.findOne({slug: req.params.slug})
  .then((article) => {if (!article.published) return res.status(401).send()
    res.render('article', article)
  })
}

 

下面4个函数,用于API 

 GET /api/articles API route(在admin page使用), 在网址输入框输入:

http://localhost:3000/api/articles会获得JSON格式的数据。

exports.list = (req, res, next) => {
  req.collections
    .articles
    .find({})
    .toArray((error, articles) => {
      if (error) return next(error)
      res.send({articles: articles})
  })
}

 

POST /api/articles API routes (used in the admin page),

exports.add = (req, res, next) => {
  if (!req.body.article) return next(new Error('No artilce payload'))
  let article = req.body.article
  article.published = false
  req.collections.articles.insert(article, (error, articleResponse) => {
    if (error) {
      return next(error)
    }
    res.send(articleResponse)
  })
}

req.body获得提交的key/value数据(具体见文档,或者文章底部的解释。)

 

PUT /api/articles/:id API(admin页面):

(文章使用updateById方法,我改为updateOne方法)

exports.edit = (req, res, next) => {
  if (!req.params.id) return next(new Error('No article id'))
  // 不知道改的对不对??
  req.collections.articles.updateOne(
    {"_id": req.params.id},
    {$set: req.body.article},
    (error, result) => {
      if (error) return next(error)
      res.send({affectedCount: result.modifiedCount})
    }
  )
}

 DELETE /api/articles/:id API route

exports.del = (req, res, next) => {
  if (!req.params.id) return next(new Error('No article ID.'))
  req.collections.articles.deleteOne(
    {"_id": req.params.id},
    (error, result) => {
      if (error) return next(error)
      res.send({affectedCount: result.deletedCount})
    }
  )
}

 

 

后续的函数:

其实routes的脚本,至关于Rails中的controller的功能,和数据库交互。还包括一些model的功能,如一些验证。

// get article POST page 
// http://localhost:3000/post, 进入文章建立页面,建立一篇新文章。
exports.post = (req, res, next) => { if (!req.body.title) { return res.render('post') } } exports.postArticle = (req, res, next) => { // 表格必须都填入信息,不然渲染回post,并附带一个错误信息: if (!req.body.title || !req.body.slug || !req.body.text) { return res.render('post', {error: 'Fill title, slug and text.'}) } const article = { title: req.body.title, slug: req.body.slug, text: req.body.text, published: false } req.collections.articles.insert(article, (error, articleResponse) => { if (error) return next(error) res.render('post', {message: 'Article was added. Publish it on Admin page'}) }) } // Get /admin 页面,取数据 exports.admin = (req, res, next) => { req.collections .articles.find({}, {sort: {_id: -1}}) .toArray((error, articles) => { if (error) return next(error) res.render('admin', {articles: articles}) }) } 

 

最后

view的template: 下载代码,⚠️模版使用pug,须要下载并在app.js内培训app.set('view engine', 'pug')

下载public文件夹下的文件:其中包括bootstrap, 和jquery

教程使用的bootstrap已通过期。去官网下载一个相近的版本3.4.0,用编译好的。解压后复制到public/css。

jquery直接复制上面的链接。

补充 style.css, blog.js。

 

以后进行测试,

而后,打开本地数据库,mongod, 运行在localhost和使用默认的端口27017。

而后,启动app.js脚本: node app.js, 便可访问

备注:

❌:

admin页面,ation的delete和update行为。不能写入数据库! 

// PUT /api/articles/:id API route
exports.edit = (req, res, next) => {
  if (!req.params.id) return next(new Error('No article id'))
  console.log("edit!!!")
  req.collections.articles.updateOne(
    {_id: req.params.id},
    {$set: req.body.article},
    (error, result) => {
      console.log("callback, edit!!!")
      if (error) return next(error)
      res.send()
    }
  )
}
// DELETE /api/articles/:id API route
exports.del = (req, res, next) => {
  if (!req.params.id) return next(new Error('No article ID.'))
  req.collections.articles.deleteOne(
    {_id: req.params.id},
    (error, result) => {
      if (error) return next(error)
      res.send({affectedCount: result.deletedCount})
    }
  )
}

 

花费了2个多小时问题未找到。暂时搁置!

req.collections.articles的值是一个复杂的对象,来自app.js脚本中:

const mongoose = require('mongoose')
const dbUrl = process.env.MONGOHQ_URL || 'mongodb://127.0.0.1:27017/blog'

mongoose.connect(dbUrl)
const db = mongoose.connection;

const collections = {
  articles: db.collection('articles'),
  users: db.collection('users')
}

let app = express()

app.use((req, res, next) => {
  if (!collections.articles || !collections.users) {
    return next(new Error("No collections."))
  }
  req.collections = collections return next()
})

 

req.collections.articles可使用find方法。

猜想: 它链接到了mongoDB/mongoose。

可是我尝试下面的脚本❌,

db.collection("articles").find({})返回一个undefined。很奇怪

var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/blog', {useNewUrlParser: true});

var db = mongoose.connection;
console.log(db.collection("articles").find())

 

 

奇怪?db.collections无效?返回一个{}空hash。方法定义 :A hash of the collections associated with this connection

 



 

插入知识点Express的Response

res对象:表明一个Express app当获得一个http请求后发送的http响应。

为了方便使用res的写法,能够根据回调函数本身定义的参数名字写。

res对象是Node原生对象的升级版,它支持全部的内建fields和方法。

 

res.send([body])

发送http响应。

参数body,能够是Buffer object, string, object, array。

//例子
res.send('<p>some html</p>')
res.status(404).send({error: 'something blew up'})

 

res.render(view,[locals], [callback])

渲染一个view, 并发送这个渲染的HTML string给客户端。

参数:

view: 一个字符串,渲染视图的路径,能够是绝对或相对路径。

locals:一个对象,传入view的数据。

callback: function(err, html){} 。若是提供了回调函数,就不会自动执行响应,须要使用res.send()

 

res.redirect([status,] path)

res.redirect('/foo/bar');
res.redirect('http://example.com');
res.redirect(301, 'http://example.com');
res.redirect('../login');

  

req.params

This property is an object containing properties mapped to the named route “parameters”.

一个对象:包含请求的URl的参数/值对儿。

Route parameters

Route path: /users/:userId/books/:bookId
Request URL: http://localhost:3000/users/34/books/8989
req.params: { "userId": "34", "bookId": "8989" }

 

req.body

在request的body内包括了提交的key/value data.

默认是undefined。当你使用body-parsing中间件时,如body-parser, multer

 

body-parser

$ npm install body-parser

//API
var bodyParser = require('body-parser')

// 在程序的app.js脚本内,express的实例app
//app.use(bodyParser.json())for parsing application/json
// app.use(bodyParser.urlencoded({extended: true}))for parsing application/x-www-form-urlencoded

 

db.collection_name.drop()

删除这个collection


 

 

mongoose

https://mongoosejs.com/docs/api.html#connection_Connection-dropCollection

 

Connection.prototype.dropCollection()

//start.js
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/test', {useNewUrlParser: true});

var db = mongoose.connection;
//假设test数据库内有messages collection,下面的命令会完全删除这个collection
db.dropCollection("messages")

 

相似命令

Connection.prototype.dropDatabase() 返回Promise

 

Connection.prototype.collection(),方法

console.log(db.collection("inventory")) 返回collection实例。 
Not typically needed by applications. Just talk to your collection through your model.
对程序来讲没什么用途,只是经过你的model告诉你的collection。
相关文章
相关标签/搜索