初识 express

前言

虽说在下是 springboot 新手,但是作为前端接口调试而言,springboot 实在太过重量级。现在发现了一个作为测试服务器相当好用的玩意: NodeJs。话不多说,开搞。

  1. 安装 nodejs
  2. 在一个新文件夹初始化 npm npm init --yes
  3. 安装 express npm i express

设置 url 映射,并开启服务器监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// helloExpress.js
// 1、引入 express
var express = require('express')

// 2、创建应用对象
var app = express()

// 3、创建路由规则
// request 是对请求报文的封装
// response 是对响应报文的封装
app.get('/', (request, response) => {
response.send('Hello Express')
})

// 4、监听端口启动服务
app.listen(8000, () => {
console.log('服务已经启动,8000 端口监听中 ...')
})

启动 express 服务器 node helloExpress.js

打开浏览器,地址栏输入 localhost:8000 显示页面 nice!

express 可以轻松地帮我们开启一个服务端。

但是使用发现,当服务端代码发生改变,我们需要重新启动服务,有点麻烦,接下来介绍一个热重载工具。

使用 nodemon 帮助我们热更新服务端代码

  1. 安装命令 npm i -g nodemon
  2. 换成 nodemon helloExpress.js 来启动服务。

处理跨域

既然开启了服务器,那就一定要提一下 CORS 跨域。

跨域的意思是违反浏览器的同源策略。浏览器为了用户安全,在前端和服务器端使用不同的协议、端口、主机名进行通信时,会将 responseblock 掉。

CORS 是一个官方的标准,通过在服务器中设置响应头提供跨域服务。

1
2
3
4
5
6
7
8
//这次使用的是 `app.all` 他表示 get/post/put/delete/patch 方法都可以进入到该 controller 中。
app.all('cross_origin_server', (request, response) => {
response.setHeader('Access-Control-Allow-Origin', '*')
let data = {
greeting: 'hello! I have crossed origin',
}
response.send(data)
})

上面的 url controller 中通过设置响应头 Access-Control-Allow-Origin = * 实现跨域,* 的含义是对所有浏览器请求提供服务。

但是默认情况下,该设置只会开放简单请求的跨域,即满足:

  • 请求方法为 GET、POST、HEAD 中的三种
  • 请求头不能超出预置的 9 种请求头

如果想让 PUT、PATCH、DELETE 等请求方法也能跨域,或者在自定义请求头时跨域,需要设置如下两个 CORS 响应头

1
2
Access-Control-Allow-Headers: *
Access-Control-Allow-Methods: *

它们分别设置了允许发送自定义请求头,允许使用所有方法跨域(默认只允许 get 和 post 跨域)。

最后告诉大家一个好消息,上面所说的全都不用管,有个 express 中间件帮我们解决了以上问题。

npm i cors

1
2
3
4
5
const cors = require('cors')

//...some initialize code

app.use(cors())

获取请求参数、查询字符串、请求体

服务器端要完成有效的响应,就需要解析和处理请求过来的数据。

常见的请求数据有三种:

  • param,在 url 地址上的请求参数
  • query,在 ? 后面的查询字符串,也叫 queryString
  • body,请求体
  1. 获取请求参数 param,我们通过在请求 url 上设置 可变路径 来捕获,然后使用 req.params 获取
1
2
3
app.get('/userInfo/:id', (req, res) => {
console.log(req.params.id)
})
  1. 获取查询字符串 query,使用 req.query 来获取
1
2
3
app.get('/userInfo', (req, res) => {
console.log(req.query)
})
  1. 获取请求体,这里需要用到内置的中间件 express.json() 还有 express.urlencoded()

    根据需要来配置,当然一起配置也是没有冲突的。
    接着通过 req.body 获取请求体

1
2
3
4
5
6
7
app.use(express.json())
// 或者
app.use(express.urlencoded())

app.post('/addUser', (req, res) => {
console.log(req.body)
})

介绍基础概念

通过上面的例子,我们可以开启一个 express 服务器来做响应了,但是还不够,我们了解一些基础概念才能更好地使用它。

请求对象

express 的 req 是对内置模块 http 的 IncomingMessage 进一步封装,下面是原生 http 的属性

request.method, request.url, request.headers, request.ip

express 扩展的属性和方法

  • req.query 获取查询字符串的对象形式
  • req.params 获取请求参数的对象形式
  • req.body 获取请求体(只有在使用请求体解析中间件时才有)

响应对象

express 的 res 也是对内置模块 http 的 ServerResponse 进一步封装,下面是原生 http 的属性

response.statusCode = 200 设置响应状态码
response.setHeader() 设置响应头
response.end() 添加最后一次数据并结束响应
response.write() 往缓冲区添加数据

express 扩展的属性和方法

  • res.send() 添加最后一次数据并结束响应(支持 buffer -> Buffer.from(),json 的自动 stringify,发送 html 文档,发送普通字符串)
  • res.json() 添加一次数据,不会结束响应(自动 stringify)
  • res.status() 设置 statusCode
  • res.cookie(key, value [, config]) 发送 cookie

配套的数据存储方案

express 作为服务器,它的作用是处理请求,发出响应。

在实际开发中,我们更多通过服务器来存储数据,下面介绍一下数据存储方案。

  1. 使用 .json 文件存储 javascript 对象数据
  2. 通过 nodejs 的 fs 模块对 json 文件进行读取和写入
  3. 使用 promisify 封装 fs 的函数(readFile 和 writeFile)

警告:此方案在高并发写入时会产生脏写,仅供学习参考,勿用于实际生产项目。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 通过 db.js 封装文件读写操作
const fs = require('fs')
const { promisify } = require('util')
const path = require('path')

const readFile = promisify(fs.readFile)
const writeFile = promisify(fs.whiteFile)

const dbPath = path.join(__dirname, './db.json')

//读取
exports.getDb = async () => {
const data = await readFile(dbPath)
return JSON.parse(data)
}

//写入
exports.saveDb = async data => {
//这里stringify的参数可以生成带缩进的json字符串
await writeFile(dbPath, JSON.stringify(data, null, ' '))
}

路由模块

到目前为止,我们都是在 app 实例上绑定请求处理函数,如果接口增加,server.js 文件会越来越大,浏览和修改都不方便,如何把请求处理函数模块化呢。

使用 express.Router() 创建路由模块,它就相当于 app 的小弟。

  1. 将接口按业务做代码分割,划分多个 .js 路由模块文件
  2. 每个模块文件中使用 express.Router() 创建路由实例 router。
  3. 将请求处理函数绑定到 router 实例上
  4. 通过 module.exports 导出路由实例 router
  5. 在 app.js 中将路由实例当作中间件来加载
1
2
3
4
5
6
7
8
9
// 这里为路由模块,比如叫 userRouter.js
const express = require('express')
const userRouter = express.Router()

userRouter.get('/getUserInfo', (req, res) => {
//查 user 数据并发送
})

module.exports = userRouter
1
2
3
4
5
6
7
8
9
10
11
12
// 这里是 app 实例创建的地方,比如叫 server.js
const express = require(express)

// 通过模块的方式导入路由
const userRouter = require('./userRouter')

const app = express()

// 将路由当作中间件来加载,我们的 app 就拥有了该路由的所有url映射。
app.use(userRouter)

//... 其他代码

这样,我们就可以把 API 接口拆分到不同的文件中管理,是不是很方便呢。

中间件

在前面的案例中,无论是解决请求体,还是解决跨域问题,我们都使用到了中间件。

中间件是什么呢?中间件是一个个处理函数,多个中间件函数和路由函数之间共享同一组 request 和 response 对象。

如同废水处理厂处理废水时,需要将废水经过多个处理环节进行处理一样。我们的服务器得到请求后,也是能够分多个阶段来处理数据,中间件既能挂载数据留给下游的阶段,也能集中处理数据,或者起到拦截器或路由守卫的作用,这都取决于你的函数如何定义。

中间件必须接收 3 个参数:

  request,response,next

比如下面这个中间件函数

1
2
3
4
const middleware = function (req, res, next) {
console.log('有人来了')
next()
}

我们有如下方式使用它

作为全局中间件,通过 app.use 来注册。这样所有的请求进来时,都会优先执行中间件函数,然后再做 url 映射匹配。

1
2
3
4
app.use(middleware)
app.get('/userInfo', (req, res) => {
//...获取用户信息并发送
})

作为局部中间件,将其作为路由函数的形参。只有匹配到该路由时,才会执行中间件函数,然后才到请求处理函数。

1
2
3
app.get('/userInfo', middleware, (req, res) => {
//...获取用户信息并发送
})
1
2
3
4
// 如果想使用多个中间件,可以这样用喔
app.get('/userInfo', mw1, mw2, (req, res) => {
//...获取用户信息并发送
})

中间件的使用注意事项:

  1. 中间件必须主动调用 next() 转交控制权。如果不转交控制权,就不会往下走。
  2. 中间件必须配置在所有路由函数的前面,才能起作用。这是有顺序要求的。

错误级别的中间件

在讲错误级别中间件之前,先看下官方定义的中间件分类:

  - 应用级别中间件:绑定在 app 上的中间件
  - 路由级别中间件:绑定在 router 上的中间件
  - 错误级别中间件:特殊中间件,要配置在所有路由函数之后。
  - 内置中间件:在 express 对象上的中间件
  - 第三方中间件:需要 npm install 的中间件

不管怎么分类,其实它们的用法都是一样的,但唯独错误级别中间件不同。

我们的路由处理函数,或者中间件函数,在执行发生错误时,会直接崩溃掉。为了防止崩溃,我们通常需要使用 try…catch… 语句来捕获错误。

但是这么多的路由处理函数,这么多的中间件,每一个都去 try…catch… 未免太苦工了。

为此,错误级别中间件油然而生,它专门用来捕获我们整个项目中发生的异常错误,从而防止项目异常崩溃。

它必须接收 4 个参数:

  err, request ,response, next

并且它需要放在所有中间件函数和路由函数的后面。

当我们的路由处理函数和中间件函数发生错误时,不需要 try…catch… , 会直接跳到错误级别中间件做集中处理。

1
2
3
4
app.use(function(err,req,res,next)=>{
console.log(err.message)
res.send('Error:' + err.message)
})

静态资源托管

express.static() 中间件用于资源托管

路径可以是相对路径

1
2
3
app.use(express.static('public'))
app.use(express.static('../15_内置指令'))
app.use(express.static('./clock'))

除了这种方式,也可以手动提供某个资源的 url

比如下面的代码就是用来测试 v-cloak 指令的。用来延迟引入 vue.js

vue.js 的文件路径要按你实际的来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const { resolve } = require('path')

// 其他代码...

// 注意:root 参数需要绝对路径
app.get('/delayResource/:duration/vuejs', (req, res) => {
console.log(req.params)
const duration = req.params.duration
setTimeout(() => {
res.sendFile(
'vue.js',
{
root: resolve(__dirname, '../js/'),
},
err => {
if (err) console.log(err)
else console.log('success')
}
)
}, duration)
})

当然,你也可以不用 sendFile 方法,自己用 fs 模块来发送文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const fs = require('fs')
const {resolve} = require('path')

// 其他代码...

app.get('/delayResource/:duration/vuejs', (req, res) => {
const duration = req.params.duration
const filePath = resolve(__dirname, '../js/vue.js')
fs.readFile(filePath, (err, data) => {
if (err) res.send('报错拉')
setTimeout(() => {
res.send(data)
}, duration)
})
})