<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                &emsp;&emsp;本次分析的KOA版本是[2.13.1](https://github.com/koajs/koa),它非常輕量,諸如路由、模板等功能默認都不提供,需要自己引入相關的中間件。 &emsp;&emsp;源碼的目錄結構比較簡單,主要分為3部分,\_\_tests\_\_,lib和docs,從名稱中就可以了解到。 &emsp;&emsp;\_\_tests\_\_是單元測試,lib是核心代碼,docs是文檔。在lib目錄中只有4個文件。 ~~~ ├── __tests__ ------------------------ 單元測試 ├── docs ----------------------------- 文檔 ├── lib ------------------------------ 源碼目錄 │ ├── application.js --------------- 運行 │ ├── context.js ------------------- 上下文 │ ├── request.js ------------------- 請求 │ ├── response.js ------------------ 響應 ~~~ &emsp;&emsp;閱讀源碼除了能學到不經常使用的概念之外,還能學到各種軟件開發思路,見識到各種類型的第三方庫,對于提升自己的日常編碼很有幫助。 ## 一、package.json &emsp;&emsp;在package.json文件中,可以看到KOA的入口是?application.js。 ~~~ "main": "lib/application.js", ~~~ &emsp;&emsp;在devDependencies中,依賴的是ESLint和單元測試庫。 &emsp;&emsp;在dependencies中,好幾個都是與通信有關的庫,還有幾個工具庫,具體包括: * [accepts](https://www.npmjs.com/package/accepts):為給定的 req 創建一個新的 Accepts 對象。 * [cache-content-type](https://www.npmjs.com/package/cache-content-type):與 mime-types 的 contentType 方法相同,但緩存了結果。 * [content-disposition](https://www.npmjs.com/package/content-disposition):創建和解析 HTTP Content-Disposition 頭。 * [content-type](https://www.npmjs.com/package/content-type):根據 RFC 7231 創建和解析 HTTP Content-Type 頭。 * [cookies](https://www.npmjs.com/package/cookies):一個用于獲取和設置 HTTP(S) cookie 的 node.js 模塊。 * [debug](https://www.npmjs.com/package/debug):一個模仿 Node.js 核心調試技術的小型 JavaScript 調試實用程序。 * [delegates](https://www.npmjs.com/package/delegates):創建一個委托實例,讓一個對象可以直接訪問其屬性對象的屬性和方法(在下一篇中會詳細說明)。 * [destroy](https://www.npmjs.com/package/destroy):銷毀一個流,確保流被銷毀,處理不同的 API 和 Node.js 錯誤。 * [encodeurl](https://www.npmjs.com/package/encodeurl):將 URL 編碼為百分比編碼形式,不包括已編碼的序列。 * [escape-html](https://www.npmjs.com/package/escape-html):將特殊字符轉換成HTML實體。例如?foo & bar =》foo &amp; bar。 * [fresh](https://www.npmjs.com/package/fresh):HTTP 響應新鮮度測試。 * [http-assert](https://www.npmjs.com/package/http-assert):狀態碼斷言,像 Koa 中的 ctx.throw() 一樣,但是有一個守衛。 * [http-errors](https://www.npmjs.com/package/http-errors):為 Express、Koa、Connect 等創建 HTTP 錯誤。 * [koa-compose](https://www.npmjs.com/package/koa-compose):組合給定的中間件,KOA的插件。 * [on-finished](https://www.npmjs.com/package/on-finished):當 HTTP 請求關閉、完成或出錯時執行回調。 * [only](https://www.npmjs.com/package/only):指定屬性白名單,然后只返回這幾個屬性。 * [parseurl](https://www.npmjs.com/package/parseurl):解析給定請求對象的 URL(req.url 屬性)并返回結果,結果與 url.parse 相同。在 req.url 不變的同一個 req 上多次調用此函數將返回一個緩存的解析對象。 * [statuses](https://www.npmjs.com/package/statuses):返回已知 HTTP 狀態代碼的狀態消息字符串。 * [type-is](https://www.npmjs.com/package/type-is):檢查請求的內容類型是否是 content-type 中的一種類型。 * [vary](https://www.npmjs.com/package/vary):將給定的頭字段添加到 res 的 Vary 響應頭中。 ## 二、application.js &emsp;&emsp;application.js是KOA的入口文件,在此文件中,會引入lib目錄的另外3個文件,以及多個依賴庫。 ~~~ const debug = require('debug')('koa:application') const onFinished = require('on-finished') const response = require('./response') const compose = require('koa-compose') const context = require('./context') const request = require('./request') const statuses = require('statuses') const Emitter = require('events') const util = require('util') const Stream = require('stream') const http = require('http') const only = require('only') const { HttpError } = require('http-errors') ~~~ &emsp;&emsp;在下面的代碼中,去掉了大部分的方法體,只留下了方法名和注釋。其中Application繼承自Emitter,這樣就能監聽和觸發自定義事件了。 ~~~ /** * 繼承自 Emitter.prototype */ module.exports = class Application extends Emitter { constructor (options) { } /** * 簡寫: * http.createServer(app.callback()).listen(...) */ listen (...args) { } /** * JSON格式化 */ toJSON () { return only(this, ['subdomainOffset', 'proxy', 'env']) } /** * Inspect implementation. */ inspect () { return this.toJSON() } /** * 使用給定的中間件 fn */ use (fn) { } /** * 請求處理程序回調,用于本機 http 服務器 */ callback () { } /** * 在回調中處理請求 */ handleRequest (ctx, fnMiddleware) { } /** * 初始化一個新的上下文 */ createContext (req, res) { } /** * 默認錯誤處理程序 */ onerror (err) { } /** * 幫助 TS 用戶遵守 CommonJS、ESM、bundler mismatch * @see https://github.com/koajs/koa/issues/1513 */ static get default () { return Application } } /** * 響應助手 */ function respond (ctx) { } /** * 使庫的消費者可以使用 HttpError,這樣消費者就不會直接依賴于 `http-errors` */ module.exports.HttpError = HttpError ~~~ &emsp;&emsp;在看過源碼后,再來閱讀一段簡單的demo,在初始化KOA實例后,調用了Application的 use() 和 listen() 兩個方法。 ~~~ const Koa = require("koa"); const app = new Koa(); app.use(async (ctx, next) => { ctx.body = "hello,KOA"; }); app.listen(3000); ~~~ **1)構造函數** &emsp;&emsp;在構造函數中,會聲明各種參數,包括代理信息、環境變量等。 &emsp;&emsp;其中[Object.create()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/create)用于創建一個新對象,帶著指定的原型對象和屬性。 &emsp;&emsp;因為在同一個應用中可能會有多個KOA實例,所以為了防止相互污染,通過Object.create()的拷貝將他們不再引用同一個地址。 ~~~ constructor (options) { super() options = options || {} // 參數 this.proxy = options.proxy || false// 是否代碼模式 this.subdomainOffset = options.subdomainOffset || 2 this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For' // 代理 IP 頭,默認為 X-Forwarded-For this.maxIpsCount = options.maxIpsCount || 0 // 從代理 IP 標頭讀取的最大 IP,默認為 0(表示無窮大) this.env = options.env || process.env.NODE_ENV || 'development' // 環境變量 if (options.keys) this.keys = options.keys this.middleware = [] this.context = Object.create(context) // 創建一個新的context this.request = Object.create(request)// 創建一個新的request this.response = Object.create(response)// 創建一個新的response // util.inspect.custom support for node 6+ /* istanbul ignore else */ if (util.inspect.custom) { this[util.inspect.custom] = this.inspect } } ~~~ **2)use()** &emsp;&emsp;在KOA實例中,會維護一個中間件數組(middleware),在添加fn之前,會利用[typeof](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/typeof)判斷其是否是函數類型。 ~~~ use (fn) { if (typeof fn !== 'function') throw new TypeError('middleware must be a function!') this.middleware.push(fn) return this } ~~~ &emsp;&emsp;KOA的中間件采用的是著名的洋蔥模型,后面會細說。 **3)listen()** &emsp;&emsp;listen()內部直接調用[http.createServer()](https://nodejs.org/dist/latest-v18.x/docs/api/http.html#httpcreateserveroptions-requestlistener)創建一個server,監聽指定端口,并且每個請求都會回調當前實例的callback()方法。 ~~~ listen (...args) { const server = http.createServer(this.callback()) return server.listen(...args) } ~~~ &emsp;&emsp;在callback()方法中,會調用洋蔥模型的compose()函數,監聽error事件(回調error()函數),最后處理請求調用handleRequest()方法。 ~~~ callback () { // 包裝所有的中間件,返回一個可執行函數,compose()是洋蔥模型的實現 const fn = compose(this.middleware) // 若未指定error事件,那么創建error事件監聽器 if (!this.listenerCount('error')) { this.on('error', this.onerror) } const handleRequest = (req, res) => { // 為ctx包裝Node原生的req和res,并且每個請求都是單獨的ctx const ctx = this.createContext(req, res) // 實例的handleRequest(),并不是遞歸 return this.handleRequest(ctx, fn) } return handleRequest } ~~~ **4)compose()** &emsp;&emsp;中間件通常用于完成一些全局的特定功能,例如權限驗證、錯誤處理、日志添加等。 &emsp;&emsp;下面是一個簡單的中間件示例,用于處理500響應。 ~~~ export default () => async (ctx, next) => { try { await next(); } catch (error) { ctx.status = 500; ctx.body = { error: String(error), stack: error.stack }; } }; ~~~ &emsp;&emsp;compose()引用自koa-compose庫,在該庫中,中間件會被next()函數分成兩部分,先執行next()之前的部分,在請求處理完畢后,再執行next()后面的部分。 &emsp;&emsp;下圖是官方給的一張中間件執行順序示意圖。 :-: ![](https://img.kancloud.cn/b3/53/b353e7e458d7542b366c1139b01b1a07_1169x1000.gif =800x) &emsp;&emsp;在下圖中,每一層相當于是一個中間件,在request時,處理的是next()的前半部分,在response時,處理的是其后半部分。 :-: ![](https://img.kancloud.cn/d0/88/d088e7f855ba2ebeb2056dbd6f75a530_683x619.jpeg =400x) &emsp;&emsp;下面就是koa-compose庫的所有代碼,已加注釋,為了便于理解,我已經將可執行的代碼放到[codepen](https://codepen.io/strick/pen/XWVQmgQ)中,在線調試。 ~~~ 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!') } /** * 返回一個函數 * context就是ctx * next()函數就是下一個中間件函數 */ return function (context, next) { // 上一個中間件的索引 let index = -1 // 啟動dispatch()函數,初始值是0 return dispatch(0) function dispatch (i) { // 以免在一個中間件內,調用多次next() if (i <= index) return Promise.reject(new Error('next() called multiple times')) index = i // fn就是中間件函數 let fn = middleware[i] // 中間件都已執行過一次,fn是undefined if (i === middleware.length) fn = next // 終止遞歸 if (!fn) return Promise.resolve() try { // fn是中間件,dispatch()就是下一個中間件的next()函數 return Promise.resolve(fn(context, dispatch.bind(null, i + 1))) } catch (err) { return Promise.reject(err) } } } } ~~~ &emsp;&emsp;函數分為幾步: * 第一步是檢查中間件數組和中間件的類型。 * 第二步是返回一個函數,參數是 ctx 和 next(),其中 next() 就是下一個中間件函數。 * 第三步是調用 dispatch(0) 啟動中間件的運行,并且在一個中間件中,不允許多次調用 next() 函數。 * 第四步是遞歸地依次為每一個要執行的中間件傳遞參數,其第二個參數是下一個 dispatch() 函數。 &emsp;&emsp;遞歸過程中的 dispatch() 其實就是中間件中的 next() 函數。 &emsp;&emsp;Promise.resolve(fn(context, dispatch.bind(null, i + 1))) 會先運行一次中間件,然后遇到 next(),就去運行下一個中間件,遞歸終止后,再回溯處理中間件余下的邏輯。 **5)createContext()** &emsp;&emsp;每次HTTP請求都生成一個新的context,與其他請求中的context之間相互隔離。 ~~~ createContext (req, res) { // 每次HTTP請求都生成一個新的context 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 // 掛載Node原生的req和res 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 // 可自定義的狀態,例如koa-jwt庫就使用了該屬性 context.state = {} return context } ~~~ &emsp;&emsp;context具備高內聚的特征,因為它能訪問KOA提供的所有數據和方法。 &emsp;&emsp;并且還預留了一個state屬性,可用于傳遞自定義的狀態值。 **6)handleRequest()** &emsp;&emsp;在 handleRequest() 函數中,會運行中間件函數,以及處理響應的不同情況。 ~~~ /** * 在回調中處理請求 * @param {*} ctx 上下文 * @param {*} fnMiddleware 可執行的中間件函數 * @returns */ 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) } ~~~ &emsp;&emsp;respond()函數內容比較多,包括為格式化JSON格式的body,流類型的body調用pipe(),為HEAD請求加 Content-Length 頭等。 &emsp;&emsp;官方也提供了屬性,來繞開上述這些處理。 ~~~ function respond (ctx) { // 允許繞過KOA的處理 if (ctx.respond === false) return if (!ctx.writable) return const res = ctx.res let body = ctx.body const code = ctx.status // code不是已知的狀態碼 if (statuses.empty[code]) { // strip headers ctx.body = null return res.end() } // HEAD請求 if (ctx.method === 'HEAD') { // 加Content-Lengthh頭 if (!res.headersSent && !ctx.response.has('Content-Length')) { const { length } = ctx.response if (Number.isInteger(length)) ctx.length = length } return res.end() } // status body if (body == null) { if (ctx.response._explicitNullBody) { ctx.response.remove('Content-Type') ctx.response.remove('Transfer-Encoding') ctx.length = 0 return res.end() } if (ctx.req.httpVersionMajor >= 2) { body = String(code) } else { body = ctx.message || String(code) } if (!res.headersSent) { ctx.type = 'text' ctx.length = Buffer.byteLength(body) } return res.end(body) } // 對body的三種類型采用不同的處理 if (Buffer.isBuffer(body)) return res.end(body) if (typeof body === 'string') return res.end(body) if (body instanceof Stream) return body.pipe(res) // JSON格式的body body = JSON.stringify(body) if (!res.headersSent) { ctx.length = Buffer.byteLength(body) } res.end(body) } ~~~ 參考資料: [koa源碼解析](https://zhuanlan.zhihu.com/p/104621640) [高質量 - Koa 源碼解析](https://segmentfault.com/a/1190000021109975) [超級詳細的koa源碼解析](https://juejin.cn/post/6855129007508488206) ***** > 原文出處: [博客園-Node.js躬行記](https://www.cnblogs.com/strick/category/1688575.html) [知乎專欄-Node.js躬行記](https://zhuanlan.zhihu.com/pwnode) 已建立一個微信前端交流群,如要進群,請先加微信號freedom20180706或掃描下面的二維碼,請求中需注明“看云加群”,在通過請求后就會把你拉進來。還搜集整理了一套[面試資料](https://github.com/pwstrick/daily),歡迎閱讀。 ![](https://box.kancloud.cn/2e1f8ecf9512ecdd2fcaae8250e7d48a_430x430.jpg =200x200) 推薦一款前端監控腳本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不僅能監控前端的錯誤、通信、打印等行為,還能計算各類性能參數,包括 FMP、LCP、FP 等。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看