快來,你想要的koa知識幾乎都在這裡了!

henryzp發表於2018-05-02

之前用koa寫過不少的demo,加一個實際的線上應用,但一直沒有怎麼看過它的原始碼。

這次抽空看了一下原始碼。

它其實只有4個檔案:

  • application.js (主檔案)
  • context.js (建立網路請求的上下文物件)
  • request.js (包裝 koa 的 request 物件)
  • response.js (包裝 koa 的 response物件)

alt

通過package.json檔案,我們可以清楚地看到:

alt

application.js是入口檔案,那麼進去看看吧。

alt

核心方法

  • listen
  • use

基礎用法

const Koa = require('koa');
const app = new Koa();
app.listen(3000);
app.use((ctx, next) => {
      ctx.body = "hello"
})
複製程式碼

listen

alt

就是起了一個服務。

這裡有一個debug模組,可以用它來做一些除錯。(使用前提是你的環境變數設定了DEBUG,不然看不到輸出)

callback函式程式碼:

alt

use

use方法,原始碼給的註釋是:

Use the given middleware fn.

Koa裡面就是通過一個個的中介軟體來構建整個服務。

use方法的實現超級簡單:

alt

上面callback函式中,有一個程式碼:

const fn = componse(this.middleware)
複製程式碼

它就是用來組合所有的中介軟體

中介軟體

alt

比如我們有這樣一段程式碼:

let fn1 = (ctx,next)=>{
    console.log(1);
    next();
    console.log(2);
}
let fn2 = (ctx,next)=>{
    console.log(3);
    next();
    console.log(4);
}
let fn3 = (ctx,next)=>{
    console.log(5);
    next();
    console.log(6);
}
複製程式碼

希望能得到:1、3、5、6、4、2的結果。

這個程式碼比較容易:

let fns = [fn1,fn2,fn3]; 
function dispatch(index){
    let middle = fns[index];
    // 判斷一下臨界點
    if(fns.length === index) return function(){}
    middle({},()=>dispatch(index+1));
}
dispatch(0);
複製程式碼

理解了同步的寫法,當中介軟體的寫法為asyn await時,就好寫了。

function dispatch(index){
    let middle = fns[index];
    if(fns.length === index) return Promise.resolve()
    return Promise.resolve(middle({},()=>dispatch(index+1)))
}
複製程式碼

一起來看一下compose的程式碼吧:

alt

核心邏輯和上面的程式碼差不多,無非是在邏輯判斷上更加地嚴謹一些。

容易犯的錯誤

const Koa = require('koa');
const app = new Koa();
app.listen(3000);
function ajax(){
    return new Promise(function(resolve,reject){
        setTimeout(function(){
            resolve("123");
        },3000)
    })
}
app.use(async (ctx,next)=>{
    console.log(1);
    next();
    console.log(2);
});

app.use(async (ctx, next) => {
    ctx.body = await ajax();
})
複製程式碼

上面的結果是not found,原因是第一個中介軟體那裡沒有await next。

ctx

alt

我們再去看createContext的原始碼實現:

alt

request

request.js

alt

就是對之前req物件重新包裝了一層。

這裡用了高階的語法: get/set,類似Object.definePrototype,主要可以在set的時候做一些邏輯處理。

response

response.js

和request.js的處理方式類似。這裡我摘抄一段body的寫法:

{
    get body() {
        return this._body;
    },
    set body(val) {
        const original = this._body;
        this._body = val;

        // no content
        if (null == val) {
            if (!statuses.empty[this.status]) this.status = 204;
            this.remove('Content-Type');
            this.remove('Content-Length');
            this.remove('Transfer-Encoding');
            return;
        }

        // set the status
        if (!this._explicitStatus) this.status = 200;

        // set the content-type only if not yet set
        const setType = !this.header['content-type'];

        // string
        if ('string' == typeof val) {
            if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text';
            this.length = Buffer.byteLength(val);
            return;
        }

        // buffer
        if (Buffer.isBuffer(val)) {
            if (setType) this.type = 'bin';
            this.length = val.length;
            return;
        }

        // stream
        if ('function' == typeof val.pipe) {
            onFinish(this.res, destroy.bind(null, val));
            ensureErrorHandler(val, err => this.ctx.onerror(err));

            // overwriting
            if (null != original && original != val) this.remove('Content-Length');

            if (setType) this.type = 'bin';
            return;
        }

        // json
        this.remove('Content-Length');
        this.type = 'json';
    }
}
複製程式碼

context

context.js檔案所做的事情就比較有意思了。

alt

它做了一層代理,將request下的一些屬性方法以及response下的一些屬性方法直接掛載在ctx物件上。

譬如之前要通過ctx.request.url來得到請求路徑,現在只要寫成ctx.url即可。

delegate這個庫,我們來簡單看一眼,主要看兩個方法即可:

alt

我們可以再簡化一下:

let proto = {

}
function delateGetter(property,name){
    proto.__defineGetter__(name,function(){
        return this[property][name];
    })
}
function delateSetter(property,name){
    proto.__defineSetter__(name,function(val){
        this[property][name] = val;
    })
}
delateGetter('request','query');
delateGetter('request','method')
delateGetter('response','body');
delateSetter('response','body');
複製程式碼

相信看了之後,對這個實現邏輯有了一個比較清晰的認知。

一些中介軟體的實現

看完koa的原始碼,我們可以知道koa本身非常小,實現地比較優雅,可以通過寫中介軟體來實現自己想要的。

常用的中介軟體大概有:static、body-parser、router、session等。

koa-static

koa-static是一個簡單的靜態中介軟體,它的原始碼在這裡,核心邏輯實現是由koa-send完成,不過我翻了一下,裡面沒有etag的處理。

alt

我們自己也可以寫一個最最簡單的static中介軟體:

const path = require('path');
const util = require('util');
const fs = require('fs');
const stat = util.promisify(fs.stat);
function static(p){
    return async (ctx,next)=>{
        try{
            p = path.join(p,'.'+ctx.path);
            let stateObj = await stat(p);
            console.log(p);
            if(stateObj.isDirectory()){
                
            }else{
                ctx.body = fs.createReadStream(p);
            }
        }catch(e){
            console.log(e)
            await next();
        }
    }
}    
複製程式碼

body-parser

基礎程式碼如下:

function bodyParser(){
    return async (ctx,next)=>{
        await new Promise((resolve,reject)=>{
            let buffers = [];
            ctx.req.on('data',function(data){
                buffers.push(data);
            });
            ctx.req.on('end',function(){
                ctx.request.body = Buffer.concat(buffers)
                resolve();
            });
        });
        await next();
    }
}
module.exports = bodyParser;
複製程式碼

無非Buffer.concat(buffers)會有幾種情況需要處理一下,如form、json、file等。

koa-bodyparser中,它用co-body包裝了一層。

form和json的處理相對比較容易:

querystring.parse(buff.toString()); // form的處理

JSON.parse(buff.toString()); // json的處理
複製程式碼

這裡需要說一下的是,file是如何處理的:

alt

這裡需要封裝一個Buffer.split方法,來得到中間的幾塊內容,再進行切割處理。

Buffer.prototype.split = function(sep){
    let pos = 0;
    let len = Buffer.from(sep).length;
    let index = -1;
    let arr = [];
    while(-1!=(index = this.indexOf(sep,pos))){
        arr.push(this.slice(pos,index));
        pos = index+len;
    }
    arr.push(this.slice(pos));
    return arr;
}
複製程式碼
// 核心實現
let type = ctx.get('content-type');
let buff = Buffer.concat(buffers);
let fields = {}
if(type.includes('multipart/form-data')){
    let sep = '--'+type.split('=')[1];
    let lines = buff.split(sep).slice(1,-1);
    lines.forEach(line=>{
        let [head,content] = line.split('\r\n\r\n');
        head = head.slice(2).toString();
        content = content.slice(0,-2);
        let [,name] = head.match(/name="([^;]*)"/);
        if(head.includes('filename')){
            // 取除了head的部分
            let c = line.slice(head.length+6);
            let p = path.join(uploadDir,Math.random().toString());
            require('fs').writeFileSync(p,c)
            fields[name] = [{path:p}];
        } else {
           fields[name] = content.toString();
        }
    })
}
ctx.request.fields = fields;
複製程式碼

當然像koa-better-body裡面用的file處理,並沒有使用split。它用了formidable

alt

alt

擷取操作都是在multipart_parser.js檔案中處理的。

koa-router

基礎用法

var Koa = require('koa');
var Router = require('koa-router');

var app = new Koa();
var router = new Router();

router.get('/', (ctx, next) => {
  // ctx.router available
});

app
  .use(router.routes())
  .use(router.allowedMethods());
複製程式碼

原理

掘金上有一篇文章:解讀並實現一個簡單的koa-router

alt

我這邊也按自己看原始碼的思路分析一下吧。

router.routes是返回一箇中介軟體:

Router.prototype.routes = Router.prototype.middleware = function () {
  var router = this;

  var dispatch = function dispatch(ctx, next) {
    debug('%s %s', ctx.method, ctx.path);

    var path = router.opts.routerPath || ctx.routerPath || ctx.path;
    var matched = router.match(path, ctx.method);
    var layerChain, layer, i;

    if (ctx.matched) {
      ctx.matched.push.apply(ctx.matched, matched.path);
    } else {
      ctx.matched = matched.path;
    }

    ctx.router = router;

    if (!matched.route) return next();

    var matchedLayers = matched.pathAndMethod
    var mostSpecificLayer = matchedLayers[matchedLayers.length - 1]
    ctx._matchedRoute = mostSpecificLayer.path;
    if (mostSpecificLayer.name) {
      ctx._matchedRouteName = mostSpecificLayer.name;
    }

    layerChain = matchedLayers.reduce(function(memo, layer) {
      memo.push(function(ctx, next) {
        ctx.captures = layer.captures(path, ctx.captures);
        ctx.params = layer.params(path, ctx.captures, ctx.params);
        ctx.routerName = layer.name;
        return next();
      });
      return memo.concat(layer.stack);
    }, []);

    return compose(layerChain)(ctx, next);
  };

  dispatch.router = this;

  return dispatch;
};
複製程式碼

它做的事情就是請求進來會經過 router.match,然後將匹配到的 route 的執行函式 push 進陣列,並通過 compose(koa-compose) 函式合併返回且執行。

像我們寫router.get/post,所做的事就是註冊一個路由,然後往this.stack裡面塞入layer的例項:

alt

alt

另外像匹配特殊符號,如:/:id/:name,它是利用path-to-regexp來做處理的。

看懂上面這些,再結合掘金的那篇原始碼分析基本能搞的七七八八。

總結

Koa的東西看上去好像比較簡單似的,但是還是有很多東西沒有分析到,比如原始碼中的proxy等。

不過根據二八法則,我們基本上只要掌握80%的原始碼實現就行了。

最後的最後

為我的部落格打個廣告,歡迎訪問:小翼的前端天地

相關文章