手撕源碼實(shí)現(xiàn)一個(gè)Koa。(手撕源碼什么意思)

      網(wǎng)友投稿 707 2022-05-30

      官方簡(jiǎn)介

      Koa 是一個(gè)新的 web 框架,由 Express 幕后的原班人馬打造, 致力于成為 web 應(yīng)用和 API 開(kāi)發(fā)領(lǐng)域中的一個(gè)更小、更富有表現(xiàn)力、更健壯的基石。 通過(guò)利用 async 函數(shù),Koa 幫你丟棄回調(diào)函數(shù),并有力地增強(qiáng)錯(cuò)誤處理。 Koa 并沒(méi)有捆綁任何中間件, 而是提供了一套優(yōu)雅的方法,幫助您快速而愉快地編寫(xiě)服務(wù)端應(yīng)用程序。

      前置知識(shí)

      node http

      使用http搭建服務(wù)

      step1:引入http

      var http = require('http');

      step2: createServer方法創(chuàng)建http服務(wù)

      let server=http.createServer(function (request, response) { response.writeHead(200, {'Content-Type': 'text/plain'}); response.end('Hello World'); })

      step3:監(jiān)聽(tīng)端口

      server.listen(8000);

      koa簡(jiǎn)單用法

      const Koa = require('koa'); const app = new Koa(); app.use(async ctx => { ctx.body = 'Hello World'; }); app.listen(3000);

      核心目錄

      application.js: 入口,導(dǎo)出koa。

      context.js:上下文

      request.js:處理請(qǐng)求

      response.js:處理響應(yīng)

      導(dǎo)出koa類(lèi)

      上面我們簡(jiǎn)單介紹了koa的用法,可以看出koa有兩個(gè)核心的函數(shù),一個(gè)use,一個(gè)listen。我們先來(lái)實(shí)現(xiàn)這兩個(gè)函數(shù)。

      我們已經(jīng)預(yù)先學(xué)習(xí)了http的用法,那么事情就很好辦了!

      koa類(lèi)

      application.js

      先定義一個(gè)koa的基本結(jié)構(gòu)。

      class Application { use() { } listen() { } } module.exports = Application

      app.listen

      主要是將http.listen封裝一下。

      class Application { callback = (req, res) => { res.end('Hello World\n'); } listen() { const server = http.createServer(this.callback); console.log(...arguments) server.listen(...arguments) } }

      測(cè)試一下:test.js

      const Koa=require('./application.js') const app=new Koa() app.listen(3000)

      可以正常訪問(wèn)

      app.use

      在前置知識(shí)中我們看到,app.use接收一個(gè)回調(diào)函數(shù),同時(shí)傳入一個(gè)ctx上下文,這里ctx將request和response封裝起來(lái)。為了清晰易懂,我們先不進(jìn)行上下文的封裝。

      app.use(async ctx => { ctx.body = 'Hello World'; });

      那么use簡(jiǎn)單的處理如下:

      class Application { use(fn) { this.fn=fn } }

      此時(shí)use接收了一個(gè)函數(shù),這個(gè)函數(shù)的執(zhí)行的時(shí)機(jī)是在訪問(wèn)網(wǎng)站的時(shí)候,那么此時(shí)就需要在創(chuàng)建http服務(wù)的時(shí)候,傳入這個(gè)函數(shù)。最好的方式就是放在listen的callbak中調(diào)用。

      callback = (req, res) => { this.fn(req, res) }

      最終代碼

      let http = require('http') class Application { use(fn) { this.fn=fn } callback = (req, res) => { this.fn(req, res) } listen() { const server = http.createServer(this.callback); server.listen(...arguments) } } module.exports = Application

      測(cè)試:test.js

      const Koa=require('./application.js') const app=new Koa() app.use((req, res) => { res.end('Hello World\n'); }) app.listen(3000)

      可以正常訪問(wèn)

      封裝ctx

      明確一個(gè)事實(shí):每個(gè)請(qǐng)求都是獨(dú)立的,對(duì)于原生的http請(qǐng)求來(lái)說(shuō),每次請(qǐng)求的response和request是不同的。對(duì)于koa中的ctx,則表示每次請(qǐng)求的ctx都是一個(gè)新的ctx。

      ctx的結(jié)構(gòu)

      為了看到ctx的結(jié)構(gòu),我們先使用源koa打印一下ctx。最終得到的結(jié)果如下所示,有了這個(gè)結(jié)構(gòu)我們就可以實(shí)現(xiàn)一個(gè)自己的ctx。

      下面這個(gè)格式是console.dir(ctx)的結(jié)果(刪掉了一些具體的內(nèi)容),從下面的內(nèi)容,我們可以得出ctx的結(jié)構(gòu)。。

      { request: { app: Application { }, req: IncomingMessage { }, res: ServerResponse { }, ctx: [Circular], response: { app: [Application], req: [IncomingMessage], res: [ServerResponse], ctx: [Circular], request: [Circular] }, originalUrl: '/' }, response: { app: Application { }, req: IncomingMessage { }, res: ServerResponse { }, ctx: [Circular], request: { app: [Application], req: [IncomingMessage], res: [ServerResponse], ctx: [Circular], response: [Circular], originalUrl: '/' } }, app: Application { }, req: IncomingMessage { }, res: ServerResponse { }, originalUrl: '/', state: {} }

      context.js

      context.js 主要定義了context的具體結(jié)構(gòu)以及提供的方法。

      Koa Context 將 node 的 request 和 response 對(duì)象封裝到單個(gè)對(duì)象中,為編寫(xiě) Web 應(yīng)用程序和 API 提供了許多有用的方法。 這些操作在 HTTP 服務(wù)器開(kāi)發(fā)中頻繁使用,它們被添加到此級(jí)別而不是更高級(jí)別的框架,這將強(qiáng)制中間件重新實(shí)現(xiàn)此通用功能。

      request.js和response.js文件

      在核心目錄,我們提到了這兩個(gè)文件,這兩個(gè)文件此時(shí)就派上了用場(chǎng)。這兩個(gè)文件具體實(shí)現(xiàn)了啥呢?這兩個(gè)文件定義了ctx.resopnse和ctx.request的結(jié)構(gòu),也就是上面使用dir輸出的結(jié)果。在koa中文文檔中可以具體的看到結(jié)構(gòu),可以自行查閱。

      Koa Request 對(duì)象是在 node 的 原生請(qǐng)求對(duì)象之上的抽象,提供了諸多對(duì) HTTP 服務(wù)器開(kāi)發(fā)有用的功能。

      Koa Response 對(duì)象是在 node 的原生響應(yīng)對(duì)象之上的抽象,提供了諸多對(duì) HTTP 服務(wù)器開(kāi)發(fā)有用的功能。

      實(shí)現(xiàn)ctx

      const context={ } module.exports=context

      const request={ } module.exports=request

      const resposne={ } module.exports=response

      我們?cè)谏厦鎸?dǎo)出koa章節(jié)中可以看到,在app.use的時(shí)候,我們傳的參數(shù)是(request,response),源koa傳的的ctx,所以我們就知道了,koa是在app.use的時(shí)候創(chuàng)建了一個(gè)ctx。

      在本章開(kāi)頭的時(shí)候,我們又提到每次請(qǐng)求的ctx都是全新的ctx。

      綜合以上兩點(diǎn),我們可以基本編寫(xiě)出下面的代碼。(為了代碼的清晰易讀,我們封裝了一個(gè)createcontext函數(shù)來(lái)創(chuàng)建上下文。)

      const Context = require('./context') const Request = require('./request') const Response = require('./response') class Application { constructor(){ this.context = Object.create(Context); this.request = Object.create(Request); this.response = Object.create(Response); } use(fn) { this.fn = fn } createContext = (req, res) => { const ctx = Object.create(this.context); const request = Object.create(this.request); const response = Object.create(this.response); ctx.app = request.app = response.app = this ctx.request = request; ctx.request.req = ctx.req = req; ctx.response = response; ctx.response.res = ctx.res = res; ctx.originalUrl = request.originalUrl = req.url ctx.state = {} return ctx } callback = (req, res) => { let ctx = this.createContext(req, res) this.fn(ctx) } listen() { const server = http.createServer(this.callback); console.log(...arguments) server.listen(...arguments) } }

      首先我們?cè)赾onstructor中定義了一個(gè)context對(duì)象,這里會(huì)在constructor定義是因?yàn)镵oa的app上默認(rèn)導(dǎo)出context屬性。

      app.context 是從其創(chuàng)建 ctx 的原型。您可以通過(guò)編輯 app.context 為 ctx 添加其他屬性。這對(duì)于將 ctx 添加到整個(gè)應(yīng)用程序中使用的屬性或方法非常有用,這可能會(huì)更加有效(不需要中間件)和/或 更簡(jiǎn)單(更少的 require()),而更多地依賴(lài)于ctx,這可以被認(rèn)為是一種反模式。

      例如,要從 ctx 添加對(duì)數(shù)據(jù)庫(kù)的引用:

      app.context.db = db(); app.use(async ctx => { console.log(ctx.db); });

      然后再callback中,我們針對(duì)response和request進(jìn)行了二次封裝。

      再來(lái)看看這段代碼:

      >app.context.db = db(); >app.use(async ctx => { > console.log(ctx.db); >});

      再使用use之前,通過(guò)app.context對(duì)context進(jìn)行了修改。當(dāng)使用use函數(shù)的時(shí)候,是不是直接進(jìn)入了callback函數(shù),此時(shí)的this.context已經(jīng)是修改過(guò)的了。

      手撕源碼,實(shí)現(xiàn)一個(gè)Koa。(手撕源碼什么意思)

      const Koa=require('./application.js') const app=new Koa() app.use((ctx) => { // 測(cè)試1 ctx.response.res.end(" hello my koa") // 測(cè)試2 ctx.res.end(" hello my koa") }) app.listen(3000,()=>{ console.log('3000') })

      正常訪問(wèn)!

      封裝request.js

      明確一個(gè)事實(shí):request類(lèi)的屬性是通過(guò)getter和setter設(shè)置的。為什么會(huì)這樣設(shè)置?這樣設(shè)置的好處是可以方便的設(shè)置和獲取到值。是不是有點(diǎn)懵逼!請(qǐng)聽(tīng)我細(xì)細(xì)道來(lái)。

      先來(lái)看一下Koa中request類(lèi)所綁定的屬性,官方鏈接。

      我這里簡(jiǎn)單的列舉幾個(gè):

      ### request.header= 設(shè)置請(qǐng)求頭對(duì)象。 ### request.headers 請(qǐng)求頭對(duì)象。別名為 `request.header`. ### request.headers= 設(shè)置請(qǐng)求頭對(duì)象。別名為 `request.header=` ### request.url 獲取請(qǐng)求 URL. ### request.url= 設(shè)置請(qǐng)求 URL, 對(duì) url 重寫(xiě)有用。 ### request.originalUrl 獲取請(qǐng)求原始URL。

      這里對(duì)于每個(gè)屬性都有設(shè)置和獲取的功能,使用getter和setter可以很好的實(shí)現(xiàn)。

      這里的每個(gè)屬性是如何獲取的呢?還記得我們?cè)趓equest綁定了一個(gè)啥?node http原生的request(req)對(duì)不對(duì),當(dāng)我們使用Object.create并ctx.request.req=req之后,對(duì)于當(dāng)前的request對(duì)象是不是都有了一個(gè)req屬性。那么是不是可以通過(guò)getter進(jìn)行獲取。

      get url () { return this.req.url },

      這里的每個(gè)屬性是如何設(shè)置的,如果我們對(duì)request本身設(shè)置有效嗎?

      例如下面的結(jié)構(gòu):

      const request={ url:'', header:{ } }

      const request={ set url (val) { this.req.url = val } get url () { return this.req.url }, }

      request.socket的getter

      socket在這里指套接字。套接字的概念這里不贅述!

      get socket () { return this.req.socket },

      request.protocol的getter

      返回請(qǐng)求協(xié)議,“https” 或 “http”。當(dāng) app.proxy 是 true 時(shí)支持 X-Forwarded-Proto。

      先判斷套接字中是否存在encrypted(加密),如果加密,就是https,

      X-Forwarded-Proto用來(lái)確定客戶端與代理服務(wù)器或者負(fù)載均衡服務(wù)器之間的連接所采用的傳輸協(xié)議(HTTP 或 HTTPS)

      X-Forwarded-Proto: https X-Forwarded-Proto: http

      get protocol () { if (this.socket.encrypted) return 'https' if (!this.app.proxy) return 'http' const proto = this.get('X-Forwarded-Proto') return proto ? proto.split(/\s*,\s*/, 1)[0] : 'http' },

      這里有一個(gè)get函數(shù),主要時(shí)根據(jù)字段,從請(qǐng)求頭中獲取數(shù)據(jù)。

      get (field) { const req = this.req switch (field = field.toLowerCase()) { case 'referer': case 'referrer': return req.headers.referrer || req.headers.referer || '' default: return req.headers[field] || '' } },

      request.host的getter

      存在時(shí)獲取主機(jī)(hostname:port)。當(dāng) app.proxy 是 true 時(shí)支持 X-Forwarded-Host,否則使用 Host。

      get host () { const proxy = this.app.proxy let host = proxy && this.get('X-Forwarded-Host') if (!host) { if (this.req.httpVersionMajor >= 2) host = this.get(':authority') if (!host) host = this.get('Host') } if (!host) return '' return host.split(/\s*,\s*/, 1)[0] },

      request.origin的getter

      獲取URL的來(lái)源,包括 protocol 和 host。

      例如我請(qǐng)求:http://localhost:3000/index?a=3,

      origin返回的是http://localhost:3000

      get origin () { return `${this.protocol}://${this.host}` },

      request.href的getter

      獲取完整的請(qǐng)求URL,包括 protocol,host 和 url。

      href支持解析 GET http://example.com/foo

      例如我訪問(wèn)http://localhost:3000/index?a=3

      href返回http://localhost:3000/index?a=3

      get href () { if (/^https?:\/\//i.test(this.originalUrl)) return this.originalUrl return this.origin + this.originalUrl },

      注意:這里的this.originalUrl在封裝ctx的時(shí)候已經(jīng)綁定過(guò)了

      request.header 的getter和setter

      請(qǐng)求頭對(duì)象。這與 node http.IncomingMessage 上的 headers 字段相同

      get header () { return this.req.headers }, set header (val) { this.req.headers = val },

      request的屬性是很多的,我們就不展開(kāi)了,反正知道了原理,大家慢慢自己加吧。

      封裝response.js

      對(duì)比request的封裝,response的封裝稍微有些不同,因?yàn)椋瑢?duì)于request來(lái)說(shuō)大部分的封裝是getter,而response的封裝大部分都是setter

      在request部分我們闡述了三個(gè)使用getter和setter的原因。在resposne中最主要的原因我覺(jué)得是改變set的對(duì)象。

      其實(shí)想一想很簡(jiǎn)單,例如在網(wǎng)絡(luò)請(qǐng)求中我們會(huì)經(jīng)常遇到各種狀態(tài):404 200等等,這些在node的http模塊中,是用resposne.status進(jìn)行改變的。假設(shè)我們?cè)趉oa的response直接設(shè)置,你覺(jué)得會(huì)有用嗎?簡(jiǎn)單概括一句話:koa的request和respsone是對(duì)nodehttp模塊的二次封裝,并且底層還是對(duì)nodehttp模塊的操作。

      response.status的getterh和setter

      獲取響應(yīng)狀態(tài)。默認(rèn)情況下,response.status 設(shè)置為 404 而不是像 node 的 res.statusCode 那樣默認(rèn)為 200。

      默認(rèn)’404’,這里的默認(rèn)是在何時(shí)默認(rèn)的時(shí)候呢,其實(shí)是在接收到請(qǐng)求后就設(shè)置為404,也就是說(shuō)在callback的時(shí)候開(kāi)始設(shè)置為404。(注意:http中res.statusCode用來(lái)標(biāo)記狀態(tài)碼,在Koa中這個(gè)被封裝成status)

      callback = (req, res) => { let ctx = this.createContext(req, res) const res = ctx.res res.statusCode = 404 this.fn(ctx) }

      response.status的實(shí)現(xiàn)

      get status () { return this.res.statusCode }, set status (code) { if (this.headerSent) return assert(Number.isInteger(code), 'status code must be a number') assert(code >= 100 && code <= 999, `invalid status code: ${code}`) this._explicitStatus = true this.res.statusCode = code if (this.req.httpVersionMajor < 2) this.res.statusMessage = statuses[code] if (this.body && statuses.empty[code]) this.body = null },

      response.body的getter和setter

      首先我們要知道body是用來(lái)干嘛的。body是用來(lái)設(shè)置響應(yīng)主體的,也就是返回響應(yīng)的內(nèi)容的。這些內(nèi)容支持以下格式:

      string 寫(xiě)入

      Buffer 寫(xiě)入

      Stream 管道

      Object || Array JSON-字符串化

      null 無(wú)內(nèi)容響應(yīng)

      nodehttp中是 res.end(“我是返回內(nèi)容”) 返回響應(yīng)內(nèi)容的。在koa中我們是通過(guò)ctx.body="" 來(lái)設(shè)置響應(yīng)內(nèi)容的。這里有人會(huì)問(wèn)了,ctx.body和resopnse.body 有啥關(guān)系。其實(shí)他們是一個(gè)東西,ctx里面封裝了response.body。

      koa中通過(guò)設(shè)置ctx.body,就能返回內(nèi)容,其實(shí)本質(zhì)還是使用了res.end(),通過(guò)res.end(ctx.body)來(lái)返回內(nèi)容。res.end的調(diào)用時(shí)機(jī)在這里是放在callback中(具體的原因我們后面會(huì)說(shuō)到)

      const response = { _body: undefined, get body() { return this._body }, set body(originContent) { this.res.statusCode = 200; this._body = originContent; } };

      封裝context.js

      先談?wù)凨oa用到的delegates。這是一個(gè)實(shí)現(xiàn)了代理模式的包。對(duì)于Koa來(lái)說(shuō),context就是response和request的代理,通過(guò)ctx可以直接拿到request和response的屬性和方法。

      下面的是Koa主要用到的兩個(gè)方法。其實(shí)最終的效果和封裝request和response的效果一致。

      __defineGetter__ 方法可以將一個(gè)函數(shù)綁定在當(dāng)前對(duì)象的指定屬性上,當(dāng)那個(gè)屬性的值被讀取時(shí),你所綁定的函數(shù)就會(huì)被調(diào)用。

      __defineSetter__ 方法可以將一個(gè)函數(shù)綁定在當(dāng)前對(duì)象的指定屬性上,當(dāng)那個(gè)屬性被賦值時(shí),你所綁定的函數(shù)就會(huì)被調(diào)用。

      (這兩個(gè)方法已廢棄: 該特性已經(jīng)從 Web 標(biāo)準(zhǔn)中刪除,雖然一些瀏覽器目前仍然支持它,但也許會(huì)在未來(lái)的某個(gè)時(shí)間停止支持,請(qǐng)盡量不要使用該特性。)

      Delegator.prototype.setter = function (name) { var proto = this.proto; var target = this.target; this.setters.push(name); proto.__defineSetter__(name, function (val) { return this[target][name] = val; }); return this; };

      Delegator.prototype.getter = function (name) { var proto = this.proto; var target = this.target; this.getters.push(name); proto.__defineGetter__(name, function () { return this[target][name]; }); return this; };

      這里我們將delegates的核心邏輯抽離,封裝context

      function defineGetter(target, key) { context.__defineGetter__(key, function () { return this[target][key] }) } function defineSetter(target, key) { context.__defineSetter__(key, function (value) { this[target][key] = value }) }

      const context = {}; defineGetter('request', 'path') defineGetter('response', 'body') ) module.exports = context;

      這里我們就列了兩個(gè),其他的不再贅述。

      ctx.body再追述

      在上面我們談到了response.body以及ctx通過(guò)代理模式,拿到了response.body.

      在Koa的源碼中,針對(duì)不同格式的內(nèi)容進(jìn)行了不同的處理.大家簡(jiǎn)單看一下就可以。

      response = { set body (val) { const original = this._body this._body = val // no content if (val == null) { if (!statuses.empty[this.status]) { if (this.type === 'application/json') { this._body = 'null' return } this.status = 204 } return } // 內(nèi)容存在(設(shè)置了內(nèi)容),這是狀態(tài)碼為200 if (!this._explicitStatus) this.status = 200 // string 字符串 if (typeof val === 'string') { if (setType) this.type = /^\s* this.ctx.onerror(err)) // overwriting if (original != null) this.remove('Content-Length') } if (setType) this.type = 'bin' return } // json this.remove('Content-Length') this.type = 'json' } },

      ctx.body最終是在res.end()返回的,這個(gè)時(shí)機(jī)是在callback中調(diào)用的。

      我們通過(guò)app.use來(lái)傳入我們要執(zhí)行的方法,這個(gè)方法里面有ctx.body的賦值。

      app.use((ctx) => { console.log(ctx.request.href) ctx.body="123" })

      在callback中我們先創(chuàng)建了上下文,然后我們調(diào)用了傳入的方法。

      callback = (req, res) => { let ctx = this.createContext(req, res) this.fn(ctx) }

      那么我們是不是應(yīng)該在fn執(zhí)行結(jié)束之后,調(diào)用res.end(),因?yàn)檫@個(gè)時(shí)候body才被賦值

      callback = (req, res) => { let ctx = this.createContext(req, res) this.fn(ctx) let body = ctx.body; if (body) { res.end(body); } else { res.end('Not Found') } }

      小結(jié)

      至此已經(jīng)實(shí)現(xiàn)了koa的基本內(nèi)容。

      具體來(lái)說(shuō):

      response通過(guò)getter和setter,封裝了nodehttp的res

      request通過(guò)getter和setter,封裝了nodehttp的req

      ctx通過(guò)代理,拿到了response和request的屬性方法

      Koa

      版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。

      上一篇:Excel怎樣為圖表添加達(dá)標(biāo)線(怎樣在excel圖表中添加標(biāo)準(zhǔn)線)
      下一篇:萬(wàn)字講明白TiDB數(shù)據(jù)庫(kù)丨【綻放吧!數(shù)據(jù)庫(kù)】(數(shù)據(jù)庫(kù) tidb)
      相關(guān)文章
      国产亚洲av人片在线观看| 亚洲精品色在线网站| 亚洲精品国产电影| 亚洲精品无码中文久久字幕| 亚洲国产乱码最新视频| 国产成+人+综合+亚洲专| 亚洲福利电影在线观看| 色婷婷亚洲十月十月色天| 亚洲综合一区二区精品导航| 亚洲国产老鸭窝一区二区三区| 久久亚洲精品成人综合| 亚洲韩国—中文字幕| 久久亚洲精品中文字幕无码 | 国产亚洲人成网站在线观看| mm1313亚洲精品无码又大又粗| 成人精品国产亚洲欧洲| 一本久久综合亚洲鲁鲁五月天| 亚洲成av人片天堂网老年人 | 久久久亚洲裙底偷窥综合| 久久水蜜桃亚洲av无码精品麻豆| 久久亚洲AV成人无码电影| 久久精品亚洲精品国产色婷| 亚洲欧洲日产国产最新| 亚洲男人天堂2018av| 亚洲精品无码久久久久久| 亚洲AV无码男人的天堂| 亚洲精品国产va在线观看蜜芽| 亚洲日本va午夜中文字幕久久| 自拍偷自拍亚洲精品被多人伦好爽| 亚洲中文字幕无码久久2017| 亚洲成AV人片一区二区| 蜜芽亚洲av无码精品色午夜| 亚洲国产成人久久| 国产成人亚洲综合网站不卡| 亚洲av永久无码一区二区三区| 亚洲国产综合人成综合网站| 亚洲老妈激情一区二区三区| 麻豆亚洲AV永久无码精品久久 | 精品亚洲成在人线AV无码| 亚洲欧洲av综合色无码| 亚洲国产日韩在线观频|