Practical Node.js (2018版) 第8章:Building Node.js REST API Servers


Building Node.js REST API Servers with Express.js and Hapi

Modern-day web developers use an architecture consisting of a thick client and a a thin back-end layer。如AngularJS,ReactJs, VueJS。用来建立厚厚的client。

另一方面,他们使用REST APIs建立thin back-end layer。

 a representational state transfer (REST) web application programing interface (API) service。

这种结构,被称为厚客户端或者SPA单页面程序,变得越来越流行。因为它们有以下优势:

  • SPA更快,因为它们渲染网页元素在浏览器内,无需总是从服务器取HTML.
  • bandwidth更小,因为一旦它加载,大多数的页面布局位置相同,所以浏览器只需使用JSON格式的数据来改变网页的元素。
  • 相同的后端REST API可以服务多客户apps/consumers, web app是其中之一。
  • 不理解👇:
  • There is a separation of concerns, i.e., the clients can be replaced without compromising the integrity of the core business logic, and vice versa.
  • UI/UX难以测试,尤其是事件驱动,单页面程序,这有一个增加的跨浏览器测试的复杂程度。但是,分离的业务逻辑进入back-end REST API, 这个逻辑变得容易测试:在unit和functional testing。

因此,大多数新程序接受使用REST API和Clients方法,即使开始只有一个客户。

 

使用Node创建REST API非常容易。

本章包括以下内容:

  • REST API 基础
  • Project 依赖
  • 测试:Mocha (和superagent)
  • REST API server implementation with Express and Mongoskin(替换为Mongoose)
  • 使用Hapi.js重构。Hapi(1万✨)没有Express知名。

REST API server可以处理object的创建,object和collection的retrieval检索,改变object,删除object。

本章的所有代码https://github.com/azat-co/practicalnode

 


 

RESTful API Basics

REST API变得著名是因为在分布式系统中,每个事务都需要包含关于客户机状态的足够信息。这个标准是无状态的,因为服务器上不存储关于客户机状态的信息,这使得每个请求都可以由不同的系统提供服务。这使得向上或向下缩放系统变得轻而易举。

在某种意义上,无状态服务器就像编程中的松散耦合类。许多基础设施技术使用最好的编程实践;除了松耦合之外,版本控制、自动化和持续集成都可以应用于基础设施,从而获得巨大的好处。

 

RESTFUL api的独特特性:

  • 更好的可伸缩性支持。  因为不同的组件可以独立部署到不同的服务器。
  • 取代了 Simple Object Access Protocol (SOAP )。因为它更简单的动词和名词结构。
  • 使用HTTP methods
  • JSON不是唯一选择(尽管它是最著名的)。

下面是一个简单的CRUD REST api:

Method   URL Meaning

GET

/messages.json 用JSON格式返回list of messages.

PUT

/messages.json 更新/替换所有的messages并返回JSON格式的status/error
POST   /messages.json  创建新的message,返回它的🆔
GET /messages/{id}.json 返回message,包括🆔,JSON格式的
PUT   /messages/{id}.json 更新id是{id}的message,如果{id}不存在,创建它。
DElETE    /messages/{id}.json 删除id是{id}的message, 返回status/error , JSON格式

REST is not a protocol; it's an architecture in the sense that it's more flexible than SOAP, which we know is a protocol. 

因此,REST AP可以像/messages/list.html或者/messages/list.xml。如果我们想要支持这些格式的话。

 

PUT and DELETE are idempotent methods. idempotent是一个专业术语,它的意思是server收到2个以上的类似请求,结果是相同的。

GET是安全的, nullipotent。

POST是不安全的, 非idempotent。它可能影响state和引起副作用。

 

相关文章

https://en.wikipedia.org/wiki/Representational_state_transfer

https://www.infoq.com/articles/rest-introduction

 

在我们的REST API server , 我们支持CRUD操作并使用app.use(), app.param()方法控制Express.js中间件概念。

因此,我们的app应该可以处理下面的命令,使用JSON格式

  • POST /collections/{collectiionName}
  • GET /collections/{collectionName}/{id}:  使用🆔来检索一个对象。
  • GET /collections/{collectionName}/    :   请求从collection(items)来检索任意items, 本例子,我们有query选项: up to 10 items和使用Id排序。
  • PUT /collections/{collectionName}/{id}:  request with ID来更新一个对象。
  • Delete /collections/{collectionName}/{id}

让我们通过声明依赖来开始我们的程序。


  

Project Dependencies

安装packages, 本章作者使用Mongoskin, 它比Mongoose更轻量化,它是无schema的。(作者个人喜欢,但许多开发者更喜欢安全和a schema的统一性)

 

第二个选择是framework。使用Express.js, 它是Node.js http模块的扩展。

Express.js框架有一大堆的modules插件,叫做middleware。可以供开发者选择使用,无需自己写了。

 

首先,创建文件夹。

$ mkdir rest-express
$ cd rest-express
$ npm init -y

 

npm/node.js提供多个方法安装依赖

  • Manually, one by one,  使用npm install <name> <name> ...命令。
  • As a part of package.json (这个方法最简单, 复制这个文件,然后执行npm install
  • By downloading and copying modules

注意⚠️package.js内不要有多余的逗号“,”

//... ⚠️版本选择。
  "dependencies": {
    "body-parser": "1.18.2",
    "express": "4.16.2",
    "mongodb": "2.2.33",
    "mongoskin": "2.1.0",
    "morgan": "1.9.0"           
  },
  "devDependencies": {
    "expect.js": "0.3.1",
    "mocha": "4.0.1",         
    "standard": "10.0.3",          
    "superagent": "3.8.0"
  }
}

 ⚠️:

morgan是登陆用的中间件。

superagent可以用axios替代。

expect.js可以改用chai.js

npm i mocha chai standard axios --save-dev

 


 

Test Coverage with Mocha and Superagent

 

在执行app前,写功能测试。制造HTTP请求到不久后创建的REST API server。

在测试取代开发,我们使用这些测试来建立Node.js free JSON REST api server ,使用Express.js框架和Mongoose库forMongoDB(文章使用Mongoskin)

 

using the Mocha (http://visionmedia.github.io/mocha) and superagent (http://visionmedia.github.io/superagent)[^5] libraries.

这些测试执行基本的CRUD, posting HTTP 请求到服务器。

具体安装mocha和使用见之前的博客:

https://www.cnblogs.com/chentianwei/p/10262044.html

 

现在创建test/index.js文件,并有6个程序组组:

  1. 创建一个object
  2. Retrieve an object with its ID
  3. Retrieve the whole collection
  4. Update an object by its ID
  5. Check an updated object by its ID
  6. Remove an object by its ID
const boot = require('../index.js').boot
const shutdown = require('../index.js').shutdown
const port = require('../index.js').port

const superagent = require('superagent')
const expect = require('expect.js')

before(() => {
  boot()
})

describe('express rest api server', () => {
  // ...
})

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

 

然后,写第一个测试案例,在describe内,主要代码写在callback。

发出POST 请求到一个本地的server实例,并从测试文件boot()。

当发送请求后,我们传递数据,这创建一个对象。

我们期待没有❌。最后我们保存创建的对象的🆔到id变量,以便后面的测试案例使用。

describe('express rest api server', () => {
  let id  //用于存储新创建的对象的_id, 然后使用它进行RUD的测试。
  it('post object', (done) => {
    axios({
      method: 'post',
      url: `http://localhost:${port}/collections/db2`,
      data: {name: 'John', email: 'john@rpjs.co'}
    })
    .then((response) => {
      expect(response.data._id.length).to.equal(24)
      id = response.data._id
    })
    .catch((error) => {
      if (error.response) {
        // console.log(error.response.data);
      } else if (error.request){
        // error.request是http.ClientRequest的实例
        console.log(error.request)
      } else {
        console.log("Error", error.message)
      }
      // console.log("config", error.config)
    })
    .then(done)
  })

expect, 期待创建的document的_id有24个字母。

axios使用.then(done)告诉mocha结束测试。这个方法专门用于异步代码。

👆测试案例response.data就是request.data, 因为在主文件index.js,app.post(url, callback)中的callback中使用res.send(userResponse),  userResponse就是新增的记录document。

 

这里使用了很多自然语言的语法判断,具体见chai文档)

  it('retrieves an object', (done) => {
    axios
      .get(`http://localhost:${port}/collections/db2/${id}`)
      .then((response) => {
        expect(typeof response.data).to.equal('object')
        expect(response.data._id.length).to.equal(24)
        expect(response.data._id).to.equal(id)
      })
      .then(done)
  })

  it('retrieves a collection', (done) => {
    axios
      .get(`http://localhost:${port}/collections/db2`)
      .then((response) => {
        expect(response.data.length).to.be.above(0)
        expect(response.data.map(function(item) { return item._id})).to.include(id)
      })
      .then(done)
  })

  it('updates an object', (done) => {
    axios.put(`http://localhost:${port}/collections/db2/${id}`, {
      name: 'Peter',
      email: 'peter@yahoo.com'
    })
    .then((response) => {
      expect(typeof response.data).to.equal('object')
      expect(response.data.msg).to.equal('success')
    })
    .then(done)
  })

  it('checks an updated object', (done) => {
    axios.get(`http://localhost:${port}/collections/db2/${id}`)
    .then((res) => {
      expect(typeof res.data).to.equal('object')
      expect(res.data._id.length).to.equal(24)
      expect(res.data._id).to.equal(id)
      expect(res.data.name).to.equal('Peter')
    })
    .then(done)
  })

  it('delete an object', (done) => {
    axios.delete(`http://localhost:${port}/collections/db2/${id}`)
    .then((res) => {
      // 数据库返回的数据
      expect(res.data).to.equal("Deleted successfully!")
    })
    .then(done)
  })
})

 

  

最后使用mocha test/index.js命令或者npm test运行测试。

⚠️,如果想要生成测试报告,使用-R <name>选项:

  • $mocha test -R list, 
  • $mocha test -R nyan   (有一个猫的字符图形)

⚠️: axios的Response结构是一个对象

{
  data: {},
  status: 200,
  statusText: 'ok',
  headers: {},
  config: {},
  request: {}  //它是node.js内的最后一个ClientRequest实例。
}

如果返回error, 可以使用error.response 

 


 

 

REST API Server Implementation with Express and Mongoskin

创建并打开一个index.js文件:这是我们的主文件。

⚠️这里使用了body-parser中间件,用于使用Express#req.body。

const express = require('express')
const mongoose = require('mongoose')
const bodyParser = require('body-parser')
const logger = require('morgan')
const http = require('http')

const models = require('./model/index.js')

const app = express()

app.use(bodyParser.json())
app.use(logger())
app.set('port', process.env.PORT || 3000 )

const db = mongoose.connect('mongodb://localhost:27017/db2', { useNewUrlParser: true })

 

 

然后,添加route, (这些route方法的回调函数可以抽象出来。)

app.param('collectionName', (req, res, next, collectionName) => {
  if (!models.User) {
    return next(new Error('No models.'))
  }
  req.models = models
  return next()
})

app.get('/', (req, res, next) => {
  // 提示使用/collection/messages.
  res.send('Select a collection, e.g., /collection/messages')
})

// 得到所有的user信息
app.get('/collections/:collectionName', (req, res, next) => {
  req.models.User.find({}, null, {limit: 10, sort: {_id: -1}}, (error, users) => {
    if (error) return next(error)
    res.send(users)
  })
})

// 新增一个user
app.post('/collections/:collectionName', (req, res, next) => {
  if (!req.body) {
    return next(new Error("No user payload!!!!!!"))
  }
  let user = req.body
  req.models.User.create(user, (error, userResponse) => {
    if (error) {
      return next(error)
    }
    res.send(userResponse)
  })
})

// 根据_id,查询
app.get('/collections/:collectionName/:id', (req, res, next) => {
  // req.params属性是一个对象,它包含属性映射route的参数,因此本例子req.params内有一个id属性
  if (!req.params.id) {
    return next(new Error('No user id'))
  }
  req.models.User.findById(req.params.id, (error, user) => {
    if (error) return next(error)
    res.send(user)
  })
})

// 修改
app.put('/collections/:collectionName/:id', (req, res, next) => {
  if (!req.params.id) return next(new Error('No user id'))
  if (!req.body) return next(new Error('No user payload'))

  req.models.User.findById(req.params.id, (error, user) => {
    if (error) return next(error)
    user.set(req.body)
    user.save((error, savedUser) => {
      if (error) return next(error)
      res.send((savedUser._id == req.params.id) ? {msg: 'success'} : {msg: 'error'})
    })
  })
})

app.delete('/collections/:collectionName/:id', (req, res, next) => {
  if (!req.params.id) return next(new Error('No article ID.'))
  req.models.User.deleteOne({_id: req.params.id}, (error) => {
    if (error) {
      return next(error)
    }
    // 使用send方法,向客户端发送数据。
    res.send("Deleted successfully!")
  })
})

 

 

最后建立创建一个http.Server的实例,当boot时监听端口,当shutdown时关闭服务器的连接。

const shutdown = () => {
  server.close(process.exit)
}

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

 

 

完成后,使用$mocha test -R list测试

选择:

  • 使用浏览器输入url,测试也可以。
  • 使用postman程序
  • 使用Linux的terminal的curl命令。

 


 

附加Express API

app.param([name], callback)

增加callback triggers回调触发点给route parameters。

param callbacks defined on app will be triggered only by route parameters defined on app routes.

这个方法是一个Express.js 中间件。

例如,当一个请求模式内包含一个字符串'collectionName', 并且前面有一个冒号“:”。那么这个中间件就会被触发,执行它的回调函数。

  • 参数name可以是string或者数组。
  • 回调函数的参数是请求对象,响应对象,下一个中间件next(), 参数的值和参数的名字,按照顺序排列。

 

在app.param中间件的回调函数执行后,才会执行路径的handler。

 

例子1,

:id存在于一个路径内时,会按照顺序执行下面的中间件。

app.param('id', function (req, res, next, id) {
  console.log('CALLED ONLY ONCE');
  next();
});

app.get('/user/:id', function (req, res, next) {
  console.log('although this matches');
  next();
});

app.get('/user/:id', function (req, res) {
  console.log('and this matches too');
  res.end();   
});

 

当Get /user/42时,会在控制台打印:

CALLED ONLY ONCE
although this matches
and this matches too

 

注意⚠️res.end()其实会调用Node.js的核心module--HTTP中的response.end()

表示所有的响应的头和体已经发出,服务器应该考虑这个信息完成。这个方法必须在每个response中调用。

 

例子2

如果参数name是一个数组,回调函数触发点是声明在name内的每一个元素,它们按照顺序被声明

(具体见http://expressjs.com/en/4x/api.html#app.param)


 

posted @ 2019-01-28 15:52  Mr-chen  阅读(243)  评论(0编辑  收藏  举报