本文的主要內容是經過描述做者本身學習koa源代碼的過程,來和你們一塊兒來學習koa的源碼,koa設計的初衷是 致力於成爲 web 應用和 API 開發領域中的一個更小、更富有表現力、更健壯的基石,說白了就是 小巧 ,擴展性高因此koa的源碼閱讀起來也相對容易,若是你跟着個人文章學習 還學不會那麼必定是我寫的很差。html
在上一篇文章中咱們學習了目錄結構,知道了入口文件application.js,本篇咱們就來一塊兒學習一下。node
打開 lib/appication 咱們發現這個文件也只有200多行,咱們如何閱讀這個文件?
先看 Koa項目--hello word 的啓動git
const Koa = require('koa');
const app = new Koa(); app.use(async ctx => { ctx.body = 'Hello World'; }); app.listen(3000); 複製代碼
這個基礎的項目就幹了4件事github
咱們根據下面的方式來看一下咱們的application.js 的內容。 (版本koa@2.13.0)web
看到第30行導出了application類 , 而且該類繼承 Emitter ,而後看一下 Emitter 是經過第16行的 events 模塊導入的。 node_modules裏沒有找到events模塊,說明events模塊是node的原生模塊,application類繼承了node原生的evetns模塊,不瞭解events模塊也不用糾結能夠繼續往下看,用到events模塊的內容再看就是了。api
知道了application的繼承,繼續看application的初始化。下面代碼我加了一點本身的註釋,下文會有一些解讀。數組
constructor(options) {
super(); // ①配置項信息相關 options = options || {}; this.proxy = options.proxy || false; this.subdomainOffset = options.subdomainOffset || 2; this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For'; this.maxIpsCount = options.maxIpsCount || 0; this.env = options.env || process.env.NODE_ENV || 'development'; if (options.keys) this.keys = options.keys; // ②重要的屬性 this.middleware = []; this.context = Object.create(context); this.request = Object.create(request); this.response = Object.create(response); // ③檢查相關 if (util.inspect.custom) { this[util.inspect.custom] = this.inspect; } } 複製代碼
我將上面的constructor的代碼大體分爲3塊app
const context = require('./context');
引入,也就是
lib/context.js
當咱們new 好了一個 application 對象以後,咱們開始使用調用application的方法,執行app.usedom
<!--const Koa = require('koa');-->
<!--const app = new Koa();--> // 看這一句 app.use(async ctx => { ctx.body = 'Hello World'; }); <!--app.listen(3000);--> 複製代碼
看一下use這個方法的源代碼,在application.js中的第122行。koa
use(fn) {
// ①保證參數必須爲一個function if (typeof fn !== 'function') throw new TypeError('middleware must be a function!'); // ②若是是generator函數要進行一次轉化 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狀態下的輸出 debug('use %s', fn._name || fn.name || '-'); // ④將方法push到咱們的中間件數組 this.middleware.push(fn); // ⑤想要鏈式調用,必須返回自身 return this; } 複製代碼
這個方法作的事情很是簡單
咱們繼續執行咱們的koa程序
<!--const Koa = require('koa');-->
<!--const app = new Koa();--> <!--app.use(async ctx => {--> <!-- ctx.body = 'Hello World';--> <!--});--> app.listen(3000); // 看這一句 複製代碼
看一下listen函數,在application.js 的第79行
listen(...args) {
debug('listen'); // ①調用node中的http模塊的createServer方法 const server = http.createServer(this.callback()); // ②http.Server實例調用listen方法 return server.listen(...args); } 複製代碼
這個listen也很簡單,就是對node原生的http模塊的建立服務和服務監聽進行了一次封裝
koa的listen方法中調用了callback方法,咱們來看看callback方法幹了什麼事情。 代碼在application.js 的第143行
callback() {
// ①洋蔥模型原理核心 const fn = compose(this.middleware); // ②錯誤監聽相關 if (!this.listenerCount('error')) this.on('error', this.onerror); // ③koa封裝的requestListener const handleRequest = (req, res) => { const ctx = this.createContext(req, res); return this.handleRequest(ctx, fn); }; return handleRequest; } 複製代碼
這個callback方法能夠說是 koa 事件處理邏輯的核心
先來看一下什麼是洋蔥模型
const Koa = require('koa');
let app = new Koa(); const middleware1 = async (ctx, next) => { console.log(1); await next(); console.log(6); } const middleware2 = async (ctx, next) => { console.log(2); await next(); console.log(5); } const middleware3 = async (ctx, next) => { console.log(3); await next(); console.log(4); } app.use(middleware1); app.use(middleware2); app.use(middleware3); app.use(async(ctx, next) => { ctx.body = 'hello world' }) app.listen(3001) // 輸出1,2,3,4,5,6 複製代碼
經過分析以前的代碼咱們知道 app.use(fn) 函數最主要的做用就是對 fn 進行 this.middleware.push(fn) 在上面的代碼中 咱們經過app.use(middleware1); app.use(middleware2); app.use(middleware3);
將this.middleware 數組變成了 [middleware1, middleware1, middleware1]
callback函數 第一句是執行了const fn = compose(this.middleware);
找到compose的定義 再找到koa-compose的源碼以下(將部分不影響主流程內容刪減):
function compose (middleware) {
return function (context, next) { // last called middleware # let index = -1 return dispatch(0) 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() try { return Promise.resolve(fn(context, function next () { return dispatch(i + 1) })) } catch (err) { return Promise.reject(err) } } } } 複製代碼
代碼很是的短只有幾十行
比較關鍵的就是這個dispatch函數了,它將遍歷整個middleware,而後將context和dispatch(i + 1)傳給middleware中的方法。 巧妙的實現了
context
一路傳下去給中間件
middleware
中的下一個中間件
fn
做爲將來
next
的返回值
最終經過compose將[middleware1, middleware1, middleware1]
轉變成了相似 middleware1(middleware2(middleware3()));
的函數。 運用了 compose 的特性,結合 async await 中 next 的等待執行,造成了洋蔥模型,咱們能夠利用這一特性在 next 以前對 request 進行處理,而在 next 以後對 response 進行處理。
這行代碼的意思就很明確了,就是判斷是否監聽過error信息,若是沒有監聽過error,就將錯誤信息經過 onerror 方法將錯誤信息進行包裝返回。
先來看一下這個req和res是怎麼來的,咱們上文說過this.callback()
是做爲http.createServer
的參數使用的,那麼很明顯這裏面的 req 和 res 也就是node中的經過http.createServe返回的req對象和res對象。
先是經過this.createContext函數建立了一個上下文對象 ctx,而後返回了this.handleRequest函數(和function handleRequest 不是一個函數)這裏將ctx和經過compose轉變過的middleware 做爲參數。
咱們先來看一下this.createContext函數如何建立上下文對象ctx 代碼在 177行。
createContext(req, res) {
// ① 建立context 、request、response 對象 const context = Object.create(this.context); const request = context.request = Object.create(this.request); const response = context.response = Object.create(this.response); // ② context 、request 、 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; // ③ 記錄原始url 並返回自身 context.originalUrl = request.originalUrl = req.url; context.state = {}; return context; } 複製代碼
再來看一下callback 最終返回的handleRequest函數,代碼在162行。
handleRequest(ctx, fnMiddleware) {
// ①將res的statusCode 設置爲 404 const res = ctx.res; res.statusCode = 404; // ②定義catch 時調用的onerror函數,以及正常返回時的 handleResponse函數 const onerror = err => ctx.onerror(err); const handleResponse = () => respond(ctx); // ③ 顧名思義,判斷是否結束以及調用的某方法 onFinished(res, onerror); // ④ 調用以前經過compose生產的函數 return fnMiddleware(ctx).then(handleResponse).catch(onerror); } 複製代碼
middleware1(middleware2(middleware3()))
函數,
handleResponse就是
this.response函數,經過將ctx、request、response、req、res,一路向下傳後一路向上返回最終返回結果。
還有response 函數我就不細說了,也比較簡單主要就是根據不一樣的返回狀態返回不一樣的結果,主要包括對method === "HEAD"的返回,以及對返回body類型時res對象的一些處理
最後我用一張圖來梳理整個Application.js的過程
本篇application.js咱們先分析到這裏,下一篇會講述關於context.js的內容,本文徹底按照做者本身學習源碼的過程進行描述,文筆很差讀起來可能會有一點流水帳,可是做者會努力描述清楚,而且把閱讀源碼的一些方法技巧分享,請收藏點贊支持。
手把手和你一塊兒學習Koa源碼(二)——Appilication
本文使用 mdnice 排版