Express

官网传送门(opens new window)

基于 Node.js 平台,快速、开放、极简的 Web 开发框架

Express 是用于快速创建服务器的第三方模块。

Express 初体验

基本使用

创建服务器,监听客户端请求,并返回内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const express = require('express')
// 创建 web 服务器
const app = express()

// 监听客户端的 GET 和 POST 请求,并向客户端响应具体的内容
app.get('/user', (req, res) => {
res.send({ name: 'zs', age: 20, gender: '男' })
})
app.post('/user', (req, res) => {
res.send('请求成功')
})

app.get('/', (req, res) => {
// 通过 req.query 可以获取到客户端发送过来的查询参数
console.log(req.query)
res.send(req.query)
})

// 这里的 :id 是一个动态的参数
app.get('/user/:id/:username', (req, res) => {
// req.params 是动态匹配到的 URL 参数,默认是一个空对象
console.log(req.params)
res.send(req.params)
})

app.listen(80, () => {
console.log('express server running at http://127.0.0.1')
})

托管静态资源

  • 通过 express.static() 方法可创建静态资源服务器,向外开放访问静态资源。
1
2
3
4
5
6
7
8
9
10
11
app.use(express.static('public'))
app.use(express.static('files'))
app.use('/public', express.static('public'))

/*
可直接访问 public, files 目录下的静态资源
http://localhost:3000/images/bg.jpg

通过带有 /public 前缀的地址访问 public 目录下的文件
http://localhost:8080/public/images/logo.png
*/

Express 路由

创建路由模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// router.js

const express = require('express')
// 创建路由对象
const router = express.Router()

// 挂载具体路由
router.get('/user/list', (req, res) => {
res.send('Get user list.')
})
router.post('/user/add', (req, res) => {
res.send('Add new user.')
})

// 向外导出路由对象
module.exports = router

注册路由模块:

1
2
3
4
5
6
7
8
9
10
const express = require('express')
const router = require('./router')
const app = express()

// 注册路由模块,添加访问前缀
app.use('/api', router)

app.listen(80, () => {
console.log('http://127.0.0.1')
})

Express 中间件

  • 中间件是指流程的中间处理环节
  • 服务器收到请求后,可先调用中间件进行预处理
  • 中间件是一个函数,包含 req, res, next 三个参数,next() 参数把流转关系交给下一个中间件或路由

注意:

  • 在注册路由之前注册中间件(错误级别中间件除外)
  • 中间件可连续调用多个
  • 别忘记调用 next() 函数
  • next() 函数后别写代码
  • 多个中间件共享 reqres对象

全局中间件

  • 通过 app.use() 定义的中间件为全局中间件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const express = require('express')
const app = express()

// 定义第一个全局中间件
app.use((req, res, next) => {
console.log('调用了第1个全局中间件')
next()
})
// 定义第二个全局中间件
app.use((req, res, next) => {
console.log('调用了第2个全局中间件')
next()
})

app.get('/user', (req, res) => {
res.send('User page.')
})

app.listen(80, () => {
console.log('http://127.0.0.1')
})

局部中间件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const express = require('express')
const app = express()

// 定义中间件函数
const mw1 = (req, res, next) => {
console.log('调用了第一个局部生效的中间件')
next()
}

const mw2 = (req, res, next) => {
console.log('调用了第二个局部生效的中间件')
next()
}

// 两种定义局部中间件的方式
app.get('/hello', mw2, mw1, (req, res) => res.send('hello page.'))
app.get('/about', [mw1, mw2], (req, res) => res.send('about page.'))

app.get('/user', (req, res) => res.send('User page.'))

app.listen(80, function () {
console.log('Express server running at http://127.0.0.1')
})

中间件分类

  1. 应用级别的中间件

    • 通过 app.use()app.get()app.post() ,绑定到 app 实例上的中间件
  2. 路由级别的中间件

    • 绑定到 express.Router() 实例上的中间件,叫做路由级别的中间件。用法和应用级别中间件没有区别。应用级别中间件是绑定到 app 实例上,路由级别中间件绑定到 router 实例上。
1
2
3
4
5
6
7
8
9
const app = express()
const router = express.Router()
// 绑定到路由 router 实例上
router.use(function (req, res, next) {
console.log(1)
next()
})

app.use('/', router)
  1. 错误级别的中间件
    • 用来捕获整个项目中发生的异常错误,从而防止项目异常崩溃的问题
    • 错误级别中间件的处理函数中,必须有 4 个形参,形参顺序从前到后分别是 (err, req, res, next)
    • 错误级别的中间件必须注册在所有路由之后
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const express = require('express')
const app = express()

app.get('/', (req, res) => {
throw new Error('服务器内部发生了错误!')
res.send('Home page.')
})

// 定义错误级别的中间件,捕获整个项目的异常错误,从而防止程序的崩溃
app.use((err, req, res, next) => {
console.log('发生了错误!' + err.message)
res.send('Error:' + err.message)
})

app.listen(80, function () {
console.log('Express server running at http://127.0.0.1')
})
  1. Express 内置中间件

自 Express 4.16.0 版本开始,Express 内置了 3 个常用的中间件,极大的提高了 Express 项目的开发效率和体验:

  • express.static 快速托管静态资源的内置中间件,例如: HTML 文件、图片、CSS 样式等(无兼容性)

  • express.json 解析 JSON 格式的请求体数据(有兼容性,仅在 4.16.0+ 版本中可用)

  • express.urlencoded 解析 URL-encoded 格式的请求体数据(有兼容性,仅在 4.16.0+ 版本中可用)

1
2
3
app.use(express.static(__dirname, './public'))
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
  1. 第三方中间件(cors等)

CORS 跨域资源共享

安装配置

1
2
3
npm install cors
const cord = require('cors')
app.use(cors()) // app.js 使用中间件
  • CORS(Cross-Origin Resource Sharing,跨域资源共享)解决跨域,浏览器是通过 HTTP 响应头决定是否阻止客户端跨域获取资源
  • 浏览器的同源安全策略默认会阻止网页 “跨域” 获取资源。但如果接口服务器配置了 CORS 相关的 HTTP 响应头,就可解除浏览器端的跨域访问限制
  • CORS 主要在服务器端进行配置。客户端浏览器无须做任何额外的配置,即可请求开启了 CORS 的接口。
  • CORS 在浏览器中有兼容性。只有支持 XMLHttpRequest Level2 的浏览器,才能正常访问开启了 CORS 的服务端接口(例如:IE10+、Chrome4+、FireFox3.5+)。

CORS 常见响应头

Access-Control-Allow-Origin:制定了允许访问资源的外域源

1
2
res.setHeader('Access-Control-Allow-Origin', 'http://example.com')
res.setHeader('Access-Control-Allow-Origin', '*')

Access-Control-Allow-Headers:表示此次请求中允许哪些 header 字段

  • 默认情况下,CORS 仅支持客户端向服务器发送如下的 9 个请求头:
    • AcceptAccept-LanguageContent-LanguageDPRDownlinkSave-DataViewport-WidthWidthContent-Type (Content-Type 的仅限于 text/plainmultipart/form-dataapplication/x-www-form-urlencoded 三者之一)
  • 如果客户端向服务器发送了额外的请求头信息,则需要在服务器端,通过 Access-Control-Allow-Headers 对额外的请求头进行声明,否则这次请求会失败!
1
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Custom-Header')

Access-Control-Allow-Methods:表示此次请求中允许使用的请求方法

  • 默认情况下,CORS 仅支持客户端发起 GET、POST、HEAD 请求。如果客户端希望通过 PUT、DELETE 等方式请求服务器的资源,则需要在服务器端,通过 Access-Control-Alow-Methods 来指明实际请求所允许使用的 HTTP 方法
1
2
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, DELETE, HEAD')
res.setHEader('Access-Control-Allow-Methods', '*')

CORS 请求分类

简单请求

  • 请求方式:GET、POST、HEAD 三者之一
  • HTTP 头部信息不超过以下几种字段:无自定义头部字段、Accept、Accept-Language、Content-Language、DPR、Downlink、Save-Data、Viewport-Width、Width 、Content-Type(只有三个值 application/x-www-formurlencoded、multipart/form-data、text/plain)

预检请求

  • 请求方式为 GET、POST、HEAD 之外的请求 Method 类型
  • 请求头中包含自定义头部字段
  • 向服务器发送了 application/json 格式的数据

在浏览器与服务器正式通信之前,浏览器会先发送 OPTION 请求进行预检,以获知服务器是否允许该实际请求,所以这一次的 OPTION 请求称为“预检请求”。服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据

Express 中配置 Mysql

创建数据库连接池 db/index.js

1
2
3
4
5
6
7
8
9
10
11
12
const mysql = require('mysql')

// 创建数据库连接对象
const db = mysql.createPool({
host: '127.0.0.1',
user: 'root',
password: '123456',
database: 'forum',
})

// 向外共享 db 数据库连接对象
module.exports = db

操作 mysql 数据库 router_handler/user.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
const db = require('../db/index')

const selectSql = 'select * from users'
db.query(selectSql, (err, results) => {
if (err) return console.log(err.message)
console.log(results)
})

const user = { username: 'de', password: '123456'}
const insertSql = 'insert into users (username, password) values(?, ?)'
db.query(insertSql, [user.username, user.password], (err, result)=>{
if (err) return console.log(err.message)
if(result.affectedRows === 1) {
console.log('插入成功')
}
})

// 快速插入数据
const user = { username: 'de', password: '123456'}
const insertSql2 = 'insert into users set ?'
db.query(insertSql2, user, (err, res) => {
if(err) return console.log(err.message)
if(res.affectedRows === 1) {
console.log("Success")
}
})

// 更新数据
const user = {id: 12, username: 'de', password: '123de'}
const updateSql = 'update users set username= ?, password= ? where id= ?'
db.query(updateSql, [user.username, user.password, user.id], (err, res) => {
if(err) return console.log(err.message)
if(res.affectedRows === 1) {
console.log("更新成功")
}
})

// 更新数据的快捷方式
const user = {id: 12, username: 'dewen', password: '123dewen'}
const updateSql = 'update users set ? where id= ?'
db.query(updateSql, [user, user.id], (err, res) => {
if(err) return console.log(err.message)
if(res.affectedRows === 1) {
console.log("更新成功")
}
})

// 删除
const deleteSql = 'delete from users where id= ?'
db.query(deleteSql, 12, (err, res)=> {
if(err) return console.log(err.message)
if(res.affectedRows === 1) {
console.log('删除成功')
}
})

//标记删除
const deleteStr = 'update users set status=? where id=?'
db.query(deleteStr, [1, 1], (err, res)=> {
if(err) return console.log(err.message)
if(res.affectedRows === 1) {
console.log("执行标记删除成功")
}
})

Express 身份验证

Session 认证机制

  • 服务端渲染推荐使用 Session 认证机制

session

配置express-session中间件

1
2
3
4
5
6
7
8
const session = require('express-session')
app.use(
session({
secret: '10wen.github.io', // secret 的值为任意字符串
resave: false,
saveUninitalized: true,
})
)

session存储数据:中间件配置成功后,可通过 req.session 访问 session 对象,存储用户信息

1
2
3
4
5
6
app.post('/api/login', (req, res) => {
req.session.user = req.body // 向session添加用户信息
req.session.isLogin = true

res.send({ status: 0, msg: 'login done' })
})

session取数据

1
2
3
4
5
6
app.get('/api/username', (req, res) => {
if (!req.session.isLogin) {
return res.send({ status: 1, msg: 'fail' })
}
res.send({ status: 0, msg: 'success', username: req.session.user.username }) // 取出用户信息返回
})

清空 session

1
2
3
4
5
app.post('/api/logout', (req, res) => {
// 清空当前客户端的session信息
req.session.destroy()
res.send({ status: 0, msg: 'logout done' })
})

JWT 认证机制

  • 前后端分离推荐使用 JWT(JSON Web Token)认证机制,是目前最流行的跨域认证解决方案

  • 用户的信息通过 Token 字符串的形式,保存在客户端浏览器中。服务器通过还原 Token 字符串的形式来认证用户的身份

JWT

1. 安装:jsonwebtokenexpress-jwt

  • jsonwebtoken 用于生成 JWT 字符串
  • express-jwt 用于将 JWT 字符串解析还原成 JSON 对象
1
npm install jsonwebtoken express-jwt

2. 定义密钥:secret

  • 用户生成和解密 Token
1
2
3
4
5
const jwt = require('jsonwebtoken')
const expressJWT = require("express-jwt")

// TODO_02:定义 secret 密钥,建议将密钥命名为 secretKey
const secretKey = '10wen.github.io'

3. 生成 Token

1
2
3
4
5
6
7
8
9
10
11
12
app.post('/api/login', function (req, res) {
const userInfo = req.body
// TODO_03:在登录成功之后,调用 jwt.sign() 方法生成 JWT 字符串。并通过 token 属性发送给客户端
// (用户信息对象, 加密密钥, 配置对象,可以配置 token 的有效期)
// 不要把 密码 加密到 token 字符串中
const tokenStr = jwt.sign({username: userInfo.username}, secretKey, {expiresIn: '30s'})
res.send({
status: 200,
message: '登录成功!',
token: tokenStr // 要发送给客户端的 token 字符串
})
})

4. JWT 字符串还原为 JSON 对象 (app.js)

  • 客户端访问有权限的接口时,需通过请求头的 Authorization 字段,将 Token 字符串发送到服务器进行身份认证
  • 服务器可以通过 express-jwt 中间件将客户端发送过来的 Token 解析还原成 JSON 对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// unless({ path: [/^\/api\//] }) 指定哪些接口无需访问权限
// 在路由之前配置解析 Token 的中间件
const expressJWT = require('express-jwt')
const config = require('./config')
app.use(
expressJWT({
secret: config.jwtSecretKey,
algorithms: ["HS256"],
// 无token请求不进行解析,并且抛出异常,给错误中间件处理
credentialsRequired: false,
}).unless({
path: [
"/login",
"/register",
"/index",
{ url: /^\/articles\/.*/, methods: ["GET"] },
{ url: /^\/content\/.*/, methods: ["GET"] },
],
})
);

// app.use(expressJWT({ secret: secretKey }).unless({ path: [/^\/api\//] }))

5. 获取用户信息

  • 当 express-jwt 中间件配置成功后,中间件就可以把解析出来的用户信息,挂载到 req.user 属性上,使用 req.user 对象,来访问从 JWT 字符串中解析出来的用户信息
1
2
3
4
5
6
7
8
9
app.get('/admin/getinfo', function (req, res) {
// TODO_05:使用 req.user 获取用户信息,并使用 data 属性将用户信息发送给客户端
console.log(req.user)
res.send({
status: 200,
message: '获取用户信息成功!',
data: req.user // 要发送给客户端的用户信息
})
})

6. 捕获解析 JWT 失败后产生的错误

  • 当使用 express-jwt 解析 Token 字符串时,如果客户端发送过来的 Token 字符串过期或不合法,会产生一个解析失败的错误,影响项目的正常运行
  • 通过 Express 的错误中间件,捕获这个错误并进行相关的处理
1
2
3
4
5
6
app.use((err, req, res, next) => {
if (err.name === 'UnauthorizedError') {
return res.send({ status: 401, message: 'Invalid token' })
}
res.send({ status: 500, message: 'Unknown error' })
})