Node.js express模块(二)

Node.js express模块(一)_qfc_128220的博客-CSDN博客

通过前面对比http模块,express模块在开发一个简单服务器的体验,可以发现express模块完全碾压了http模块,接下来我们详细学习下express模块。

目录

认识express模块

express中间件概念及注册中间件的方法

express中间件可以做什么

express中间件分类

应用级别中间件

路由级别中间件

错误处理中间件

express内置中间件


认识express模块

express模块也是用来开发web服务器的,下面代码就是基于express开发一个简单的服务器

const express = require('express')

const app = express()

app.listen(80, '127.0.0.1', () => {
    console.log('服务器启动成功')
})

通过以上代码我们可以分析出:

express模块对外暴露的是一个方法,调用该方法即可创建一个服务器实例。

通过该服务器实例监听主机上的某个端口,即可成功启服务器。

这其实和http模块创建服务器的模式差不多。

const http = require('http')

const app = http.createServer()

app.listen(80, '127.0.0.1', ()=>{
    console.log('服务器启动成功')
})

对比来看,只能说express方法其实就是对http.createServer方法的封装。

express提供了高度分离的路由分配策略

const express = require('express')

const app = express()

app.get('/', (req, res)=>{
	
})

app.post('/', (req, res)=>{
	
})

app.listen(80, '127.0.0.1', () => {
    console.log('服务器启动成功')
})

可以发现,在路由分配处理上,express也做了较为彻底的封装,对比原先http模块高度耦合的路由分配策略

const http = require('http')
const url = require('url')

const app = http.createServer()

app.on('request', (req, res)=>{
    const {pathname} = url.parse(req.url)
    const method = req.method.toUpperCase()

    if(method === 'GET' && pathname === '/') {
        
    }

    if(method === 'POST' && pathname === '/') {
        
    }
})

app.listen(80, '127.0.0.1', ()=>{
    console.log('服务器启动成功')
})

express将原先处于request事件回调中的路由分配,全部解耦出来了,并且将method === 'GET' 这样的判断直接封装为了服务器实例上的方法app.get,并将路由pathname作为了app.get('/', callback)的第一个参数,只能说非常符合前端开发的路由思路,类似于ajax.get的模式。

这里还需要注意的是:express的路由分配app.get或app.post的回调函数的参数req,res其实都来源request监听事件回调的req,res。

express对req,res对象做了非常好的扩展,所谓扩展,即在不改变req,res对象原有属性和方法的基础上添加新的属性和方法。

http模块的req对象是IncomingMessage类型,res对象是ServerResponse类型。原始req,res对象上常用的属性和方法包括如下:

req对象属性以及方法 含义
req.method http请求行中的请求方法,常用的有GET和POST
req.url http请求行中的请求URL,包含pathname,params以及query
req.httpVersion http请求行中的http版本号
req.headers http请求头
data事件 通过监听req对象的data事件收集http请求体数据块
end事件 通过监听req对象的end事件组装收集到的http请求体数据块
res对象的属性以及方法 含义
res.statusCode http响应行状态码
res.statusMessage http响应行状态描述
res.setHeader(name, value) http响应头
res.writeHeader(stuasCode,statudeMessage, headers) http响应行,响应头
res.write(chunk, encoding, callback) http响应体(进行写入)
res.end(chunk, encoding, callback) http响应体(结束写入,并发送)

express没有对req,res对象的上述方法做任何改变,而是另外在req,res对象上新增了一批常用的属性和方法,详情见expressjs.com官网。

Express 4.x - API Reference (expressjs.com)

下面是几个比较常用的新增属性

express模块对req对象的扩展 含义
req.query GET请求URL中的请求字符串
req.params 请求URL中的动态参数
req.path 请求URL中的路径pathname
req.body POST请求体(application/json,application/x-www-form-urlencoded)
req.fields POST请求体(application/json,application/x-www-form-urlencoded,multipart/form-data)
req.files POST请求体(multipart/form-data)

需要注意的是:express没有直接解析出POST请求体放到req.body中,因为POST请求体的格式有多种,常见的有:

application/json、application/x-www-form-urlencoded、multipart/form-data

可以发现,POST请求体数据格式多样,express无法预知该以何种格式去解析,但是express提供了一些内置中间件来由使用者指定数据解析方式。

application/json格式请求体,可以使用express.json()中间件来解析

const express = require('express')

const app = express()

app.use(express.json()) // express.json()中间件来解析application/json请求体数据,并挂载到req.body上

app.post('/', (req, res)=>{
	res.status(200).json({
		'req.body': req.body
	})
})

app.listen(80, '127.0.0.1', () => {
    console.log('服务器启动成功')
})

Node.js express模块(二)_第1张图片

application/x-www-form-urlencoded格式请求体,可以使用express.urlencoded({extended:false})来解析

const express = require('express')

const app = express()

app.use(express.urlencoded({extended:false}))
// express.urlencoded({extended:false}) 中间件来解析请求字符串格式的请求体,并将解析后数据挂载到req.body上

app.post('/', (req, res)=>{
	res.status(200).json({
		'req.body': req.body
	})
})

app.listen(80, '127.0.0.1', () => {
    console.log('服务器启动成功')
})

Node.js express模块(二)_第2张图片

而multipart/form-data格式的请求体格式比较复杂,可能包含二进制文件数据,也可能包含普通字符串数据,或二者兼有,此时推荐使用第三方模块express-formidable来进行数据解析,该第三方模块是以express中间件为目标开发的,专门用于给express服务器解析multipart/form-data数据的。该模块会将解析完的数据挂载req.fields和req.files字段上,它们分别表示multipart/form-data数据中的普通字符串数据和二进制文件数据。

const express = require('express')
const formidable = require('express-formidable')

const app = express()

app.use(formidable({
	multiples: true // 该配置是支持上传多个文件
}))

app.post('/', (req, res)=>{
	res.status(200).json({
		'req.fields': req.fields,
		'req.files': req.files
	})
})

app.listen(80, '127.0.0.1', () => {
    console.log('服务器启动成功')
})

Node.js express模块(二)_第3张图片

Node.js express模块(二)_第4张图片

Node.js express模块(二)_第5张图片

另外,express-formidable也能解析application/json,application/x-www-form-urlencoded格式的请求体,只是此时需要通过req.fields来获取请求体数据而已

Node.js express模块(二)_第6张图片

Node.js express模块(二)_第7张图片

express模块对于res对象的扩展 含义
res.status(code) 设置HTTP响应码
res.sendStatus(code) 设置HTTP响应码,并直接返回
res.set(field, value) / res.set({field:value,...}) 设置一个或多个响应头
res.type(type) 设置响应头的Content-Type,其中type可以是常见的文件后缀名,如json,html
res.cookie(name, value, options) 设置响应头中的cookie,name和value分别是cookie的键值,options用于设置cookie的一些关键信息,如服务器地址,cookie有效期等
res.clearCookie(name, options) 根据cookie名删除响应头中对应cookie信息
res.json(body) 设置响应体,并发送。相较于write和end方法的优点是:res.json(body)底层调用了JSON.stringify方法,只要body可以被JSON.stringify即可
res.send(body) 设置响应体,并发送。相较于res.json方法,send方法不仅可以发送JS对象和数组,还可以发送Buffer类型数据,以及HTML字符串,并且可以自动识别body数据类型,设置响应头Content-Type。如果是Buffer类型数据,则send方法自动设置Content-Type为application/octet-stream,如果是HTML字符串,则send方法自动设置Content-Type为text/html,如果是JS数组或对象,则自动设置为application/json

Node.js express模块(二)_第8张图片

Node.js express模块(二)_第9张图片Node.js express模块(二)_第10张图片

Node.js express模块(二)_第11张图片

Cookie设置,下面是Cookie的一些配置信息

Node.js express模块(二)_第12张图片

domain:cookie来源,一般就是服务器地址

encode:一个同步方法,用于对cookie数据进行URL编码

expires:cookie的失效日期,如果没有设置,或设置为0,表示cookie在本次会话结束后失效,即浏览器关闭后失效

httpOnly:cookie存在安全问题,我们可以通过浏览器的BOM下的JavaScript方法来获取到浏览器缓存中的cookie信息,所以通过设置httpOnly,表示该cookie不能被通过JavaScript从浏览器缓存中取出来,而是只用于浏览器与服务器之间交互时使用。

maxAge:为正数时表示cookie可以有效时间毫秒数,为负数时,表示cookie信息仅在本次会话有效,且不会持久化到cookie文件中,只会临时缓存在内存中,当会话结束,内存中缓存的cookie信息就会被清楚,为0时,表示删除磁盘或内存中的cookie信息。

path:服务器对外提供多个地址,服务器可以针对不同地址设置不同的Cookie,这里的path配置就是标识cookie作用地址,默认/

secure:让cookie只在HTTPS交互中使用

signed:???

sameSite:???

Node.js express模块(二)_第13张图片

 可以发现cookie的maxAge配置其实和expires配置存在交叉,expires是设置失效日期,maxAge是设置有效时间(毫秒数),一般来说设置毫秒数更简单一点,所以我们通常使用maxAge来设置有效时间,另外maxAge设置为0时,表示删除该cookie,为负数时,表示会话级cookie。

另外的,express默认对cookie数据进行了URL编码,默认将cookie绑定给了/地址。

Node.js express模块(二)_第14张图片

不设置httpOnly,则可以使用JavaScript取出cookie中的信息

Node.js express模块(二)_第15张图片

设置httpOnly,表示cookie只用于浏览器和服务器之间交互,不能被浏览器通过其他途径暴露出去,保证了cookie的安全。

Node.js express模块(二)_第16张图片

 send传入JS对象或数组,首先会自动转为字符串发送给客户端,并设置响应头为application/json

Node.js express模块(二)_第17张图片

send传入字符串,则自动设置响应头Content-Type为text/html

Node.js express模块(二)_第18张图片

send传入Buffer类型数据,则自动设置响应头Content-Type为application/octet-stream

express中间件概念及注册中间件的方法

中间件middleWare是express的一个重要概念,它类似于AOP编程中的切面,或者axios,vue中的拦截器。

express中间件本质是一个函数,只是这个函数的参数有限制,必须依次是(req,res,next),其中req,res参数就是服务器实例监听request事件的req对象和res对象,next是一个放行函数,放行函数的作用是将代码执行权交到下一个中间件。

express中间件必须注册到服务器实例上。为什么呢?思考一下,我们期望中间件做什么事?做类似于切面和拦截器的工作,那么中间件拦截什么呢?我们很容易就能想到,拦截http请求,然后进行切面编程。而http请求首先必然会触发服务器实例的request事件,而express服务器实例处理http请求req对象,不需要通过监听request事件,而是可以通过app.get,app.post这样的服务器实例的请求方法来捕获。

即:我们可以将使用app.get,app.post来注册中间件

Node.js express模块(二)_第19张图片

而app.get,app.post注册的中间件其实就是它们的回调函数,我们已经知道express中间件是一个函数,且函数参数必须依次是(req,res,next),上面代码可以验证app.get的回调函数是一个中间件,因为两个app.get同时监听/请求,按照定义顺序,http请求首先匹配到第一个app.get,于是执行其注册的中间件函数,发现直接next放行到下一个中间件,所以第一个app.get的res.status.json没有执行发送http响应,反而第二个app.get发送了http响应。而每次http请求都只能有一次http响应。

我们需要再验证下next放行执行权到下一个中间件后,如果本中间件代码还没执行完,那么其他中间件代码执行完还会回头吗?

Node.js express模块(二)_第20张图片

其实从前面代码的执行日志就可以看出,第二个app.get执行完后由于没有next,所以回头执行了第一个app.get next后续代码,此时执行res.status.json,服务器发现Http响应已发出,所以不能针对一个http请求,发送两次http响应。

或者通过下面例子验证

const express = require('express')
const formidable = require('express-formidable')
const app = express()

app.use(formidable())

app.get('/', (req, res, next)=>{
	console.log(1)
    next()
	console.log(3)
})

app.get('/', (req, res, next)=>{
	console.log(2)
})

app.listen(80, '127.0.0.1', () => {
    console.log('服务器启动成功')
})

Node.js express模块(二)_第21张图片

 发现执行顺序是1,2,3,显然next放行到下一中间件后,等待其执行完成,没有后续next,则返回前一个中间件继续执行next后续代码。

以上是对app.get这种app.METHOD注册中间件的认识,以及中间件的next方法的作用以及运行流程的分析。

但是我们发现一个问题app.METHOD这种方式注册中间件,由于限制了http请求方法,请求路径,所以只能算一种指定位置的拦截器,而不能算是切面。真正的切面是在任意位置都可以拦截。

express提供了另一种高灵活性的注册中间件的方式:app.use

Node.js express模块(二)_第22张图片

app.use可以不指定http请求方法,只指定请求路径,即拦截某个请求路径的请求而不论请求方法

app.use可以不指定http请求方法,也不指定请求路径,即拦截所有请求

我们常用app.use来做切面。

express中间件可以做什么

前面我们分析出,express中间件是用来进行拦截和做切面的,其中拦截只能说是手段,切面才是目的。

express中间件本质是一个函数,并且参数依次是(req,res,next),其中req对应http请求对象,res对应http响应对象,next对应放行函数,从中间件函数的三个形参我们就知道它是来做什么的:

1、处理req对象(比如挂载属性或方法),方便后续中间件使用,所有中间件req参数都是同一个

2、通过res对象,设置http响应,结束本次http请求过程

3、放行执行权到下一个中间件

express中间件分类

按照中间件功能分的话,有三类:

1、应用级别中间件(中间件注册在app上,即服务器实例上)

2、路由级别中间件(中间件注册在路由器上,即express.Router()对象上)

3、错误处理中间件(专门用于捕获错误的中间件)

按照来源不同,可以分为两类:

1、express内置中间件(express.json(), express.urlencoded())

2、第三方模块中间件(express-formidable())

应用级别中间件

所谓应用级别中间件,指的就是将中间件函数注册在app实例上,注册方式有app.METHOD,以及app.use。我们需要注意的是应用级别中间件的注册情况:

情况1:不限定请求方法,不限定请求路径

app.use((req,res,next)=>{})

情况2:限定请求路径,不限定请求方法

app.use('/', (req,res,next)=>{})

情况3:限定请求方法,限定请求路径

app.METHOD('/', (req,res,next)=>{})

METHOD可以是任意的HTTP请求方法,如get,post,delete,patch等

以上是关于http请求接入的处理,即拦截的工作。

下面需要说一下切面的工作,即中间件函数的情况,前面注册中间件都是注册一个,而实际上,可以注册多个中间件,形成一个内部堆栈

情况4:注册多个中间件函数

app.use('/', (req,res,next)=>{}, (req,res,next)=>{})

比如这里注册了两个中间件,此时我们需要注意的是,如果第一个中间件执行了next函数,进行放行,那么是放行到内部堆栈的下一个中间件,还是全局的下一个中间件呢?

Node.js express模块(二)_第23张图片

从执行结果可以看出,内部堆栈中的中间件next放行,如果内部堆栈中还有下一个中间件函数,则交给内部堆栈的下一个中间件函数,否则就交给全局的下一个中间件。

那么如果我们想提前终止内部堆栈后续中间件的执行,而直接跳到全局后续中间件执行, 该如何做呢?

express提供了一个固定的写法,就是在堆栈中间件中通过 next('route') 这个固定写法来跳出内部堆栈,直接执行全局后续中间件。

注意只在由app.METHOD()/route.METHOD()中定义的middleware函数中生效。

Node.js express模块(二)_第24张图片

注意上面代码,如果next('route')是在app.use注册的中间件函数中使用,则无法跳过内部堆栈

Node.js express模块(二)_第25张图片

路由级别中间件

我们当前可以使用app.use或app.METHOD进行路由分配,此时会有两个问题:

1、路由直接注册到了服务器实例上,会导致服务器实例重心偏向于处理路由分配,而不是全局切面

2、路由直接注册到服务器实例上,我们无法进行高效地进行路由分层,公共路径归并

所以express提供了路由级别的中间件,该中间件是express的一个内置中间件,直接获取 express.Router() 返回的路由器即可,路由器相当于一个迷你的服务器,也可以通过HTTP请求方法来设置路由分配。 

Node.js express模块(二)_第26张图片

Node.js express模块(二)_第27张图片

 Node.js express模块(二)_第28张图片

通过上面代码,我可以看出,路由器可以作为中间件注册到app实例上 ,并且可以将公共路径直接作为app.use的接入路径,特殊路径作为router.METHOD的接入路径,形成了公共路径归并,并可以做一些公共预处理逻辑。

另外路由器实例还可以继续细分其他子路由器:
Node.js express模块(二)_第29张图片

Node.js express模块(二)_第30张图片

 Node.js express模块(二)_第31张图片

 Node.js express模块(二)_第32张图片

 这种子路由器策略,有利于业务的高度解耦

错误处理中间件

在中间件执行权的传递过程中,难免会发送中间件函数执行报错的情况,那么此时我们需要如何处理呢?

Node.js express模块(二)_第33张图片

最常见的处理思路就是try...catch捕获错误,并处理

Node.js express模块(二)_第34张图片 通常对错误的处理就是 记录错误日志,并给客户端反馈一个友好的提示 

其实,理论上,服务器对于所有的中间件产生的错误都要使用同一套错误处理逻辑,所以我们需要抽取一个错误处理公共方法,但是express提倡使用中间件来代替传统的公共函数,所以express提出了错误处理中间件,使用起来也很简单,即在所有中间件的定义的收尾处定义错误处理中间件,其他中间件发送错误时,通过next(错误信息),将错误传递给错误处理中间件。

错误处理中间件也是一个函数,但是不同于应用级别中间件,路由级别中间件的参数要求,错误处理中间件函数有四个参数,并且符合Node.js的错误优先原则,第一个参数是err,用于接收其他中间件next(错误信息),其余参数依次是req、res、next。

const express = require('express')
const formidable = require('express-formidable')
const app = express()

app.use(formidable())

app.use((req,res,next)=>{
	try {
		throw new Error('出错了')
	} catch(e) {
		next(e)
	}
})

app.use((err, req, res, next) => {
	let timestamp = Date.now() // 提供了一个错误id,帮助开发为客户解决问题
	console.log('错误处理中间件',timestamp, err) // 记录错误日志
	res.status(500).send({ // 给客户端响应友好日志
		msg:'服务器正在维护,请稍后再试:' + timestamp
	})
})

app.listen(80, '127.0.0.1', () => {
    console.log('服务器启动成功')
})

Node.js express模块(二)_第35张图片

此时我可以知道next是触发错误处理中间件执行的关键,但是next又涉及到将执行权交给内部堆栈中间件,以及业务处理中间件,我们有必要梳理一下next的用法。

next目前有三种传参情况:不传参,传"route",传非空非"route”的任意数据

上面三种传参情况分别对应着不同的跳转情况

next() :如果存在下一个内部堆栈中间件函数,则跳转到下一个内部堆栈中间件执行,否则跳转到下一个全局中间件执行

next('route') : 如果是app.METHOD或router.METHOD中间件函数中使用该跳转,则直接跳转到下一个全局中间件执行,否则和next()功能相同

next(非空非route):直接跳转到错误处理中间件

express内置中间件

express提供了以下常用的内置中间件

express.json() 解析Content-Type为application/json的POST请求体,并将解析后得到的JS对象,挂载到req.body上
express.urlencoded() 解析Content-Type为application/x-www-form-urlencode的POST请求体,并将解析后得到的JS对象,挂载到req.body上
express.text() 解析Content-Type为text/plain的POST请求体,并将解析后得到的JS对象,挂载到req.body上
express.raw() 解析Content-Type为application/octet-stream的POST请求体,并将解析后得到的JS对象,挂载到req.body上
express.static() 托管静态资源(如html,css,js文件)

这里我们需要想一想为啥内置中间件需要设计成方法调用的形式,我们知道express中间件就是一个函数,那么这里为啥不直接设计成

express.json = (req,res,next) => { //.... }

因为,这样设计,中间件的灵活性就降低了,比如express.json是解析application/json的请求体,那么如果请求体的编码格式可能多种多样,可能是uft8,也可能是gbk,那么此时express.json需要如何适配呢?难道在内部逻辑中写一个很复杂的编码格式猜测?其实如果外部传入指定的编码后,这个需求就很好解决。但是如果设计成上面固定形参的方式,我们就很难接收外部数据。

所以搞一个柯里化函数是最好的选择

express.json = (options) => {

        return (req,res,next) => {

                // 获取options外部数据来进行逻辑处理

        }

}

而其他的内置中间件都支持传入一个options对象来指定一些配置信息

我们来试着使用以下express.static()中间件,该中间件用于托管静态资源,什么叫托管静态资源呢?

其实就是 浏览器 想访问 服务器上的一些静态资源(如html,css,js,图片),此时服务器所要做的工作就是将这些静态资源文件响应给浏览器,即服务器的工作不涉及任何业务数据处理,只需要转发资源文件即可,类似于一个托管。所以我们需要给中间件指定被托管资源文件所在路径,以及一些其他配置信息,即参数设计应该是:express.static(path, options) 

Node.js express模块(二)_第36张图片

其他的详细options配置可以看Express 4.x - API Reference (expressjs.com) 

你可能感兴趣的:(nodejs,node.js,后端,express)