目錄
什麼是koa框架? " >什麼是koa框架?
koa原始碼結構" >koa原始碼結構
application.js
context.js
request.js、response.js
實作koa2的四大模組" >實作koa2的四大模組
模块四:错误捕获和错误处理
结尾" >结尾
首頁 web前端 js教程 什麼是KOA框架?詳解實現koa2的四大模組

什麼是KOA框架?詳解實現koa2的四大模組

Jun 25, 2021 am 11:45 AM
node

koa是一個基於node實現的一個新的web框架,這篇文章跟大家介紹一下koa2框架,帶大家理解和實作一個koa框架需要實現四個大模組。

什麼是koa框架?

koa是一個基於node實現的一個新的web框架,它是由express框架的原始班人馬打造的。它的特色是優雅、簡潔、表達力強、自由度高。它更express相比,它是一個更輕量的node框架,因為它所有功能都透過插件實現,這種插拔式的架構設計模式,很符合unix哲學。 【推薦學習:《nodejs 教程》】

koa框架現在更新到了2.x版本,本文從零開始,循序漸進,講解koa2的框架源碼結構和實現原理,展示和詳解koa2框架源碼中的幾個最重要的概念,然後手把手教大家親自實現一個簡易的koa2框架,幫助大家學習和更深層次的理解koa2,看完本文以後,再去對照koa2的源碼進行查看,相信你的思路將會非常的順暢。

本文所用的框架是koa2,它跟koa1不同,koa1使用的是generator co.js的執行方式,而koa2中使用了async/await,因此本文的程式碼和demo需要運行在node 8版本及其以上,如果讀者的node版本較低,建議升級或安裝babel-cli,用其中的babel-node來執行本文涉及的程式碼。

koa原始碼結構

什麼是KOA框架?詳解實現koa2的四大模組

#上圖是koa2的原始碼目錄結構的lib資料夾,lib資料夾下放著四個koa2的核心檔案:application.js、context.js、request.js、response.js。

application.js

application.js是koa的入口文件,它向外導出了創建class實例的建構函數,它繼承了events,這樣就會賦予框架事件監聽和事件觸發的能力。 application也露出了一些常用的api,像是toJSON、listen、use等等。

listen的實作原理其實就是對http.createServer進行了一個封裝,重點是這個函數中傳入的callback,它裡麵包含了中間件的合併,上下文的處理,對res的特殊處理。

use是收集中間件,將多個中間件放入一個快取佇列中,然後透過koa-compose這個外掛程式進行遞歸組合呼叫這一些列的中間件。

context.js

這部分就是koa的應用上下文ctx,其實就一個簡單的物件暴露,裡面的重點在delegate,這個就是代理,這個就是為了開發者方便而設計的,例如我們要存取ctx.repsponse.status但是我們透過delegate,可以直接存取ctx.status存取到它。

request.js、response.js

這兩部分就是對原生的res、req的一些運算了,大量使用es6的get和set的一些語法,去取headers或設定headers、還有設定body等等,這些就不詳細介紹了,有興趣的讀者可以自行看原始碼。

實作koa2的四大模組

上文簡述了koa2原始碼的大體框架結構,接下來我們來實作一個koa2的框架,筆者認為理解和實作一個koa框架需要實作四個大模組,分別是:

  • #封裝node http server、建立Koa類別建構子

  • 建構request、response、context物件

  • 中間件機制與剝洋蔥模型的實作

  • 錯誤擷取與錯誤處理

下面我們就逐一分析實現。

模組一:封裝node http server和建立Koa類別建構子

閱讀koa2的原始碼得知,實作koa的伺服器應用程式和連接埠監聽,其實就是基於node的原生程式碼進行了封裝,如下圖的程式碼就是透過node原生程式碼實現的伺服器監聽。

1

2

3

4

5

6

7

8

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');

});

登入後複製

我們需要將上面的node原生程式碼封裝實作成koa的模式:

1

2

3

4

const http = require('http');

const Koa = require('koa');

const app = new Koa();

app.listen(3000);

登入後複製

實作koa的第一步就是對以上的這個過程進行封裝,為此我們需要建立application .js實作一個Application類別的建構子:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

let http = require('http');

class Application {   

    constructor() {       

        this.callbackFunc;

    }

    listen(port) {       

        let server = http.createServer(this.callback());

        server.listen(port);

    }

    use(fn) {

        this.callbackFunc = fn;

    }

    callback() {

        return (req, res) => {

            this.callbackFunc(req, res);

        };

    }

}

module.exports = Application;

登入後複製

然後建立ex​​ample.js,引入application.js,執行伺服器實例啟動監聽程式碼:

1

2

3

4

5

6

7

8

9

let Koa = require('./application');

let app = new Koa();

app.use((req, res) => {

    res.writeHead(200);

    res.end('hello world');

});

app.listen(3000, () => {

    console.log('listening on 3000');

});

登入後複製

现在在浏览器输入localhost:3000即可看到浏览器里显示“hello world”。现在第一步我们已经完成了,对http server进行了简单的封装和创建了一个可以生成koa实例的类class,这个类里还实现了app.use用来注册中间件和注册回调函数,app.listen用来开启服务器实例并传入callback回调函数,第一模块主要是实现典型的koa风格和搭好了一个koa的简单的架子。接下来我们开始编写和讲解第二模块。

模块二:构造request、response、context对象

阅读koa2的源码得知,其中context.js、request.js、response.js三个文件分别是request、response、context三个模块的代码文件。context就是我们平时写koa代码时的ctx,它相当于一个全局的koa实例上下文this,它连接了request、response两个功能模块,并且暴露给koa的实例和中间件等回调函数的参数中,起到承上启下的作用。

request、response两个功能模块分别对node的原生request、response进行了一个功能的封装,使用了getter和setter属性,基于node的对象req/res对象封装koa的request/response对象。我们基于这个原理简单实现一下request.js、response.js,首先创建request.js文件,然后写入以下代码:

1

2

3

4

5

6

let url = require('url');

module.exports = {

    get query() {

        return url.parse(this.req.url, true).query;

    }

};

登入後複製

这样当你在koa实例里使用ctx.query的时候,就会返回url.parse(this.req.url, true).query的值。看源码可知,基于getter和setter,在request.js里还封装了header、url、origin、path等方法,都是对原生的request上用getter和setter进行了封装,笔者不再这里一一实现。

接下来我们实现response.js文件代码模块,它和request原理一样,也是基于getter和setter对原生response进行了封装,那我们接下来通过对常用的ctx.body和ctx.status这个两个语句当做例子简述一下如果实现koa的response的模块,我们首先创建好response.js文件,然后输入下面的代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

module.exports = {

    get body() {

        return this._body;

    },

    set body(data) {

        this._body = data;

    },

    get status() {

        return this.res.statusCode;

    },

    set status(statusCode) {

        if (typeof statusCode !== 'number') {

            throw new Error('something wrong!');

        }

        this.res.statusCode = statusCode;

    }

};

登入後複製

以上代码实现了对koa的status的读取和设置,读取的时候返回的是基于原生的response对象的statusCode属性,而body的读取则是对this._body进行读写和操作。这里对body进行操作并没有使用原生的this.res.end,因为在我们编写koa代码的时候,会对body进行多次的读取和修改,所以真正返回浏览器信息的操作是在application.js里进行封装和操作。

现在我们已经实现了request.js、response.js,获取到了request、response对象和他们的封装的方法,然后我们开始实现context.js,context的作用就是将request、response对象挂载到ctx的上面,让koa实例和代码能方便的使用到request、response对象中的方法。现在我们创建context.js文件,输入如下代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

let proto = {};

 

function delegateSet(property, name) {

    proto.__defineSetter__(name, function (val) {

        this[property][name] = val;

    });

}

 

function delegateGet(property, name) {

    proto.__defineGetter__(name, function () {

        return this[property][name];

    });

}

 

let requestSet = [];

let requestGet = ['query'];

 

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;

登入後複製

context.js文件主要是对常用的request和response方法进行挂载和代理,通过context.query直接代理了context.request.query,context.body和context.status代理了context.response.body与context.response.status。而context.request,context.response则会在application.js中挂载

本来可以用简单的setter和getter去设置每一个方法,但是由于context对象定义方法比较简单和规范,在koa源码里可以看到,koa源码用的是__defineSetter__和__defineSetter__来代替setter/getter每一个属性的读取设置,这样做主要是方便拓展和精简了写法,当我们需要代理更多的res和req的方法的时候,可以向context.js文件里面的数组对象里面添加对应的方法名和属性名即可。

目前为止,我们已经得到了request、response、context三个模块对象了,接下来就是将request、response所有方法挂载到context下,让context实现它的承上启下的作用,修改application.js文件,添加如下代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

let http = require('http');

let context = require('./context');

let request = require('./request');

let response = require('./response');

 

createContext(req, res) {      

   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;

}

登入後複製

可以看到,我们添加了createContext这个方法,这个方法是关键,它通过Object.create创建了ctx,并将request和response挂载到了ctx上面,将原生的req和res挂载到了ctx的子属性上,往回看一下context/request/response.js文件,就能知道当时使用的this.res或者this.response之类的是从哪里来的了,原来是在这个createContext方法中挂载到了对应的实例上,构建了运行时上下文ctx之后,我们的app.use回调函数参数就都基于ctx了。

模块三:中间件机制和剥洋葱模型的实现

目前为止我们已经成功实现了上下文context对象、 请求request对象和响应response对象模块,还差一个最重要的模块,就是koa的中间件模块,koa的中间件机制是一个剥洋葱式的模型,多个中间件通过use放进一个数组队列然后从外层开始执行,遇到next后进入队列中的下一个中间件,所有中间件执行完后开始回帧,执行队列中之前中间件中未执行的代码部分,这就是剥洋葱模型,koa的中间件机制。

koa的剥洋葱模型在koa1中使用的是generator + co.js去实现的,koa2则使用了async/await + Promise去实现的,接下来我们基于async/await + Promise去实现koa2中的中间件机制。首先,假设当koa的中间件机制已经做好了,那么它是能成功运行下面代码的:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

let Koa = require('../src/application');

 

let app = new Koa();

 

app.use(async (ctx, next) => {

    console.log(1);

    await next();

    console.log(6);

});

 

app.use(async (ctx, next) => {

    console.log(2);

    await next();

    console.log(5);

});

 

app.use(async (ctx, next) => {

    console.log(3);

    ctx.body = "hello world";

    console.log(4);

});

 

app.listen(3000, () => {

    console.log('listenning on 3000');

});

登入後複製

运行成功后会在终端输出123456,那就能验证我们的koa的剥洋葱模型是正确的。接下来我们开始实现,修改application.js文件,添加如下代码:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

compose() {

    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();

    };

}

 

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();

        return fn(ctx);

    };

}

登入後複製

koa通过use函数,把所有的中间件push到一个内部数组队列this.middlewares中,剥洋葱模型能让所有的中间件依次执行,每次执行完一个中间件,遇到next()就会将控制权传递到下一个中间件,下一个中间件的next参数,剥洋葱模型的最关键代码是compose这个函数:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

compose() {

        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();

        };

    }

登入後複製

createNext函数的作用就是将上一个中间件的next当做参数传给下一个中间件,并且将上下文ctx绑定当前中间件,当中间件执行完,调用next()的时候,其实就是去执行下一个中间件。

1

2

3

4

for (let i = len - 1; i >= 0; i--) {

        let currentMiddleware = this.middlewares[i];

        next = createNext(currentMiddleware, next);

 }

登入後複製

上面这段代码其实就是一个链式反向递归模型的实现,i是从最大数开始循环的,将中间件从最后一个开始封装,每一次都是将自己的执行函数封装成next当做上一个中间件的next参数,这样当循环到第一个中间件的时候,只需要执行一次next(),就能链式的递归调用所有中间件,这个就是koa剥洋葱的核心代码机制。

到这里我们总结一下上面所有剥洋葱模型代码的流程,通过use传进来的中间件是一个回调函数,回调函数的参数是ctx上下文和next,next其实就是控制权的交接棒,next的作用是停止运行当前中间件,将控制权交给下一个中间件,执行下一个中间件的next()之前的代码,当下一个中间件运行的代码遇到了next(),又会将代码执行权交给下下个中间件,当执行到最后一个中间件的时候,控制权发生反转,开始回头去执行之前所有中间件中剩下未执行的代码,这整个流程有点像一个伪递归,当最终所有中间件全部执行完后,会返回一个Promise对象,因为我们的compose函数返回的是一个async的函数,async函数执行完后会返回一个Promise,这样我们就能将所有的中间件异步执行同步化,通过then就可以执行响应函数和错误处理函数。

当中间件机制代码写好了以后,运行我们的上面的例子,已经能输出123456了,至此,我们的koa的基本框架已经基本做好了,不过一个框架不能只实现功能,为了框架和服务器实例的健壮,还需要加上错误处理机制。

模块四:错误捕获和错误处理

要实现一个基础框架,错误处理和捕获必不可少,一个健壮的框架,必须保证在发生错误的时候,能够捕获到错误和抛出的异常,并反馈出来,将错误信息发送到监控系统上进行反馈,目前我们实现的简易koa框架还没有能实现这一点,我们接下加上错误处理和捕获的机制。

1

throw new Error('oooops');

登入後複製

基于现在的框架,如果中间件代码中出现如上错误异常抛出,是捕获不到错误的,这时候我们看一下application.js中的callback函数的return返回代码,如下:

1

return fn(ctx).then(respond);

登入後複製

可以看到,fn是中间件的执行函数,每一个中间件代码都是由async包裹着的,而且中间件的执行函数compose返回的也是一个async函数,我们根据es7的规范知道,async返回的是一个promise的对象实例,我们如果想要捕获promise的错误,只需要使用promise的catch方法,就可以把所有的中间件的异常全部捕获到,修改后callback的返回代码如下:

1

return fn(ctx).then(respond).catch(onerror);

登入後複製

现在我们已经实现了中间件的错误异常捕获,但是我们还缺少框架层发生错误的捕获机制,我们希望我们的服务器实例能有错误事件的监听机制,通过on的监听函数就能订阅和监听框架层面上的错误,实现这个机制不难,使用nodejs原生events模块即可,events模块给我们提供了事件监听on函数和事件触发emit行为函数,一个发射事件,一个负责接收事件,我们只需要将koa的构造函数继承events模块即可,构造后的伪代码如下:

1

let EventEmitter = require('events');class Application extends EventEmitter {}

登入後複製

继承了events模块后,当我们创建koa实例的时候,加上on监听函数,代码如下:

1

2

3

4

let app = new Koa();

 

app.on('error', err => {    console.log('error happends: ', err.stack);

});

登入後複製

这样我们就实现了框架层面上的错误的捕获和监听机制了。总结一下,错误处理和捕获,分中间件的错误处理捕获和框架层的错误处理捕获,中间件的错误处理用promise的catch,框架层面的错误处理用nodejs的原生模块events,这样我们就可以把一个服务器实例上的所有的错误异常全部捕获到了。至此,我们就完整实现了一个轻量版的koa框架了。

结尾

前为止,我们已经实现了一个轻量版的koa框架了,我们实现了封装node http server、创建Koa类构造函数、构造request、response、context对象、中间件机制和剥洋葱模型的实现、错误捕获和错误处理这四个大模块,理解了这个轻量版koa的实现原理,再去看koa2的源码,你就会发现一切都豁然开朗,koa2的源码无非就是在这个轻量版基础上加了很多工具函数和细节的处理,限于篇幅笔者就不再一一介绍了。

更多编程相关知识,请访问:编程视频!!

以上是什麼是KOA框架?詳解實現koa2的四大模組的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
2 週前 By 尊渡假赌尊渡假赌尊渡假赌
倉庫:如何復興隊友
4 週前 By 尊渡假赌尊渡假赌尊渡假赌
Hello Kitty Island冒險:如何獲得巨型種子
3 週前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

nvm 怎麼刪除node nvm 怎麼刪除node Dec 29, 2022 am 10:07 AM

nvm 怎麼刪除node

node專案中如何使用express來處理檔案的上傳 node專案中如何使用express來處理檔案的上傳 Mar 28, 2023 pm 07:28 PM

node專案中如何使用express來處理檔案的上傳

Node服務怎麼進行Docker鏡像化?極致優化詳解 Node服務怎麼進行Docker鏡像化?極致優化詳解 Oct 19, 2022 pm 07:38 PM

Node服務怎麼進行Docker鏡像化?極致優化詳解

深入淺析Node的進程管理工具'pm2” 深入淺析Node的進程管理工具'pm2” Apr 03, 2023 pm 06:02 PM

深入淺析Node的進程管理工具'pm2”

Pi Node教學:什麼是Pi節點?如何安裝和設定Pi Node? Pi Node教學:什麼是Pi節點?如何安裝和設定Pi Node? Mar 05, 2025 pm 05:57 PM

Pi Node教學:什麼是Pi節點?如何安裝和設定Pi Node?

聊聊用pkg將Node.js專案打包為執行檔的方法 聊聊用pkg將Node.js專案打包為執行檔的方法 Dec 02, 2022 pm 09:06 PM

聊聊用pkg將Node.js專案打包為執行檔的方法

使用Angular和Node進行基於令牌的身份驗證 使用Angular和Node進行基於令牌的身份驗證 Sep 01, 2023 pm 02:01 PM

使用Angular和Node進行基於令牌的身份驗證

node server.js 報錯怎麼辦 node server.js 報錯怎麼辦 Dec 29, 2022 pm 04:19 PM

node server.js 報錯怎麼辦

See all articles