週五組內同學討論搞一些好玩的東西,有人提到了類似『5分鐘實現koa』,『100行實現react』的創意,仔細想了以後,5分鐘實現koa並非不能實現,遂有了這篇部落格。
準備
先開啟koa官網,隨意找出了一個代表koa核心功能的的demo就可以,如下
const Koa = require('koa');
const app = new Koa();
// x-response-time
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});
// logger
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}`);
});
// response
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
複製程式碼
最終要實現的效果是實現的一個5min-koa模組,直接將程式碼中第一行替換為const Koa = require('./5min-koa');
,程式可以正常執行就可以了。
Koa的核心
通過koa官網得知,app.listen方法實際上是如下程式碼的簡寫
const http = require('http');
const Koa = require('koa');
const app = new Koa();
http.createServer(app.callback()).listen(3000);
複製程式碼
所以我們可以先把app.listen實現出來
class Koa {
constructor() {}
callback() {
return (req, res) => {
// TODO
}
}
listen(port) {
http.createServer(this.callback()).listen(port);
}
}
複製程式碼
koa的核心分為四部分,分別是
- context 上下文
- middleware 中介軟體
- request 請求
- responce 響應
Context
我們先來實現一個最簡化版的context,如下
class Context {
constructor(app, req, res) {
this.app = app
this.req = req
this.res = res
// 為了儘可能縮短實現時間,我們直接使用原生的res和req,沒有實現ctx上的ctx.request ctx.response
// ctx.request ctx.response只是在原生res和req上包裝處理了一層
}
// 實現一些demo中使用到的ctx上代理的方法
get set() { return this.res.setHeader }
get method() { return this.req.method }
get url() { return this.req.url }
}
複製程式碼
這樣就完成了一個最基本的Context,別看小,已經夠用了。 每一次有新的請求,都會建立一個新的ctx物件。
Middleware
koa的中介軟體是一個非同步函式,接受兩個引數,分別是ctx和next,其中ctx是當前的請求上下文,next是下一個中介軟體(也是非同步函式),這樣想來,我們需要一個維護中介軟體的陣列,每次呼叫app.use就是往陣列中push一個一步函式。所以use方法實現如下
use(middleware) {
this.middlewares.push(middleware)
}
複製程式碼
每次有新的請求,我們都需要把這次請求的上下文灌進陣列中的每一箇中介軟體裡。單單灌進ctx還不夠,還要使每個中介軟體都能通過next函式呼叫到下一個中介軟體。當我們呼叫next函式時,一般是不需要傳引數的,而被呼叫的中介軟體中一定會接收到ctx和next兩個引數。
呼叫方不需要傳參,被呼叫方卻能接到引數,這讓我立刻想到bind方法,只要將每一箇中介軟體所需要的ctx和next都提前繫結好,問題就解決了。下面的程式碼就是通過bind方法,將使用者傳入的middleware列表轉換成next函式列表
let bindedMiddleware = []
for (let i = middlewares.length - 1; i >= 0; i--) {
if (middlewares.length == i + 1) {
// 最後一箇中介軟體,next方法設定為Promise.resolve
bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, Promise.resolve))
} else {
bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, bindedMiddleware[0]))
}
}
複製程式碼
最後我們就得到了一個next函式陣列,也就是bindedMiddleware這個變數了。
Request
http.createServer中的回撥函式,每次接收到請求的時候會被呼叫,所以我們在上面callback方法的TODO位置,編寫處理請求的程式碼, 並將上面的middleware列表轉next函式列表的程式碼放入其中。
function handleRequest(ctx, middlewares) {
if (middlewares && middlewares.length > 0) {
let bindedMiddleware = []
for (let i = middlewares.length - 1; i >= 0; i--) {
if (middlewares.length == i + 1) {
bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, Promise.resolve))
} else {
bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, bindedMiddleware[0]))
}
}
return bindedMiddleware[0]()
} else {
return Promise.resolve()
}
}
複製程式碼
Responce
我們簡單出來下相應就好了,直接將ctx.body傳送給客戶端。
function handleResponse (ctx) {
return function() {
ctx.res.writeHead(200, { 'Content-Type': 'text/plain' });
ctx.res.end(ctx.body);
}
}
複製程式碼
完成Koa類的實現
koa的app例項上面帶有on,emit等方法,這是node events模組實現好的東西。直接讓Koa類繼承自events模組就好了。 我們再將上面實現出來的handleRequest和handleResponse方法放入koa類的callback方法中,得到最終我們實現的Koa,一共58行程式碼,如下
const http = require('http');
const Emitter = require('events');
class Context {
constructor(app, req, res) {
this.app = app;
this.req = req;
this.res = res;
}
get set() { return this.res.setHeader }
get method() { return this.req.method }
get url() { return this.req.url }
}
class Koa extends Emitter{
constructor(options) {
super();
this.options = options
this.middlewares = [];
}
use(middleware) {
this.middlewares.push(middleware);
}
callback() {
return (req, res) => {
let ctx = new Context(this, req, res);
handleRequest(ctx, this.middlewares).then(handleResponse(ctx));
}
}
listen(port) {
http.createServer(this.callback()).listen(port);
}
}
function handleRequest(ctx, middlewares) {
if (middlewares && middlewares.length > 0) {
let bindedMiddleware = [];
for (let i = middlewares.length - 1; i >= 0; i--) {
if (middlewares.length == i + 1) {
bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, Promise.resolve));
} else {
bindedMiddleware.unshift(middlewares[i].bind(ctx.app, ctx, bindedMiddleware[0]));
}
}
return bindedMiddleware[0]();
} else {
return Promise.resolve();
}
}
function handleResponse (ctx) {
return function() {
ctx.res.writeHead(200, { 'Content-Type': 'text/plain' });
ctx.res.end(ctx.body);
}
}
module.exports = Koa;
複製程式碼
試試跑一下篇首的Demo,沒什麼問題。
結語
簡版實現,碼糙理不糙,展示出了koa核心的東西,但少了錯誤處理,也完全沒有考慮效能啥的,需要完善的地方還很多很多。
筆者在寫了這個5分鐘koa以後去看了koa原始碼,發現實現思路基本就是這樣,相信經過我的這個5分鐘koa的洗禮,你去看koa原始碼一樣小菜一碟。
Done!