手把手和你一塊兒學習Koa源碼(二)——Appilication

前言

本文的主要內容是經過描述做者本身學習koa源代碼的過程,來和你們一塊兒來學習koa的源碼,koa設計的初衷是 致力於成爲 web 應用和 API 開發領域中的一個更小、更富有表現力、更健壯的基石,說白了就是 小巧擴展性高因此koa的源碼閱讀起來也相對容易,若是你跟着個人文章學習 還學不會那麼必定是我寫的很差html

在上一篇文章中咱們學習了目錄結構,知道了入口文件application.js,本篇咱們就來一塊兒學習一下。node

Application

打開 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

  • require Koa,也就是導入了咱們這個application.js
  • 建立一個實例,new 了一個咱們application導出的類
  • 調用了這個實例的use方法,傳入了一個function
  • 調用了listen 方法,監聽了3000端口

咱們根據下面的方式來看一下咱們的application.js 的內容。 (版本koa@2.13.0)web

先看導出的內容

看到第30行導出了application類 , 而且該類繼承 Emitter ,而後看一下 Emitter 是經過第16行的 events 模塊導入的。 node_modules裏沒有找到events模塊,說明events模塊是node的原生模塊,application類繼承了node原生的evetns模塊,不瞭解events模塊也不用糾結能夠繼續往下看,用到events模塊的內容再看就是了。api

constructor

知道了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

  • ①配置項相關部分
    option經過new 的時候傳入的參數來控制自身的一些屬性,經過名字能夠大體的猜到什麼意思可是也不能明確究竟是幹什麼的,也不知道在什麼狀況下會使用,因此不用糾結,大體知道是有一些屬性是在這裏定義的而且給了一些默認值就行了,用的時候再來看就是了。
  • ②重要的屬性
    若是你使用過koa,那你就能夠大體猜到這幾個屬性是幹什麼的
    • middleware koa能夠實現洋蔥模型的中間件工做的基礎
    • context 上下文對象,經過 const context = require('./context');引入,也就是 lib/context.js
    • request koa 的 Request 對象,就是 lib/request.js
    • response koa 的 Response 對象,就是 lib/response.js
  • ③檢查相關
    也不用糾結用到再說

use

當咱們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;  } 複製代碼

這個方法作的事情很是簡單

  • ①保證參數必須爲一個function
  • ②若是是generator函數要進行一次轉化
    • 先判斷是否爲generator函數,看到 isGeneratorFunction這個方法名就知道是幹什麼的,我也無論怎麼實現的(想要寫出高質量的代碼,命名真的很是重要
    • 描述了v3版本再也不支持gennorator函數做爲參數,同時告訴你怎麼去轉化
    • convert方法是引入的koa-convert包,簡單來講功能就是Generator函數轉換成將async函數。更準確的說是將Generator函數轉換成使用co包裝成的Promise對象。
  • ③debug狀態下的輸出
    咱們看到不少項目都有debug模式會將執行信息暴露出來,用的就是相似的方法,不影響主流程就不深刻看了。
  • 將方法push到咱們的中間件數組
    整個use方法最核心的就這一句了,將use調用的方法push到middleware數組中,本質上app.use的調用就是爲了完成將參數(方法)push到middleware數組中這件事。
  • ⑤想要鏈式調用,必須返回自身

listen

咱們繼續執行咱們的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模塊的建立服務和服務監聽進行了一次封裝

  • ①調用node中的http模塊的createServer方法
    這塊先看一下node文檔的 http模塊,瞭解一下http.createserver方法的使用,以及返回了 http.server實例
  • ②http.Server實例調用listen方法
    能夠看一下 server.listen的文檔,一般最簡單的使用通常就是傳一個端口號。

callback

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 事件處理邏輯的核心

  • ①compose 洋蔥模型的核心原理

先來看一下什麼是洋蔥模型

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,而後將contextdispatch(i + 1)傳給middleware中的方法。 巧妙的實現了

  1. context一路傳下去給中間件
  2. middleware中的下一個中間件 fn做爲將來 next的返回值

最終經過compose將[middleware1, middleware1, middleware1] 轉變成了相似 middleware1(middleware2(middleware3()));的函數。 運用了 compose 的特性,結合 async await 中 next 的等待執行,造成了洋蔥模型,咱們能夠利用這一特性在 next 以前對 request 進行處理,而在 next 以後對 response 進行處理。

  • ②錯誤監聽相關
    調用 this.listenerCount 函數, 我找了application.js代碼中沒有相關函數,想起Application繼承 自 events 就翻閱了一下node的api, listenerCount方法找到了主要功能以下

這行代碼的意思就很明確了,就是判斷是否監聽過error信息,若是沒有監聽過error,就將錯誤信息經過 onerror 方法將錯誤信息進行包裝返回。

  • ③koa封裝的requestListener ,這裏定義了一個handleRequest函數並返回

先來看一下這個req和res是怎麼來的,咱們上文說過this.callback()是做爲http.createServer的參數使用的,那麼很明顯這裏面的 reqres 也就是node中的經過http.createServe返回的req對象和res對象
先是經過this.createContext函數建立了一個上下文對象 ctx,而後返回了this.handleRequest函數(和function handleRequest 不是一個函數)這裏將ctx和經過compose轉變過的middleware 做爲參數。

createContext

咱們先來看一下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;  } 複製代碼
  • ① 建立context 、request、response 對象 經過context.js、request.js、response.js 分別建立對象具體內容會在後面講,這裏只要知道在這裏建立便可
  • ② context 、request 、 response 互相綁定 在koa使用中咱們常常會經過 app.ctx.request 這種方式調用,就是在這塊進行的互相掛載比較簡單就很少解釋了,同時注意一下 ctx.request !== ctx.req
  • ③ 記錄原始url 並返回自身 記錄一下原始的url信息,以及建立了一個state對象(也不用管用到時候再說)而且返回了context實例,也便是咱們說的 上下文對象ctx,同時咱們也知道這個上下文對象在這裏得到了request實例、response實例以及經過node返回的 req和res。

handleRequest

再來看一下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);  } 複製代碼
  • ①將res的statusCode 設置爲 404
  • ②定義catch 時調用的onerror函數,以及正常返回時的 handleResponse函數
  • ③ 顧名思義,判斷是否結束以及調用的某方法
  • ④ 調用以前經過compose生產的函數 這個就是咱們最終經過listen --> callback --> handleRequest 返回的結果, 這裏面的fnMiddleware 就是咱們生成的 middleware1(middleware2(middleware3()))函數, handleResponse就是 this.response函數,經過將ctx、request、response、req、res,一路向下傳後一路向上返回最終返回結果。

還有response 函數我就不細說了,也比較簡單主要就是根據不一樣的返回狀態返回不一樣的結果,主要包括對method === "HEAD"的返回,以及對返回body類型時res對象的一些處理

梳理

最後我用一張圖來梳理整個Application.js的過程

總結

本篇application.js咱們先分析到這裏,下一篇會講述關於context.js的內容,本文徹底按照做者本身學習源碼的過程進行描述,文筆很差讀起來可能會有一點流水帳,可是做者會努力描述清楚,而且把閱讀源碼的一些方法技巧分享,請收藏點贊支持。

相關文章

手把手和你一塊兒學習Koa源碼(一)——目錄結構

手把手和你一塊兒學習Koa源碼(二)——Appilication

本文使用 mdnice 排版

相關文章
相關標籤/搜索