1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        【保姆級(jí)解析】我是如何從工作的視角看 Koa 源碼的?

        共 24013字,需瀏覽 49分鐘

         ·

        2021-04-14 08:13


        1 原生實(shí)現(xiàn)

        1.1 啟動(dòng)一個(gè)服務(wù)

        node起一個(gè)服務(wù)有多簡(jiǎn)單,相信只要會(huì)上網(wǎng),都能搜到類似下面的代碼快速啟動(dòng)一個(gè)服務(wù)。

        const http = require('http')
        const handler = ((req, res) => {
          res.end('Hello World!')
        })
        http
          .createServer(handler)
          .listen(
            8888,
            () => {
              console.log('listening 127.0.0.1:8888')
            }
          )

        訪問(wèn)127.0.0.1:8888就可以看到頁(yè)面上出現(xiàn)了'hello world!。隨后就會(huì)發(fā)現(xiàn)修改路由還是請(qǐng)求方式,都只能拿到這樣一個(gè)字符串。

        curl 127.0.0.1:8888
        curl curl -X POST http://127.0.0.1:8888
        curl 127.0.0.1:8888/about

        這個(gè)時(shí)候肯定就會(huì)去找相關(guān)文檔[1],然后發(fā)現(xiàn)剛剛回調(diào)函數(shù)的 req 居然內(nèi)有乾坤。我們可以使用 method 屬性和 url 屬性針對(duì)不同的方法和路由返回不同的結(jié)果。于是很容易就想到類似下面的寫法:

        const http = require('http')
        const handler = ((req, res) => {
          let resData = '404 NOT FOUND!'
          const { method, path } = req
          switch (path) {
            case '/':
              if (method === 'get') {
                resData = 'Hello World!'
              } else if (method === 'post') {
                resData = 'Post Method!'
              }
              break
            case '/about':
              resData = 'Hello About!'
          }
          res.end = resData
        })
        http
          .createServer(handler)
          .listen(
            8888,
            () => {
              console.log('listening 127.0.0.1:8888')
            }
          )

        但是一個(gè)服務(wù)不可能只有這么幾個(gè)接口跟方法啊,總不能每加一個(gè)就增加一個(gè)分支吧,這樣 handler 得變得多長(zhǎng)多冗余,于是又很容易想到抽離 handler ,將 pathmethod 解耦。

        1.2 策略模式解耦

        如何解耦呢?從在新手村的代碼中可以發(fā)現(xiàn)策略模式[2]剛好可以拿來(lái)解決這個(gè)問(wèn)題:

        const http = require('http')
        class Application {
          constructor () {
            // 收集route和method對(duì)應(yīng)的回調(diào)函數(shù)
            this.$handlers = new Map()
          }
          // 注冊(cè)handler
          register (method, path, handler) {
            let pathInfo = null
            if (this.$handlers.has(path)) {
              pathInfo = this.$handlers.get(path)
            } else {
              pathInfo = new Map()
              this.$handlers.set(path, pathInfo)
            }
            // 注冊(cè)回調(diào)函數(shù)
            pathInfo.set(method, handler)
          }
          use () {
            return (request, response) => {
              const { url: path, method } = request
              this.$handlers.has(path) && this.$handlers.get(path).has(method)
                ? this.$handlers.get(path).get(method)(request, response)
                : response.end('404 NOT FOUND!')
            }
          }
        }
        const app = new Application()
        app.register('GET''/', (req, res) => {
          res.end('Hello World!')
        })
        app.register('GET''/about', (req, res) => {
          res.end('Hello About!')
        })
        app.register('POST''/', (req, res) => {
          res.end('Post Method!')
        })
        http
          .createServer(app.use())
          .listen(
            8888,
            () => {
              console.log('listening 127.0.0.1:8888')
            }
          )

        1.3 符合DRY原則

        但是這個(gè)時(shí)候就會(huì)發(fā)現(xiàn):

        • 如果手抖把 method 方法寫成了小寫,因?yàn)?Http.Request.method 都是大寫,無(wú)法匹配到正確的 handler ,于是返回 '404 NOT FOUND'
        • 如果我想在響應(yīng)數(shù)據(jù)前增加一些操作,比如為每個(gè)請(qǐng)求增加一個(gè)時(shí)間戳,表示請(qǐng)求的時(shí)間,就必須修改每個(gè) register 中的 handler 函數(shù),不符合DRY原則

        此時(shí)再修改一下上面的代碼,利用 Promise 實(shí)現(xiàn)按順序執(zhí)行 handler。

        const http = require('http')
        class Application {
          constructor() {
            // 收集route和method對(duì)應(yīng)的回調(diào)函數(shù)
            this.$handlers = new Map()
            // 暴露get和post方法
            this.get = this.register.bind(this'GET')
            this.post = this.register.bind(this'POST')
          }
          // 注冊(cè)handler
          register(method, path, ...handlers) {
            let pathInfo = null
            if (this.$handlers.has(path)) {
              pathInfo = this.$handlers.get(path)
            } else {
              pathInfo = new Map()
              this.$handlers.set(path, pathInfo)
            }
            // 注冊(cè)回調(diào)函數(shù)
            pathInfo.set(method, handlers)
          }
          use() {
            return (request, response) => {
              const { url: path, method } = request
              if (
                this.$handlers.has(path) &&
                this.$handlers.get(path).has(method)
              ) {
                const _handlers = this.$handlers.get(path).get(method)
                _handlers.reduce((pre, _handler) => {
                  return pre.then(() => {
                    return new Promise((resolve, reject) => {
                      _handler.call({}, request, response, () => {
                        resolve()
                      })
                    })
                  })
                }, Promise.resolve())
              } else {
                response.end('404 NOT FOUND!')
              }
            }
          }
        }
        const app = new Application()
        const addTimestamp = (req, res, next) => {
          setTimeout(() => {
            this.timestamp = Date.now()
            next()
          }, 3000)
        }
        app.get('/', addTimestamp, (req, res) => {
          res.end('Hello World!' + this.timestamp)
        })
        app.get('/about', addTimestamp, (req, res) => {
          res.end('Hello About!' + this.timestamp)
        })
        app.post('/', addTimestamp, (req, res) => {
          res.end('Post Method!' + this.timestamp)
        })
        http
          .createServer(app.use())
          .listen(
            8888,
            () => {
              console.log('listening 127.0.0.1:8888')
            }
          )

        1.4 降低用戶心智

        但是這樣依舊有點(diǎn)小瑕疵,用戶總是在重復(fù)創(chuàng)建 Promise,用戶可能更希望無(wú)腦一點(diǎn),那我們給用戶暴露一個(gè) next 方法,無(wú)論在哪里執(zhí)行 next 就會(huì)進(jìn)入下一個(gè) handler,豈不美哉?。?!

        class Application {
        // ...
          use() {
            return (request, response) => {
              const { url: path, method } = request
              if (
                this.$handlers.has(path) &&
                this.$handlers.get(path).has(method)
              ) {
                const _handlers = this.$handlers.get(path).get(method)
                _handlers.reduce((pre, _handler) => {
                  return pre.then(() => {
                    return new Promise(resolve => {
                     // 向外暴露next方法,由用戶決定什么時(shí)候進(jìn)入下一個(gè)handler
                      _handler.call({}, request, response, () => {
                        resolve()
                      })
                    })
                  })
                }, Promise.resolve())
              } else {
                response.end('404 NOT FOUND!')
              }
            }
          }
        }
        // ...
        const addTimestamp = (req, res, next) => {
          setTimeout(() => {
            this.timestamp = new Date()
            next()
          }, 3000)
        }

        2 Koa核心源碼解析

        上面的代碼一路下來(lái),基本上已經(jīng)實(shí)現(xiàn)了一個(gè)簡(jiǎn)單中間件框架,用戶可以在自定義中間件,然后在業(yè)務(wù)邏輯中通過(guò) next() 進(jìn)入下一個(gè) handler,使得整合業(yè)務(wù)流程更加清晰。但是它只能推進(jìn)中間件的執(zhí)行,沒有辦法跳出中間件優(yōu)先執(zhí)行其他中間件。比如在koa中,一個(gè)中間件是類似這樣的:

        const Koa = require('koa');
        let app = new Koa();
        const middleware1 = async (ctx, next) => { 
          console.log(1); 
          await next();  
          console.log(2);   
        }
        const middleware2 = async (ctx, next) => { 
          console.log(3); 
          await next();  
          console.log(4);   
        }
        const middleware3 = async (ctx, next) => { 
          console.log(5); 
          await next();  
          console.log(6);   
        }
        app.use(middleware1);
        app.use(middleware2);
        app.use(middleware3);
        app.use(async(ctx, next) => {
          ctx.body = 'hello world'
        })
        app.listen(8888)

        可以看到控制臺(tái)輸出的順序是1, 3, 5, 6, 4, 2,這就是koa經(jīng)典的洋蔥模型。

        接下來(lái)我們一步步解析koa的源碼[3],可以看到總共只有4個(gè)文件,如果去掉注釋,合起來(lái)代碼也就1000多行。

        文件功能
        applicaiton.jskoa程序的入口,管理和調(diào)用中間件,處理http.createServer的回調(diào),將請(qǐng)求的request和response代理至context上
        request.js對(duì)http.createServer回調(diào)函數(shù)中的request的封裝,各種getter、setter以及額外屬性
        response.js對(duì)http.createServer回調(diào)函數(shù)中的response的封裝,各種getter、setter以及額外屬性
        context.js代理request和response,并向外暴露一些功能

        創(chuàng)建Koa實(shí)例的時(shí)候,Koa做的事情其實(shí)并不多,設(shè)置實(shí)例的一些配置,初始化中間件的隊(duì)列,使用 Object.create 繼承 context、requestresponse。

        2.1 constructor

        constructor(options) {
        super();
        // 實(shí)例的各種配置,不用太關(guān)注
          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;
        // 最重要的實(shí)例屬性,用于存放中間
          this.middleware = [];
        // 繼承其他三個(gè)文件中的對(duì)象
          this.context = Object.create(context);
          this.request = Object.create(request);
          this.response = Object.create(response);
        }

        因?yàn)镵oa僅用于中間件的整合以及請(qǐng)求響應(yīng)的監(jiān)聽,所以我們最關(guān)注的Koa的兩個(gè)實(shí)例方法就是 uselisten。一個(gè)用來(lái)注冊(cè)中間件,一個(gè)用來(lái)啟動(dòng)服務(wù)并監(jiān)聽端口。

        2.2 use

        功能非常簡(jiǎn)單,注冊(cè)中間件,往實(shí)例屬性middleware列表中推入中間件。

        use(fn) {
          if (typeof fn !== 'function'throw new TypeError('middleware must be a function!');
        // 利用co庫(kù)轉(zhuǎn)換generator函數(shù),v3版本會(huì)移除,直接使用promise以及async...await
          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 || '-');
          this.middleware.push(fn);
        // 用于鏈?zhǔn)阶?cè)中間件 app.use(xxx).use(xxx)...
          return this;
        }

        2.3 listen

        它的實(shí)現(xiàn)非常簡(jiǎn)單,就是直接調(diào)用 http.createServer 創(chuàng)建服務(wù),并直接執(zhí)行server.listen[4]的一些操作。稍微特殊一點(diǎn)地方是 createServer 傳入的參數(shù)是調(diào)用實(shí)例方法 callback 的返回值。

        listen(...args) {
          debug('listen');
        // 創(chuàng)建服務(wù)
          const server = http.createServer(this.callback());
        // 透?jìng)鲄?shù),執(zhí)行http模塊的server.listen
          return server.listen(...args);
        }

        2.4 callback

        • 調(diào)用 compose 方法,將所有中間件轉(zhuǎn)換成 Promise 執(zhí)行,并返回一個(gè)執(zhí)行函數(shù)。
        • 調(diào)用父類 Emitter 中的 listenerCount 方法判斷是否注冊(cè)了 error 事件的監(jiān)聽器,若沒有則為 error 事件注冊(cè) onerror 方法。
        • 定義傳入 createServer 中的處理函數(shù),這個(gè)處理函數(shù)有2個(gè)入?yún)?,分別是 requestresponse ,通過(guò)調(diào)用 createContext 方法把 requestresponse 封裝成 ctx 對(duì)象,然后把 ctx 和第一步的執(zhí)行函數(shù) fn 傳入 handleRequest 方法中。
        callback() {
        // 后面會(huì)講解koa-compose,洋蔥模型的核心,轉(zhuǎn)換中間件的執(zhí)行時(shí)機(jī)。
          const fn = compose(this.middleware);
        // 繼承自Emitter,如果沒有error事件的監(jiān)聽器,為error事件注冊(cè)默認(rèn)的事件監(jiān)聽方法onerror
          if (!this.listenerCount('error')) this.on('error'this.onerror);
        // 
          const handleRequest = (req, res) => {
        // 調(diào)用createContext方法把req和res封裝成ctx對(duì)象
            const ctx = this.createContext(req, res);
            return this.handleRequest(ctx, fn);
          };
          return handleRequest;
        }

        2.5 createContext

        createContext 的作用是將前面講到的 contextrequest,response 三個(gè)文件暴露出來(lái)的對(duì)象封裝在一起,并額外增加app、req、res等,方便在ctx中獲取各類信息。

        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;
        }

        2.6 handleRequest

        • 獲得res,將狀態(tài)默認(rèn)置為404
        • 定義失敗的回調(diào)函數(shù)和中間件執(zhí)行成功的回調(diào)函數(shù),其中失敗回調(diào)函數(shù)調(diào)用 context 中的 onerror 函數(shù),不過(guò)最終還是觸發(fā)app中注冊(cè)的 onerror 函數(shù);成功回調(diào)函數(shù)調(diào)用 respond 方法,讀取 ctx 信息,把數(shù)據(jù)寫入 res 中并響應(yīng)請(qǐng)求。
        • 使用 on-finished 模塊確保一個(gè)流在關(guān)閉、完成和報(bào)錯(cuò)時(shí)都會(huì)執(zhí)行相應(yīng)的回調(diào)函數(shù)。
        • 執(zhí)行中間件函數(shù) fnMiddleware,類似于 Promise.all,當(dāng)全部中間件處理成功后,執(zhí)行 handleResponse ,否則捕獲異常。
        handleRequest(ctx, fnMiddleware) {
          const res = ctx.res;
          res.statusCode = 404;
          const onerror = err => ctx.onerror(err);
          const handleResponse = () => respond(ctx);
          onFinished(res, onerror);
          return fnMiddleware(ctx).then(handleResponse).catch(onerror);
        }

        3 Koa-compose

        koa-compose源碼[5]非常簡(jiǎn)略:

        • 首先校驗(yàn)一下入?yún)⒌暮戏ㄐ裕罱K返回一個(gè)函數(shù)。
        • 該函數(shù)內(nèi)部使用 index 作為標(biāo)識(shí)記錄當(dāng)前執(zhí)行的中間,并返回從第一個(gè)中間件執(zhí)行 dispatch 的結(jié)果。如果一個(gè)中間件內(nèi)部多次執(zhí)行 next() 方法,就會(huì)出現(xiàn)i的值等于 index,于是會(huì)報(bào)錯(cuò) reject 掉。
        • 根據(jù) index 取出中間件列表中的中間件,將 contextdispatch(i + 1) 中間件的入?yún)?ctxnext 傳入,當(dāng)中間件執(zhí)行 next() 方法時(shí),就會(huì)按順序執(zhí)行下一個(gè)中間件,且將當(dāng)前中間件放入執(zhí)行棧中,最后當(dāng)i等于中間件數(shù)組長(zhǎng)度時(shí)候,即沒有其他中間件了,就將入?yún)?next(在Koa源碼里是undefined)賦值給fn,此時(shí)fn未定義,于是返回空的 resolved 狀態(tài)的 promise
        • 當(dāng)最核心的中間件執(zhí)行完成后,自然會(huì)觸發(fā) await 向下執(zhí)行,開始執(zhí)行上一個(gè)中間件,最終就形成了從外向里,再?gòu)睦锵蛲獾难笫[模型。
        // 入?yún)⑹且粋€(gè)中間件列表,返回值是一個(gè)函數(shù)
        function compose (middleware{
        // 檢查中間的合法性
          if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
          for (const fn of middleware) {
            if (typeof fn !== 'function'throw new TypeError('Middleware must be composed of functions!')
          }
        // 核心
          return function (context, next{
        // 設(shè)置初始索引值
            let index = -1
        // 立即執(zhí)行dispatch,傳入0,并返回結(jié)果
            return dispatch(0)
            function dispatch (i{
        // 防止在一個(gè)中間件中多次調(diào)用next
              if (i <= index) return Promise.reject(new Error('next() called multiple times'))
              index = i
        // 拿出中間件列表中的第i個(gè)中間件,賦值給fn
              let fn = middleware[i]
        // 中間件全部執(zhí)行完成,將next賦值給fn,不過(guò)針對(duì)Koa源碼而言,next一直為undefined(其他地方不一定)
              if (i === middleware.length) fn = next
        // 沒有可執(zhí)行的中間件,之間resolve掉promise
              if (!fn) return Promise.resolve()
              try {
        // 相當(dāng)于實(shí)現(xiàn)Promise.all,通過(guò)對(duì)外暴露next回調(diào)函數(shù)遞歸執(zhí)行promise,保證中間件執(zhí)行的順序滿足棧的特性
                return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
              } catch (err) {
                return Promise.reject(err)
              }
            }
          }
        }

        4 Koa-router

        上面解決了中間件的執(zhí)行順序問(wèn)題,但是路由這一塊就比較尷尬,因?yàn)槲覀兛赡苁褂脦в袇?shù)的路由,比如 app.get('/:userName', (res, req) => {/* xxxx */}) ,原先處理路由的方法就不適用了,此時(shí)可以引入koa-router中間件,像下面一樣使用。

        const Koa = require('koa')
        const Router = require('koa-router')
        const app = new Koa()
        const router = new Router()
        router.get('/'async ctx => {
          ctx.body = 'Hello World!'
        })
        router.get('/:userName'async ctx => {
          ctx.body = `Hello ${ctx.params.userName}!`
        })
        app
          .use(router.routes())
          .use(router.allowedMethods())
          .listen(8888)

        koa-router源碼[6]都放在lib文件夾下面,就兩個(gè)文件:

        文件功能
        layer.js內(nèi)部使用各種正則表達(dá)式從入?yún)?dāng)中獲取相應(yīng)數(shù)據(jù),存放請(qǐng)求的路由、method、路由對(duì)應(yīng)的正則匹配、路由中的參數(shù)、路由對(duì)應(yīng)的中間件等
        router.jsRouter的具體實(shí)現(xiàn),提供對(duì)外暴露的注冊(cè)方法get、post等,處理路由的中間件等
        // 注冊(cè)路由,綁定中間件
        Router.prototype.register = function (path, methods, middleware, opts{
          opts = opts || {};
          const router = this;
          const stack = this.stack;
        // 支持多個(gè)path綁定中間件
          if (Array.isArray(path)) {
            for (let i = 0; i < path.length; i++) {
              const curPath = path[i];
              router.register.call(router, curPath, methods, middleware, opts);
            }
            return this;
          }
        // 創(chuàng)建路由
          const route = new Layer(path, methods, middleware, {
            end: opts.end === false ? opts.end : true,
            name: opts.name,
            sensitive: opts.sensitive || this.opts.sensitive || false,
            strict: opts.strict || this.opts.strict || false,
            prefix: opts.prefix || this.opts.prefix || "",
            ignoreCaptures: opts.ignoreCaptures
          });
          if (this.opts.prefix) {
            route.setPrefix(this.opts.prefix);
          }
        // 增加中間件參數(shù)
          for (let i = 0; i < Object.keys(this.params).length; i++) {
            const param = Object.keys(this.params)[i];
            route.param(param, this.params[param]);
          }
          stack.push(route);
          debug('defined route %s %s', route.methods, route.path);
          return route;
        };
        // 對(duì)外暴露get、post等方法
        for (let i = 0; i < methods.length; i++) {
          function setMethodVerb(method{
            Router.prototype[method] = function(name, path, middleware{
              if (typeof path === "string" || path instanceof RegExp) {
                middleware = Array.prototype.slice.call(arguments2);
              } else {
                middleware = Array.prototype.slice.call(arguments1);
                path = name;
                name = null;
              }
              this.register(path, [method], middleware, {
                name: name
              });
              return this;
            };
          }
          setMethodVerb(methods[i]);
        }

        5 相關(guān)文檔

        • koa onion model[7]
        • https://en.wikipedia.org/wiki/Strategy_pattern[8]
        • https://robdodson.me/posts/javascript-design-patterns-strategy/[9]

        參考資料

        [1]

        相關(guān)文檔: https://nodejs.org/api/http.html

        [2]

        策略模式: https://en.wikipedia.org/wiki/Strategy_pattern

        [3]

        koa的源碼: https://github.com/koajs/koa

        [4]

        server.listen: http://nodejs.cn/api/net.html#net_class_net_server

        [5]

        koa-compose源碼: https://github.com/koajs/compose/blob/master/index.js

        [6]

        koa-router源碼: https://github.com/koajs/router

        [7]

        koa onion model: https://programmer.group/koa-onion-model.html

        [8]

        https://en.wikipedia.org/wiki/Strategy_pattern: https://en.wikipedia.org/wiki/Strategy_pattern

        [9]

        https://robdodson.me/posts/javascript-design-patterns-strategy/: https://robdodson.me/posts/javascript-design-patterns-strategy/

        ?? 謝謝支持

        瀏覽 23
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            什么网站可以看毛片 | 国产精品久久久久久久久久青青 | 狠狠狠狠狠狠狠狠狠 | 波多野结衣久久电影 | 久久久久久久麻豆 | 国产欧美在线播放 | 裸体美女一级毛片免费艳福AV | 国产农村乱婬片A片AAA图片 | 亚洲无码婷婷国产 | 男人日女人的视频软件 |