Node.js express模块(一)_qfc_128220的博客-CSDN博客
通过前面对比http模块,express模块在开发一个简单服务器的体验,可以发现express模块完全碾压了http模块,接下来我们详细学习下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('服务器启动成功')
})
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('服务器启动成功')
})
而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('服务器启动成功')
})
另外,express-formidable也能解析application/json,application/x-www-form-urlencoded格式的请求体,只是此时需要通过req.fields来获取请求体数据而已
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 |
Cookie设置,下面是Cookie的一些配置信息
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:???
可以发现cookie的maxAge配置其实和expires配置存在交叉,expires是设置失效日期,maxAge是设置有效时间(毫秒数),一般来说设置毫秒数更简单一点,所以我们通常使用maxAge来设置有效时间,另外maxAge设置为0时,表示删除该cookie,为负数时,表示会话级cookie。
另外的,express默认对cookie数据进行了URL编码,默认将cookie绑定给了/地址。
不设置httpOnly,则可以使用JavaScript取出cookie中的信息
设置httpOnly,表示cookie只用于浏览器和服务器之间交互,不能被浏览器通过其他途径暴露出去,保证了cookie的安全。
send传入JS对象或数组,首先会自动转为字符串发送给客户端,并设置响应头为application/json
send传入字符串,则自动设置响应头Content-Type为text/html
send传入Buffer类型数据,则自动设置响应头Content-Type为application/octet-stream
中间件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来注册中间件
而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放行执行权到下一个中间件后,如果本中间件代码还没执行完,那么其他中间件代码执行完还会回头吗?
其实从前面代码的执行日志就可以看出,第二个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('服务器启动成功')
})
发现执行顺序是1,2,3,显然next放行到下一中间件后,等待其执行完成,没有后续next,则返回前一个中间件继续执行next后续代码。
以上是对app.get这种app.METHOD注册中间件的认识,以及中间件的next方法的作用以及运行流程的分析。
但是我们发现一个问题app.METHOD这种方式注册中间件,由于限制了http请求方法,请求路径,所以只能算一种指定位置的拦截器,而不能算是切面。真正的切面是在任意位置都可以拦截。
express提供了另一种高灵活性的注册中间件的方式:app.use
app.use可以不指定http请求方法,只指定请求路径,即拦截某个请求路径的请求而不论请求方法
app.use可以不指定http请求方法,也不指定请求路径,即拦截所有请求
我们常用app.use来做切面。
前面我们分析出,express中间件是用来进行拦截和做切面的,其中拦截只能说是手段,切面才是目的。
express中间件本质是一个函数,并且参数依次是(req,res,next),其中req对应http请求对象,res对应http响应对象,next对应放行函数,从中间件函数的三个形参我们就知道它是来做什么的:
1、处理req对象(比如挂载属性或方法),方便后续中间件使用,所有中间件req参数都是同一个
2、通过res对象,设置http响应,结束本次http请求过程
3、放行执行权到下一个中间件
按照中间件功能分的话,有三类:
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函数,进行放行,那么是放行到内部堆栈的下一个中间件,还是全局的下一个中间件呢?
从执行结果可以看出,内部堆栈中的中间件next放行,如果内部堆栈中还有下一个中间件函数,则交给内部堆栈的下一个中间件函数,否则就交给全局的下一个中间件。
那么如果我们想提前终止内部堆栈后续中间件的执行,而直接跳到全局后续中间件执行, 该如何做呢?
express提供了一个固定的写法,就是在堆栈中间件中通过 next('route') 这个固定写法来跳出内部堆栈,直接执行全局后续中间件。
注意只在由app.METHOD()/route.METHOD()中定义的middleware函数中生效。
注意上面代码,如果next('route')是在app.use注册的中间件函数中使用,则无法跳过内部堆栈
我们当前可以使用app.use或app.METHOD进行路由分配,此时会有两个问题:
1、路由直接注册到了服务器实例上,会导致服务器实例重心偏向于处理路由分配,而不是全局切面
2、路由直接注册到服务器实例上,我们无法进行高效地进行路由分层,公共路径归并
所以express提供了路由级别的中间件,该中间件是express的一个内置中间件,直接获取 express.Router() 返回的路由器即可,路由器相当于一个迷你的服务器,也可以通过HTTP请求方法来设置路由分配。
通过上面代码,我可以看出,路由器可以作为中间件注册到app实例上 ,并且可以将公共路径直接作为app.use的接入路径,特殊路径作为router.METHOD的接入路径,形成了公共路径归并,并可以做一些公共预处理逻辑。
这种子路由器策略,有利于业务的高度解耦
在中间件执行权的传递过程中,难免会发送中间件函数执行报错的情况,那么此时我们需要如何处理呢?
最常见的处理思路就是try...catch捕获错误,并处理
通常对错误的处理就是 记录错误日志,并给客户端反馈一个友好的提示
其实,理论上,服务器对于所有的中间件产生的错误都要使用同一套错误处理逻辑,所以我们需要抽取一个错误处理公共方法,但是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('服务器启动成功')
})
此时我可以知道next是触发错误处理中间件执行的关键,但是next又涉及到将执行权交给内部堆栈中间件,以及业务处理中间件,我们有必要梳理一下next的用法。
next目前有三种传参情况:不传参,传"route",传非空非"route”的任意数据
上面三种传参情况分别对应着不同的跳转情况
next() :如果存在下一个内部堆栈中间件函数,则跳转到下一个内部堆栈中间件执行,否则跳转到下一个全局中间件执行
next('route') : 如果是app.METHOD或router.METHOD中间件函数中使用该跳转,则直接跳转到下一个全局中间件执行,否则和next()功能相同
next(非空非route):直接跳转到错误处理中间件
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)
其他的详细options配置可以看Express 4.x - API Reference (expressjs.com)