真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

如何從頭實現(xiàn)一個node.js的koa框架

前言

創(chuàng)新互聯(lián)專注為客戶提供全方位的互聯(lián)網(wǎng)綜合服務(wù),包含不限于成都網(wǎng)站設(shè)計、成都網(wǎng)站制作、外貿(mào)網(wǎng)站建設(shè)、鄆城網(wǎng)絡(luò)推廣、小程序開發(fā)、鄆城網(wǎng)絡(luò)營銷、鄆城企業(yè)策劃、鄆城品牌公關(guān)、搜索引擎seo、人物專訪、企業(yè)宣傳片、企業(yè)代運營等,從售前售中售后,我們都將竭誠為您服務(wù),您的肯定,是我們最大的嘉獎;創(chuàng)新互聯(lián)為所有大學生創(chuàng)業(yè)者提供鄆城建站搭建服務(wù),24小時服務(wù)熱線:028-86922220,官方網(wǎng)址:www.cdcxhl.com

koa.js是最流行的node.js后端框架之一,有很多網(wǎng)站都使用koa進行開發(fā),同時社區(qū)也涌現(xiàn)出了一大批基于koa封裝的企業(yè)級框架。然而,在這些亮眼的成績背后,作為核心引擎的koa代碼庫本身,卻非常的精簡,不得不讓人驚嘆于其巧妙的設(shè)計。

在平時的工作開發(fā)中,筆者是koa的重度用戶,因此對其背后的原理自然也是非常感興趣,因此在閑暇之余進行了研究。不過本篇文章,并不是源碼分析,而是從相反的角度,向大家展示如何從頭開發(fā)實現(xiàn)一個koa框架,在這個過程中,koa中最重要的幾個概念和原理都會得到展現(xiàn)。相信大家在看完本文之后,會對koa有一個更深入的理解,同時在閱讀本文之后再去閱讀koa源碼,思路也將非常的順暢。

首先放出筆者實現(xiàn)的這個koa框架代碼庫地址:simpleKoa

需要說明的是,本文實現(xiàn)的koa是koa 2版本,也就是基于async/await的,因此需要node版本在7.6以上。如果讀者的node版本較低,建議升級,或者安裝babel-cli,利用其中的babel-node來運行例子。

四條主線

筆者認為,理解koa,主要需要搞懂四條主線,其實也是實現(xiàn)koa的四個步驟,分別是

  • 封裝node http Server
  • 構(gòu)造resquest, response, context對象
  • 中間件機制
  • 錯誤處理

下面就一一進行分析。

主線一:封裝node http Server: 從hello world說起

首先,不考慮框架,如果使用原生http模塊來實現(xiàn)一個返回hello world的后端app,代碼如下:

let http = require('http');
let server = http.createServer((req, res) => {
res.writeHead(200);
res.end('hello world');
});
server.listen(3000, () => {
console.log('listenning on 3000');
});

實現(xiàn)koa的第一步,就是對這個原生的過程進行封裝,為此,我們首先創(chuàng)建application.js實現(xiàn)一個Application對象:

// application.js
let http = require('http');
class Application {
/**
* 構(gòu)造函數(shù)
*/
constructor() {
this.callbackFunc;
}
/**
* 開啟http server并傳入callback
*/
listen(...args) {
let server = http.createServer(this.callback());
server.listen(...args);
}
/**
* 掛載回調(diào)函數(shù)
* @param {Function} fn 回調(diào)處理函數(shù)
*/
use(fn) {
this.callbackFunc = fn;
}
/**
* 獲取http server所需的callback函數(shù)
* @return {Function} fn
*/
callback() {
return (req, res) => {
this.callbackFunc(req, res);
};
}
}
module.exports = Application;

然后創(chuàng)建example.js:

let simpleKoa = require('./application');
let app = new simpleKoa();
app.use((req, res) => {
res.writeHead(200);
res.end('hello world');
});
app.listen(3000, () => {
console.log('listening on 3000');
});

可以看到,我們已經(jīng)初步完成了對于http server的封裝,主要實現(xiàn)了app.use注冊回調(diào)函數(shù),app.listen語法糖開啟server并傳入回調(diào)函數(shù)了,典型的koa風格。

但是美中不足的是,我們傳入的回調(diào)函數(shù),參數(shù)依然使用的是req和res,也就是node原生的request和response對象,這些原生對象和api提供的方法不夠便捷,不符合一個框架需要提供的易用性。因此,我們需要進入第二條主線了。

主線二:構(gòu)造request, response, context對象

如果閱讀koa文檔,會發(fā)現(xiàn)koa有三個重要的對象,分別是request, response, context。其中request是對node原生的request的封裝,response是對node原生response對象的封裝,context對象則是回調(diào)函數(shù)上下文對象,掛載了koa request和response對象。下面我們一一來說明。

首先要明確的是,對于koa的request和response對象,只是提供了對node原生request和response對象的一些方法的封裝,明確了這一點,我們的思路是,使用js的getter和setter屬性,基于node的對象req/res對象封裝koa的request/response對象。

規(guī)劃一下我們要封裝哪些易用的方法。這里在文章中為了易懂,姑且只實現(xiàn)以下方法:

對于simpleKoa request對象,實現(xiàn)query讀取方法,能夠讀取到url中的參數(shù),返回一個對象。

對于simpleKoa response對象,實現(xiàn)status讀寫方法,分別是讀取和設(shè)置http response的狀態(tài)碼,以及body方法,用于構(gòu)造返回信息。

而simpleKoa context對象,則掛載了request和response對象,并對一些常用方法進行了代理。

首先創(chuàng)建request.js:

// request.js
let url = require('url');
module.exports = {
get query() {
return url.parse(this.req.url, true).query;
}
};

很簡單,就是導出了一個對象,其中包含了一個query的讀取方法,通過url.parse方法解析url中的參數(shù),并以對象的形式返回。需要注意的是,代碼中的this.req代表的是node的原生request對象,this.req.url就是node原生request中獲取url的方法。稍后我們修改application.js的時候,會為koa的request對象掛載這個req。

然后創(chuàng)建response.js:

// response.js
module.exports = {
get body() {
return this._body;
},
/**
* 設(shè)置返回給客戶端的body內(nèi)容
*
* @param {mixed} data body內(nèi)容
*/
set body(data) {
this._body = data;
},
get status() {
return this.res.statusCode;
},
/**
* 設(shè)置返回給客戶端的stausCode
*
* @param {number} statusCode 狀態(tài)碼
*/
set status(statusCode) {
if (typeof statusCode !== 'number') {
throw new Error('statusCode must be a number!');
}
this.res.statusCode = statusCode;
}
};

也很簡單。status讀寫方法分別設(shè)置或讀取this.res.statusCode。同樣的,這個this.res是掛載的node原生response對象。而body讀寫方法分別設(shè)置、讀取一個名為this._body的屬性。這里設(shè)置body的時候并沒有直接調(diào)用this.res.end來返回信息,這是考慮到koa當中我們可能會多次調(diào)用response的body方法覆蓋性設(shè)置數(shù)據(jù)。真正的返回消息操作會在application.js中存在。

然后我們創(chuàng)建context.js文件,構(gòu)造context對象的原型:

// context.js
module.exports = {
get query() {
return this.request.query;
},
get body() {
return this.response.body;
},
set body(data) {
this.response.body = data;
},
get status() {
return this.response.status;
},
set status(statusCode) {
this.response.status = statusCode;
}
};

可以看到主要是做一些常用方法的代理,通過context.query直接代理了context.request.query,context.body和context.status代理了context.response.body與context.response.status。而context.request,context.response則會在application.js中掛載。

由于context對象定義比較簡單并且規(guī)范,當實現(xiàn)更多代理方法時候,這樣一個一個通過聲明的方式顯然有點笨,js中,設(shè)置setter/getter,可以通過對象的__defineSetter__和__defineSetter__來實現(xiàn)。為此,我們精簡了上面的context.js實現(xiàn)方法,精簡版本如下:

let proto = {};
// 為proto名為property的屬性設(shè)置setter
function delegateSet(property, name) {
proto.__defineSetter__(name, function (val) {
this[property][name] = val;
});
}
// 為proto名為property的屬性設(shè)置getter
function delegateGet(property, name) {
proto.__defineGetter__(name, function () {
return this[property][name];
});
}
// 定義request中要代理的setter和getter
let requestSet = [];
let requestGet = ['query'];
// 定義response中要代理的setter和getter
let responseSet = ['body', 'status'];
let responseGet = responseSet;
requestSet.forEach(ele => {
delegateSet('request', ele);
});
requestGet.forEach(ele => {
delegateGet('request', ele);
});
responseSet.forEach(ele => {
delegateSet('response', ele);
});
responseGet.forEach(ele => {
delegateGet('response', ele);
});
module.exports = proto;

這樣,當我們希望代理更多request和response方法的時候,可以直接向requestGet/requestSet/responseGet/responseSet數(shù)組中添加method的名稱即可(前提是在request和response中實現(xiàn)了)。

最后讓我們來修改application.js,基于剛才的3個對象原型來創(chuàng)建request, response, context對象:

// application.js
let http = require('http');
let context = require('./context');
let request = require('./request');
let response = require('./response');
class Application {
/**
* 構(gòu)造函數(shù)
*/
constructor() {
this.callbackFunc;
this.context = context;
this.request = request;
this.response = response;
}
/**
* 開啟http server并傳入callback
*/
listen(...args) {
let server = http.createServer(this.callback());
server.listen(...args);
}
/**
* 掛載回調(diào)函數(shù)
* @param {Function} fn 回調(diào)處理函數(shù)
*/
use(fn) {
this.callbackFunc = fn;
}
/**
* 獲取http server所需的callback函數(shù)
* @return {Function} fn
*/
callback() {
return (req, res) => {
let ctx = this.createContext(req, res);
let respond = () => this.responseBody(ctx);
this.callbackFunc(ctx).then(respond);
};
}
/**
* 構(gòu)造ctx
* @param {Object} req node req實例
* @param {Object} res node res實例
* @return {Object} ctx實例
*/
createContext(req, res) {
// 針對每個請求,都要創(chuàng)建ctx對象
let ctx = Object.create(this.context);
ctx.request = Object.create(this.request);
ctx.response = Object.create(this.response);
ctx.req = ctx.request.req = req;
ctx.res = ctx.response.res = res;
return ctx;
}
/**
* 對客戶端消息進行回復
* @param {Object} ctx ctx實例
*/
responseBody(ctx) {
let content = ctx.body;
if (typeof content === 'string') {
ctx.res.end(content);
}
else if (typeof content === 'object') {
ctx.res.end(JSON.stringify(content));
}
}
}

可以看到,最主要的是增加了createContext方法,基于我們之前創(chuàng)建的context 為原型,使用Object.create(this.context)方法創(chuàng)建了ctx,并同樣通過Object.create(this.request)和Object.create(this.response)創(chuàng)建了request/response對象并掛在到了ctx對象上面。此外,還將原生node的req/res對象掛載到了ctx.request.req/ctx.req和ctx.response.res/ctx.res對象上。

回過頭去看我們之前的context/request/response.js文件,就能知道當時使用的this.res或者this.response之類的是從哪里來的了,原來是在這個createContext方法中掛載到了對應的實例上。一張圖來說明其中的關(guān)系:

構(gòu)建了運行時上下文ctx之后,我們的app.use回調(diào)函數(shù)參數(shù)就都基于ctx了。

下面一張圖描述了ctx對象的結(jié)構(gòu)和繼承關(guān)系:

如何從頭實現(xiàn)一個node.js的koa框架

最后回憶我們的ctx.body方法,并沒有直接返回消息體,而是將消息存儲在了一個變量屬性中。為了每次回調(diào)函數(shù)處理結(jié)束之后返回消息,我們創(chuàng)建了responseBody方法,主要作用就是通過ctx.body讀取存儲的消息,然后調(diào)用ctx.res.end返回消息并關(guān)閉連接。從方法中知道,我們的body消息體可以是字符串,也可以是對象(會序列化為字符串返回)。注意這個方法的調(diào)用是在回調(diào)函數(shù)結(jié)束之后調(diào)用的,而我們的回調(diào)函數(shù)是一個async函數(shù),其執(zhí)行結(jié)束后會返回一個Promise對象,因此我們只需要在其后通過.then方法調(diào)用我們的responseBody即可,這就是this.callbackFunc(ctx).then(respond)的意義。

然后我們來測試一下目前為止的框架。修改example.js如下:

let simpleKoa = require('./application');
let app = new simpleKoa();
app.use(async ctx => {
ctx.body = 'hello ' + ctx.query.name;
});
app.listen(3000, () => {
console.log('listening on 3000');
});

可以看到這個時候我們通過app.use傳入的已經(jīng)不再是原生的function (req, res)回調(diào)函數(shù),而是koa2中的async函數(shù),接收ctx作為參數(shù)。為了測試,在瀏覽器訪問localhost:3000?name=tom,可以看到返回了'hello tom',符合預期。

這里再插入分析一個知識概念。從剛才的實現(xiàn)中,我們知道了this.context是我們的中間件中上下文ctx對象的原型。因此在實際開發(fā)中,我們可以將一些常用的方法掛載到this.context上面,這樣,在中間件ctx中,我們也可以方便的使用這些方法了,這個概念就叫做ctx的擴展,一個例子是阿里的egg.js框架已經(jīng)把這個擴展機制作為一部分,融入到了框架開發(fā)中。

下面就展示一個例子,我們寫一個echoData的方法作為擴展,傳入errno, data, errmsg,能夠給客戶端返回結(jié)構(gòu)化的消息結(jié)果:

let SimpleKoa = require('./application');
let app = new SimpleKoa();
// 對ctx進行擴展
app.context.echoData = function (errno = 0, data = null, errmsg = '') {
this.res.setHeader('Content-Type', 'application/json;charset=utf-8');
this.body = {
errno: errno,
data: data,
errmsg: errmsg
};
};
app.use(async ctx => {
let data = {
name: 'tom',
age: 16,
sex: 'male'
}
// 這里使用擴展,方便的返回utf-8格式編碼,帶有errno和errmsg的消息體
ctx.echoData(0, data, 'success');
});
app.listen(3000, () => {
console.log('listenning on 3000');
});

主線三:中間件機制

到目前為止,我們成功封裝了http server,并構(gòu)造了context, request, response對象。但最重要的一條主線卻還沒有實現(xiàn),那就是koa的中間件機制。

關(guān)于koa的中間件洋蔥執(zhí)行模型,koa 1中使用的是generator + co.js執(zhí)行的方式,koa 2中則使用了async/await。關(guān)于koa 1中的中間件原理,我曾寫過一篇文章進行解釋,請移步:深入探析koa之中間件流程控制篇

這里我們實現(xiàn)的是基于koa 2的,因此再描述一下原理。為了便于理解,假設(shè)我們有3個async函數(shù):

async function m1(next) {
console.log('m1');
await next();
}
async function m2(next) {
console.log('m2');
await next();
}
async function m3() {
console.log('m3');
}

我們希望能夠構(gòu)造出一個函數(shù),實現(xiàn)的效果是讓三個函數(shù)依次執(zhí)行。首先考慮想讓m2執(zhí)行完畢后,await next()去執(zhí)行m3函數(shù),那么顯然,需要構(gòu)造一個next函數(shù),作用是調(diào)用m3,然后作為參數(shù)傳給m2

let next1 = async function () {
await m3();
}
m2(next1);
// 輸出:m2,m3

進一步,考慮從m1開始執(zhí)行,那么,m1的next參數(shù)需要是一個執(zhí)行m2的函數(shù),并且給m2傳入的參數(shù)是m3,下面來模擬:

let next1 = async function () {
await m3();
}
let next2 = async function () {
await m2(next1);
}
m1(next2);
// 輸出:m1,m2,m3

那么對于n個async函數(shù),希望他們按順序依次執(zhí)行呢?可以看到,產(chǎn)生nextn的過程能夠抽象為一個函數(shù):

function createNext(middleware, oldNext) {
return async function () {
await middleware(oldNext);
}
}
let next1 = createNext(m3, null);
let next2 = createNext(m2, next1);
let next3 = createNext(m1, next2);
next3();
// 輸出m1, m2, m3

進一步精簡:

let middlewares = [m1, m2, m3];
let len = middlewares.length;
// 最后一個中間件的next設(shè)置為一個立即resolve的promise函數(shù)
let next = async function () {
return Promise.resolve();
}
for (let i = len - 1; i >= 0; i--) {
next = createNext(middlewares[i], next);
}
next();
// 輸出m1, m2, m3

至此,我們也有了koa中間件機制實現(xiàn)的思路,新的application.js如下:

/**
* @file simpleKoa application對象
*/
let http = require('http');
let context = require('./context');
let request = require('./request');
let response = require('.//response');
class Application {
/**
* 構(gòu)造函數(shù)
*/
constructor() {
this.middlewares = [];
this.context = context;
this.request = request;
this.response = response;
}
// ...省略中間
/**
* 中間件掛載
* @param {Function} middleware 中間件函數(shù)
*/
use(middleware) {
this.middlewares.push(middleware);
}
/**
* 中間件合并方法,將中間件數(shù)組合并為一個中間件
* @return {Function}
*/
compose() {
// 將middlewares合并為一個函數(shù),該函數(shù)接收一個ctx對象
return async ctx => {
function createNext(middleware, oldNext) {
return async () => {
await middleware(ctx, oldNext);
}
}
let len = this.middlewares.length;
let next = async () => {
return Promise.resolve();
};
for (let i = len - 1; i >= 0; i--) {
let currentMiddleware = this.middlewares[i];
next = createNext(currentMiddleware, next);
}
await next();
};
}
/**
* 獲取http server所需的callback函數(shù)
* @return {Function} fn
*/
callback() {
return (req, res) => {
let ctx = this.createContext(req, res);
let respond = () => this.responseBody(ctx);
let fn = this.compose();
return fn(ctx).then(respond);
};
}
// ...省略后面
}
module.exports = Application;

可以看到,首先對app.use進行改造了,每次調(diào)用app.use,就向this.middlewares中push一個回調(diào)函數(shù)。然后增加了一個compose()方法,利用我們前文分析的原理,對middlewares數(shù)組中的函數(shù)進行組裝,返回一個最終的函數(shù)。最后,在callback()方法中,調(diào)用compose()得到最終回調(diào)函數(shù),并執(zhí)行。

改寫example.js驗證一下中間件機制:

let simpleKoa = require('./application');
let app = new simpleKoa();
let responseData = {};
app.use(async (ctx, next) => {
responseData.name = 'tom';
await next();
ctx.body = responseData;
});
app.use(async (ctx, next) => {
responseData.age = 16;
await next();
});
app.use(async ctx => {
responseData.sex = 'male';
});
app.listen(3000, () => {
console.log('listening on 3000');
});
// 返回{ name: "tom", age: 16, sex: "male"}

例子中一共三個中間件,分別對responseData增加了name, age, sex屬性,最后返回該數(shù)據(jù)。

至此,一個koa框架基本已經(jīng)浮出水面了,不過我們還需要進行最后一個主線的分析:錯誤處理。

主線四:錯誤處理

一個健壯的框架,必須保證在發(fā)生錯誤的時候,能夠捕獲錯誤并有降級方案返回給客戶端。但顯然現(xiàn)在我們的框架還做不到這一點,假設(shè)我們修改一下例子,我們的中間件中,有一個發(fā)生錯誤拋出了異常:

let simpleKoa = require('./application');
let app = new simpleKoa();
let responseData = {};
app.use(async (ctx, next) => {
responseData.name = 'tom';
await next();
ctx.body = responseData;
});
app.use(async (ctx, next) => {
responseData.age = 16;
await next();
});
app.use(async ctx => {
responseData.sex = 'male';
// 這里發(fā)生了錯誤,拋出了異常
throw new Error('oooops');
});
app.listen(3000, () => {
console.log('listening on 3000');
});

這個時候訪問瀏覽器,是得不到任何響應的,這是因為異常并沒有被我們的框架捕獲并進行降級處理。回顧我們application.js中的中間件執(zhí)行代碼:

// application.js
// ...
callback() {
return (req, res) => {
let ctx = this.createContext(req, res);
let respond = () => this.responseBody(ctx);
let fn = this.compose();
return fn(ctx).then(respond);
};
}
// ...

其中我們知道,fn是一個async函數(shù),執(zhí)行后返回一個promise,回想promise的錯誤處理是怎樣的?沒錯,我們只需要定義一個onerror函數(shù),里面進行錯誤發(fā)生時候的降級處理,然后在promise的catch方法中引用這個函數(shù)即可。

于此同時,回顧koa框架,我們知道在錯誤發(fā)生的時候,app對象可以通過app.on('error', callback)訂閱錯誤事件,這有助于我們幾種處理錯誤,比如打印日志之類的操作。為此,我們也要對Application對象進行改造,讓其繼承nodejs中的events對象,然后在onerror方法中emit錯誤事件。改造后的application.js如下:

/**
* @file simpleKoa application對象
*/
let EventEmitter = require('events');
let http = require('http');
let context = require('./context');
let request = require('./request');
let response = require('./response');
class Application extends EventEmitter {
/**
* 構(gòu)造函數(shù)
*/
constructor() {
super();
this.middlewares = [];
this.context = context;
this.request = request;
this.response = response;
}
// ...
/**
* 獲取http server所需的callback函數(shù)
* @return {Function} fn
*/
callback() {
return (req, res) => {
let ctx = this.createContext(req, res);
let respond = () => this.responseBody(ctx);
let onerror = (err) => this.onerror(err, ctx);
let fn = this.compose();
// 在這里catch異常,調(diào)用onerror方法處理異常
return fn(ctx).then(respond).catch(onerror);
};
}
// ...
/**
* 錯誤處理
* @param {Object} err Error對象
* @param {Object} ctx ctx實例
*/
onerror(err, ctx) {
if (err.code === 'ENOENT') {
ctx.status = 404;
}
else {
ctx.status = 500;
}
let msg = err.message || 'Internal error';
ctx.res.end(msg);
// 觸發(fā)error事件
this.emit('error', err);
}
}
module.exports = Application;

可以看到,onerror方法的對異常的處理主要是獲取異常狀態(tài)碼,當err.code為'ENOENT'的時候,返回的消息頭設(shè)置為404,否則默認設(shè)置為500,然后消息體設(shè)置為err.message,如果異常中message屬性為空,則默認消息體設(shè)置為'Internal error'。此后調(diào)用ctx.res.end返回消息,這樣就能保證即使異常情況下,客戶端也能收到返回值。最后通過this.emit出發(fā)error事件。

然后我們寫一個example來驗證錯誤處理:

let simpleKoa = require('./application');
let app = new simpleKoa();
app.use(async ctx => {
throw new Error('ooops');
});
app.on('error', (err) => {
console.log(err.stack);
});
app.listen(3000, () => {
console.log('listening on 3000');
});

瀏覽器訪問'localhost:3000'的時候,得到返回'ooops',同時http狀態(tài)碼為500 。同時app.on('error')訂閱到了異常事件,在回調(diào)函數(shù)中打印出了錯誤棧信息。

關(guān)于錯誤處理,這里多說一點。雖然koa中內(nèi)置了錯誤處理機制,但是實際業(yè)務(wù)開發(fā)中,我們往往希望能夠自定義錯誤處理方式,這個時候,比較好的辦法是在最開頭增加一個錯誤捕獲中間件,然后根據(jù)錯誤進行定制化的處理,比如:

// 錯誤處理中間件
app.use(async (ctx, next) => {
try {
await next();
}
catch (err) {
// 在這里進行定制化的錯誤處理
}
});
// ...其他中間件

至此,我們就完整實現(xiàn)了一個輕量版的koa框架。

結(jié)語

完整的simpleKoa代碼庫地址為:simpleKoa,里面還附帶了一些example。

理解了這個輕量版koa的實現(xiàn)原理,讀者還可以去看看koa的源碼,會發(fā)現(xiàn)機制和我們實現(xiàn)的框架是非常類似的,無非是多了一些細節(jié),比如說,完整koa的context/request/response方法上面掛載了更多好用的method,或者很多方法中容錯處理更好等等。具體在本文中就不展開講了,留給感興趣的讀者去探索吧~。

以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持創(chuàng)新互聯(lián)。


分享名稱:如何從頭實現(xiàn)一個node.js的koa框架
當前網(wǎng)址:http://weahome.cn/article/giispg.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部