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/await
async 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 的值
// 改写 readFileData
async 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 handler
onerror(app)
处理 postData 即 post 数据,这里有 enableTypes 可以接收很多种数据
json 可以把 postData 的数据变为 JSON 格式的数据
// middlewares
app.use(bodyparser({
enableTypes:['json', 'form', 'text']
}))
app.use(json())
logger() 日志
koa-static、views 和前端相关
获取当前请求的耗时
// logger
app.use(async (ctx, next) => {
const start = new Date()
await next()
const ms = new Date() - start
console.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.query
ctx.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.body
const query = ctx.query
ctx.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({
// 配置cookie
cookie: {
path: '/',
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000
},
// 配置 redis
store: 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({
...
// 配置 redis
store: 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;`
// 返回 promise
return 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.author
const 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 就是要删除博客的id
const 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.request
body.author = ctx.session.username
const 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.username
const 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.body
const data = await login(username, password)
if (data.username) {
req.session.username = data.username
req.session.realname = data.realname
ctx.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_ENV
if (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();
// logger
app.use(async (ctx, next) => {
await next();
const rt = ctx.response.get('X-Response-Time');
console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});
// x-response-time
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});
// response
app.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();
// logger
app.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-time
app.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');
});
// response
app.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 中间件的使用和原理