koakoa是Node.js的Web开发框架。它由Express原班人马打造,致力于成为更小、表现力更强、更健壮的Web框架。使用koa编写web应用,通过组合不同的生成器,可以避免回调函数的重复繁琐嵌套,大大提高错误处理的效率。Koa在内核方法中没有绑定任何中间件,它只提供了一个轻量级优雅的函数库,使得编写Web应用得心应手。与Express的区别这里简单介绍一下koa和Express的主要区别:Express封装了很多内置的中间件,比如connect和router,而koa相对轻量级,开发者可以根据需要自定义框架他们自己的需要;Express是基于回调来处理中间件,而koa是基于async/await;在异步执行中间件时,Express并没有严格按照onion模型来执行中间件,而koa是严格遵循的(体现在中间件是异步函数会区别对待)。因此,我们需要先介绍一下洋葱模型。洋葱模型我们都知道洋葱是一层一层地包裹起来,一层层递进的,但是现在我们不看它的三维结构,而是要把洋葱切开。从切面看,如图:可以看出,要穿过洋葱的中心点,必须先把洋葱皮逐层穿透到中心点,然后再穿过皮从中心点逐层。这里有一个特点:进去的时候要穿透多少层表皮,出去的时候要穿透多少层表皮。先穿透表皮,再穿透表皮,符合我们所说的栈表,先进后出的原则。Express和koa都是基于中间件实现的。中间件主要用于请求拦截和修改请求或响应结果。中间件(可以理解为一个类或者功能模块)的执行方式需要基于洋葱模型。洋葱皮可以认为是中间件:从外到内的过程就是一个关键字next();如果没有调用next(),则不会调用下一个中间件;而从内到外是每个中间件执行完成后,进入原来的上层中间件,直到最外层。也就是说对于异步中间件,koa和Express在某些情况下会出现不同的代码执行顺序。异步差异同样的逻辑,先来Express:constexpress=require('express')constapp=express()app.use(async(req,res,next)=>{conststart=Date.now();console.log(1)awaitnext();console.log(2)})app.use(async(req,res,next)=>{console.log('3')awaitnext()awaitnewPromise((resolve)=>setTimeout(()=>{console.log(`wait1000msend`);resolve()},1000));console.log('4')})app.use((req,res,next)=>{console.log(5);res.send('helloexpress')})app.listen(3001)console.log('serverlisteningatport3001')通常,我们期望返回结果序列是:135wait1000msend42但实际上结果是:1352wait1000msend4Koa代码,逻辑相同:constKoa=require('koa');constapp=newKoa();app.use(async(ctx,next)=>{console.log(1)awaitnext();console.log(2)});app.use(async(ctx,next)=>{console.log(3)等待下一个();阿瓦itnewPromise((resolve)=>setTimeout(()=>{console.log(`wait1000msend`);resolve()},1000));控制台日志(4)});//响应应用程序。使用(asyncctx=>{console.log(5)ctx.body='HelloKoa';});app.listen(3000);console.log('appstart:http://localhost:3000')response区别大多数情况下,koa和Express的response没有区别,只是写法略有不同。前者需要ctx.body=xxx,后者需要使用res.send或者res.json等方法。但以下几种情况,是快递做不到的。constKoa=require('koa');constapp=newKoa();//x-response-timeapp.use(async(ctx,next)=>{conststart=Date.now();awaitnext();constms=Date.now()-start;ctx.set('X-Response-Time',`${ms}ms`);});//responseapp.use(asyncctx=>{ctx.body='HelloKoa';});app.listen(3000);console.log('appstart:http://localhost:3000')上面的代码主要是想给所有接口添加一个响应头,这个响应头代表这个接口函数的执行时间。在Express中,你可以这样写:constexpress=require('express')constapp=express()app.use(async(req,res,next)=>{conststart=Date.now();awaitnext();constms=Date.now()-开始;res.header('X-Response-Time',`${ms}ms`);})app.use((req,res,next)=>{res.send('helloexpress')})app.listen(3001)console.log('serverlisteningatport3001')但是请求的时候会报错:Error[ERR_HTTP_HEADERS_SENT]:Cannotsetheadersaftertheyaresend给客户端,因为res.send已经是发送响应的意思了,你又想重新设置响应头,这是不允许的。也就是说,Express会直接使用res.send等方法进行响应,而koa会等到所有中间件都完成后再响应。中间件主要处理逻辑Koa的中间件处理逻辑很简单,主要放在koa-compose中:for(constfnofmiddleware){if(typeoffn!=='function')thrownewTypeError('中间件必须由函数组成!')}/***@param{Object}context*@return{Promise}*@apipublic*/returnfunction(context,next){//最后调用的中间件#letindex=-1returndispatch(0)functiondispatch(i){if(i<=index)returnPromise.reject(newError('next()被多次调用'))index=iletfn=middleware[i]if(i===middleware.length)fn=nextif(!fn)returnPromise.resolve()try{returnPromise.resolve(fn(context,dispatch.bind(null,i+1)));}}catch(err){returnPromise.reject(err)}}}}每个中间件调用的next()其实就是这个:dispatch.bind(null,i+1)还是利用闭包递归的特性来执行一个一个,每次执行都会返回一个承诺。贴一下koa中间件的执行过程:oakbenchmarksagainstthekoaframeworkofnodejs。Deno开发者参考它开发了一个oak框架。用法几乎一模一样,学习成本很低。建议使用它。代码如下:import{Application}from"https://deno.land/x/oak/mod.ts";constapp=newApplication();app.use(async(ctx,next)=>{conststart=Date.now();console.log(1)awaitnext();console.log(2)constms=Date.now()-start;ctx.response.headers.set('X-Response-Time',`${ms}ms`);});app.use(async(ctx,next)=>{conststart=Date.now();console.log(3)awaitnext();awaitnewPromise((resolve)=>setTimeout(()=>{console.log(`wait1000msend`);resolve('wait')},1000));console.log(4)constms=Date.now()-开始;console.log(`${ctx.request.method}${ctx.request.url}-${ms}`);});app.use((ctx)=>{console.log(5)ctx.response.body="HelloDeno!";});console.log('appstart:http://localhost:3002')awaitapp.listen({port:3002});中间件处理逻辑吧中间件处理逻辑在这里:/**Composem多个中间件函数合并为一个中间件函数。*/导出函数compose,>(middleware:Middleware[],):(context:T,next?:()=>Promise|未定义=中间件[i];if(i===middleware.length){fn=next;}如果(!fn){返回;}awaitfn(context,dispatch.bind(null,i+1));}返回调度(0);};}可以看出和koa几乎一模一样oak的简化版接下来,我们写一个oak/koa的简化版来实现上面的示例功能。现实前,先看下Deno的http服务代码://开始监听localhost的8080端口.constserver=Deno.listen({port:8080});console.log(`HTTPwebserverrunning.Accessitat:http://localhost:8080/`);//到服务器的连接将作为异步迭代器产生。forawait(constconnofserver){//为了不阻塞,我们需要单独处理每个连接//无需等待函数serveHttp(conn);}asyncfunctionserveHttp(conn:Deno.Conn){//这会将网络连接“升级”为HTTP连接。consthttpConn=Deno.serveHttp(conn);//通过HTTP连接发送的每个请求都将作为来自HTTP连接的异步迭代器产生。forawait(constrequestEventofhttpConn){//本机HTTP服务器使用网络标准`Request`和`Response`//对象。constbody=`你的用户代理是:\n\n${requestEvent.request.headers.get("user-agent",)??“未知”}`;//请求前夕nt的.respondWith()方法是我们将响应发送回客户端的方式。requestEvent.respondWith(newResponse(body,{status:200,}),);}}通过上面的代码,一个http://localhost:8080服务所以,我们的代码也很简单:classApplication{middlewares:Middleware[]=[];使用(回调:中间件){this.middlewares.push(回调);}asynclisten(config:{port:number;}){constmiddlewares=this.middlewares;constserver=Deno.listen(config);console.log(`HTTP网络服务器正在运行。访问它:http://localhost:${config.port}/`);//与服务器的连接将作为异步可迭代对象放弃。forawait(constconnofserver){//为了不阻塞,我们需要单独处理每个连接//而不是等待函数serveHttp(conn);}asyncfunctionserveHttp(conn:Deno.Conn){//这会将网络连接“升级”为HTTP连接。consthttpConn=Deno.serveHttp(conn);//通过HTTP连接发送的每个请求都将作为异步迭代器生成//从HTTP连接。forawait(constrequestEventofhttpConn){constctx:Context={request:requestEvent.request,response:{body:'',status:200,headers:{_headers:{},set(key:string,value:string|number){(this._headersasany)[key]=value;},get(key:string){return(this._headersasany)[key]}}}};console.log(requestEvent.request.url);等待撰写(中间件)(ctx);constbody=ctx.response.body;requestEvent.respondWith(新响应(正文,{状态:ctx.response.status,标题:ctx.response.headers._headers}),);}}}}这样就实现了一个简单的使用中间件处理消息的功能。至于如何实现路由,留给大家吧。总结本文介绍了Node.js的两大主流web框架koa和Express的区别,以及koa的中间件处理逻辑。可见koa的设计思路是非常精妙的。接着引出Deno类似的oak框架,旨在通过对比两者在使用上的差异,让大家对Deno有一个简单的了解。下一篇将重点安利Deno,带你从Node走向Deno。本文参考:浅谈Nodejs框架中的“洋葱模型”,再也不怕面试官问你express和koa的区别
