手写一个简版的koa框架

479 阅读6分钟

1. Koa 概述

Koa 是一个新的 web 框架, 致力于成为 web 应用和 API 开发领域中的一个更小、更富有 表现力、更健壮的基石。koa是Express的下一代机遇node.js 的web框架

Koa2 完全使用Promse并配合async 来实现异步

1.1 为什么需要koa框架,他解决了什么问题

我们首先来看下原生的http模块

const http = require('http')
const fs = require('fs')
const server = http.createServer((request, response) => {
    // response.end('hello ...')
    const { url, method ,headers} = request
    if (url === '/' && method === 'GET'){
        // 静态页面服务
        fs.readFile('index.html',(err,data) => {
            response.statusCode = 200
            response.setHeader('Content-Type','text/html')
            response.end(data)
        })
    }else if(url === '/users' && method === 'GET'){
        // Ajax服务
        response.writeHead(200,{
            'Content-Type''application/json'
        })
        response.end(JSON.stringify({
            name : 'laowang'
        }))
    }else if(method === 'GET' && headers.accept.indexOf('image/*') !== -1){
        // 图片文件服务
        fs.createReadStream('./'+url).pipe(response)
    }

})
server.listen(3000)

我们看上面原生的http 模块其实已经够强大啦,静态页面服务,Ajax 服务,图片服务都能满足,后端不就是做这些事件的呀!仔细观察会发现一以下问题

  1. 路由问题if else => 过多 (可以用策略模式解决,比如vue-router react-router 的实现都用了策略模式)

  2. 重复代码比较多 比如statusCode 赋值问题 Content-Type 设置问题

  3. 请求体解析与响应体包装,原始代码过于臃肿

  4. 请求的解析源代码太多

  5. api不优雅(比如response.end()为什么是end,不是很了解node的小伙伴是不会明白的,不能名知意)

  6. 切面描述不方便(AOP 切面编程)比如一个转账的逻辑在用户处理前需要鉴权,在操作前后需要日志 原生的不能很好的处理这个

    面向切面编程一般分为语言级,和框架级的,前端一般都是框架级别的。例如:axios vue路由首位,vue 生命周期钩子。解决方式引入洋葱圈模型

2. Koa2 的特点

  1. 轻量,无捆绑
  2. 中间价架构
  3. 优雅的API 设计
  4. 增强的错误处理

安装 npm i koa -S

中间件机制,请求,响应处理

中间件机制主要看请求响应之间

const Koa = require('koa')
const app = new Koa()

// 通过引入洋葱圈模型的方式实现AOP
app.use(async (ctx, next) => {
   // ctx 上下文 
    const start = Date.now();
    await next() //  执行下一行
    const end = Date.now();
    // 计算时间搓,耗时差值
    console.log(`请求${ctx.url}耗时间${parseInt(end - start)}ms`)
})
app.use(async (ctx, next) => {
    const expire = Date.now() + 100;
    // 循环延迟一段时间在执行
    while (Date.now() < expire) {
        console.log("aaaa")
        // 优雅的api
        ctx.body = { name'tom' }
    }

})
app.listen(3000() => {
    console.log("端口起于3000")
})

// 通过洋葱圈模型和 优雅api的提供让我将原生的node.js 进行规模的应用的可能

/* 
koa2 没什么没看见引入路由?
koa2 的前身是express 是内置路由的,但是koa2 把路由拆分出去啦,因为中间件引入策略模式可以很轻松的实现路由 中间件
*/

3. Koa 原理源码分析

看下面原生的http 代码 考虑如何去封装

const http = require('http')
const server = http.createServer((req, res)=>{
    res.writeHead(200)
    res.end('hi mingming')
})
server.listen(3001,()=>console.log('监听端口3000')
})
  1. 考虑封装 要求看不到具体实现,而且可以设置业务逻辑 其实业务逻辑只有(req, res)=>{ res.writeHead(200) res.end('hi mingming') }

仿照koa2 的代码 去封装比如自己写个koa;就这样使用(伪代码)

const mykoa = require('./mykoa')
const app = new mykoa()

app.use((req, res) => {
    res.writeHead(200)
    res.end('hi mingming')
})

app.listen(3000() => {
    console.log("server at 3000")
})

封装的代码: 目标是用更简单化,流程化,模块化的方式实现回调部分

const http  = require('http');
class mykoa {
    listen(...args){
        const server = http.createServer((req,res)=>{
            this.callback(req,res)
        })
         // 开启端口
        server.listen(...args)
    }
    use(callback){
        this.callback = callback
    }
}
module.exports = mykoa

目前为止mykoa 只是个马甲,要真正实现目标还需要引入上下文(context) 和中间件机制(middleware)

3.1 koa 上下文context

Koa 为了能够简化API引入上下文context的概念,将原始请求对象req和响应对象res 封装并且挂在到context 上,并且在context上设置ge tter和setter,从而简化操作。

koa源码

使用方法接近koa

app.use(ctx=>{
  ctx.body="hehe"
})

3.2 知识储备getter/settet 方法

const wkm={
    msg:{
        name:'wkm'
    },
    get name(){
        return this.msg.name
    },
    set name(val){
        this.msg.name=val
    }
}
console.log(wkm.name)
// 修改 name
wkm.name="wkmhaha"
console.log(wkm.name)
console.log("aaa")

3.3 参照源码封装request, response 和context

// request js
module.exports = {
  get url() {
    return this.req.url;
  },
    get method(){
    return this.req.method.toLowerCase()
  }
};

// response js
module.exports = {
  get body() {
    return this._body;
  },
  set body(val) {
    this._body = val;
} };
// context.js
module.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
} };

// mykoa.js
const http  = require('http');
let context = require('./context');
let request = require("./response");
let response = require("./response");
class mykoa {
    listen(...args){
        const server = http.createServer((req,res)=>{
            // 创建上下文
            let ctx = this.createContext(req,res)
            this.callback(ctx)
            // 响应
            res.end(ctx.body)
           
        })
        server.listen(...args);
    }
    use(callback){
       console.log("回调函数11111",callback);
        this.callback = callback
    }
    // 构建上下文,把res和req 挂载在ctx 之上,并且在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 =req;
        ctx.res = ctx.response.res = res;
        console.log(ctx);
        return ctx
    }
}
module.exports = mykoa

上面代码实现啦上下文,下面我们来开中间件的封装

4. 实现koa的中间件机制

Koa中间件机制:koa中间件机制就是函数式 组合compose的概念,将一组需要顺序执行的函数符合成一个函数,外层函数的参数实际是内层函数的返回值。洋葱圈模型可以形象表示这种极致,是源码中的精髓和难点

4.1 中间件知识储备:函数组合

const add  = (x,y) =>x+y
const square = z => z*z;
const fn= (x,y)=> square(add(x,y)) 
console.log(fn(1,1))

上面就算是两次函数组合调用,我们可以把他合成一个函数

const add  = (x,y) =>x+y
const square = z => z*z;
const compose = (fn1,fn2) =>(...args)=>fn2(fn1(...args))
const fn = compose(add,square)
console.log("susess",fn(1,1))

多个函数组合:中间件的数目是不固定的,我们可以用数组来模拟

 
const compose = (...[first,...other]) => (...args) => {
  let ret = first(...args)
  other.forEach(fn => {
    ret = fn(ret)
  })
return ret }
const fn = compose(add,square)
console.log(fn(12))

异步的中间件:上面的函数都是同步的,挨个遍历即可,如果是异步函数,那么他就应该是个promise,我们要支持async+await 的中间件,所以我们要等待异步结束后在执行下一个中间件

function compose(middlewares) {
  return function() {
return dispatch(0);
// 执行第0个
function dispatch(i) {
      let fn = middlewares[i];
      if (!fn) {
        return Promise.resolve();
      }
      return Promise.resolve(
        fn(function next() {
// promise完成后,再执行下一个
          return dispatch(i + 1);
        })
); }
}; }
async function fn1(next) {
  console.log("fn1");
  await next();
  console.log("end fn1");
}
async function fn2(next) {
  console.log("fn2");
  await delay();
  await next();
  console.log("end fn2");
}
function fn3(next) {
  console.log("fn3");
}
function delay() {
  return new Promise((reslove, reject) => {
    setTimeout(() => {
      reslove();
}, 2000); });
 }
const middlewares = [fn1, fn2, fn3];
const finalFn = compose(middlewares);
finalFn();
 
```js

### 4.2 函数组合应用到koa 中

```js
// mykoa.js
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')
class mykoa {
    constructor(){
        this.middlewares = []
    }
    listen(...args) {
        const server = http.createServer(async (req, res) => {

            // 创建上下文
            const ctx = this.createContext(req, res)
            const fn = this.compose(this.middlewares)
            await fn(ctx)
            // this.callback(req,res)
            // this.callback(ctx)
            res.end(ctx.body)
        })
        server.listen(...args)
    }
    // use(callback) {
    //     this.callback = callback
    // }
    use(middleware){
        this.middlewares.push(middleware)
    }
    createContext(req, res) {
        const ctx = Object.create(context)
        ctx.request = Object.create(request)
        ctx.response = Object.create(response)

        ctx.req = ctx.request.req = req
        ctx.res = ctx.response.res = res
        return ctx
    }
    compose(middlewares) {
        return function (ctx) {
            return dispatch(0)
            function dispatch(i) {
                let fn = middlewares[i]
                if (!fn) {
                    return Promise.resolve()
                }
                return Promise.resolve(
                    fn(ctx, function next() {
                        return dispatch(i + 1)
                    })
                )
            }
        }
    }
}

module.exports = mykoa
// index.js
const mykoa = require('./mykoa')
const app = new mykoa()

const delay = () => new Promise(resolve => setTimeout(() => resolve() ,2000));
app.use(async (ctx, next) => {
  ctx.body = "1";
  await next();
  ctx.body += "5";
});
app.use(async (ctx, next) => {
  ctx.body += "2";
  await delay();
  await next();
  ctx.body += "4";
});
app.use(async (ctx, next) => {
  ctx.body += "3";
});

app.listen(4000() => {
    console.log("server at 4000")
})

5 koa 路由策略模式

// router.js
class Router {
    constructor() {
      this.stack = [];
    }
  
    register(path, methods, middleware) {
      let route = {path, methods, middleware}
      this.stack.push(route);
    }
    // 现在只支持get和post,其他的同理
    get(path,middleware){
      this.register(path, 'get', middleware);
    }
    post(path,middleware){
      this.register(path, 'post', middleware);
    }
    routes() {
      let stock = this.stack;
      return async function(ctx, next) {
        let currentPath = ctx.url;
        console.log("url",ctx.url,ctx.method)
        let route;
  
        for (let i = 0; i < stock.length; i++) {
          let item = stock[i];
          if (currentPath === item.path && item.methods.indexOf(ctx.method) >= 0) {
            // 判断path和method
            route = item.middleware;
            break;
          }
        }
  
        if (typeof route === 'function') {
          route(ctx, next);
          return;
        }
  
        await next();
      };
    }
  }
  module.exports = Router;

const mykoa = require('./mykoa');
const app = new mykoa();


const Router = require('./router')
const router = new Router()

router.get('/index'async ctx => { ctx.body = 'index page'; });
router.get('/post'async ctx => { ctx.body = 'post page'; });
router.get('/list'async ctx => { ctx.body = 'list page'; });
router.post('/index'async ctx => { ctx.body = 'post page'; });

// 路由实例输出父中间件 router.routes()
app.use(router.routes());

app.listen(3000)

以上就简单的实现个简版的koa,如果不对的的地方希望大家指点呀