前言
本系列文章是根据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服务
REST
是Representational 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