中间件原理
koa向外暴漏出了两个最重要的方法:
- listen():用于启动服务
- use():用于注册中间件,use字面意思就是用到,用户想要koa做的所有事情都是通过use方法传递给koa的,这些方法统称为中间件。
由koa源码不难发现,koa把所有的中间件串起来组成了一个洋葱圈模型。一般情况下,正常的请求会经过同一中间件两次.当然也不能武断的说就是两次,就是洋葱圈模型,当只走完一个中间件就直接响应返回了(例如鉴权失败时),此时就是一次,也就不是洋葱圈了。
koa-static
用法
- 此中间件的作用是响应静态文件的读取请求
- 两个基本配置:
- root:静态文件所在的目录
- opts:一些参数,例如设置缓存、默认名称
- 一个重要参数:要响应的文件的信息,例如名称,相对root的路径等
- 具体用法参照koa-static
模拟实现
const fs = require('fs')
const path = require('path')
const util = require('util')
const Event = require('events')
const mime = require("mime");
const stat = util.promisify(fs.stat)
class TinyKoaStaitc extends Event {
constructor(root, opts) {
super()
this.root = root
this.opts = opts
}
serve(filepath) {
let _this = this
let opts = Object.assign({}, this.opts)
opts.root = path.resolve(path.join(this.root, filepath))
async function server(ctx, next) {
try {
let stats = await stat(opts.root)
opts.headers.map(item => {
ctx.set(item.key, item.value)
})
ctx.set("Content-Type", `${mime.getType(opts.root)};charset=utf8`);
ctx.set('Content-Length', stats.size)
ctx.set('last-modified', stats.ctime)
ctx.stauts = 200
ctx.body = fs.createReadStream(opts.root)
} catch (err) {
ctx.status = 404
_this.emit('static-server-error', util.inspect(err))
}
}
return server
}
}
module.exports = TinyKoaStaitc
fs.stat是一个异步方法,用util.promisify包装为一个返回promise的方法,当然stat也有其同步的形式。此处千万要注意事件循环机制,如果promisify的stat没有await而是直接在then回调中写响应的话,返给客户端的永远时404
调用:
const Koa=require('koa')
// const Koa=require('./src/tiny-koa.js')
const debug=require('debug')('app')
const koaStatic=require('./src/middleware/tiny-koa-static.js')
const staticServer=new koaStatic('./src/static',{
headers:[
{
key:'cache-control',
value:'max-age=10,private'
}
]
})
const app=new Koa()
const port=9001
app.use(staticServer.serve('./template.html'))
app.listen(port,(err)=>{
// console.log(process.env)
debug(`start at ${port}`)
})
staticServer.on('static-server-error',(error)=>{
console.log(error);
})
koa-bodyparser
用法
模拟实现
const qs = require("querystring");
const Event = require('events')
class TinyKoaBodyParser extends Event {
constructor() {
super()
}
parser() {
function parser(ctx, next) {
let body = {}
let buffers = []
ctx.req.on('data', chunk => {
buffers.push(chunk)
})
ctx.req.on('end', _ => {
let bufs = Buffer.concat(buffers).toString()
let contentType = ctx.get("Content-Type").toLowerCase();
if (contentType === "application/x-www-form-urlencoded") {
ctx.request.body = querystring.parse(data);
} else if (contentType === "applaction/json") {
// 如果是 json,则将字符串格式的对象转换成对象赋值给 ctx.request.body
ctx.request.body = JSON.parse(data);
}
next()
})
}
}
}
由于此中间件用于往request.body上挂载请求体的,要让后续的中间件都能从body上取到请求体,next() 必须等待挂载结束后才能执行,挂载过程应该是一个可以await的promise,修改代码如下:
parser() {
return async function parser(ctx, next) {
await new Promise((resolve, reject) => {
let buffers = []
ctx.req.on('data', chunk => {
buffers.push(chunk)
})
ctx.req.on('end', _ => {
let bufs = Buffer.concat(buffers).toString()
let contentType = ctx.get("Content-Type").toLowerCase();
if (contentType === "application/x-www-form-urlencoded") {
ctx.request.body = querystring.parse(data);
} else if (contentType === "applaction/json") {
// 如果是 json,则将字符串格式的对象转换成对象赋值给 ctx.request.body
ctx.request.body = JSON.parse(data);
}
resolve()
})
})
await next()
}
}
koa-session
用法
const CONFIG = {
key: 'koa:sess', // 设置cookie的id
maxAge: 86400000, // cookie的有效期
autoCommit: true, /** (boolean) automatically commit headers (default true) */
overwrite: true, /** (boolean) can overwrite or not (default true) */
httpOnly: true, /** (boolean) httpOnly or not (default true) */
signed: true, // 是否签名
rolling: false, /** (boolean) Force a session identifier cookie to be set on every response. The expiration is reset to the original maxAge, resetting the expiration countdown. (default is false) */
renew: false, /** (boolean) renew session when session is nearly expired, so we can always keep user logged in. (default is false)*/
};
// 注册koa-session中间件
app.use(session(CONFIG, app));
app.use(ctx => {
// ignore favicon
if (ctx.path === '/favicon.ico') return;
let n = ctx.session.views || 0; //从session上取值
ctx.session.views = ++n; //给保存在session上的变量赋值
ctx.body = n + ' views';
});
原理分析
由于http协议是无状态的,每一次响应请求时服务器是不能知道请求来源的身份,除非请求自报家门,服务器分析相关的信息后识别出请求出自谁。所以在第一次响应请求时,服务器就额外给客户端颁发一把钥匙,服务器自己在某处(db、redis…)存一个只有这个钥匙能打开的宝箱,下次此客户端再发请求时,会自动带上钥匙,如果服务器想要知道是谁再访问或者访问者的相关信息,只需要拿着钥匙去开对应的宝箱即可。因此每个客户端都对应着一把钥匙,也对应着一个宝箱。这个钥匙就是cookie,这个宝箱就是session。