Koa原始碼分析

_xiadd_發表於2018-01-20

node基礎

const http = require('http');

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});
server.listen(3000);
複製程式碼

nodejs主要的原生模組http,fs,path;基於http模組http模組的createServer方法建立一個http.server例項,函式會自動繫結到request事件,最後server監聽3000埠,處理請求。request事件包括兩個引數req,res;req可寫流,res可讀流,通過req請求,res迴應。

事件驅動四類解決方式

回撥
var i = 0;//記錄sleep()函式呼叫的次數
function sleep(ms, callback){
    setTimeout(function(){
        if(i < 2){
            i++;
            callback("finish", null);  
        }else{
            callback(null, new Error('i>2'));
        }
    }, ms);
}
//第一次呼叫
sleep(1000, function (val, err) {
    if (err) console.log(err.message);
    else {
        console.log(val);
        //第二次呼叫
        sleep(1000, function (val, err) {
            if (err) console.log(err.message);
            else {
                console.log(val);
                //第三次呼叫
                sleep(1000, function (val, err) {
                    if (err) console.log(err.message);
                    else {
                        console.log(val);
                    }
                });
            }
        });
    }
});//輸出結果分別為:finish,finish,i>2。
複製程式碼
事件監聽
var i = 0;
var events = require('events');
var emitter = new events.EventEmitter();//建立事件監聽器的一個物件
function sleep(ms) {
    var emitter = new require('events')();
    setTimeout(function () {
        console.log('finish!');
        i++;
        if (i > 2) emitter.emit('error', new Error('i>2'));
        else emitter.emit('done', i);
    }, ms);
}
var emit = sleep(1000);
emit.on('done',function (val) {
    console.log('成功:' + val);
})
emit.on('error',function(err){
    console.log('出錯了:' + err.message);
})
複製程式碼

對於callback的改進,使用事件監聽的形式進行操作。每次呼叫非同步函式都會返回一個EvetEmitter物件。在函式內部,可以根據需求來觸發不同的事件。在函式外部對不同的事件進行監聽,然後做出相應的處理。

promise
var i = 0;
function sleep(ms) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log('finished');
            i++;
            if (i > 2) reject(new Error('i>2'));
            else resolve(i);
        }, ms);
    })
}

sleep(1000).then(function (val) {
    console.log(val);
    return sleep(1000)
}).then(function (val) {
    console.log(val);
    return sleep(1000)
}).then(function (val) {
    console.log(val);
    return sleep(1000)
}).catch(function (err) {
    console.log(err.message);
})
複製程式碼

promise類似只觸發兩個事件resolve和reject的event物件,但是不同的是事件具有即時性,觸發之後這個狀態後事件就消失了。將原本層層巢狀的回撥函式展開,視覺效果更好。

generator->async/await
var i = 0;
function sleep(ms) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log('finished');
            i++;
            if (i >= 2) reject(new Error('i>2'));
            else resolve(i);
        }, ms);
    })
}

(async function () {
    try {
        var val;
        val = await sleep(1000);
        console.log(val);
        val = await sleep(1000);
        console.log(val);
        val = await sleep(1000);
        console.log(val);
    }
    catch (err) {
        console.log(err.message);
    }
} ())
複製程式碼

await關鍵字只能在async函式中才能使用,也就是說你不能在任意地方使用await。await關鍵字後跟一個promise物件,函式執行到await後會退出該函式,直到事件輪詢檢查到Promise有了狀態resolve或reject 才重新執行這個函式後面的內容。

Koa

Koa is a new Web framework designed by the team behind Express, which aims to be a smaller, more expressive, and more robust foundation for Web applications and APIs.

Koa 是一個新的 web 框架,由 Express 幕後的原班人馬打造, 致力於成為 web 應用和 API 開發領域中的一個更小、更富有表現力、更健壯的基石。 通過利用 async 函式,Koa 幫你丟棄回撥函式,並有力地增強錯誤處理。 Koa 並沒有捆綁任何中介軟體, 而是提供了一套優雅的方法,幫助您快速而愉快地編寫服務端應用程式。

koa Hello World
const Koa = require('koa');
const app = new Koa();

// logger

app.use(async (ctx, next) => {
  await next();
  const rt = ctx.response.get('X-Response-Time');
  console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});

// x-response-time

app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  ctx.set('X-Response-Time', `${ms}ms`);
});

// response

app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);
複製程式碼

koa只封裝上下文,請求,響應的中介軟體容器,koa1基於generator,koa2基於async/await實現瀑布流式開發方式。

Koa特性

洋蔥圈模型

基於AOP面向切面程式設計

面向切面程式設計(AOP)是一種非侵入式擴充物件、方法和函式行為的技術。通過 AOP 可以從“外部”去增加一些行為,進而合併既有行為或修改既有行為。

舉例--果園生產

原始流程

Koa原始碼分析
AOP後流程
Koa原始碼分析

koa洋蔥圈切面示範
var koa = require('koa');
var app = new koa();

app.use(async (ctx, next) => {
  console.log(1)
  await next();
  console.log(5)
});

app.use(async (ctx, next) => {
  console.log(2)
  await next();
  console.log(4)
});

app.use(async (ctx, next) => {
  console.log(3)
  ctx.body = 'Hello World';
});
// 訪問http://localhost:3000
// 列印出1、2、3、4、5
複製程式碼
koa洋蔥圈圖

koa洋蔥

async/await相對於express的主流promise寫法是更好的callback hell解決方式,同時,結合使用trycatch更方便定位錯誤。

koa2的洋蔥圈設計模型相對express來說方便處理後置邏輯,express是線性的處理流程,在中介軟體的編寫上,koa更簡潔,非同步越多,優勢越明顯。

koa原始碼

koa目錄
.
├── application.js
├── context.js
├── request.js
└── response.js
複製程式碼
  • application.js是框架入口檔案,封裝中介軟體處理流程,暴露出公用api
  • context.js代理了req和res的部分方法,處理應用上下文。
  • request.js對原生request再封裝,處理請求。
  • response.js對原生response再封裝,處理響應。
application.js解讀

入口

...
const response = require('./response');
const compose = require('koa-compose');
const context = require('./context');
const request = require('./request');
const Emitter = require('events');
...
// 暴露出來繼承自event.Emitter的方法,提供給使用者新建例項
module.exports = class Application extends Emitter {
    constructor() {
        super();
        this.proxy = false; // 是否信任proxy header,預設false 
        this.middleware = [];   // 儲存通過app.use(middleware)註冊的中介軟體
        this.subdomainOffset = 2;   //配置忽略的.subdomains
        this.env = process.env.NODE_ENV || 'development';   // 環境變數,預設為 NODE_ENV 或 ‘development’
        this.context = Object.create(context);  // context模組,通過context.js建立
        this.request = Object.create(request);  // request模組,通過request.js建立
        this.response = Object.create(response);    // response模組,通過response.js建立
    }
    ...
複製程式碼

1.原生nodejs中90%以上的方法繼承自event.Emitter。因為是事件驅動,event.Emitter中主要有on/addListener,emit,,removeListeners,removeAllListeners等事件鉤子;因為事件驅動,大部分情況下是黑盒,所以提供鉤子,不用關注具體狀態,只關注具體事件點即可。

2.使用object.create不會保留原建構函式的屬性,不執行原建構函式。object.create的好處同時也減少記憶體消耗。

use

  use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    if (isGeneratorFunction(fn)) {
      deprecate('Support for generators will be removed in v3. ' +
                'See the documentation for examples of how to convert old middleware ' +
                'https://github.com/koajs/koa/blob/master/docs/migration.md');
      fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-'); //debug node包,相當於console.log(),可配置開發開啟,上線自動過濾
    this.middleware.push(fn);
    return this;
  }
複製程式碼

koa2提供了對koa1generator型別中介軟體的適配,use主要是用於收集中介軟體到middleware中。

listen
 listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
複製程式碼

依照原生模組封裝了listen方法,鏈式呼叫(擴充套件運算子apply),在使用之前就呼叫this.callback()方法——初始化中介軟體,形成上下文物件。

callback
  callback() {
    //通過compose()來處理middleware返回的是一個函式
    const fn = compose(this.middleware);
    console.log(this.listenerCount)
    if (!this.listenerCount('error')) this.on('error', this.onerror); //listenerCount從Emitter裡繼承來,做錯誤處理

    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
  }
複製程式碼

使用koa-compose模組來處理use接收來的middleware,compose方法接收引數是函式的陣列,返回一個執行後返回Promise.resolve...

通過createContext函式以及context等模組將req和res和並出一個context(ctx)方便開發者處理請求響應。

this.handleRequest處理接收request請求時的一些方法。

callcack()先執行,返回的handleRequest函式在當有request請求時進行響應,同時處理response請求。(主要就是提前給各事件提前增加監聽器關係)

koa-compose
function compose(middleware) {
  //middleware必須是一個陣列
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    // middleware的每一個元素都必須是函式
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  return function (context, next) {
    let index = -1//index記錄已處理元素
    return dispatch(0)// 從陣列的第一個元素開始dispatch
    function dispatch(i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i// 存下當前的索引——以及處理過的函式元素
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()//最後處理的中介軟體還有next的情況處理
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
複製程式碼
以洋蔥切面示意為例展開
dispatch(0)
Promise.resolve(function(context, next){
	console.log(1)
	await next();
	console.log(5)
}());
複製程式碼
dispatch(1)
Promise.resolve(function(context, 中介軟體2){
	console.log(1)
	await Promise.resolve(function(context, next){
		console.log(2)
		await next();
		console.log(4)
	}())
    console.log(5)
}());
複製程式碼
dispatch(2)
Promise.resolve(function(context, 中介軟體2){
	console.log(1)
	await Promise.resolve(function(context, next){
		console.log(2)
		await Promise.resolve(function(context){
            console.log(5)
	    }())
		console.log(4)
	}())
    console.log(5)
}());
複製程式碼
createContext
 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.originalUrl = request.originalUrl = req.url;
    context.state = {};
    return context;
  }
複製程式碼

createContext主要是掛載request和response以及自建處理方法到context上下文物件中,整合請求響應減少開發成本;需要注意的是state物件提供給了中介軟體記錄狀態,提高中介軟體效率。

handleRequest
  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;//預設處理狀態,未處理則404
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    //主要處理http請求的一些收尾工作
    onFinished(res, onerror);
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
複製程式碼

context.js

delegate
delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
  .method('remove')
  .method('vary')
  .method('set')
  .method('append')
  .method('flushHeaders')
  .access('status')
  .access('message')
複製程式碼

koa2的屬性代理,主要方便開發者更容易獲取到一些屬性。由delegate模組幫助實現。

koa2腳手架

koa-generator

koa-generator類似express風格的腳手架,可幫助快速配置建成nodejs應用

npm install -g koa-generator
koa2 /tmp/foo && cd /tmp/foo
npm install $$ npm start
複製程式碼
注意
app.use(async (context, next) => {
    if (context.request.method === 'OPTIONS') {
        context.response.status = 200
        context.response.set('Access-Control-Allow-Origin', context.request.headers.origin)
        context.response.set('Access-Control-Allow-Headers', 'content-type')
    } else {
        await next()//別忘記next()
    }
})
//非同步操作要用promise封裝
const findAllUsers = () => {
  return new Promise((resolve, reject) => {
    User.find({}, (err, doc) => {
      if (err) {
        reject(err);
      }
      resolve(doc);
    });
  });
};

 
app.use(async (context, next) => {
    // 非同步運算元據庫
    let result = await findAllUsers()
    next() //next操作不是和await連線使用的。
    context.response.status = 200
    context.response.set('Access-Control-Allow-Origin', context.request.headers.origin)
    context.response.set('Access-Control-Allow-Headers', 'content-type')
    context.response.message = '讀取成功'
      ctx.body = {
        succsess: '成功',
        result
  };
})

app.use(async (context, next) => {
    // 非同步運算元據庫
    console.log('test')
})


複製程式碼

相關文章