1. 如何理解Koa?
一个基于原始http服务和express的框架,它的实现原理很简单,因为把众多应用都外包出去作为中间件使用,所以我们只需要关注它是如何集成中间件的,还有上下文是如何联系的。
2. koa原理
至此,就需要掌握两点:
2.1 中间件机制
首先实现一个函数,内层函数的返回值作为外层函数的参数,类似于: fn1(fn2(fn3(...args))) ,由于请求从外向内,响应自内向外,我们把中间件机制可以用洋葱圈模型形象的表示
实现的方法也有多种,比如 reduce, 嵌套函数等,这里用一种比较简单的写法表示
// 处理同步const compose = (...[first, ...other] => (...args) => {let ret = first(...args)other.forEach(fn=>{ret = fn(ret)})return ret})// 处理异步function compose(middlewares){return dispatch(0)function dispatch(i){const fn = middleware[i]if(!fn){return Promise.resolve()}return Promise.resolve(fn(()=>dispatch(i+1)))}}
2.2 context上下文
context 的作用是简化API,将第一次请求对象贯穿全文可以一直向下传递,同时封装了 req,res的 getter和setter方法, 使用如下:
app.use(ctx=>{ctx.body = 'hello'})
核心代码如下:
// 分别封装request,response,context// request.jsmodule.exports = {get url(){return this.req.url},get method(){return this.req.method.toLowerCase()}}// response.jsmodule.exports = {get body(){return this._body},set body(val){this._body = val}}// context.jsmodule.exports = {get url(){return this.request.url},get body(){return this.response.body},set body(val){this.response.body = val},get method(){return this.request.method}}
声明koa类
// 导入依赖const context = require('./context')const request = reuqire('./reuqest')...class Koa {listen(...args){const server = http.createServer((req,res)=>{let ctx = this.createContext(req, res)this.callback(ctx)res.end(ctx.body)})// ...}// 1. 构建上下⽂, 把res和req都挂载到ctx之上,// 2. 在ctx.req和ctx.request.req同时保存createContext(req, res){const ctx = Object.create(context)ctx.request = Object.create(request)ctx.response = Object.create(response)ctx.req = ctx.request.req = reqctx.res = ctx.response.res = resreturn ctx;}}
2.3 汇总
分别处理完 context 和 compose的实现,实际应用中肯定不是各搞各的,我们把他们合起来应用
- 首先在compose中传入上下文, 这样不管在middleware哪一层,都可以使用到context
compose(middlewares){return function(ctx){ // 改动一: 最外层传参ctxreturn dispatch(0);function dispatch(i){// ...return Promise.resolve(fn(ctx, ()=> dispatch(i+1)) 改动二: 递归传参ctx)}}}
- 其次在 koa.listen方法中加入中间件
listen(...args){const server = http.createServer(async (req,res)=>{// step1: 创建上下文const ctx = this.createContext(req, res)// step2: 合成中间件const fn = this.compose(this.middlewares)// step3: 传入上下文并执行合成await fn(ctx);// step4: 返回响应res.end(ctx.body)})server.listen(...args)}
我们来看看完整的Koa类
const http = require("http");const context = require("./context");const request = require("./request");const response = require("./response");class Koa {constructor() {this.middlewares = []; // 初始化中间件数组},use(middleware) {this.middlewares.push(middleware); // 将中间件加到数组⾥},listen(...args){const server = http.createServer(async (req,res)=>{const ctx = this.createContext(req, res)const fn = this.compose(this.middlewares)await fn(ctx);res.end(ctx.body)})server.listen(...args)},compose(middlewares){return function(ctx){return dispatch(0);function dispatch(i){const fn = middleware[i]if(!fn){return Promise.resolve()}return Promise.resolve(fn(ctx, ()=> dispatch(i+1)))}}},createContext(req, res){const ctx = Object.create(context)ctx.request = Object.create(request)ctx.response = Object.create(response)ctx.req = ctx.request.req = reqctx.res = ctx.response.res = resreturn ctx;}}module.exports = Koa
2.4 题外话:middleware的实现
从compose方法我们可以看出, middleware是一层套一层,所以每个middleware必然有承上启下的作用,即接收上一层ctx, 执行本次任务,next进入下一层,就像下面这样
const mid = async (ctx, next) => {// 来到中间件,洋葱圈左边next() // 进⼊其他中间件// 再次来到中间件,洋葱圈右边};
举个请求拦截的示例:
module.exports = async (ctx, next) => {const {res, req} = ctx;const blackList = ['127.0.0.1'];const ip = getClientIP(req);// 拒绝黑名单中的ip访问if (blackList.includes(ip)){ctx.body = 'not allowed';}else{await next}}function getClientIP(req){return (req.headers['x-forwarded-for'] || // 判断有无反向代理IPreq.connection.remoteAddress || // 判断connection远程IPreq.socket.remoteAddress || // 判断后端socket IPreq.connection.socket.remoteAddress)}
