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.md

学习mongodb的官方网站:

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

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

 

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

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

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

 

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

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

因此使用mongoose代替。

 


 

我真的喜欢使用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
//for parsing application/json
app.use(bodyParser.json())
// for parsing application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({extended: true}))

 

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。

 

posted @ 2019-01-14 18:22  Mr-chen  阅读(533)  评论(0编辑  收藏  举报