2021-11-24
10-1 开始
使用 koa2
- express 中间件是异步回调,koa2 原生支持 async/await
- 新开发框架和系统,都开始基于 koa2,例如 egg.js
- express 虽然未过时,但是 koa2 肯定是未来的趋势
目录
- async/await 语法介绍,安装和使用 koa2
- 开发接口,连接数据库,实现登录,日志记录
- 分析 koa2 中间件原理
介绍 async/await
打开 promise 那个例子
接着写读文件那个例子
将依次读取文件内容的方法进行改写,使用 async/await 方法进行书写
// async/awaitasync function readFileData() {const aData = await getFileContent('a.json')console.log('a data', aData)const bData = await getFileContent(aData.next)console.log('b data', bData)const cData = await getFileContent(bData.next)console.log('c data', cData)}
结果
和 promise 打印出来的结果相同
async await 要点:
1.await 后面可以追加 promise对象
async function readAData() {const aData = await getFileContent('a.json')return aData}async function test() {const data = await readAData() // 在这里追加了 promise 对象console.log(data) // 正常打印 data 即 aData 的值}test()
- await 必须包裹在 async 函数里面
- async 函数执行返回的也是一个 promise 对象
- 通过 try/catch 截获 promise 中的 reject 的值
// 改写 readFileDataasync function readFileData() {// 同步写法try {const aData = await getFileContent('a.json')console.log('a data', aData)const bData = await getFileContent(aData.next)console.log('b data', bData)const cData = await getFileContent(bData.next)console.log('c data', cData)} catch (err) {console.error(err) // 获取 reject 的值}}
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 进入
然后 npm install 安装
看 package.json 之后发现还缺一个 cross-env ,然后安装一个
npm i cross-env —save-dev
然后修改script
"scripts": {..."dev": "cross-env NODE_ENV=dev ./node_modules/.bin/nodemon bin/www","prd": "cross-env NODE_ENV=production pm2 start bin/www",...},
介绍 app.js
是服务的初始化文件
- 各个插件的作用
- 思考各个插件的实现原理(结合之前学过的知识)
- 处理 get 请求和 post 请求
onerror 错误监测
// error handleronerror(app)
处理 postData 即 post 数据,这里有 enableTypes 可以接收很多种数据
json 可以把 postData 的数据变为 JSON 格式的数据
// middlewaresapp.use(bodyparser({enableTypes:['json', 'form', 'text']}))app.use(json())
logger() 日志
koa-static、views 和前端相关
获取当前请求的耗时
// loggerapp.use(async (ctx, next) => {const start = new Date()await next()const ms = new Date() - startconsole.log(`${ctx.method} ${ctx.url} - ${ms}ms`)})
10-3 介绍路由
想要知道路由怎么用,先要看下示例中的路由怎么用的:打开 routes/index.js
routes/index.js
首先看第一行,发现 koa2 的路由(koa-router)是独立于 koa2的
const router = require('koa-router')()
这里的 render 是直接结合 index.pug 这个模板来进行渲染,传入 title 变量来
router.get('/', async (ctx, next) => {await ctx.render('index', {title: 'Hello Koa 2!'})})
类似的,返回 string 和 json
router.get('/string', async (ctx, next) => {ctx.body = 'koa2 string'})router.get('/json', async (ctx, next) => {ctx.body = {title: 'koa2 json'}})

回调函数必须使用 async 语法
ctx 即 context 是 express 中 res,req 的集合体
routes/users.js
第三行就不太一样
表示前缀,后续的都需要使用这个为前提
router.prefix('/users')
自定义路由-get
新建 blog.js
const router = require('koa-router')()router.prefix('/api/blog')router.get('/list', function (ctx, next) {const query = ctx.queryctx.body = {errno: 0,data: ['get blog list'],query}})module.exports = router
然后注册一下路由
const blog = require('./routes/blog')...app.use(blog.routes(), blog.allowedMethods())...
自定义路由-post
新建 user.js
const router = require('koa-router')()router.prefix('/api/user')router.post('/login', async function (ctx, next) {const { username, password } = ctx.request.bodyconst query = ctx.queryctx.body = {errno: 0,username,password}})module.exports = router
然后返回 app.js 中注册一下路由
const user = require('./routes/user')...app.use(user.routes(), user.allowedMethods())...
打开 postman 来测试一下
成功~
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 中进行引入
const session = require('koa-generic-session')const redisStore = require('koa-redis')
写连接 session 一定要在注册路由之前写
类似于 express 连接session
// session 配置app.keys = ['Lb@#c_13323']app,use(session({// 配置cookiecookie: {path: '/',httpOnly: true,maxAge: 24 * 60 * 60 * 1000},// 配置 redisstore: redisStore({all: '127.0.0.1:6379' // 写死本地的 redis(应在配置文件中定义)})}))// route...
然后在 use.js 中加一个 session-test 做一个测试
router.get('/session-test', async function (ctx, next) {if (ctx.session.viewCount == null) {ctx.session.viewCount = 0}ctx.session.viewCount++ctx.body = {errno: 0,viewCount: ctx.session.viewCount}})
访问两次
成功~
连接 redis-cli 发现值是正确存储的
10-6 开发路由-准备工作
路由
- 复用之前的代码,如 mysql、登录中间件、controller、model
- 初始化路由,并开发接口
- 所有相关的 api
- 测试联调
- 启动前端代码,然后通过 nginx 来联调
安装 mysql 和 xss
npm i mysql xss —save
然后复用之前的代码,拷贝 express 中的 conf、db、model、controller、utils 文件夹到 koa 中
然后修改一下 redis 的 config 文件
// 引入配置文件const { REDIS_CONF } = require('./conf/db')...app.use(session({...// 配置 redisstore: redisStore({all: `${REDIS_CONF.host}:${REDIS_CONF.port}`})}))
然后修改下 controller/blog.js
全部修改为 async await 语法来写的内容
const xss = require('xss')const { exec } = require('../db/mysql')const getList = async (author, keyword) => {let sql = `select * from blogs where 1=1 `if (author) {sql += `and author='${author}'`}if (keyword) {sql += `and title like '%${keyword}%' `}sql += `order by createtime desc;`// 返回 promisereturn await exec(sql)}const getDetail = async (id) => {const sql = `select * from blogs where id='${id}'`const rows = await exec(sql)return rows[0]}const newBlog = async (blogData = {}) => {// blogData 是一个博客对象,包含 title content author属性const title = xss(blogData.title)const content = xss(blogData.content)const author = blogData.authorconst createtime = Date.now()const sql = `insert into blogs (title, content, author, createtime)values ('${title}', '${content}', '${author}', ${createtime});`const insertData = await exec(sql)return {id: insertData.insertId}}const updateBlog = async (id, blogData = {}) => {// blogData 是一个博客对象,包含 title content 属性// 更新时只需要更新blogData的 title content 属性const title = xss(blogData.title)const content = xss(blogData.content)const sql = `update blogs set title='${title}',content='${content}' where id=${id}`const updateData = await exec(sql)if (updateData.affectedRows > 0) {return true}return false}const deleteBlog = async (id, author) => {// id 就是要删除博客的idconst sql = `delete from blogs where id=${id} and author='${author}'`const deleteData = await exec(sql)if (deleteData.affectedRows > 0) {return true}return false}module.exports = {getList,getDetail,newBlog,updateBlog,deleteBlog}
然后修改 controller/user.js
const { exec, escape } = require('../db/mysql')const { genPassword } = require('../utils/crypt')const login = async (username, password) =>{username= escape(username)// 生成加密密码password = genPassword(password)password= escape(password)const sql = `select username,realname from users where username=${username} and password=${password}`const rows = await exec(sql)return rows[0] || {}}module.exports = {login}
最后修改一下相关联的loginCheck
const { ErrorModel } = require('../model/resModel')module.exports = async (ctx, next) => {if (ctx.session.username) {await next()return}ctx.body = new ErrorModel('未登录')}
至此,准备工作完毕了,然后下一步继续开发路由
2021-11-29
10-7 开发路由-代码演示
制作路由,参照 express 中的一些操作
全部改为 ctx async await 写法
const router = require('koa-router')()const { getList, getDetail, newBlog, updateBlog, deleteBlog } = require('../controller/blog')const { SuccessModel, ErrorModel } = require('../model/resModel')const loginCheck = require('../middleware/loginCheck')router.prefix('/api/blog')router.get('/list', async function (ctx, next) {let author = ctx.query.author || ''const keyword = ctx.query.keyword || ''// 修改登录权限if (ctx.query.isadmin) {// 管理员界面if (ctx.session.username == null) {// 未登录情况ctx.body = new ErrorModel('未登录')return}// 强制查询自己的博客author = ctx.session.username}const listData = await getList(author, keyword)ctx.body = new SuccessModel(listData)})router.get('/detail', async function (ctx, next) {const listData = await getDetail(ctx.query.id)ctx.body = new SuccessModel(listData)})router.post('/new', async function (ctx, next) {const { body } = ctx.requestbody.author = ctx.session.usernameconst data = await newBlog(body)ctx.body = new SuccessModel(data)})router.post('/update', async function (ctx, next) {const data = await updateBlog(ctx.query.id, ctx.request.body)if (data) {ctx.body = new SuccessModel()return}ctx.body = new ErrorModel('update failed')})router.delete('/delete', async function (ctx, next) {const author = ctx.session.usernameconst data = await deleteBlog(ctx.query.id, author)if (data) {ctx.body = new SuccessModel()return}ctx.body = new ErrorModel('update failed')})module.exports = router
制作登录需求
const router = require('koa-router')()const { login } = require('../controller/user')const { SuccessModel, ErrorModel } = require('../model/resModel')router.prefix('/api/user')router.post('/login', async function (ctx, next) {const { username, password } = ctx.request.bodyconst data = await login(username, password)if (data.username) {req.session.username = data.usernamereq.session.realname = data.realnamectx.body = new SuccessModel()return}ctx.body = new ErrorModel('login failed')})module.exports = router
10-8 开发路由(联调)
没什么问题,就是利用 koa2 来进行联调
10-9 日志
日志
- access log 记录,使用 morgan
- express 是自带 morgan,但是 koa2 没有,需要自己引入
- 自定义日志使用 console.log 和 console.error
- 日志文件拆分,日志内存分析,之前讲过不再赘述
虽然 koa2 中自带 koa-logger 这个插件,但是他只是在控制台中改变了显示的格式,变成阅读体验比较好的方式
const logger = require('koa-logger')app.use(logger())

所以要借用 morgan 来打印真正的日志
但是 morgan 是仅支持 express 的,所以需要一个能支持 koa 插件的工具来支持 morgan 和 koa 的结合使用
就是:koa-morgan
使用 npm install koa-morgan —save 来进行安装
然后新建记录日志的文件夹 logs
在里面新建 access.log
然后来到 app.js 中
书写记录日志的相关代码
const path = require('path')const fs = require('fs')const morgan = require('koa-morgan')...// 写日志const ENV = process.env.NODE_ENVif (ENV !== 'production') {// 开发环境或者测试环境app.use(morgan('dev'))} else {// 线上环境const logFileName = path.join(__dirname, 'logs', 'access.log')const writeStream = fs.createWriteStream(logFileName, {flags: 'a'})app.use(morgan('combined', {stream: writeStream}));}
2021-11-30
10-10 中间件原理-分析
koa2 中间件原理
- 回顾中间件使用
- 分析如何实现
- 代码演示
实操
新建 koa2-test 的目录
然后初始化 npm 目录
然后把 https://koa.bootcss.com/ 的实例复制过来
const Koa = require('koa');const app = new Koa();// loggerapp.use(async (ctx, next) => {await next();const rt = ctx.response.get('X-Response-Time');console.log(`${ctx.method} ${ctx.url} - ${rt}`);});// x-response-timeapp.use(async (ctx, next) => {const start = Date.now();await next();const ms = Date.now() - start;ctx.set('X-Response-Time', `${ms}ms`);});// responseapp.use(async ctx => {ctx.body = 'Hello World';});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,运行时间
洋葱圈模型
分析实现
- app.use 用来注册中间件,先收集起来
- 实现 next 机制,即上一个通过 next 触发下一个
- 不涉及 method 和 url 的判断
10-11 中间件原理-代码演示
像 express 一样,在 lib下新建 koa2 文件夹,然后新建 like-koa2.js
然后书写整体的结构
const http = require('http')class LikeKoa2 {constructor() {// 存放中间件的地方this.middlewareList = []}// 注册中间件use(fn) {this.middlewareList.push(fn)// return this 可以链式调用return this}callback() {return (req, res) => {}}// 监听中间件listen(...args) {const server = http.createServer(this.callback())server.listen(...args)}}module.exports = LikeKoa2
分析 next() 方法的实现
// 组合中间件function compose(middlewareList) {return function (ctx) {function dispatch(i) {const fn = middlewareList[i]try {// 这里写 Promise.resolve 的原因是防止未写 async 而引起报错return Promise.resolve(fn(ctx, dispatch.bind(null, i + 1)))} catch (err) {Promise.reject(err)}}return dispatch(0)}}
dispatch 是传入 i 是一个个中间件,首先 dispatch(0) 是执行第一个中间件,然后执行 try catch 一个个的执行后续的中间件。在 try 中,执行 fn 传入 ctx 对标实际执行的 ctx,dispatch.bind 就可以执行下一个中间件,真正执行后续的中间件逻辑
完善 LikeKoa2
const http = require('http')// 组合中间件function compose(middlewareList) {return function (ctx) {function dispatch(i) {const fn = middlewareList[i]try {// 这里写 Promise.resolve 的原因是防止未写 async 而引起报错return Promise.resolve(fn(ctx, dispatch.bind(null, i + 1)))} catch (err) {Promise.reject(err)}}return dispatch(0)}}class LikeKoa2 {constructor() {// 存放中间件的地方this.middlewareList = []}// 注册中间件use(fn) {this.middlewareList.push(fn)// return this 可以链式调用return this}createContext(req, res) {return {req,res}// ctx.query = req.query}handleRequest(ctx, fn) {return fn(ctx)}callback() {const fn = compose(this.middlewareList)return (req, res) => {const ctx = this.createContext(req, res)return this.handleRequest(ctx, fn)}}// 监听中间件listen(...args) {const server = http.createServer(this.callback())server.listen(...args)}}module.exports = LikeKoa2
10-12 总结
尝试用一下
修改之前洋葱圈的代码
const Koa = require('./like-koa2');const app = new Koa();// loggerapp.use(async (ctx, next) => {console.log('onion 1 start');await next();const rt = ctx['X-Response-Time'];console.log(`${ctx.req.method} ${ctx.req.url} - ${rt}`);console.log('onion 1 end');});// x-response-timeapp.use(async (ctx, next) => {console.log('onion 2 start');const start = Date.now();await next();const ms = Date.now() - start;ctx['X-Response-Time'] = `${ms}ms`;console.log('onion 2 end');});// responseapp.use(async ctx => {console.log('onion 3 start');ctx.res.end('This is like koa2')console.log('onion 3 end');});app.listen(3000);
node 如何做中间层
https://coding.imooc.com/learn/questiondetail/y0K5g683WEpXe2QN.html
总结
- 使用 async/await 的好处
- koa2 的使用,以及如何操作 session redis 日志
- koa2 中间件的使用和原理


