01、介紹
- Koa -- 基於 Node.js 平臺的下一代 web 開發框架
- Koa 是一個新的 web 框架,由 Express 幕後的原班人馬打造, 致力於成為 web 應用和 API 開發領域中的一個更小、更富有表現力、更健壯的基石。
- 與其對應的 Express 來比,Koa 更加小巧、精壯,本文將帶大家從零開始實現 Koa 的原始碼,從根源上解決大家對 Koa 的困惑
本文 Koa 版本為 2.7.0, 版本不一樣原始碼可能會有變動
原始碼倉庫 已經開放,如果本文對你有幫助,歡迎 star ~~
02、原始碼目錄介紹
- Koa 原始碼目錄截圖
- 通過原始碼目錄可以知道,Koa主要分為4個部分,分別是:
- application: Koa 最主要的模組, 對應 app 應用物件
- context: 對應 ctx 物件
- request: 對應 Koa 中請求物件
- response: 對應 Koa 中響應物件
- 這4個檔案就是 Koa 的全部內容了,其中 application 又是其中最核心的檔案。我們將會從此檔案入手,一步步實現 Koa 框架
03、實現一個基本伺服器
-
程式碼目錄
-
my-application
const {createServer} = require('http'); module.exports = class Application { constructor() { // 初始化中介軟體陣列, 所有中介軟體函式都會新增到當前陣列中 this.middleware = []; } // 使用中介軟體方法 use(fn) { // 將所有中介軟體函式新增到中介軟體陣列中 this.middleware.push(fn); } // 監聽埠號方法 listen(...args) { // 使用nodejs的http模組監聽埠號 const server = createServer((req, res) => { /* 處理請求的回撥函式,在這裡執行了所有中介軟體函式 req 是 node 原生的 request 物件 res 是 node 原生的 response 物件 */ this.middleware.forEach((fn) => fn(req, res)); }) server.listen(...args); } } 複製程式碼
-
index.js
// 引入自定義模組 const MyKoa = require('./js/my-application'); // 建立例項物件 const app = new MyKoa(); // 使用中介軟體 app.use((req, res) => { console.log('中介軟體函式執行了~~~111'); }) app.use((req, res) => { console.log('中介軟體函式執行了~~~222'); res.end('hello myKoa'); }) // 監聽埠號 app.listen(3000, err => { if (!err) console.log('伺服器啟動成功了'); else console.log(err); }) 複製程式碼
-
執行入口檔案
index.js
後,通過瀏覽器輸入網址訪問http://localhost:3000/
, 就可以看到結果了~~ -
神奇吧!一個最簡單的伺服器模型就搭建完了。當然我們這個極簡伺服器還存在很多問題,接下來讓我們一一解決
04、實現中介軟體函式的 next 方法
- 提取
createServer
的回撥函式,封裝成一個callback
方法(可複用)// 監聽埠號方法 listen(...args) { // 使用nodejs的http模組監聽埠號 const server = createServer(this.callback()); server.listen(...args); } callback() { const handleRequest = (req, res) => { this.middleware.forEach((fn) => fn(req, res)); } return handleRequest; } 複製程式碼
- 封裝
compose
函式實現next
方法/** * 負責執行中介軟體函式的函式 * @param middleware 中介軟體陣列 * @return {function} */ function compose(middleware) { // compose方法返回值是一個函式,這個函式返回值是一個promise物件 // 當前函式就是排程 return (req, res) => { // 預設呼叫一次,為了執行第一個中介軟體函式 return dispatch(0); function dispatch(i) { // 提取中介軟體陣列的函式fn let fn = middleware[i]; // 如果最後一箇中介軟體也呼叫了next方法,直接返回一個成功狀態的promise物件 if (!fn) return Promise.resolve(); /* dispatch.bind(null, i + 1)) 作為中介軟體函式呼叫的第三個引數,其實就是對應的next 舉個栗子:如果 i = 0 那麼 dispatch.bind(null, 1)) --> 也就是如果呼叫了next方法 實際上就是執行 dispatch(1) --> 它利用遞迴重新進來取出下一個中介軟體函式接著執行 fn(req, res, dispatch.bind(null, i + 1)) --> 這也是為什麼中介軟體函式能有三個引數,在呼叫時我們傳進來了 */ return Promise.resolve(fn(req, res, dispatch.bind(null, i + 1))); } } } 複製程式碼
- 使用
compose
函式callback () { // 執行compose方法返回一個函式 const fn = compose(this.middleware); const handleRequest = (req, res) => { // 呼叫該函式,返回值為promise物件 // then方法觸發了, 說明所有中介軟體函式都被呼叫完成 fn(req, res).then(() => { // 在這裡就是所有處理的函式的最後階段,可以允許返回響應了~ }); } return handleRequest; } 複製程式碼
- 修改入口檔案 index.js 程式碼
// 引入自定義模組 const MyKoa = require('./js/my-application'); // 建立例項物件 const app = new MyKoa(); // 使用中介軟體 app.use((req, res, next) => { console.log('中介軟體函式執行了~~~111'); // 呼叫next方法,就是呼叫堆疊中下一個中介軟體函式 next(); }) app.use((req, res, next) => { console.log('中介軟體函式執行了~~~222'); res.end('hello myKoa'); // 最後的next方法沒發呼叫下一個中介軟體函式,直接返回Promise.resolve() next(); }) // 監聽埠號 app.listen(3000, err => { if (!err) console.log('伺服器啟動成功了'); else console.log(err); }) 複製程式碼
- 此時我們實現了
next
方法,最核心的就是compose
函式,極簡的程式碼實現了功能,不可思議!
05、處理返回響應
- 定義返回響應函式
respond
function respond(req, res) { // 獲取設定的body資料 let body = res.body; if (typeof body === 'object') { // 如果是物件,轉化成json資料返回 body = JSON.stringify(body); res.end(body); } else { // 預設其他資料直接返回 res.end(body); } } 複製程式碼
- 在
callback
中呼叫callback() { const fn = compose(this.middleware); const handleRequest = (req, res) => { // 當中介軟體函式全部執行完畢時,會觸發then方法,從而執行respond方法返回響應 const handleResponse = () => respond(req, res); fn(req, res).then(handleResponse); } return handleRequest; } 複製程式碼
- 修改入口檔案 index.js 程式碼
// 引入自定義模組 const MyKoa = require('./js/my-application'); // 建立例項物件 const app = new MyKoa(); // 使用中介軟體 app.use((req, res, next) => { console.log('中介軟體函式執行了~~~111'); next(); }) app.use((req, res, next) => { console.log('中介軟體函式執行了~~~222'); // 設定響應內容,由框架負責返回響應~ res.body = 'hello myKoa'; }) // 監聽埠號 app.listen(3000, err => { if (!err) console.log('伺服器啟動成功了'); else console.log(err); }) 複製程式碼
- 此時我們就能根據不同響應內容做出處理了~當然還是比較簡單的,可以接著去擴充套件~
06、定義 Request 模組
// 此模組需要npm下載
const parse = require('parseurl');
const qs = require('querystring');
module.exports = {
/**
* 獲取請求頭資訊
*/
get headers() {
return this.req.headers;
},
/**
* 設定請求頭資訊
*/
set headers(val) {
this.req.headers = val;
},
/**
* 獲取查詢字串
*/
get query() {
// 解析查詢字串引數 --> key1=value1&key2=value2
const querystring = parse(this.req).query;
// 將其解析為物件返回 --> {key1: value1, key2: value2}
return qs.parse(querystring);
}
}
複製程式碼
07、定義 Response 模組
module.exports = {
/**
* 設定響應頭的資訊
*/
set(key, value) {
this.res.setHeader(key, value);
},
/**
* 獲取響應狀態碼
*/
get status() {
return this.res.statusCode;
},
/**
* 設定響應狀態碼
*/
set status(code) {
this.res.statusCode = code;
},
/**
* 獲取響應體資訊
*/
get body() {
return this._body;
},
/**
* 設定響應體資訊
*/
set body(val) {
// 設定響應體內容
this._body = val;
// 設定響應狀態碼
this.status = 200;
// json
if (typeof val === 'object') {
this.set('Content-Type', 'application/json');
}
},
}
複製程式碼
08、定義 Context 模組
// 此模組需要npm下載
const delegate = require('delegates');
const proto = module.exports = {};
// 將response物件上的屬性/方法克隆到proto上
delegate(proto, 'response')
.method('set') // 克隆普通方法
.access('status') // 克隆帶有get和set描述符的方法
.access('body')
// 將request物件上的屬性/方法克隆到proto上
delegate(proto, 'request')
.access('query')
.getter('headers') // 克隆帶有get描述符的方法
複製程式碼
09、揭祕 delegates 模組
module.exports = Delegator;
/**
* 初始化一個 delegator.
*/
function Delegator(proto, target) {
// this必須指向Delegator的例項物件
if (!(this instanceof Delegator)) return new Delegator(proto, target);
// 需要克隆的物件
this.proto = proto;
// 被克隆的目標物件
this.target = target;
// 所有普通方法的陣列
this.methods = [];
// 所有帶有get描述符的方法陣列
this.getters = [];
// 所有帶有set描述符的方法陣列
this.setters = [];
}
/**
* 克隆普通方法
*/
Delegator.prototype.method = function(name){
// 需要克隆的物件
var proto = this.proto;
// 被克隆的目標物件
var target = this.target;
// 方法新增到method陣列中
this.methods.push(name);
// 給proto新增克隆的屬性
proto[name] = function(){
/*
this指向proto, 也就是ctx
舉個栗子:ctx.response.set.apply(ctx.response, arguments)
arguments對應實參列表,剛好與apply方法傳參一致
執行ctx.set('key', 'value') 實際上相當於執行 response.set('key', 'value')
*/
return this[target][name].apply(this[target], arguments);
};
// 方便鏈式呼叫
return this;
};
/**
* 克隆帶有get和set描述符的方法.
*/
Delegator.prototype.access = function(name){
return this.getter(name).setter(name);
};
/**
* 克隆帶有get描述符的方法.
*/
Delegator.prototype.getter = function(name){
var proto = this.proto;
var target = this.target;
this.getters.push(name);
// 方法可以為一個已經存在的物件設定get描述符屬性
proto.__defineGetter__(name, function(){
return this[target][name];
});
return this;
};
/**
* 克隆帶有set描述符的方法.
*/
Delegator.prototype.setter = function(name){
var proto = this.proto;
var target = this.target;
this.setters.push(name);
// 方法可以為一個已經存在的物件設定set描述符屬性
proto.__defineSetter__(name, function(val){
return this[target][name] = val;
});
return this;
};
複製程式碼
10、使用 ctx 取代 req 和 res
- 修改 my-application
const {createServer} = require('http'); const context = require('./my-context'); const request = require('./my-request'); const response = require('./my-response'); module.exports = class Application { constructor() { this.middleware = []; // Object.create(target) 以target物件為原型, 建立新物件, 新物件原型有target物件的屬性和方法 this.context = Object.create(context); this.request = Object.create(request); this.response = Object.create(response); } use(fn) { this.middleware.push(fn); } listen(...args) { // 使用nodejs的http模組監聽埠號 const server = createServer(this.callback()); server.listen(...args); } callback() { const fn = compose(this.middleware); const handleRequest = (req, res) => { // 建立context const ctx = this.createContext(req, res); const handleResponse = () => respond(ctx); fn(ctx).then(handleResponse); } return handleRequest; } /** * 建立context 上下文物件的方法 * @param req node原生req物件 * @param res node原生res物件 */ createContext(req, res) { /* 凡是req/res,就是node原生物件 凡是request/response,就是自定義物件 這是實現互相掛載引用,從而在任意物件上都能獲取其他物件的方法 */ 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; return context; } } // 將原來使用req,res的地方改用ctx function compose(middleware) { return (ctx) => { return dispatch(0); function dispatch(i) { let fn = middleware[i]; if (!fn) return Promise.resolve(); return Promise.resolve(fn(ctx, dispatch.bind(null, i + 1))); } } } function respond(ctx) { let body = ctx.body; const res = ctx.res; if (typeof body === 'object') { body = JSON.stringify(body); res.end(body); } else { res.end(body); } } 複製程式碼
- 修改入口檔案 index.js 程式碼
// 引入自定義模組 const MyKoa = require('./js/my-application'); // 建立例項物件 const app = new MyKoa(); // 使用中介軟體 app.use((ctx, next) => { console.log('中介軟體函式執行了~~~111'); next(); }) app.use((ctx, next) => { console.log('中介軟體函式執行了~~~222'); // 獲取請求頭引數 console.log(ctx.headers); // 獲取查詢字串引數 console.log(ctx.query); // 設定響應頭資訊 ctx.set('content-type', 'text/html;charset=utf-8'); // 設定響應內容,由框架負責返回響應~ ctx.body = '<h1>hello myKoa</h1>'; }) // 監聽埠號 app.listen(3000, err => { if (!err) console.log('伺服器啟動成功了'); else console.log(err); }) 複製程式碼
到這裡已經寫完了 Koa 主要程式碼,有一句古話 - 看萬遍程式碼不如寫上一遍。 還等什麼,趕緊寫上一遍吧~ 當你能夠寫出來,再去閱讀原始碼,你會發現原始碼如此簡單~