Koa源碼分析

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迴應。node

事件驅動四類解決方式git

回調
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對象。在函數內部,能夠根據需求來觸發不一樣的事件。在函數外部對不一樣的事件進行監聽,而後作出相應的處理。github

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對象,可是不一樣的是事件具備即時性,觸發以後這個狀態後事件就消失了。將本來層層嵌套的回調函數展開,視覺效果更好。web

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.express

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

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 能夠從「外部」去增長一些行爲,進而合併既有行爲或修改既有行爲。api

舉例--果園生產

原始流程 數組

AOP後流程

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
// 打印出一、二、三、四、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')
})


複製代碼
相關文章
相關標籤/搜索