node koa原始碼解釋
再次思考:從瀏覽器輸入 URL 到頁面展示過程的過程中發生了什麼?
通過前面的基礎學習,我們瞭解了基於 Web 的應用基本流程:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-LWuReu4F-1608449778043)(./assets/e9ac4df2f8db06466fee94cad4401522.svg)]
通過上圖不難發現,無論具體應用功能如何變化, 服務端
處理任務核心三個步驟:③、④、⑤ 中,③ 和 ⑤ 的模式基本是固定的(因為HTTP協議規範了),而 ④ 是最大的變數。
如果我們每次開發一個新的應用都要把 ③ 和 ⑤ 的邏輯重新實現一遍就會特別的麻煩。所以,我們可以封裝一個框架(庫)把 ③ 和 ⑤ 的邏輯進行統一處理,然後通過某種方式,把 ④ 的處理暴露給框架使用者。
Koa
資源:
官網:https://koajs.com/
中文:https://koa.bootcss.com/
- 基於 NodeJS 的 web 框架,致力於 web 應用和 API 開發。
- 由 Express 原班人馬打造。
- 支援 async。
- 更小、更靈活、更優雅。
安裝
當前最新 Koa 依賴 node v7.6.0+、ES2015+ 以及 async 的支援。
具體請關注官網說明(依賴會隨著版本的變化而變化)。
參考:https://koajs.com/#introduction
# 安裝 koa
npm i koa
# 或者
yarn add koa
核心
Koa
對 NodeJS
原生 IncomingMessage
和 ServerResponse
物件和解析響應通用流程進行了包裝,並提供了幾個核心類(物件)用於其它各種使用者業務呼叫。
- Application 物件
- Context 物件
- Request 物件
- Response 物件
Application 物件
該物件是 Koa
的核心物件,通過該物件來初始化並建立 WebServer
。
/**
* File: /node_modules/koa/lib/application.js
***/
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;
}
}
建構函式對 Application
建立進行了一些初始化工作,暫時不需要關注這裡的太多細節,後續關注。
應用程式碼
/**
* File: /app.js
***/
const Koa = require('koa');
const app = new Koa();
listen 方法
WebServer
並不是在 Application
物件建立的時候就被建立的,而是在呼叫了 Application
下的 listen
方法的時候在建立。
/**
* File: /node_modules/koa/lib/application.js
***/
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
通過原始碼可以看到,其本質還是通過 NodeJS
內建的 http
模組的 createServer
方法建立的 Server
物件。並且把 this.callback()
執行後的結果(函式)作為後續請求的回撥函式。
/**
* File: /node_modules/koa/lib/application.js
***/
callback() {
const fn = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error', this.onerror);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
通過上述程式碼的分析,實際上請求執行的回撥函式式 callback
返回的 handleRequest
函式,且該函式接收的 req
和 res
引數就是 NodeJS
中 HTTP
模組內建的兩個物件 IncomingMessage
和 ServerResponse
物件。其中:
const ctx = this.createContext(req, res);
這裡, Koa
會呼叫 Application
物件下的 createContext
方法對 req
和 res
進行包裝,生成 Koa
另外一個核心物件: Context
物件 - 後續分析。
return this.handleRequest(ctx, fn);
接著呼叫 Application
物件下的 handleRequest
方法進行請求處理,並傳入:
- ctx: 前面提到的
Context
物件。 - fn: 這個實際上是
const fn = compose(this.middleware);
這段程式碼得到的是一個執行函式,這裡又稱為:中介軟體函式
。
應用程式碼
/**
* File: /app.js
***/
const Koa = require('koa');
const app = new Koa();
app.listen(8888);
中介軟體函式
所謂的中介軟體函式,其實就是一開始我們提到的 ④,首先, Application
物件中會提供一個屬性 this.middleware = [];
,它是一個陣列,用來儲存 ④ 需要處理的各種業務函式。這些業務函式會通過 Application
下的 use
方法進行註冊(類似事件註冊)。
為什麼叫中介軟體
因為它是在 請求
之後, 響應
之前呼叫的函式,所以就叫它 中介軟體函式
。
響應流程處理
通過上述流程分析,可以看到,每一個請求都會執行到 Application
物件下的 handleRequest
方法。
/**
* File: /node_modules/koa/lib/application.js
***/
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);
}
這裡的 fnMiddleware
就是一系列中介軟體函式執行後結果(一個 Promise
物件),當所有中介軟體函式執行完成以後,會通過 then
呼叫 handleResponse
,也就是呼叫了 respond
這個方法。
響應處理
/**
* File: /node_modules/koa/lib/application.js
***/
function respond(ctx) {
// allow bypassing koa
if (false === ctx.respond) return;
if (!ctx.writable) return;
const res = ctx.res;
let body = ctx.body;
const code = ctx.status;
// ignore body
if (statuses.empty[code]) {
// strip headers
ctx.body = null;
return res.end();
}
if ('HEAD' === ctx.method) {
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 (null == body) {
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);
}
// responses
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' == typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);
// body: json
body = JSON.stringify(body);
if (!res.headersSent) {
ctx.length = Buffer.byteLength(body);
}
res.end(body);
}
上面這個函式就是 Koa
在處理完各種中介軟體函式以後,最後進行響應的邏輯。
Koa 的流程
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-PnyjrWVB-1608449778045)(./assets/lake_card_mindmap.png)]
中介軟體
首先, Application
物件通過一個陣列來儲存中介軟體:
/**
* File: lib/application.js
***/
constructor() {
// ...
this.middleware = [];
// ...
}
註冊中介軟體函式
其次,Application
物件提供了一個 use
方法來註冊中介軟體函式:
/**
* File: lib/application.js
***/
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
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);
return this;
}
應用程式碼
/**
* File: /app.js
***/
const Koa = require('koa');
const app = new Koa();
app.use( (ctx) => {
ctx.body = 'Hello!';
} );
app.listen(8888);
中介軟體的執行
中介軟體的執行實際來源另外一個獨立模組: koa-compose
提供的 compose
函式。
/**
* File: lib/application.js
***/
callback() {
const fn = compose(this.middleware);
// ...
}
/**
* Module: koa-compose
* File: index.js
***/
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!')
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
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, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
compose 函式
組合 - 把多個函式組合成一個函式執行。
上面這個 compose
函式核心就在:
Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
它會在執行當前中介軟體函式的時候,把下一個中介軟體函式作為當前中介軟體函式的第二個引數傳入(next)。這樣就可以實現對多箇中介軟體函式執行流程進行把控。
應用程式碼
/**
* File: /app.js
***/
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
console.log('a');
});
app.use(async ctx => {
console.log('b');
});
app.use(async ctx => {
console.log('c');
});
app.use(async ctx => {
console.log('d');
});
app.listen(8888);
輸出
a
我們會發現,當我們訪問這個 WebServer
的時候,後端伺服器上列印的只有 a
。這是因為當第一個中介軟體函式執行以後,後續的中介軟體是需要通過當前執行中介軟體函式的第二個引數去顯式的呼叫才能執行的。
應用程式碼
/**
* File: /app.js
***/
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
console.log('a');
next();
});
app.use(async (ctx, next) => {
console.log('b');
next();
});
app.use(async (ctx, next) => {
console.log('c');
next();
});
app.use(async (ctx, next) => {
console.log('d');
next();
});
app.listen(8888);
輸出
a
b
c
d
通過以上的程式碼改造,我們會發現就實現了 a
、 b
、 c
、 d
的輸出了。
因為函式呼叫的棧(LIFO - Last In First Out - 後進先出)特性,所以:
應用程式碼
/**
* File: /app.js
***/
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
console.log('a - start');
next();
console.log('a - end');
});
app.use(async (ctx, next) => {
console.log('b - start');
next();
console.log('b - end');
});
app.use(async (ctx, next) => {
console.log('c - start');
next();
console.log('c - end');
});
app.use(async (ctx, next) => {
console.log('d - start');
next();
console.log('d - end');
});
app.listen(8888);
輸出
a - start
b - start
c - start
d - start
d - end
c - end
b - end
a - end
我們給這種特性現象起了一個很形象的名字:
洋蔥模型
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-WOEzcASV-1608449778047)(./assets/image.png)]
好處
框架這麼設計的好處在哪呢? - 擴充套件
應用程式碼
/**
* File: /app.js
***/
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
// 這是我們開始的某個業務邏輯
ctx.body = 'hello';
});
app.listen(8888);
現在,我們希望在不改變原有中介軟體邏輯的基礎上進行一些擴充套件,比如在現有 body
內容後面新增 ',kkb!'
這個字串,我們就可以利用中介軟體特性來進行擴充套件了:
應用程式碼
/**
* File: /app.js
***/
const Koa = require('koa');
const app = new Koa();
// 在不改變原有中介軟體邏輯程式碼基礎上進行擴充套件
app.use(async (ctx, next) => {
// 注意這裡,我們是要在原有中介軟體邏輯之後新增新的邏輯,所以先 呼叫 next。
next();
ctx.body += ', kkb!';
});
app.use(async (ctx, next) => {
// 這是我們開始的某個業務邏輯
ctx.body = 'hello';
});
app.listen(8888);
next 呼叫取決中介軟體的具體需求。放置在你想要呼叫的任何階段。
非同步的中介軟體
有的時候,我們的中介軟體邏輯中會包含一些非同步任務:
應用程式碼
/**
* File: /app.js
***/
const Koa = require('koa');
const app = new Koa();
// 在不改變原有中介軟體邏輯程式碼基礎上進行擴充套件
app.use(async (ctx, next) => {
// 注意這裡,我們是要在原有中介軟體邏輯之後新增新的邏輯,所以先 呼叫 next
next();
ctx.body += ', kkb!';
});
app.use(async (ctx, next) => {
setTimeout(() => {
ctx.body = 'hello';
}, 1000);
});
app.listen(8888);
我們會發現,還不等定時器執行, Koa
就已經返回(處理響應了)。我們需要把任務包裝成 Promise
的:
應用程式碼
/**
* File: /app.js
***/
const Koa = require('koa');
const app = new Koa();
// 在不改變原有中介軟體邏輯程式碼基礎上進行擴充套件
app.use(async (ctx, next) => {
// 注意這裡需要使用 await 來處理非同步的任務
await next();
ctx.body += ', kkb!';
});
app.use(async (ctx, next) => {
// 返回一個 Promise
return new Promise((resolve, reject) => {
setTimeout(() => {
ctx.body = 'hello';
resolve();
}, 1000);
});
});
app.listen(8888);
輸出(客戶端-如:瀏覽器)
hello, kkb!
注意:這裡需要注意中介軟體的註冊順序!
應用程式碼
/**
* File: /app.js
***/
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
// 返回一個 Promise
return new Promise((resolve, reject) => {
setTimeout(() => {
ctx.body = 'hello';
resolve();
}, 1000);
});
});
// 在不改變原有中介軟體邏輯程式碼基礎上進行擴充套件
app.use(async (ctx, next) => {
// 注意這裡需要使用 await 來處理非同步的任務
await next();
ctx.body += ', kkb!';
});
app.listen(8888);
輸出(客戶端-如:瀏覽器)
hello
Context 物件
Koa
的核心物件之一,它為 Koa
框架內部提供了重要的各種介面,同時也通過這個物件代理了 Application
、 Request
、 Response
物件的訪問,簡而言之,後續框架的提供的各種方法都是通過該物件來完成的。
/**
* File: lib/application.js
***/
constructor() {
// ...
this.context = Object.create(context);
// ...
}
context 物件
這裡的 context
物件,來源於 lib/context.js
,提供一些基礎方法,同時對 Request
和 Response
物件做了代理訪問:
/**
* File: lib/context.js
***/
'use strict';
/**
* Module dependencies.
*/
const util = require('util');
const createError = require('http-errors');
const httpAssert = require('http-assert');
const delegate = require('delegates');
const statuses = require('statuses');
const Cookies = require('cookies');
const COOKIES = Symbol('context#cookies');
/**
* Context prototype.
*/
const proto = module.exports = {
/**
* util.inspect() implementation, which
* just returns the JSON output.
*
* @return {Object}
* @api public
*/
inspect() {
if (this === proto) return this;
return this.toJSON();
},
/**
* Return JSON representation.
*
* Here we explicitly invoke .toJSON() on each
* object, as iteration will otherwise fail due
* to the getters and cause utilities such as
* clone() to fail.
*
* @return {Object}
* @api public
*/
toJSON() {
return {
request: this.request.toJSON(),
response: this.response.toJSON(),
app: this.app.toJSON(),
originalUrl: this.originalUrl,
req: '<original node req>',
res: '<original node res>',
socket: '<original node socket>'
};
},
/**
* Similar to .throw(), adds assertion.
*
* this.assert(this.user, 401, 'Please login!');
*
* See: https://github.com/jshttp/http-assert
*
* @param {Mixed} test
* @param {Number} status
* @param {String} message
* @api public
*/
assert: httpAssert,
/**
* Throw an error with `status` (default 500) and
* `msg`. Note that these are user-level
* errors, and the message may be exposed to the client.
*
* this.throw(403)
* this.throw(400, 'name required')
* this.throw('something exploded')
* this.throw(new Error('invalid'))
* this.throw(400, new Error('invalid'))
*
* See: https://github.com/jshttp/http-errors
*
* Note: `status` should only be passed as the first parameter.
*
* @param {String|Number|Error} err, msg or status
* @param {String|Number|Error} [err, msg or status]
* @param {Object} [props]
* @api public
*/
throw(...args) {
throw createError(...args);
},
/**
* Default error handling.
*
* @param {Error} err
* @api private
*/
onerror(err) {
// don't do anything if there is no error.
// this allows you to pass `this.onerror`
// to node-style callbacks.
if (null == err) return;
if (!(err instanceof Error)) err = new Error(util.format('non-error thrown: %j', err));
let headerSent = false;
if (this.headerSent || !this.writable) {
headerSent = err.headerSent = true;
}
// delegate
this.app.emit('error', err, this);
// nothing we can do here other
// than delegate to the app-level
// handler and log.
if (headerSent) {
return;
}
const { res } = this;
// first unset all headers
/* istanbul ignore else */
if (typeof res.getHeaderNames === 'function') {
res.getHeaderNames().forEach(name => res.removeHeader(name));
} else {
res._headers = {}; // Node < 7.7
}
// then set those specified
this.set(err.headers);
// force text/plain
this.type = 'text';
// ENOENT support
if ('ENOENT' == err.code) err.status = 404;
// default to 500
if ('number' != typeof err.status || !statuses[err.status]) err.status = 500;
// respond
const code = statuses[err.status];
const msg = err.expose ? err.message : code;
this.status = err.status;
this.length = Buffer.byteLength(msg);
res.end(msg);
},
get cookies() {
if (!this[COOKIES]) {
this[COOKIES] = new Cookies(this.req, this.res, {
keys: this.app.keys,
secure: this.request.secure
});
}
return this[COOKIES];
},
set cookies(_cookies) {
this[COOKIES] = _cookies;
}
};
/**
* Custom inspection implementation for newer Node.js versions.
*
* @return {Object}
* @api public
*/
/* istanbul ignore else */
if (util.inspect.custom) {
module.exports[util.inspect.custom] = module.exports.inspect;
}
/**
* Response delegation.
*/
delegate(proto, 'response')
.method('attachment')
.method('redirect')
.method('remove')
.method('vary')
.method('has')
.method('set')
.method('append')
.method('flushHeaders')
.access('status')
.access('message')
.access('body')
.access('length')
.access('type')
.access('lastModified')
.access('etag')
.getter('headerSent')
.getter('writable');
/**
* Request delegation.
*/
delegate(proto, 'request')
.method('acceptsLanguages')
.method('acceptsEncodings')
.method('acceptsCharsets')
.method('accepts')
.method('get')
.method('is')
.access('querystring')
.access('idempotent')
.access('socket')
.access('search')
.access('method')
.access('query')
.access('path')
.access('url')
.access('accept')
.getter('origin')
.getter('href')
.getter('subdomains')
.getter('protocol')
.getter('host')
.getter('hostname')
.getter('URL')
.getter('header')
.getter('headers')
.getter('secure')
.getter('stale')
.getter('fresh')
.getter('ips')
.getter('ip');
context 物件的初始化
通過上面程式碼,我們可以看到,在 Application
物件初始化的時候,會建立一個 Context
物件,並掛載到 Application
的 context
屬性下。同時在中介軟體執行的時候,還會對這個 context
進行包裝,並把包裝後的 context
物件作為中介軟體函式的第一個引數進行傳入,所以我們就可以通過中介軟體函式的第一個引數來呼叫這個 context
物件了。
/**
* File: lib/application.js
***/
callback() {
// ...
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
}
/**
* File: lib/application.js
***/
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;
}
ctx.state 屬性
有的時候,我們需要在多箇中介軟體中傳遞共享資料,雖然我們可以通過 context
物件進行掛載,但是這樣會對 context
物件造成汙染, context
物件為我們提供了一個專門用來共享這類使用者資料的名稱空間( context.state = {}
)。
應用程式碼
/**
* File: /app.js
***/
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
ctx.state.user = {id: 1, name: 'zMouse'};
});
app.use(async (ctx, next) => {
ctx.body = `Hello, ${ctx.state.user.name}`;
});
app.listen(8888);
ctx.throw([status], [msg], [properties])
用來手動丟擲一個包含 狀態碼、狀態碼文字 以及 其它資訊的錯誤。狀態預設為:500。
應用程式碼
/**
* File: /app.js
***/
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next) => {
ctx.throw(401, 'access_denied', { user: user });
});
app.use(async (ctx, next) => {
ctx.body = 'kkb!';
});
app.listen(8888);
app.on(‘error’, callback)
配合著 Application
物件的 on
方法(繼承至 NodeJS 的 Emitter
)來捕獲 throw
錯誤。
/**
* File: lib/application.js
***/
module.exports = class Application extends Emitter {
// ...
}
應用程式碼
/**
* File: /app.js
***/
const Koa = require('koa');
const app = new Koa();
app.on('error', async (err) => {
console.log('error');
// app 會自動根據錯誤向前端進行響應。
});
app.use(async (ctx, next) => {
ctx.throw(401, 'access_denied', { user: user });
});
app.use(async (ctx, next) => {
ctx.body = 'kkb!';
});
app.listen(8888);
Request 物件
Koa` 通過 `Getter` 、 `Setter` 對 `Request` 進行了封裝,具體程式碼: `node_modules/koa/lib/request.js
Response 物件
Koa` 通過 `Getter` 、 `Setter` 對 `Response` 進行了封裝,具體程式碼: `node_modules/koa/lib/response.js
Context 代理
為了方便對 Request
和 Response
物件進行操作, Koa
通過 delegates
對 Context
進行了代理訪問處理,使得可以通過 Context
即可操作對應的 Request
和 Response
:
/**
* File: lib/context.js
***/
// ...
/**
* Response delegation.
*/
delegate(proto, 'response')
.method('attachment')
.method('redirect')
.method('remove')
.method('vary')
.method('has')
.method('set')
.method('append')
.method('flushHeaders')
.access('status')
.access('message')
.access('body')
.access('length')
.access('type')
.access('lastModified')
.access('etag')
.getter('headerSent')
.getter('writable');
/**
* Request delegation.
*/
delegate(proto, 'request')
.method('acceptsLanguages')
.method('acceptsEncodings')
.method('acceptsCharsets')
.method('accepts')
.method('get')
.method('is')
.access('querystring')
.access('idempotent')
.access('socket')
.access('search')
.access('method')
.access('query')
.access('path')
.access('url')
.access('accept')
.getter('origin')
.getter('href')
.getter('subdomains')
.getter('protocol')
.getter('host')
.getter('hostname')
.getter('URL')
.getter('header')
.getter('headers')
.getter('secure')
.getter('stale')
.getter('fresh')
.getter('ips')
.getter('ip');
引數:proto
delegate
的第一個引數 proto
就是 Context
物件。
引數:‘response’ 和 ‘response’
delegate
的第二個引數 'response'
和 'request'
就是需要被代理訪問的 Request
物件和 Response
物件。
method 方法
代理物件的對應方法。
access 方法
代理物件屬性的 getter
和 setter
。
getter 方法
代理物件屬性的 getter
。
setter 方法
代理物件屬性的 setter
。
相關文章
- koa@2.5.0原始碼解讀原始碼
- 【koa】koa-bodyparser原始碼原始碼
- node進階——之事無鉅細手寫koa原始碼原始碼
- node之koa核心程式碼
- Koa 原始碼解析原始碼
- Koa原始碼解析原始碼
- 10分鐘理解 Node.js koa 原始碼架構設計Node.js原始碼架構
- koa原始碼閱讀[1]-koa與koa-compose原始碼
- koa2--delegates模組原始碼解讀原始碼
- koa2原始碼解讀與總結原始碼
- 從koa-session原始碼解讀session原理Session原始碼
- koa原始碼總結原始碼
- Koa 原始碼淺析原始碼
- koa原始碼閱讀[2]-koa-router原始碼
- 從淺入深瞭解Koa2原始碼原始碼
- 手寫@koa/router原始碼原始碼
- koa原始碼中的promise原始碼Promise
- koa-convert原始碼分析原始碼
- koa2原始碼解析原始碼
- koa原始碼筆記(二)原始碼筆記
- koa原始碼閱讀[0]原始碼
- 手寫Koa.js原始碼JS原始碼
- Koa 原始碼閱讀筆記原始碼筆記
- koa2原始碼解讀及實現一個簡單的koa2框架原始碼框架
- Node.js+koa2Node.js
- useSyncExternalStoreExports 狀態原始碼解釋Export原始碼
- 深入koa原始碼(二):核心庫原理原始碼
- 一探 koa-session 原始碼Session原始碼
- CORS原理及@koa/cors原始碼解析CORS原始碼
- 如何閱讀原始碼–Koa為例原始碼
- Koa2 原始碼學習(下)原始碼
- koa2核心原始碼淺析原始碼
- Koa2 原始碼學習(上)原始碼
- koa2中介軟體koa和koa-compose原始碼分析原理(一)原始碼
- koa原始碼閱讀[3]-koa-send與它的衍生(static)原始碼
- 線上直播系統原始碼,Node.js中使用Koa實現上傳圖片功能原始碼Node.js
- 以太坊原始碼分析(43)node原始碼分析原始碼
- 一個學習 Koa 原始碼的例子原始碼