>  기사  >  웹 프론트엔드  >  KOA 프레임워크란 무엇인가요? koa2 구현을 위한 4가지 주요 모듈에 대한 자세한 설명

KOA 프레임워크란 무엇인가요? koa2 구현을 위한 4가지 주요 모듈에 대한 자세한 설명

青灯夜游
青灯夜游앞으로
2021-06-25 11:45:057494검색

koa는 node를 기반으로 한 새로운 웹 프레임워크입니다. 이 글에서는 koa2 프레임워크를 소개하고 Koa 프레임워크를 구현하는 데 필요한 4가지 주요 모듈을 이해하고 구현하는 데 도움을 줄 것입니다.

koa 프레임워크란 무엇인가요?

koa는 node를 기반으로 한 새로운 웹 프레임워크입니다. Express 프레임워크의 원팀에 의해 만들어졌습니다. 우아함, 단순함, 강한 표현력, 높은 자유도가 특징입니다. Express에 비해 모든 기능이 플러그인을 통해 구현되기 때문에 더 가벼운 노드 프레임워크입니다. 이 플러그인 아키텍처 디자인 모델은 Unix 철학과 매우 일치합니다. [추천 학습: "nodejs Tutorial"]

koa 프레임워크가 버전 2로 업데이트되었습니다. 가장 중요한 개념을 설명하고, 간단한 koa2 프레임워크를 구현하는 방법을 단계별로 가르쳐서 koa2를 학습하고 이해하는 데 도움을 줍니다. 이 기사를 읽은 후 koa2의 소스 코드를 확인하면 아이디어가 매우 원활해질 것입니다.

이 글에서 사용된 프레임워크는 koa2이며, 이는 koa1과 다릅니다. koa1은 Generator+co.js 실행 방법을 사용하는 반면, koa2는 async/await를 사용하므로 이 글의 코드와 데모는 노드 8에서 실행되어야 합니다. 위에서 독자의 노드 버전이 낮은 경우 babel-cli를 업그레이드하거나 설치하고 babel-node를 사용하여 이 기사에 포함된 코드를 실행하는 것이 좋습니다.

koa 소스 코드 구조

KOA 프레임워크란 무엇인가요? koa2 구현을 위한 4가지 주요 모듈에 대한 자세한 설명

위 그림은 koa2의 소스 코드 디렉터리 구조 중 lib 폴더입니다. lib 폴더 아래에는 koa2의 4가지 핵심 파일인 application.js, context.js, request.js, response.js.

application.js

application.js는 클래스 인스턴스를 생성하기 위한 생성자를 내보내며, 이는 프레임워크에 이벤트를 모니터링하고 트리거하는 기능을 제공합니다. 또한 애플리케이션은 toJSON, 수신 대기, 사용 등과 같이 일반적으로 사용되는 일부 API를 노출합니다.

listen의 구현 원리는 실제로 http.createServer를 캡슐화한 것입니다. 초점은 미들웨어 병합, 컨텍스트 처리 및 res의 특수 처리를 포함하는 이 함수에 전달된 콜백에 있습니다.

미들웨어를 수집하고 여러 미들웨어를 캐시 큐에 넣은 다음 koa-compose 플러그인을 사용하여 이러한 미들웨어 시리즈를 재귀적으로 결합하고 호출하는 데 사용됩니다.

context.js

이 부분은 koa의 애플리케이션 컨텍스트 ctx입니다. 실제로는 위임자에 초점이 맞춰져 있습니다. 예를 들어 ctx.repsponse.status에 액세스하려는 경우 위임자를 통해 ctx.status에 액세스하면 직접 액세스할 수 있습니다.

request.js, response.js

이 두 부분은 기본 res 및 req에 대한 일부 작업입니다. 헤더를 가져오거나 헤더를 설정하고 본문을 설정하기 위해 es6의 get 및 set 구문을 많이 사용합니다. 잠깐, 자세한 내용은 소개하지 않겠습니다. 관심 있는 독자는 스스로 소스 코드를 읽어보세요.

koa2의 4가지 주요 모듈 구현

위에서는 koa2 소스 코드의 일반적인 프레임워크 구조에 대해 간략하게 설명했습니다. 다음으로 koa 프레임워크를 이해하고 구현하려면 다음이 필요하다고 저자는 생각합니다. 4개의 주요 모듈 구현:

  • 노드 http 서버 캡슐화, Koa 클래스 생성자 생성

  • 요청, 응답, 컨텍스트 개체 구성

  • 미들웨어 메커니즘 구현 및 양파 껍질 벗기기

  • 오류 캡처 및 오류 처리

하나씩 분석하고 구현해 보겠습니다.

모듈 1: 노드 http 서버 캡슐화 및 Koa 클래스 생성자 생성

koa2의 소스 코드를 읽으면서 우리는 Koa의 서버 애플리케이션과 포트 모니터링이 실제로 노드의 네이티브 코드를 기반으로 캡슐화되어 있음을 알게 되었습니다. 아래 코드는 노드 네이티브 코드를 통해 구현된 서버 모니터링입니다.

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

위 노드 네이티브 코드를 koa 모드로 캡슐화해야 합니다.

const http = require('http');
const Koa = require('koa');
const app = new Koa();
app.listen(3000);

koa를 구현하는 첫 번째 단계는 위 프로세스를 캡슐화하는 것입니다. 이를 위해 애플리케이션 클래스 함수의 구조를 구현하기 위해 application.js를 만들어야 합니다. :

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;

그런 다음 example.js를 만들고, application.js를 도입하고, 서버 인스턴스를 실행하고 청취 코드를 시작합니다.

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文件,然后写入以下代码:

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文件,然后输入下面的代码:

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文件,输入如下代码:

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文件,添加如下代码:

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的中间件机制已经做好了,那么它是能成功运行下面代码的:

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文件,添加如下代码:

    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这个函数:

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()的时候,其实就是去执行下一个中间件。

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框架还没有能实现这一点,我们接下加上错误处理和捕获的机制。

throw new Error('oooops');

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

return fn(ctx).then(respond);

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

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

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

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

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

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 구현을 위한 4가지 주요 모듈에 대한 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 juejin.cn에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제