手撸一个Koa框架

为了更好的了解Koa实现的原理,我们会自己手写一个带核心功能的Koa框架。
我们先看一下如果使用原生的node.js api 是如何实现一个http server 的。

const http = require('http')

const app = http.createServer((req, rsp) => {
    rsp.writeHead(200, { 'Content-Type': 'text/plain'})
    rsp.end('Hello world')
})

app.listen(3000, () => {
    console.log('listening at port 3000')
})

可以看到,createServer 方法的callback 中提供两个参数req 和 rsp 分别代表 请求体和返回体,我们可以通过对rsp返回体对象进行设置header返回头的各种参数,最后返回的数据会放在end 方法当中,可以是普通的字符串,json,xml,图片或其他数据流。

实现koa框架的步骤

实现一个koa框架主要需要用到以下的文件:

  1. request.js, 主要封装原生nodejs中urlmethod的getter参数.

  2. response.js, 主要提供了koa中的body属性的getter 和setter。

  3. context.js,对应koa中ctx参数类型 。原生nodejs的api中,主要通过req和rsp操纵请求参数和返回内容。Koa中把请求和返回都融合到了ctx对应的context类中。

  4. lzh_koa.js,对应koa中Koa类,是这个自己实现的框架的主类,提供各种主要的方法如 listen,use以及内部的compose方法,用于把koa调用use方法之后的各个中间件融合到一起的功能。

开始实现

1.request.js

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

2.response.js

module.exports = {
    get body() {
        return this._body
    },
    set body(val) {
        this._body = val
    }
}

3.context.js, 支持从上面自己封装的request和response中提取以及设置对应的参数

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
    } 
};

4.lzh_koa.js

compose 函数

这个类当中有一个高阶函数compose,用于把middlewares 里边的每一个中间件负责装配到一起。

compose(middlewares) {
        return function (ctx) {
          // 执行第0个
            return dispatch(0);
            function dispatch(i) {
                let fn = middlewares[i];
                if (!fn) {
                    return Promise.resolve();
                }
                return Promise.resolve(fn(ctx, function next() {
                    // promise完成后,再执行下一个
                    return dispatch(i + 1);
                })
                );
            }
        };
    }

首先middlewares是传进来的中间件的数组, 而每一个中间件都是以闭包的形式 (ctx, next) => { // 中间件实现代码} 传进来的,所以可以把它看成是一个装满我们需要的中间件的数组。

我们会使用递归的方式完成中间件函数的装配,之所以考虑使用递归的方式因为每一个next 参数传入的值刚好是下个dispatch的返回值, 作为递归体function dispatch(i)用于按顺序递归调用传进来的中间件。而递归结束条件则是当每一个中间件都被Promise装配过了,如下所示:

let fn = middlewares[i];
if (!fn) { return Promise.resolve() }

为了方便理解,下面是一个具体些的例子,假如有三个中间件分别是fn1 fn2 fn3 分别被装进middlewares数组里边,那被compose数组装配完后的的函数就会长成下边这个样子。

return Promise.resolve(fn1(ctx, function next() {
             // promise完成后,再执行下一个
            return  Promise.resolve( fn2(ctx, function next () {
                   return Promise.resolve( fn3(ctx, function next () {} ))
                        }))
         }))

createContext 函数

// 构建上下文
    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
    }

可以看到这个函数里边用到了我们之前声明的 context request response 在这里用上了,主要我们会把 req 和 res 集中到ctx里边进行处理。

use 函数

 use(middlewares) {
        this.middlewares.push(middlewares)
    }

use 函数唯一的功能就是帮我们把中间件都加到middlewares 数组当中。

listen 函数

listen(...args) {
        const server = http.createServer(async (req, res) => {
            // 
            const ctx = this.createContext(req, res)
            
            const func = this.compose(this.middlewares)

            await func(ctx)

            res.end(ctx.body)
            // this.callback(req, res)
        })
        server.listen(...args)
  }

仿照Koa的写法,最后会有一个listen 函数对服务器端口进行监听,这里会首先把请求和返回体都合并到ctx对象中,再使用之前做好的compose方法对中间件数组进行合并供后面的统一调用 ( await func(ctx))。

最后一步就是调用res.end(ctx.body) 把相应返回了。

测试使用

最后我们起一个新的js文件进行测试使用.

const Koa = require('./lzh_koa')
const app = new Koa()
const delay = () => Promise.resolve(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 += "6";
});
app.use(async (ctx, next) => {
    ctx.body += "3";
});

app.listen(3000, () => {
    console.log('listen at 3000 host')
})

输出顺序为: 12365, 与Koa框架实现一致。

说在最后

至此我们自己就实现了一个带核心功能的Koa 框架, 当然里边还有很多细节的功能例如设置ctx返回类型等没有实现,但都可以通过扩展reponse和request类达到。

Demo地址放在 https://github.com/lzhlewis2015/lzh_koa .

你可能感兴趣的:(手撸一个Koa框架)