2021-11-10

9-1 开始

使用 express

不关注底层内容,更关心业务内容

  • express 是 nodejs 最常用的 web server 框架
  • 什么是框架?
    • 封装基本的API,制定一些标准,让开发更加简单,更加关注到数据、业务
    • 有基本的流程和标准,有一个基本的解决方案
  • 不要以为 express 过时了
    • 周下载量还有千万

目录

  • express 下载、安装和使用,express 中间件机制
    • 中间件机制可以有更加优雅的方式实现之前的一些功能
  • 开发接口,连接数据库,实现登录,日志记录
  • 分析 express 中间件原理

9-2 开始

介绍 express

  • 安装(使用脚手架 express-generator)
  • 初始化代码介绍,处理路由
  • 使用中间件

安装 express

  • 首先全局安装
    • npm install express-generator -g
  • 然后创建一个 express 的项目
    • express express-test
  • 最后启动项目
    • npm install & npm start

实操

首先全局安装 express
然后在 nodejs-learning 中创建 express 项目
image.png
然后进入后启动
访问 localhost:3000 端口
成功!
image.png
然后安装 nodemon 和 cross-env

  1. npm i nodemon cross-env --save-dev

然后打开看 express 默认提供的 www.js 和我们之前在 blog-1 中创建的对比一下,发现我们之前创建的只是提供 http 服务。在 express 中提供的会相对复杂一点。

有 views 和 public 存在,是因为 express 是为了提供一个全栈的开发环境。
但是我们现在推崇前后端分离的开发方式,因此这里给前端提供的模板,css,js就不管了

9-3 介绍 express 的入口代码

介绍 app.js

  • 各个插件的作用
  • 思考各个插件的实现原理(结合之前学过的知识)
  • 处理 get 请求和 post 请求
  1. createError:处理错误路由情况
  2. cookieParser:解析 cookie
  3. logger(使用 morgan):生成日志
  4. var app = express():这里的 app 是初始化的
  5. express.json():替换原来的 getPostData,可以直接从 req.body 中拿到数据,当 content-type 是 JSON 格式时获取
  6. express.urlencoded():从 req.body 拿到表单格式的数据,当 content-type 是 urlencoded 格式时获取
  7. app.use(‘/‘, indexRouter) 和 app.use(‘/users’, usersRouter) 是用来注册路由,是要加一个根的路由
    1. -> 拼接 / 和 indexRouter 或者 usersRouter 里面的路由 来进行访问
  8. 404调试 和 500 调试

不要深究细节,但要理解 cookiePaser 和 json() 的原理

2021-11-11

9-4 演示 express 如何处理路由

express 处理 get 和 post 请求

get 请求

新建 blog.js 和 user.js
然后尝试在 blog.js 中使用 get 请求返回 mock 数据
blog.js 中使用 res.json 返回数据

  1. var express = require('express');
  2. var router = express.Router();
  3. router.get('/list', function(req, res, next) {
  4. res.json({
  5. errno: 0,
  6. data: [1, 2, 3]
  7. })
  8. });
  9. module.exports = router;

来到 app.js 中,引入 blog.js 中导出的 router 作为 blogRouter,然后 use 一下,这样就可以拼接为 /api/blog/list

  1. ...
  2. const blogRouter = require('./routes/blog')
  3. ...
  4. app.use('/api/blog', blogRouter)
  5. ...

这样父路由和子路由就会分离,更好的修改和使用

post 请求

在 user.js 中操作
大概修改一下,和 get 请求类似

  1. var express = require('express');
  2. var router = express.Router();
  3. router.post('/login', function(req, res, next) {
  4. // 因为使用了 express.json() 所以可以直接从 req.body 中解析 post 数组
  5. const { username, password } = req.body
  6. res.json({
  7. username,
  8. password
  9. })
  10. });
  11. module.exports = router;

然后来到 app.js 中引用,和 get 请求类似

  1. ...
  2. const userRouter = require('./routes/user')
  3. ...
  4. app.use('/api/user', userRouter)
  5. ...

在 postman 中测试一下:
成功!
image.png
如果使用 urlencoded 的也同样可以,因为之前已经做了兼容

image.png

2021-11-16

9-5 express中间件

中间件机制

  • 有很多 app.use
  • 代码中的 next 参数是什么?
  • 带着这些疑问,先看一段代码演示

代码演示

创建一个目录: express-test
然后 npm init -y 一下,将入口改为 app.js
然后安装一下 express
npm i express

然后写一系列测试的代码用来体会中间件的使用方法

  1. const express = require('express')
  2. // 本次 http 请求的实例
  3. const app = express()
  4. // 在最后都要执行 next
  5. app.use((req, res, next) => {
  6. console.log('请求开始...', req.method, req.url)
  7. next()
  8. })
  9. app.use((req, res, next) => {
  10. // 假设在处理 cookie
  11. req.cookie = {
  12. useId: 'abc123'
  13. }
  14. next()
  15. })
  16. app.use((req, res, next) => {
  17. // 设置处理 post data
  18. // 异步
  19. setTimeout(() => {
  20. req.body = {
  21. a: 100,
  22. b: 200
  23. }
  24. next()
  25. })
  26. })
  27. app.use('/api', (req, res, next) => {
  28. console.log('处理 /api 路由')
  29. next()
  30. })
  31. app.get('/api', (req, res, next) => {
  32. console.log('get /api 路由')
  33. next()
  34. })
  35. app.post('/api', (req, res, next) => {
  36. console.log('post /api 路由')
  37. next()
  38. })
  39. app.get('/api/get-cookie', (req, res, next) => {
  40. // 不执行 next
  41. console.log('get /api/get-cookie')
  42. res.json({
  43. errno: 0,
  44. data: req.cookie
  45. })
  46. })
  47. app.post('/api/get-post-data', (req, res, next) => {
  48. // 不执行 next
  49. console.log('get /api/get-post-data')
  50. res.json({
  51. errno: 0,
  52. data: req.body
  53. })
  54. })
  55. app.use((req, res, next) => {
  56. console.log('处理 404');
  57. res.json({
  58. errno: -1,
  59. msg: '404 not found'
  60. })
  61. })
  62. app.listen(3000, () => {
  63. console.log('server is running on port 3000')
  64. })

app.use 如果第一个参数没有写路由的话,会默认执行
执行到 next() 的时候,其实是执行后续的符合条件的(路由条件)方法,比如 app.use app.get

运行一下:
image.png
image.png

image.png
总结:会根据路由名和方法来决定是否执行,如果有 next 才继续向下执行
因此每一个 app.use 就是一个个的中间件,有三个参数的方法

多中间件模式

  1. // 模拟登录验证
  2. function loginCheck(req, res, next) {
  3. console.log('模拟登录成功')
  4. setTimeout(() => {
  5. next()
  6. })
  7. }
  8. app.get('/api/get-cookie', loginCheck, (req, res, next) => {
  9. // 不执行 next
  10. console.log('get /api/get-cookie')
  11. res.json({
  12. errno: 0,
  13. data: req.cookie
  14. })
  15. })

image.png

9-6 express 介绍的总结

通过上节课的学习再看之前拿脚手架搭的东西,就可以知道所有的 app.use 就是中间件
image.png
以上全是中间件

总结

  • 初始化代码中,各个插件的作用
  • express 如何处理路由
    • blog.js user.js
  • express中间件
    • next
    • 匹配规则

9-7 express 开发博客项目-初始化环境

express 开发接口

  • 初始化项目,之前的部分代码可以复用
    • controller 和 连接 mysql的代码都可以被复用
  • 开发路由,并实现登录
    • express 的路由
    • 登录使用框架很方便
  • 记录日志

初始化环境

  • 安装插件 mysql xss
  • mysql controller resModel 相关代码可以复用
  • 初始化路由

代码演示

进入 blog-express
首先把没用的代码注释一下
在 app.js 中
image.png
报 favicon 的错的问题:
解决:
https://www.itranslater.com/qa/details/2582472172027511808

  1. 打开Chrome /问题浏览器
  2. 直接导航到favicon.ico文件,例如 [http:// localhost:3000 / favicon.ico]
  3. 通过按F5或相应的浏览器刷新(重新加载)按钮来刷新favicon.ico URL。
  4. 关闭浏览器并打开您的网站-瞧,您的收藏夹图标已更新!

然后安装 mysql 和 xss 的插件
npm i mysql xss —save

然后将连接 mysql 和 nodejs 的文件复制过来(mysql.js)
image.png
还有 conf 中的 db.js(数据库的连接内容)
同理还有 controller 和 model
以及加密用的 crypto (utils/crypt.js)

然后修改 get list 的路由数据

  1. router.get('/list', function(req, res, next) {
  2. // express 的 req.query 是原生自带的,不用手动解析
  3. let author = req.query.author || ''
  4. const keyword = req.query.keyword || ''
  5. // // 修改登录权限
  6. // if (req.query.isadmin) {
  7. // // 管理员界面
  8. // const loginCheckResult = loginCheck(req)
  9. // if (loginCheckResult) {
  10. // // 未登录情况
  11. // return loginCheckResult
  12. // }
  13. // // 强制查询自己的博客
  14. // author = req.session.username
  15. // }
  16. const result = getList(author, keyword)
  17. return result.then(listData => {
  18. // 不用 return
  19. res.json(
  20. new SuccessModel(listData)
  21. )
  22. })
  23. });

成功!
image.png

2021-11-17

9-8 express 处理 session

登录

  • 使用 express-session 和 connect-redis 来登录,简单方便
  • req.session 保存登录信息,登录校验做成 express 中间件

首先安装 express-session(能轻松实现 session 功能)
npm i express-session

然后在 app.js 中引用并写中间件

  1. ...
  2. // 引入
  3. const session = require('express-session')
  4. ...
  5. app.use(cookieParser());
  6. // 解析完 cookie 就解析 session
  7. app.use(session({
  8. // 类似于一个密匙
  9. secret: 'Lb@#c_13323',
  10. // 配置 cookie
  11. cookie: {
  12. // path 和 httpOnly 都是默认值,可以不写
  13. path: '/',
  14. httpOnly: true,
  15. // 失效时间,不同于之前在 blog-1 中做的 expires
  16. // 24小时失效
  17. maxAge: 24 * 60 * 60 * 1000
  18. }
  19. }))
  20. ....

然后 npm run dev 测试一下

成功~
成功写入 cookie 一个 值,为 connect.sid
image.png

9-9 session 连接 redis

登录继续操作
第一步:拷贝之前的登录路由然后修改
有了 express-session 之后,不需要同步到 redis 中

测试登录

  1. // 登录路由相关代码
  2. router.post('/login', function(req, res, next) {
  3. const { username, password } = req.body
  4. const result = login(username, password)
  5. return result.then(data => {
  6. if (data.username) {
  7. // 设置 session
  8. req.session.username = data.username
  9. req.session.realname = data.realname
  10. res.json(
  11. new SuccessModel()
  12. )
  13. return
  14. }
  15. res.json(
  16. new ErrorModel('login failed')
  17. )
  18. })
  19. });

第二步:测试登录成功与否
写测试代码

  1. // 测试登录
  2. router.get('/login-test', (req, res, next) => {
  3. if (req.session.username) {
  4. res.json({
  5. errno: 0,
  6. msg: '测试登录成功'
  7. })
  8. return
  9. }
  10. res.json({
  11. errno: -1,
  12. msg: '测试登录失败'
  13. })
  14. })

然后登录网页测试 http-server 并且启动 nginx
根据之前设置的 nginx ,将 blog-express 的监听端口改为 8000(www 文件中)
然后测试链接:http://localhost:8080/api/user/login-test

成功!
image.png

将数据放到 redis 中

安装两个插件:redis 和 connect-redis
npm i redis connect-redis —save
然后拷贝之前的连接 redis 的代码

  1. const { REDIS_CONF } = require('../conf/db')
  2. // 创建客户端
  3. const redisClient = redis.createClient(REDIS_CONF.port, REDIS_CONF.host)
  4. // 监控 error
  5. redisClient.on('error', err => {
  6. console.error(err)
  7. })
  8. module.exports = redisClient

然后访问 http://localhost:8080/index.html 发现会有 session了
“sess:eYuIt3r7OO-_4NjN6_I0bD0uOKRwOGk6”

2021-11-18

9-10 登录中间件

然后执行下登录
登录成功之后再看下 key
get sess:6kplosAnfb8mc8to-NJvWN43PA6W9IoI
image.png
里面有了 username 的值和 realname 的值

证明已经连接好了

然后写一个登录的中间件
新建 middleware 文件夹(src/middleware)然后写一个 loginCheck.js 用来检测 login 成功与否

  1. const { ErrorModel } = require('../model/resModel')
  2. module.exports = (req, res, next) => {
  3. if (req.session.username) {
  4. next()
  5. return
  6. }
  7. res.json(
  8. new ErrorModel('未登录')
  9. )
  10. }

然后来使用

9-11 开发路由

isAdmin 登录

修改判断是否登录逻辑
然后修改完整的获取数据的逻辑

  1. router.get('/list', function(req, res, next) {
  2. // express 的 req.query 是原生自带的,不用手动解析
  3. let author = req.query.author || ''
  4. const keyword = req.query.keyword || ''
  5. // 修改登录权限
  6. if (req.query.isadmin) {
  7. // 管理员界面
  8. if (req.session.username == null) {
  9. // 未登录情况
  10. res.json(
  11. new ErrorModel('未登录')
  12. )
  13. return
  14. }
  15. // 强制查询自己的博客
  16. author = req.session.username
  17. }
  18. const result = getList(author, keyword)
  19. return result.then(listData => {
  20. // 不用 return
  21. res.json(
  22. new SuccessModel(listData)
  23. )
  24. })
  25. });

这样当访问 localhost:8080/admin.html 时,则必须登录才能访问,不然会返回错误。
这样登录后,才会访问成功

get detail

将之前原生获取修改一下,所有的返回替代成 res.json

  1. router.get('/detail', function(req, res, next) {
  2. const result = getDetail(req.query.id)
  3. return result.then(listData => {
  4. res.json(
  5. new SuccessModel(listData)
  6. )
  7. })
  8. })

new blog

同上,只不过是 post 请求

  1. router.post('/new', loginCheck, (req, res, next) => {
  2. req.body.author = req.session.username
  3. const result = newBlog(req.body)
  4. // 这里的 data 就是返回新建成功的博客的 id
  5. return result.then(data => {
  6. res.json(
  7. new SuccessModel(data)
  8. )
  9. })
  10. })

update blog

同上

  1. router.post('/update', loginCheck, (req, res, next) => {
  2. req.body.author = req.session.username
  3. const result = updateBlog(req.query.id, req.body)
  4. return result.then(data => {
  5. if (data) {
  6. res.json(
  7. new SuccessModel()
  8. )
  9. return
  10. }
  11. res.json(
  12. new ErrorModel('update failed')
  13. )
  14. })
  15. })

delete blog

同上

  1. router.post('/del', loginCheck, (req, res, next) => {
  2. const author = req.session.username
  3. const result = deleteBlog(req.query.id, author)
  4. return result.then(data => {
  5. if (data) {
  6. res.json(
  7. new SuccessModel()
  8. )
  9. return
  10. }
  11. res.json(
  12. new ErrorModel('update failed')
  13. )
  14. })
  15. })

这种 中间件的方式比较好,结构清晰,更易读,更好控制,符合设计模式

2021-11-19

9-12 介绍 morgan

回顾一下,之前我们记录日志是怎么办的:
1.首先讲了文件操作,发现文件操作会有性能的问题
2.然后讲了如何通过stream流来实现文件的写入,一行一行的写入日志
3.然后讲了如何拆分日志文件,分析日志crontab按时间来拆分
4.最后讲了如何分析日志,通过 readline 的方式一行一行读取来分析日志

日志

下面来讲在 express 中来做日志相关的内容

  • access log 记录,直接使用脚手架推荐的 morgan
  • 自定义日志还是使用 console.log 和 console.err 即可
  • 日志文件拆分,日志内容分析,之前讲过不再赘述

9-13 使用 morgan 写日志

根据环境变更日志书写格式

我们现在在控制台中已经可以显示一些访问记录了
image.png
分析一下:依次为访问的方式,路由,相应时间,数据量(content-length)
但是有问题:
1.获取的数据种类太少
2.数据都在控制台上,不是以文件的形式存储

解决:
修改 morgan 的中间件传入参数
通过查询 express/morgan(github) 中,发现更改app.use 中的模式即可,有不同的输出在控制台内容的方式
image.png
如果把 ‘dev’ 改为 ‘combined’ 后,就可以输出更多的内容
image.png
线上环境中就用这个 combined 的模式

如何把这两种格式的数据比较灵活的结合起来用呢?
通过设置环境变量来控制不同环境来使用不同格式
首先在 package.json 中写入 生成环境的脚本

  1. {
  2. ...
  3. "scripts": {
  4. ...
  5. "prd": "cross-env NODE_ENV=production nodemon ./bin/www.js"
  6. },
  7. ...
  8. }

然后修改 app.js

  1. const ENV = process.env.NODE_ENV
  2. if (ENV !== 'production') {
  3. // 开发环境或者测试环境
  4. app.use(logger('dev'));
  5. } else {
  6. // 线上环境
  7. app.use(logger('combined', {
  8. stream: process.stdout
  9. }));
  10. }

因为要写日志到文件中,所以 stream 流不能设置为 stdout 了

设置 access.log 日志

新建 logs/access.log 在根目录下
写入要引用 path(文件路径)和 fs(文件操作)
然后修改线上环境的写入方式

  1. ...
  2. const fs = require('fs')
  3. ...
  4. if (ENV !== 'production') {
  5. ...
  6. } else {
  7. // 线上环境
  8. const logFileName = path.join(__dirname, 'logs', 'access.log')
  9. const writeStream = fs.createWriteStream(logFileName, {
  10. flags: 'a'
  11. })
  12. app.use(logger('combined', {
  13. stream: writeStream
  14. }));
  15. }

然后去切换到 prd 运行(npm run prd)看下 access.log 里的内容

写入成功
image.png

设置自定义日志

自定义日志就是在任何位置添加自己想要的内容
在里面添加内容 list 这个 api 中添加内容
image.png
如果能触发,这些都还是在控制台中打印出来的
暂时先不提怎么将这些内容放到日志文件中,讲 pm2 会继续弄

pm2 可以方便收集到日志中

总结

  • 写法上的改变,如 req.query, res.json
  • 使用 express-session, connect-redis, 登录中间件
  • 使用 morgan 控制日志输出

9-14 中间件原理介绍

express 中间件原理介绍

express 定义了一些套路,能让自己定义的一些内容和它定义好的一些内容方便在 express 中运行
学习的目的:花费相对少的时间知道这个事情做出来的,也不是造轮子。

  • 回顾中间件的使用
  • 分析如何实现
  • 代码演示-100行代码作用

回顾中间件示例

app.use 执行,三个参数具体实行,然后 next 进行下一步,app.get/post 执行路由
至少实现的接口:app.listen, app.use, app.post, app.get, next()

分析

  • app.use 用来注册中间件
  • 遇到 http 请求,根据 path (‘/‘)和 method (get、post)判断触发哪些
  • 实现 next 机制,即上一个通过 next 触发下一个

2021-11-23

9-15 中间件原理-代码实现

新建 lib/express/like-express.js

然后新建一系列内容 用来模拟 express

  1. const http = require('http')
  2. const slice = Array.prototype.slice
  3. class LikeExpress {
  4. constructor() {
  5. // 存放中间件列表
  6. this.routes = {
  7. // all, get, post 就是分别以app.use, app.get, app.post 存放的中间件
  8. all: [],
  9. get: [],
  10. post: []
  11. }
  12. }
  13. // 不管是 use,get,post,listen 都要先注册的函数,内部共有的,分析参数
  14. register(path) {
  15. // 中间件的第一个参数可能是回调函数,也可能是path
  16. const info = {}
  17. if (typeof path === 'string') {
  18. info.path = path
  19. // 把参数从第二个开始的后续全部取出来,放到 stack 中,slice.call 赋值为数组
  20. info.stack = slice.call(arguments, 1)
  21. } else {
  22. // 把参数从第一个开始的后续全部取出来,放到 stack 中,slice.call 赋值为数组
  23. info.path = '/'
  24. info.stack = slice.call(arguments, 0)
  25. }
  26. return info
  27. }
  28. use() {
  29. // 将 use 方法所有的参数全部放到 info 中去
  30. const info = this.register.apply(this, arguments)
  31. // 将所有的参数(中间件)放到all 这个列表中去
  32. this.routes.all.push(info)
  33. }
  34. get() {
  35. // 同上
  36. const info = this.register.apply(this, arguments)
  37. // 将所有的参数(中间件)放到 all 这个列表中去
  38. this.routes.get.push(info)
  39. }
  40. post() {
  41. // 同上
  42. const info = this.register.apply(this, arguments)
  43. // 将所有的参数(中间件)放到 all 这个列表中去
  44. this.routes.post.push(info)
  45. }
  46. listen() {
  47. }
  48. }
  49. // 工厂函数
  50. module.exports = () => {
  51. return new LikeExpress()
  52. }

书写 listen 方法

  1. // 解构参数,参数个数不定
  2. listen(...args) {
  3. const server = http.createServer(this.callback())
  4. // 参数透传
  5. server.listen(...args)
  6. }

书写 callback 方法

  1. match(method, url) {
  2. let stack = []
  3. // 浏览器自带的小图标,忽略掉
  4. if (url === '/favicon.ico') {
  5. return stack
  6. }
  7. // 获取 routes
  8. let curRoutes = []
  9. // 拼接 all 中所有的 路由
  10. curRoutes = curRoutes.concat(this.routes.all)
  11. // 然后根据 method 是什么来继续拼接
  12. curRoutes = curRoutes.concat(this.routes[method])
  13. curRoutes.forEach(routeInfo => {
  14. if (url.indexOf(routeInfo.path) === 0 ) {
  15. // 这种情况:当前的 url === '/api/get-cookie' 且 routeInfo.path === '/'
  16. // 当前的 url === '/api/get-cookie' 且 routeInfo.path === '/path'
  17. // 当前的 url === '/api/get-cookie' 且 routeInfo.path === '/api/get-cookie'
  18. // 符合以上类似的情况,就将 url 放到 stack 中去
  19. stack = stack.concat(routeInfo.stack)
  20. }
  21. })
  22. return stack
  23. }
  24. callback() {
  25. return (req, res) => {
  26. res.json = (data) => {
  27. // 因为要返回JSON格式,所以要设置 setHeader
  28. res.setHeader('Content-type', 'application/json')
  29. res.end(
  30. JSON.stringify(data)
  31. )
  32. }
  33. const url = req.url
  34. const method = req.method.toLowerCase()
  35. // 通过 url 和 method 来区分哪些 参数 需要访问,哪些不需要
  36. // 然后将 url 和 method 传入到 match 方法中
  37. const resultList = this.match(method, url)
  38. this.handle(req, res, resultList)
  39. }
  40. }

然后书写 handle 方法

  1. // 核心的 next 机制
  2. handle(req, res, stack) {
  3. const next = () => {
  4. // 拿到第一个匹配的中间件
  5. const middleWare = stack.shift()
  6. if (middleWare) {
  7. // 执行中间件函数
  8. middleWare(req, res, next)
  9. }
  10. }
  11. next()
  12. }

1.定义存放的组件,this.routes
2.定义 use() get() post() register(),分别存放 stack
3.callback() 定义 res.json
4.match() 定义如何访问
5.handle() 定义 next() 函数运行机制

image.png

9-16 测试中间件代码实现-总结

新建一个 test.js 用来测试

  1. const express = require('./like-express')
  2. // 本次 http 请求的实例
  3. const app = express()
  4. app.use((req, res, next) => {
  5. console.log('请求开始...', req.method, req.url)
  6. next()
  7. })
  8. app.use((req, res, next) => {
  9. // 假设在处理 cookie
  10. console.log('处理 cookie ...')
  11. req.cookie = {
  12. userId: 'abc123'
  13. }
  14. next()
  15. })
  16. app.use('/api', (req, res, next) => {
  17. console.log('处理 /api 路由')
  18. next()
  19. })
  20. app.get('/api', (req, res, next) => {
  21. console.log('get /api 路由')
  22. next()
  23. })
  24. // 模拟登录验证
  25. function loginCheck(req, res, next) {
  26. setTimeout(() => {
  27. console.log('模拟登陆成功')
  28. next()
  29. })
  30. }
  31. app.get('/api/get-cookie', loginCheck, (req, res, next) => {
  32. console.log('get /api/get-cookie')
  33. res.json({
  34. errno: 0,
  35. data: req.cookie
  36. })
  37. })
  38. app.listen(8000, () => {
  39. console.log('server is running on port 8000')
  40. })

成功
image.png

总结

  • 使用框架开发的好处(相比之前不使用框架)
    • 便利性
  • express 的使用和路由处理,以及操作 session redis 日志等
  • express 中间件的使用和原理

下一步

  • JS 的异步回调带来了很多问题,Promise 也不能解决所有
  • nodejs 已经全面支持 async/await 语法,要用起来
  • Koa2 支持 async/await