04月27, 2016

Koa 2.0 使用与内部分析

Koa 一个基于中间件的极小的 Node HTTP Server

目前已发布 2.0 版本,从这里可以看到相关信息 Koa 2.0 【注:以下 Koa 特指 Koa 2.0】

Koa 支持三种不同的中间件

  • common function
  • async function (目前需要 babel 支持)
  • generatorFunction

看下 common function 的例子感受一下

安装 Koa

npm install koa@next

新建 index.js

const Koa = require( "koa" );
const app = new Koa();

// x-response-time
app.use( ( ctx, next ) => {
    var start = new Date;
    return next().then( () => {
        var ms = new Date - start;
        ctx.set( "X-Response-Time", ms + "ms" );
    } );
} );

// logger
app.use( ( ctx, next ) => {
    const start = new Date();
    return next().then( () => {
        const ms = new Date() - start;
        console.log( `${ctx.method} ${ctx.url} - ${ms}` );
    } );
} );

// response
app.use( ctx => {
    ctx.body = "Hello Koa";
} );

app.listen( 3000 );

启动执行

node index.js

访问127.0.0.1:3000 可以看到输出的文本及响应头的x-response-time,控制台打印的时间

那么Koa是如何做的?翻开它的源码只有四个文件,每个文件代码量只有几百行而已,这也增加了我们学习它的欲望

Koa的小巧一方面源于它本身只处理和提供核心的功能(http请求与响应),一方面用了一些实用的模块,Koa本身需要的大部分功能也抽成了公用模块,所以整体看着也很清晰。同时模块基本也比较简单,看它的用法就能了大概的用意

其中 request.js 与 response.js 用了大量的 set、get 特性,可以看出基本就是原始 req、res 的辅助类,可以让我们更方便的操作请求与响应。

另外对于里面的Vary头与Content-Length设置,推荐几篇相应说明文章

HTTP 协议中 Vary 的一些研究

HTTP 协议中的 Transfer-Encoding

HTTP协议头部与Keep-Alive模式详解

好了再来看下context.js 主要是通过 delegates 模块代理了一些 request.js 与 response.js 的功能,就是上下文关联的请求与响应操作

现在说下主角 application.js,主要的功能实现都在这里

初始化时(constructor) 进行一些基础设置

use 函数的调用,其实就是往数组里堆砌传入的中间件函数

listen创建服务监听执行响应函数体

这里说下Koa最有特色的地方,首先是上下文 context 的传递

createContext(req, res) {
    const context = Object.create(this.context);
    const request = context.request = Object.create(this.request);
    const response = context.response = Object.create(this.response);
    context.app = request.app = response.app = this;
    context.req = request.req = response.req = req;
    context.res = request.res = response.res = res;
    request.ctx = response.ctx = context;
    request.response = response;
    response.request = request;
    context.onerror = context.onerror.bind(context);
    context.originalUrl = request.originalUrl = req.url;
    context.cookies = new Cookies(req, res, {
      keys: this.keys,
      secure: request.secure
    });
    context.accept = request.accept = accepts(req);
    context.state = {};
    return context;
  }

可以看到所有请求相关的都挂载到了上下文环境对象上context,给内部中间件进行传递,而辅助response、request对象也进行了相关功能的挂载,致使任何时候都能得到相应的对象进行处理

对于上面中间件的处理,也就是Koa最核心的地方,还是贴段代码

callback() {
    const fn = compose(this.middleware);

    if (!this.listeners("error").length) this.on("error", this.onerror);

    return (req, res) => {

      res.statusCode = 404;
      const ctx = this.createContext(req, res);
      onFinished(res, ctx.onerror);
      fn(ctx).then(() => respond(ctx)).catch(ctx.onerror);
    };
  }

这里middleware数组中的中间件用compose( koa-compose模块)方法进行了封装

它的每一个中间件都返回一个Promise,只有返回执行next(),才进行下一个中间件的处理,系统在最外层挂载了默认的respond 输出函数,所以Koa有了这种类似级联的处理

上面说的可能还是不太清楚,下面是提炼的精简代码感受下

// koa.js
"use strict";

const http = require( "http" );

module.exports = class Koa {
  constructor() {
    this.middleware = []
  }

  use( fn ) {
    this.middleware.push( fn )
  }

  listen() {
    const server = http.createServer( this.callback() );
    server.listen.apply( server, arguments );
  }

  callback() {
    const fn = this.compose( this.middleware );
    return ( req, res ) => {
      const ctx = this.createContext( req, res );
      fn( ctx ).then( () => respond( ctx ) )
    }
  }

  createContext( req, res ) {
    const context = {} //context上下文
    context.request = {} //request help
    context.response = {} //response help
    context.req = req
    context.res = res
    return context
  }

  compose( middleware ) {
    return function ( context, next ) {
      let index = -1

      function dispatch( i ) {
        index = i
        const fn = middleware[ i ] || next
        if ( !fn ) return Promise.resolve()
        return Promise.resolve( fn( context, function next() {
          return dispatch( i + 1 )
        } ) )
      }
      return dispatch( 0 );
    }
  }
}

function respond( ctx ) {
  ctx.res.end( ctx.body );
}

大家可以随意建一个test.js文件,执行查看下

const Koa = require( "./koa" );
const app = new Koa();

app.use( ( ctx, next ) => {
  const start = new Date();
  return next().then( () => {
    const ms = new Date() - start;
    console.log( `time: ${ms}` );
  } );
} );

app.use( ctx => {
  ctx.body = "Hello My Test";
} );

app.listen( 3000 );

其实Koa 中间件执行的模型大概是下面这个样子

fn(
    return promise.then( {
        return promise.then( {
            return promise.then( {
                return promise.then( {

                } )
            } )
        } )
    } )
).then( respond )

多个中间件的话依次从外向内执行,触发后又依次从内向外执行,最后触发最外层的response help 函数

知道了处理原理后,我们在Koa的基础上开发一个路由中间件

要做的功能很简单,匹配到提供的路由执行相应controller方法,没匹配到时按照访问url规则自动匹配controller

以下提供了一个没有任何处理的简单路由

// router.js
module.exports = {
    _list: {},
    get list() {
        return this[ "_list" ]
    },
    set list( obj ) {
        var url = obj.url;
        var cb = obj.cb;
        var list = this[ "_list" ];

        if ( url.indexOf( "*" ) != -1 ) {
            list[ "wildcard" ] = list[ "wildcard" ] || {};
            list[ "wildcard" ][ url ] = cb;
        } else {
            list[ url ] = cb;
        }
    },
    prefix: null,
    get: function ( url, cb ) {
        this.list = {
            url: url,
            cb: cb
        };
    },
    getMethod: function ( ctx ) {
        var paths = ctx.path.slice( 1 ).split( "/" );
        var methodName = paths.splice( -1, 1 )[ 0 ];
        var controll = ( this.prefix || "./" ) + paths.join( "/" );

        var ins = require( controll );
        return ins[ methodName || "index" ]
    },
    wildcard: function ( ctx ) {
        var method;
        var wildcard = this.list[ "wildcard" ];
        for ( var key in wildcard ) {
            var url = key.replace( /\*/g, "[^/]*" );
            if ( new RegExp( url ).exec( ctx.url ) ) {
                method = wildcard[ key ];
                break;
            }
        }
        return method;
    },
    routers: function () {
        var list = this.list;
        var getMethod = this.getMethod.bind( this );
        var wildcard = this.wildcard.bind( this );

        return ( ctx, next ) => {
            var method;

            if ( ctx.url == "/favicon.ico" ) {
                return next();
            }

            if ( ctx.url in list ) {
                method = list[ ctx.url ];
            } else {
                method = wildcard( ctx );
                if ( !method ) {
                    method = getMethod( ctx );
                }
            }

            return Promise.resolve( method( ctx ) ).then( () => {
                return next()
            } )
        }
    }
}

新建 test.js测试下

const Koa = require( "koa" );
const app = new Koa();

var router = require( "./router" );

router.get( "/a/b", ( ctx ) => {
    ctx.body = "/a/b";
} )

router.get( "/a/*", ( ctx ) => {
    ctx.body = "/a/*";
} )

app.use( router.routers() );

app.listen( 3000 );

可以看到匹配到的路径正常输出,同时假设当前目录下有 b/a.js 文件

访问 localhost:3000/b/a/x 时会自动查找a.js 的x方法

本文链接:https://gmiam.com/post/koa-use-analyse.html

-- EOF --

Comments

评论加载中...

注:如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理。