前端開發者學習後端(二)——koa源碼閱讀

1、代碼結構

下面是application.js代碼結構,並不是完整的源碼,方便讀者閱讀node

class Application extends Emitter{
    <!--初始化 new Koa()-->
    constructor() {
        this.proxy  //若是爲 true,則解析 "Host" 的 header 域,並支持 X-Forwarded-Host
        this.subdomainOffset //表示 .subdomains 所忽略的字符偏移量
        this.env // 默認爲 NODE_ENV or "development"
        this.middleware = [] // 中間件集合
        this.context
        this.request
        this.response
        this[util.inspect.custom] = this.inspect //將對象轉換爲字符串的方法,一般用於調試和錯誤輸出,這裏的用到node中util,this inspect調用 toJson方法中的only方法,返回對象
    }
    <!--listen方法,啓動服務-->
    listen(...args) {
        <!--實際調用node.js 中的http.createServer服務-->
        const server = http.createServer(this.callback()) //返回http.server實例
        return server.listen(...args)
        <!--這裏面的args是端口號+回調函數,因此根據node官網api規定用於TCP鏈接-->
    }
    <!--調用中間件的use方法-->
    use(fn){
        <!--第一件事判斷是不是generator函數,若是是利用koa-conver(實際這裏面利用co插件的wrap方法)轉化成async await 函數-->
        if (isGeneratorFunction(fn)) {
          fn = convert(fn)
        }
        <!--第二件事,中間件存儲到定義的變量數組中-->
        this.middleware.push(fn)
    }
    <!--http.createServer(callback)-->
    callback() {
        <!--第一步,利用koa-compose,整合全部的中間件-->
        const fn = compose(this.middleware)
        <!--閉包的形式返回-->
        const handleRequest = (req, res) => {
          <!--這裏面req是http中IncomingMessage ,res是ServerResponse 類-->
          <!--createContext方法,下面解析-->
          const ctx = this.createContext(req, res)
          <!---->
          return this.handleRequest(ctx, fn)
        }
    
        return handleRequest
    }
}
複製代碼

一、啓動koa程序

<!--做爲調試源碼的程序-->
const Koa = require("Koa")
const app = new Koa()

app.use(async(ctx, next) => {
  console.log(1)
  await next()
  console.log(2)
  ctx.body = "這是第一個中間件"
})

app.use(async(ctx, next) => {
  console.log(3)
  await next()
  console.log(4)
  ctx.body = "這是第二個中間件"
})

app.listen(4001, () => { console.log("koa server is starting") })
複製代碼

二、整個koa運行流程

koa源碼由application.js|context.js|request.js|response.js 文件組成,實際在application.js就引用了其餘三個js文件,並引用koa-compose核心文件,下面是本人經過vscode斷點調試跟蹤上面koa程序的流程圖,不過仍是但願想深刻學習的人,自行斷點調試,如圖mysql

注意:實心箭頭表明是請求後代碼的流程,線性箭頭是初始化服務加載的流程web

2、主要方法

一、理解createContext方法

createContext(req, res) {
    <!--this.context 引用個是require("./context"),下面的1.1章節-->
    const context = Object.create(this.context)
    <!--request、response都是先引用對應庫的基礎方法,而後從新建立個新的對象-->
    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、response兩個對象又包含context-->
    request.ctx = response.ctx = context
    request.response = response
    response.request = request
    context.originalUrl = request.originalUrl = req.url
    context.state = {}
    return context
}
複製代碼
1.1 查看context.js源碼主要部分
const delegate = require('delegates')
const proto = module.exports = {
    <!--有個跟上述application.js文件中inspect相似的方法,省略-->
    <!--set cookie 和 get cookie 方法-->
}
delegate(proto, 'response')
  .method('set')
  ...
  .access('body')
  ...

delegate(proto, 'request')
  .method('get')
  ...
  .access('querystring')
  ...
<!--根據npm官網對delegates的解釋就是委託事件,那麼context.js重點的做用是res,req兩個方法能加載這些委託的方法,換言之給context.js 返回的對象proto 對於request 、response 屬性增長增、讀取、設置、改變等基礎操做方法-->
複製代碼
1.2 查看context的返回的結果

利用上述的koa的啓動程序,查看createContext的返回結果,也就是context對象的方法屬性sql

{
	"request": {
		"method": "GET",
		"url": "/favicon.ico",
		"header": {
			"host": "localhost:4001",
			"connection": "keep-alive",
			"pragma": "no-cache",
			"cache-control": "no-cache",
			"sec-fetch-mode": "no-cors",
			"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36",
			"accept": "image/webp,image/apng,image/*,*/*;q=0.8",
			"sec-fetch-site": "same-origin",
			"referer": "http://localhost:4001/",
			"accept-encoding": "gzip, deflate, br",
			"accept-language": "zh-CN,zh;q=0.9"
		}
	},
	"response": {
		"status": 200,
		"message": "OK",
		"header": {}
	},
	"app": {
		"subdomainOffset": 2,
		"proxy": false,
		"env": "development"
	},
	"originalUrl": "/favicon.ico",
	"req": "<original node req>",
	"res": "<original node res>",
	"socket": "<original node socket>"
}
複製代碼

www.jianshu.com/p/bca3a00f9… 這位博主說的挺詳細的,可是我打印出來的結果和上述「request、response是相互包含的關係,兩個對象又包含context」這句話相矛盾,若是你經過打斷點看,context和request、response是相互包含的關係npm

二、理解koa-compose方法

下面是koa-compose源碼主要部分api

function compose (middleware) {
    <!--判斷是不是數組和函數,此處省略-->
    return function (context, next) {
    let index = -1
    return dispatch(0)
    function dispatch (i) {
    <!--判斷 i<==index,若是 true 的話,則說明 next() 方法調用屢次,若是你在同一個中間件中執行屢次next()方法,會出現下面的錯誤-->
      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)
        }))  }
      -->
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
複製代碼

這裏面最重要的函數是:數組

return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
複製代碼

一、上面代碼執行了中間件fn(context, next),並傳遞了 context 和 next 函數兩個參數,至於 next 函數則是返回一個 dispatch(i+1) 的執行結果;
二、這裏面用bind方式,保證在app.use()方法中使用await next()手動執行下一個中間件,因此舊方法用function next () { return dispatch(i + 1) })這種方式看起來更直觀;
三、最後(i+1)這個參數至關於執行了下一個中間件,從而造成遞歸調用。bash

3、源碼中的知識點

閱讀koa中的源碼你會發現,它涉及最多的是閉包、高階函數、函數柯里化,好比說callback方法和handleRequest()方法restful

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

    return handleRequest;
  }
  handleRequest(ctx, fnMiddleware) {
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
複製代碼

一、callback方中handleRequest是一個閉包函數,由於它能訪問callback函數的做用域變量fn;
二、callback以handleRequest函數做爲返回值,而handleRequest又以this.handleRequest(ctx, fn)做爲返回值,而且this.handleRequest(ctx, fn)參數是fn也是函數,顯而易見這些函數都是高階函數cookie

又好比說下面一段代碼就是「函數柯里化」

<!--簡化版的koa-compose,middleware參數名變成fn-->
<!--compose函數柯里化-->
function compose (middleware) {
  return function (context, next) {
    return dispatch(0)
    function dispatch (i) {
        <!--middleware參數名變成fn-->
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
    }
  }
}
<!--callback方法中調用,可是fn沒有當即執行,是一個Promise對象-->
const fn = compose(this.middleware)
<!--handleRequest方法中fn名稱變成fnMiddleware,此方法執行後,也就執行了fn方法,也就是中間件方法,纔有中間件的回調,纔有啓動程序console.log的輸出-->
handleRequest(ctx, fnMiddleware) {
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
複製代碼

4、後續更新計劃

實現一個koa+mysql的簡單項目,可是很規範的restful API

相關文章
相關標籤/搜索