Mosh的Node.js教程(六)

609 阅读12分钟

前言

本系列文章是根据Mosh大佬的视频教程全方位Node开发 - Mosh整理而成,个人觉得视频非常不错,所以计划边学习边整理成文章方便后期回顾。该视频教程是英文的,但是有中文字幕,感谢marking1212提供的中文字幕翻译。

本篇文章大纲

  • 使用Express创建RESTful服务
  • RESTful服务
  • Express简介
  • 创建第一个Web服务器
  • Nodemon
  • 环境变量
  • 路由参数

使用Express创建RESTful服务

之前我们学习了HTTP模块,我们使用它创建了一个监听3000端口的服务器,并且根据终端的请求做出对应反馈,比如根目录或者api/courses,实现这个小目标这样还行,之前的代码

const http = require('http')

const server = http.createServer((req, res) => {
  if (req.url === '/') {
    res.write('Hello world')
    res.end()
  }

  if (req.url === '/api/courses') {
    res.write(JSON.stringify([1, 2, 3]))
    res.end()
  }
})

server.listen(3000)

console.log('Listening is on port 3000...')

但是像上面这样做,在创建复杂的应用就不太理想了,因为在大型的应用系统中,就会有很多的终端地址规则,你不应该这么写if语句,后面我们会介绍Express,这是一个快速轻量级创建Web服务的框架。

RESTful服务

RESTRepresentational State Transfer状态表征转移的缩写,这听起来可能有点迷糊,但是没关系,从本质上讲,REST就是用来创建这些规范的HTTP服务的。

我们使用简单的HTTP协议原则,提供数据的增、删、改服务,我们把这些操作统称为CRUD操作(增删改查),现在我们来用实际的例子来解释这些范例。

假设有家叫vidly的公司,是做视频租赁业务的,有个用来管理用户信息的应用,在服务器上需要提供这么一个终端地址http://vidly.com/api/customers,客户端通过发送HTTP请求到这个终端来和服务器进行对话,有些你需要了解的关于终端的事情,首先终端的地址由http或者https开头,这取决于请求的类型,如果你希望数据在加密的状态下传输,你就需要使用https,然后就是这个应用的域名vidly.com,然后就是/api,这不是必要的规定,但是你可以看到很多公司遵循这个范式,他们总在地址的某个部分包含api字样,它们有些像这样,有些则作为子域名,比如api.vidly.com,这两种没有本质上的区别,之后是/customers,它代表了应用中用户的集合,在RESTful中把它看成一种资源,我们可以将诸如用户、电影、租金或者各种资源都开放出去,现在这个就是用来操作用户资源的,所有对用户资源的操作比如创建用户,更新用户信息,都以向此终端发送请求来完成,请求的种类对应着要进行的操作,所有的HTTP请求都有所谓的动作或方法,取决于它们的类型和目的。

标准的HTTP方法

  • get 获取数据
  • post 发布数据
  • put 更新数据
  • delete 删除数据

现在我们以用户资源为例来解释每个方法,为了得到用户的列表,我们向终端发送get请求/api/customers,注意这里的customers,它代表的是一组用户的列表,当我们向这个终端发送get请求时,我们的服务器会返回类似这么个东西

[
    {
        id: 1, name: ''
    },
    {
        id: 1, name: ''
    }
    ...
]

也就是一个用户对象的列表,如果我们需要一个单独的用户,就需要包含这个用户的id,/api/customers/1,然后服务器就会这样返回单独的用户对象。

为了更新用户数据,需要向终端发送一个put请求,/api/customers/1,同样需要注意的,是这里要包含需要更新的用户id,同时也需要在请求体中包括更新的用户对象本身

{
    name: ''
}

这是一个包含更新信息的完整用户对象,我们发送给服务器,服务器根据请求体的值来更新数据。

类似的,为了删除一个用户,我们需要向终端发送delete请求,这里我们不用在请求体中包含用户的对象,因为要删除的话只需要提供id就可以了。

最后为了创建一个用户,我们需要向终端发送一个post请求POST /api/customers,注意到因为是添加一个新用户,我们就不是操作特定的已存在的对象,资源地址就没有id,我们操作的是用户集合customers,我们向集合中添加一个新的用户,这就是我们需要在请求体中包含用户对象的原因, 服务器得到对象,并且在集合中创建出来,这就是RESTful的范例

GET /api/customers
GET /api/customers/1
PUT /api/customers/1
DELETE /api/customers/1
POST /api/customers

我们用一个有语义的地址来公开服务器的资源,并且支持对该资源的多种操作,使用基本的HTTP规则来支持比如增加或者更新数据。

Express简介

下面这个是我们学习node核心模块HTTP时创建的路由

const http = require('http')

const server = http.createServer((req, res) => {
  if (req.url === '/') {
    res.write('Hello world')
    res.end()
  }

  if (req.url === '/api/courses') {
    res.write(JSON.stringify([1, 2, 3]))
    res.end()
  }
})

server.listen(3000)

console.log('Listening is on port 3000...')

尽管这样可以工作,但是并不好维护,随着我们为应用定义越来越多的路由,我们需要在这个回调中定义很对if代码块,这时框架就要登场了,框架给我们一个优秀的结构,可以在保持良好维护性的前提下创建很多路由规则,有很多框架可以在node上创建Web服务器,最受欢迎的就是Express

如果你访问npmjs.org或者npmjs.com,搜索Express,我们点击进去看一下,如下图

可以看到Express每周的一个下载量,这是个非常受欢迎的框架,并且它也非常轻量级,非常快,而且有很好的文档系统,回到VSC,我们新建一个项目express-demo,回到控制台,我们进入这个文件夹,执行npm init --yes来初始化项目,然后我们安装Express,执行npm i Express,好了,接下来我们来创建第一个Web服务器。

创建第一个Web服务器

我们新建一个文件index.js,首先要做的就是导入Express模块

const express = require('express')

这个会返回一个函数,我们保存在express中,我们输入express(),可以看到返回的是一个Express对象,我们把它保存在app

const express = require('express')

const app = express()

这就代表了我们的应用,这个app对象有很多有用的方法,比如

app.get()
app.post()
app.put()
app.delete()

所有这些都代表HTTP动作或者之前说过的HTTP方法,如果你想处理发给终端的post请求,你就需要使用post方法,现在我们先来使用下get方法,我们要实现一个对应的HTTP的get请求的终端,这个方法需要两个参数

// 第一个参数是路径或者说URL
// 第二个参数是一个回调函数
// 这个函数在对给定端口使用get方法时被调用,这个回调函数有两个参数,分别是request和response
app.get('/', (req, res) => {
    
})

request参数给了我们很多了解请求的属性,如果你想了解所有这些属性,最好去查看Express的文档,这边可以查看request的所有属性,现在我们只会用到一部分属性,回到代码,当我们的get方法得到一个对根目录的请求,我们就返回给它hello world文字

app.get('/', (req, res) => {
    res.send('Hello World');
})

// 监听特定的端口
// 可选的,我们还可以给它一个回调函数,在开始监听的时候执行
app.listen(3000, () => console.log('Listening on port 3000...'))

现在回到控制台,运行node index.js,输出Listening on port 3000...,提示我们正在监听3000端口,打开浏览器,输入localhost:3000,浏览器打印出了Hello World

现在我们定义另一个路由,同样调用get方法

app.get('/api/courses', (req, res) => {
    res.send([1, 2, 3]);
})

实际的开发中,这里需要从服务器得到课程数据并且返回,但是我们这边的关注点只在如何创建终端上,我们不会进行任何的数据库操作,这里就简单的返回一个数字的数组,后面我们会去拿真的课程数据来替换这里的数组,保存数据,回到控制台,我们需要重启这个应用,使用Ctrl+C中断运行,再运行node index.js,回到浏览器,我们访问http://localhost:3000/api/courses,浏览器打印出[1,2,3]

这里我们需要留意的是,这里的实现,没有那些if代码块,我们使用get方法来创建新的路由规则,因为这样的结构,我们的应用更简洁,我们可以把路由规则移到另一个文件去,例如我们将所有与课程有关的路由合并到一个单独的文件比如courses.js,所以Express将应用变得更有结构感。

Nodemon

之前我们每次修改代码,我们都需要手动重启应用,这样太麻烦了,来看下更好的方法。

我们安装一个node包叫nodemon, 也就是node monitor的缩写,我们在控制台输入npm i -g,因为我们需要全局安装这个包

npm i -g nodemon

安装好后,我们就不用使用node命令来运行应用了,我们使用nodemon来运行。

从图中可以看到nodemon会监测文件夹下的所有文件的改动,任何文件名和扩展名。

回到代码里,我们随便修改一下代码,然后保存,可以看到控制台里,nodemon监测到了文件的更改并自动重启了应用,如图

这样就不用手工重启了。

环境变量

现在需要优化的地方,是这里写死的端口地址3000

app.listen(3000, () => console.log('Listening on port 3000....'))

在开发环境还可以使用,但是在生产环境很可能就不行了,因为当我们把应用发布到一个共享的平台上,应用可用的端口是由平台动态分配的,我们就不能保证3000端口一定可用了,优化这个的做法就是使用环境变量。

基本上node环境的共享平台,环境变量中管理端口的属性是PORT,环境变量就是进程在运行时才产生的变量,它是在应用之外设置的变量,为了读取PORT属性,我们使用process对象,这是一个全局的对象,该对象下有个属性叫env,即环境变量的缩写,然后我们就能读取PORT属性

process.env.PORT

接下来我们把代码改写一下

// 如果设置了PORT就使用,没有就默认使用3000
const port = process.env.PORT || 3000

app.listen(port, () => console.log(`Listening on port ${ port }....`))

回到控制台,我们再次运行nodemon index.js,可以看到还是输出

Listening on port 3000....

因为我们还没有设置PORT环境变量,我们来设置一下,如果是Mac,可以运行export命令来设置,如果是Window,就使用set命令

set PORT=5000

运行完,我们再次运行nodemon index.js,可以看到现在监听的是5000端口了,如下图

这就是在node应用中正确设置端口的方法。

路由参数

我们现在已经有给出一个课程列表的路由了,现在我们来看下如何获得单一课程的路由规则。

在之前说到RESTful服务的时候,如果想得到单一课程的,就要在URL中包含课程id,那么终端地址就应该是这样/api/courses/1,我们的路由应该要这样写

// :后面加上参数名,参数名可以自己定义
app.get('/api/courses/:id', (req, res) => {
    // 读取路由参数用req.params.id
    res.send(req.params.id)
})

回到浏览器,我们访问localhost:3000/api/courses/1,浏览器会显示1

如果有多个参数也是可以的,假设你开发一个支持博客的后端程序,你可能有这样的路由

app.get('/api/courses/:year/:month', (req, res) => {

})

有两个参数,这样就可以指定年和月的帖子,我们可以像之前那样获取参数

req.params.year
req.params.month

就这个例子,我们来看下params本身,把我们代码改一下

app.get('/api/courses/:year/:month', (req, res) => {
    res.send(req.params)
})

回到浏览器,我们访问localhost:3000/api/courses/2020/2,可以看到浏览器显示的是

{"year":"2020","month":"2"}

使用这种表达式,也可以读取查询字符串,也就是在问号后面的参数,例如我们可以获取2020年2月的帖子,然后以它们的名称来排序,我们的访问地址就变成这样

localhost:3000/api/courses/2020/2?sortBy=name

这个就是查询字符串,我们使用查询字符串向后端服务传递额外的参数,我们用参数提供路由必须的数据或值,使用查询字符串传递附加的内容,现在我们看下如何读取查询字符串,回到VSC,我们只要用req.query代替req.params就行了,改下代码

app.get('/api/courses/:year/:month', (req, res) => {
    res.send(req.query)
})

回到浏览器,我们再访问

localhost:3000/api/courses/2020/2?sortBy=name

浏览器显示

{"sortBy":"name"}

好了,本篇文章先到这里。

最后

感谢您的阅读,由于本人水平有限,如果文中有描述不当的地方,希望大家留言指正,以免误人。若有什么问题请留言,会尽力回答之。如果对你有帮助不要忘了分享给你的朋友哈,非常感谢。

关注

欢迎大家关注我的公众号前端帮帮忙

下篇文章预告

RESTful