2021-11-24

10-1 开始

使用 koa2

  • express 中间件是异步回调,koa2 原生支持 async/await
  • 新开发框架和系统,都开始基于 koa2,例如 egg.js
  • express 虽然未过时,但是 koa2 肯定是未来的趋势

目录

  • async/await 语法介绍,安装和使用 koa2
  • 开发接口,连接数据库,实现登录,日志记录
  • 分析 koa2 中间件原理

介绍 async/await

打开 promise 那个例子
接着写读文件那个例子
将依次读取文件内容的方法进行改写,使用 async/await 方法进行书写

  1. // async/await
  2. async function readFileData() {
  3. const aData = await getFileContent('a.json')
  4. console.log('a data', aData)
  5. const bData = await getFileContent(aData.next)
  6. console.log('b data', bData)
  7. const cData = await getFileContent(bData.next)
  8. console.log('c data', cData)
  9. }

结果
image.png
和 promise 打印出来的结果相同

async await 要点:
1.await 后面可以追加 promise对象

  1. async function readAData() {
  2. const aData = await getFileContent('a.json')
  3. return aData
  4. }
  5. async function test() {
  6. const data = await readAData() // 在这里追加了 promise 对象
  7. console.log(data) // 正常打印 data 即 aData 的值
  8. }
  9. test()
  1. await 必须包裹在 async 函数里面
  2. async 函数执行返回的也是一个 promise 对象
  3. 通过 try/catch 截获 promise 中的 reject 的值
    1. // 改写 readFileData
    2. async function readFileData() {
    3. // 同步写法
    4. try {
    5. const aData = await getFileContent('a.json')
    6. console.log('a data', aData)
    7. const bData = await getFileContent(aData.next)
    8. console.log('b data', bData)
    9. const cData = await getFileContent(bData.next)
    10. console.log('c data', cData)
    11. } catch (err) {
    12. console.error(err) // 获取 reject 的值
    13. }
    14. }

    10-2 介绍 koa2

    介绍 koa2

    express 人马做的
    koa1 使用 generator 做的
  • 安装(使用脚手架)
  • 初始化代码,处理路由
  • 使用中间件

安装 koa2

  • npm install koa-generator -g
  • koa2 koa2-test
  • npm install & npm run dev

安装完之后新建一个目录 blog-koa2: koa2 blog-koa2
然后 cd 进入
image.png
然后 npm install 安装
看 package.json 之后发现还缺一个 cross-env ,然后安装一个
npm i cross-env —save-dev
然后修改script

  1. "scripts": {
  2. ...
  3. "dev": "cross-env NODE_ENV=dev ./node_modules/.bin/nodemon bin/www",
  4. "prd": "cross-env NODE_ENV=production pm2 start bin/www",
  5. ...
  6. },

然后修改端口为 8000
启动成功
image.png

介绍 app.js

是服务的初始化文件

  • 各个插件的作用
  • 思考各个插件的实现原理(结合之前学过的知识)
  • 处理 get 请求和 post 请求

onerror 错误监测

  1. // error handler
  2. onerror(app)

处理 postData 即 post 数据,这里有 enableTypes 可以接收很多种数据
json 可以把 postData 的数据变为 JSON 格式的数据

  1. // middlewares
  2. app.use(bodyparser({
  3. enableTypes:['json', 'form', 'text']
  4. }))
  5. app.use(json())

logger() 日志
koa-static、views 和前端相关

获取当前请求的耗时

  1. // logger
  2. app.use(async (ctx, next) => {
  3. const start = new Date()
  4. await next()
  5. const ms = new Date() - start
  6. console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
  7. })

10-3 介绍路由

想要知道路由怎么用,先要看下示例中的路由怎么用的:打开 routes/index.js

routes/index.js

首先看第一行,发现 koa2 的路由(koa-router)是独立于 koa2的

  1. const router = require('koa-router')()

这里的 render 是直接结合 index.pug 这个模板来进行渲染,传入 title 变量来

  1. router.get('/', async (ctx, next) => {
  2. await ctx.render('index', {
  3. title: 'Hello Koa 2!'
  4. })
  5. })

类似的,返回 string 和 json

  1. router.get('/string', async (ctx, next) => {
  2. ctx.body = 'koa2 string'
  3. })
  4. router.get('/json', async (ctx, next) => {
  5. ctx.body = {
  6. title: 'koa2 json'
  7. }
  8. })

image.png
回调函数必须使用 async 语法
ctx 即 context 是 express 中 res,req 的集合体

routes/users.js

第三行就不太一样
表示前缀,后续的都需要使用这个为前提

  1. router.prefix('/users')

自定义路由-get

新建 blog.js

  1. const router = require('koa-router')()
  2. router.prefix('/api/blog')
  3. router.get('/list', function (ctx, next) {
  4. const query = ctx.query
  5. ctx.body = {
  6. errno: 0,
  7. data: ['get blog list'],
  8. query
  9. }
  10. })
  11. module.exports = router

然后注册一下路由

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

image.png
写入 query 也能拿到
image.png

自定义路由-post

新建 user.js

  1. const router = require('koa-router')()
  2. router.prefix('/api/user')
  3. router.post('/login', async function (ctx, next) {
  4. const { username, password } = ctx.request.body
  5. const query = ctx.query
  6. ctx.body = {
  7. errno: 0,
  8. username,
  9. password
  10. }
  11. })
  12. module.exports = router

然后返回 app.js 中注册一下路由

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

打开 postman 来测试一下
image.png
成功~

2021-11-25

10-4 介绍中间件机制

中间件机制

  • 有很多 app.use
  • 代码中的 next 参数是什么?

app.use 和 express 中的差不多
next 机制和 express差不多,只不过 async 包裹

10-5 实现 session

koa2 开发接口

  • 实现登录
  • 开发路由
  • 记录日志

实现登录

  • 和 express 类似
  • 基于 koa-generic-session 和 koa-redis

实操

安装 koa-generic-session 和 koa-redis

npm install koa-generic-session koa-redis redis —save

然后在 app.js 中进行引入

  1. const session = require('koa-generic-session')
  2. const redisStore = require('koa-redis')

写连接 session 一定要在注册路由之前写
image.png
类似于 express 连接session

  1. // session 配置
  2. app.keys = ['Lb@#c_13323']
  3. app,use(session({
  4. // 配置cookie
  5. cookie: {
  6. path: '/',
  7. httpOnly: true,
  8. maxAge: 24 * 60 * 60 * 1000
  9. },
  10. // 配置 redis
  11. store: redisStore({
  12. all: '127.0.0.1:6379' // 写死本地的 redis(应在配置文件中定义)
  13. })
  14. }))
  15. // route
  16. ...

然后在 use.js 中加一个 session-test 做一个测试

  1. router.get('/session-test', async function (ctx, next) {
  2. if (ctx.session.viewCount == null) {
  3. ctx.session.viewCount = 0
  4. }
  5. ctx.session.viewCount++
  6. ctx.body = {
  7. errno: 0,
  8. viewCount: ctx.session.viewCount
  9. }
  10. })

访问两次
image.png
成功~
连接 redis-cli 发现值是正确存储的
image.png

10-6 开发路由-准备工作

路由

  • 复用之前的代码,如 mysql、登录中间件、controller、model
  • 初始化路由,并开发接口
    • 所有相关的 api
  • 测试联调
    • 启动前端代码,然后通过 nginx 来联调

安装 mysql 和 xss
npm i mysql xss —save

然后复用之前的代码,拷贝 express 中的 conf、db、model、controller、utils 文件夹到 koa 中
然后修改一下 redis 的 config 文件

  1. // 引入配置文件
  2. const { REDIS_CONF } = require('./conf/db')
  3. ...
  4. app.use(session({
  5. ...
  6. // 配置 redis
  7. store: redisStore({
  8. all: `${REDIS_CONF.host}:${REDIS_CONF.port}`
  9. })
  10. }))

然后修改下 controller/blog.js
全部修改为 async await 语法来写的内容

  1. const xss = require('xss')
  2. const { exec } = require('../db/mysql')
  3. const getList = async (author, keyword) => {
  4. let sql = `select * from blogs where 1=1 `
  5. if (author) {
  6. sql += `and author='${author}'`
  7. }
  8. if (keyword) {
  9. sql += `and title like '%${keyword}%' `
  10. }
  11. sql += `order by createtime desc;`
  12. // 返回 promise
  13. return await exec(sql)
  14. }
  15. const getDetail = async (id) => {
  16. const sql = `select * from blogs where id='${id}'`
  17. const rows = await exec(sql)
  18. return rows[0]
  19. }
  20. const newBlog = async (blogData = {}) => {
  21. // blogData 是一个博客对象,包含 title content author属性
  22. const title = xss(blogData.title)
  23. const content = xss(blogData.content)
  24. const author = blogData.author
  25. const createtime = Date.now()
  26. const sql = `
  27. insert into blogs (title, content, author, createtime)
  28. values ('${title}', '${content}', '${author}', ${createtime});
  29. `
  30. const insertData = await exec(sql)
  31. return {
  32. id: insertData.insertId
  33. }
  34. }
  35. const updateBlog = async (id, blogData = {}) => {
  36. // blogData 是一个博客对象,包含 title content 属性
  37. // 更新时只需要更新blogData title content 属性
  38. const title = xss(blogData.title)
  39. const content = xss(blogData.content)
  40. const sql = `
  41. update blogs set title='${title}',content='${content}' where id=${id}
  42. `
  43. const updateData = await exec(sql)
  44. if (updateData.affectedRows > 0) {
  45. return true
  46. }
  47. return false
  48. }
  49. const deleteBlog = async (id, author) => {
  50. // id 就是要删除博客的id
  51. const sql = `
  52. delete from blogs where id=${id} and author='${author}'
  53. `
  54. const deleteData = await exec(sql)
  55. if (deleteData.affectedRows > 0) {
  56. return true
  57. }
  58. return false
  59. }
  60. module.exports = {
  61. getList,
  62. getDetail,
  63. newBlog,
  64. updateBlog,
  65. deleteBlog
  66. }

然后修改 controller/user.js

  1. const { exec, escape } = require('../db/mysql')
  2. const { genPassword } = require('../utils/crypt')
  3. const login = async (username, password) =>{
  4. username= escape(username)
  5. // 生成加密密码
  6. password = genPassword(password)
  7. password= escape(password)
  8. const sql = `
  9. select username,realname from users where username=${username} and password=${password}
  10. `
  11. const rows = await exec(sql)
  12. return rows[0] || {}
  13. }
  14. module.exports = {
  15. login
  16. }

最后修改一下相关联的loginCheck

  1. const { ErrorModel } = require('../model/resModel')
  2. module.exports = async (ctx, next) => {
  3. if (ctx.session.username) {
  4. await next()
  5. return
  6. }
  7. ctx.body = new ErrorModel('未登录')
  8. }

至此,准备工作完毕了,然后下一步继续开发路由

2021-11-29

10-7 开发路由-代码演示

制作路由,参照 express 中的一些操作
全部改为 ctx async await 写法

  1. const router = require('koa-router')()
  2. const { getList, getDetail, newBlog, updateBlog, deleteBlog } = require('../controller/blog')
  3. const { SuccessModel, ErrorModel } = require('../model/resModel')
  4. const loginCheck = require('../middleware/loginCheck')
  5. router.prefix('/api/blog')
  6. router.get('/list', async function (ctx, next) {
  7. let author = ctx.query.author || ''
  8. const keyword = ctx.query.keyword || ''
  9. // 修改登录权限
  10. if (ctx.query.isadmin) {
  11. // 管理员界面
  12. if (ctx.session.username == null) {
  13. // 未登录情况
  14. ctx.body = new ErrorModel('未登录')
  15. return
  16. }
  17. // 强制查询自己的博客
  18. author = ctx.session.username
  19. }
  20. const listData = await getList(author, keyword)
  21. ctx.body = new SuccessModel(listData)
  22. })
  23. router.get('/detail', async function (ctx, next) {
  24. const listData = await getDetail(ctx.query.id)
  25. ctx.body = new SuccessModel(listData)
  26. })
  27. router.post('/new', async function (ctx, next) {
  28. const { body } = ctx.request
  29. body.author = ctx.session.username
  30. const data = await newBlog(body)
  31. ctx.body = new SuccessModel(data)
  32. })
  33. router.post('/update', async function (ctx, next) {
  34. const data = await updateBlog(ctx.query.id, ctx.request.body)
  35. if (data) {
  36. ctx.body = new SuccessModel()
  37. return
  38. }
  39. ctx.body = new ErrorModel('update failed')
  40. })
  41. router.delete('/delete', async function (ctx, next) {
  42. const author = ctx.session.username
  43. const data = await deleteBlog(ctx.query.id, author)
  44. if (data) {
  45. ctx.body = new SuccessModel()
  46. return
  47. }
  48. ctx.body = new ErrorModel('update failed')
  49. })
  50. module.exports = router

制作登录需求

  1. const router = require('koa-router')()
  2. const { login } = require('../controller/user')
  3. const { SuccessModel, ErrorModel } = require('../model/resModel')
  4. router.prefix('/api/user')
  5. router.post('/login', async function (ctx, next) {
  6. const { username, password } = ctx.request.body
  7. const data = await login(username, password)
  8. if (data.username) {
  9. req.session.username = data.username
  10. req.session.realname = data.realname
  11. ctx.body = new SuccessModel()
  12. return
  13. }
  14. ctx.body = new ErrorModel('login failed')
  15. })
  16. module.exports = router

10-8 开发路由(联调)

没什么问题,就是利用 koa2 来进行联调

10-9 日志

日志

  • access log 记录,使用 morgan
    • express 是自带 morgan,但是 koa2 没有,需要自己引入
  • 自定义日志使用 console.log 和 console.error
  • 日志文件拆分,日志内存分析,之前讲过不再赘述

虽然 koa2 中自带 koa-logger 这个插件,但是他只是在控制台中改变了显示的格式,变成阅读体验比较好的方式

  1. const logger = require('koa-logger')
  2. app.use(logger())

image.png
所以要借用 morgan 来打印真正的日志
但是 morgan 是仅支持 express 的,所以需要一个能支持 koa 插件的工具来支持 morgan 和 koa 的结合使用
就是:koa-morgan
使用 npm install koa-morgan —save 来进行安装

然后新建记录日志的文件夹 logs
在里面新建 access.log

然后来到 app.js 中
书写记录日志的相关代码

  1. const path = require('path')
  2. const fs = require('fs')
  3. const morgan = require('koa-morgan')
  4. ...
  5. // 写日志
  6. const ENV = process.env.NODE_ENV
  7. if (ENV !== 'production') {
  8. // 开发环境或者测试环境
  9. app.use(morgan('dev'))
  10. } else {
  11. // 线上环境
  12. const logFileName = path.join(__dirname, 'logs', 'access.log')
  13. const writeStream = fs.createWriteStream(logFileName, {
  14. flags: 'a'
  15. })
  16. app.use(morgan('combined', {
  17. stream: writeStream
  18. }));
  19. }

然后测试一下
成功!
image.png

2021-11-30

10-10 中间件原理-分析

koa2 中间件原理

  • 回顾中间件使用
  • 分析如何实现
  • 代码演示

实操

新建 koa2-test 的目录
然后初始化 npm 目录
然后把 https://koa.bootcss.com/ 的实例复制过来

  1. const Koa = require('koa');
  2. const app = new Koa();
  3. // logger
  4. app.use(async (ctx, next) => {
  5. await next();
  6. const rt = ctx.response.get('X-Response-Time');
  7. console.log(`${ctx.method} ${ctx.url} - ${rt}`);
  8. });
  9. // x-response-time
  10. app.use(async (ctx, next) => {
  11. const start = Date.now();
  12. await next();
  13. const ms = Date.now() - start;
  14. ctx.set('X-Response-Time', `${ms}ms`);
  15. });
  16. // response
  17. app.use(async ctx => {
  18. ctx.body = 'Hello World';
  19. });
  20. app.listen(3000);

执行过程为: new 一个 Koa() 对象后,然后执行中间件(logger),执行next,然后来到下一个中间件(x-response-time),将现在的时间赋值为 start,然后执行 next,然后执行下一个中间件(response),将ctx.body 赋值为 Hello World,这个中间件执行结束且没有 next,回到上一个中间件(x-response-time),继续赋值 ms,然后设置 X-Response-Time,这个中间件执行结束且没有 next,回到上一个中间件(logger),赋值 rt 值,最后打印控制台能看到的信息,method,url,运行时间
image.png

洋葱圈模型

image.png

分析实现

  • app.use 用来注册中间件,先收集起来
  • 实现 next 机制,即上一个通过 next 触发下一个
  • 不涉及 method 和 url 的判断

10-11 中间件原理-代码演示

像 express 一样,在 lib下新建 koa2 文件夹,然后新建 like-koa2.js
然后书写整体的结构

  1. const http = require('http')
  2. class LikeKoa2 {
  3. constructor() {
  4. // 存放中间件的地方
  5. this.middlewareList = []
  6. }
  7. // 注册中间件
  8. use(fn) {
  9. this.middlewareList.push(fn)
  10. // return this 可以链式调用
  11. return this
  12. }
  13. callback() {
  14. return (req, res) => {
  15. }
  16. }
  17. // 监听中间件
  18. listen(...args) {
  19. const server = http.createServer(this.callback())
  20. server.listen(...args)
  21. }
  22. }
  23. module.exports = LikeKoa2

分析 next() 方法的实现

  1. // 组合中间件
  2. function compose(middlewareList) {
  3. return function (ctx) {
  4. function dispatch(i) {
  5. const fn = middlewareList[i]
  6. try {
  7. // 这里写 Promise.resolve 的原因是防止未写 async 而引起报错
  8. return Promise.resolve(
  9. fn(ctx, dispatch.bind(null, i + 1))
  10. )
  11. } catch (err) {
  12. Promise.reject(err)
  13. }
  14. }
  15. return dispatch(0)
  16. }
  17. }

dispatch 是传入 i 是一个个中间件,首先 dispatch(0) 是执行第一个中间件,然后执行 try catch 一个个的执行后续的中间件。在 try 中,执行 fn 传入 ctx 对标实际执行的 ctx,dispatch.bind 就可以执行下一个中间件,真正执行后续的中间件逻辑

完善 LikeKoa2

  1. const http = require('http')
  2. // 组合中间件
  3. function compose(middlewareList) {
  4. return function (ctx) {
  5. function dispatch(i) {
  6. const fn = middlewareList[i]
  7. try {
  8. // 这里写 Promise.resolve 的原因是防止未写 async 而引起报错
  9. return Promise.resolve(
  10. fn(ctx, dispatch.bind(null, i + 1))
  11. )
  12. } catch (err) {
  13. Promise.reject(err)
  14. }
  15. }
  16. return dispatch(0)
  17. }
  18. }
  19. class LikeKoa2 {
  20. constructor() {
  21. // 存放中间件的地方
  22. this.middlewareList = []
  23. }
  24. // 注册中间件
  25. use(fn) {
  26. this.middlewareList.push(fn)
  27. // return this 可以链式调用
  28. return this
  29. }
  30. createContext(req, res) {
  31. return {
  32. req,
  33. res
  34. }
  35. // ctx.query = req.query
  36. }
  37. handleRequest(ctx, fn) {
  38. return fn(ctx)
  39. }
  40. callback() {
  41. const fn = compose(this.middlewareList)
  42. return (req, res) => {
  43. const ctx = this.createContext(req, res)
  44. return this.handleRequest(ctx, fn)
  45. }
  46. }
  47. // 监听中间件
  48. listen(...args) {
  49. const server = http.createServer(this.callback())
  50. server.listen(...args)
  51. }
  52. }
  53. module.exports = LikeKoa2

10-12 总结

尝试用一下
修改之前洋葱圈的代码

  1. const Koa = require('./like-koa2');
  2. const app = new Koa();
  3. // logger
  4. app.use(async (ctx, next) => {
  5. console.log('onion 1 start');
  6. await next();
  7. const rt = ctx['X-Response-Time'];
  8. console.log(`${ctx.req.method} ${ctx.req.url} - ${rt}`);
  9. console.log('onion 1 end');
  10. });
  11. // x-response-time
  12. app.use(async (ctx, next) => {
  13. console.log('onion 2 start');
  14. const start = Date.now();
  15. await next();
  16. const ms = Date.now() - start;
  17. ctx['X-Response-Time'] = `${ms}ms`;
  18. console.log('onion 2 end');
  19. });
  20. // response
  21. app.use(async ctx => {
  22. console.log('onion 3 start');
  23. ctx.res.end('This is like koa2')
  24. console.log('onion 3 end');
  25. });
  26. app.listen(3000);

node 如何做中间层

https://coding.imooc.com/learn/questiondetail/y0K5g683WEpXe2QN.html
image.png

总结

  • 使用 async/await 的好处
  • koa2 的使用,以及如何操作 session redis 日志
  • koa2 中间件的使用和原理

image.png