node-js|使用Koa2进行Web开发(一)

这篇文章是我正在进行写作的《新时期的Node.js入门》的一部分
Connect,Express与Koa 为了更好地理解后面的内容,首先需要梳理一下Node中Web框架的发展历程
Connect
在connect官方网站提供的定义是,
Connect is a middleware layer for Node.js
开发者可以认为connect是一个Node中间件的脚手架。
Connect的源码结构十分简单,只有一个文件,去掉注释后的代码不超过两百行。
我们之所以首先提到connect,是因为connect首先在Node服务器编程中引入了middleware的概念,这种概念在当时(2010年)无疑是超前的,中间件概念的引入,将web开发变成了一个个独立的模块,使得社区的开发者们可以专注在中间件模块的开发上,
为后面express的诞生与繁荣打下了坚实的基础。
此外,connect还提出了一些关于中间件使用的规范,例如使用use方法加载中间件并且通过next方法调用中间件等
Express的诞生
Express框架是在Connect的基础上扩展而来的,它继承了connect的大部分思想,
Express的发展分为两个阶段,express3.x与express4.x
在3.x中,express依赖与connect的源码,并且内置了不少中间件,这种做法的缺点是如果内置的中间件更新了,那么开发者就不得不更新整个express
4.x中,express摆脱了对connect的依赖,并且摒弃了除了静态文件模块之外的所有中间件,只包含核心的路由处理(在express中,路由没有被当做中间件)以及其他的代码。
在过去的几年中,express取得了巨大的成功
MEAN(Mongo+Express+Angular+Node)架构成为了不少网站的开发首选,至今依旧非常流行。
Koa
但是Express依旧存在不少的缺点,对于中间件之间的异步流程控制没有提供良好的支持。
随着ES2015标准的落地,express的原班开发人法使用ES2015中的新特性重新打造了新的Web框架—Koa。
Koa的实现与connect更加相似,内部没有提供任何中间件,仅仅作为中间件的调用的脚手架。
Koa的发展同样存在两个阶段Koa1.x 和Koa2,两者之间的区别在于Koa2使用了ES2017中的新特性,这些特性已经在v7.6.0之后的Node版本中提供原生支持。
KOA入门 KOA 1.x 与KOA2
在web开发中,尽管http请求的处理是异步进行的,但我们还是希望能够顺序执行某些操作。
例如在收到http请求时,我们首先先将请求信息写入日志或者数据库,再返回对应的结果,这两个操作往往都是异步进行的,如果我们要顺序完成这两个任务,通常会使用嵌套回调的方式,或者借助一些三方模块进行异步流程控制
为了解决这个问题,Koa诞生了。
在KOA 1.x的版本中,由于当时node还没有完全实现对async/await的支持,因此使用了generator函数来作为异步处理的主要方式,此外,为了实现generator的自动执行,还使用了上一章介绍的co模块作为底层的处理逻辑—它们都是出自同一作者之手。
下面是koa1.x代码的例子
var koa = require('koa'); var app = koa(); // loggerapp.use(function *(next){ var start = new Date; yield next; var ms = new Date - start; console.log('%s %s - %s', this.method, this.url, ms); }); // responseapp.use(function *(){ this.body = 'Hello World'; }); app.listen(3000);

async/await是ES2017中的一个提案,目的是改进generator函数自动执行的问题,Koa 为此发布了2.0版本,这个版本舍弃了generator函数和模块,完全是使用async函数特性来实现的。
在Node 7.6.0及之后的版本提供了对async函数特性的语言层面的支持,因此要使用koa2,本地的Node环境必须大于7.6.0,否则就要使用babel之类的工具进行转换。
【node-js|使用Koa2进行Web开发(一)】在这个版本的实现中,Koa为我们屏蔽了回调的细节,基本可以认为回调已经“不存在了”。
除此之外,KOA和Express最大的不同之处在于Koa剥离了各种中间件,express似乎也有这种趋势,这种做法的优点是可以让框架变得更加轻量,也有助于开发者自行选择需要的中间件,缺点就是各种中间件都是由第三方开发,质量可能良莠不齐,这点和npm有些类似。
在Koa项目的github页面https://github.com/koajs 中,列出了Koa项目本身和被一些官方整理的中间件列表,开发者也可以自行在github中搜索,查找使用人数较多的一些中间件。
本文主要介绍KOA2的使用
在开始动手之前,我们首先要思考这样几个问题
  • 静态文件怎么处理
  • 路由怎么处理
  • 数据存储怎么办
  • 页面要如何渲染,使用页面模板还是框架
前三个问题属于后端范围,第四个就是前端的工作了,我们将上面的问题分成单独的模块来实现它们。
准备工作
关于使用KOA的准备工作,唯一需要注意的就是Node的版本问题了,这里给出官方推荐的安装方式
nvm install 7
npm i koa
node my-koa-app.js
KOA的Hellworld
按照惯例,从最简单的入门例子来看KOA的使用
const Koa = require('koa'); const app = new Koa(); app.use(ctx => { ctx.body = 'Hello World'; }); app.listen(3000);

可以看出,Koa没有使用在Node和express中常用的req和res对象,统一以ctx来代替
关于context对象
在处理http请求时,Node提供了request和response两个对象,Koa把两者封装到了同一个对象中,即context,缩写为ctx
在context中封装了许多方法,大部分都是从原生的request/response 对象使用委托方式得来的,如下表:
from request
? ctx.header
? ctx.headers
? ctx.method
? ctx.method=
? ctx.url
? ctx.url=
? ctx.originalUrl
? ctx.origin
? ctx.href
? ctx.path
? ctx.path=
? ctx.query
? ctx.query=
? ctx.querystring
? ctx.querystring=
? ctx.host
? ctx.hostname
? ctx.fresh
? ctx.stale
? ctx.socket
? ctx.protocol
? ctx.secure
? ctx.ip
? ctx.ips
? ctx.subdomains
? ctx.is()
? ctx.accepts()
? ctx.acceptsEncodings()
? ctx.acceptsCharsets()
? ctx.acceptsLanguages()
? ctx.get()
From response
? ctx.body
? ctx.body=
? ctx.status
? ctx.status=
? ctx.message
? ctx.message=
? ctx.length=
? ctx.length
? ctx.type=
? ctx.type
? ctx.headerSent
? ctx.redirect()
? ctx.attachment()
? ctx.set()
? ctx.append()
? ctx.remove()
? ctx.lastModified=
? ctx.etag=
关于ctx对象是如何获得这些属性和方法的,我们会在koa源码分析一节介绍
使用Koa处理http请求 在这一节里,我们不依赖任何现成的中间件,来介绍Koa是如何处理http请求的。
上面的内容也提到,koa的ctx对象中封装了request以及response对象,那么在处理http请求中,使用ctx对象就可以完成所有的处理
在上面的代码中,我们使用
ctx.body = “Hello World”
上面的代码实际上相当于
res.statusCode = 200;
res.end(“Hello World”);
ctx.body也可以写成ctx.response.body,ctx相当于ctx.request/response的别名
判断http请求类型可以通过ctx.method来进行判断,get请求的参数可以通过ctx.query获取
例如,当用户访问localhost:3000?kindName=Node时,可以使用如下的代码
app.get(‘/’, async (ctx, next) => {
console.log(ctx.query); // { kindName: ‘Node’ }
await next(); });
ctx对象的结构
虽然ctx封装了reqest和response对象的方法,但这并不代表ctx.Request就和原生的request对象完全相同.我们可以试着将原生对象和ctx封装后的对象分别打印出来进行比较
const app = require('koa')(); app.use((ctx,next)=>{ console.log(ctx.request); console.log(ctx.response); }); app.listen(3001);

ctx.request
{ method: ‘GET’, url: ‘/’, header: { host: ‘localhost:3001’,
connection: ‘keep-alive’,
‘upgrade-insecure-requests’: ‘1’,
‘user-agent’: ‘Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.133
Safari/537.36’,
accept: ‘text/html,application/xhtml+xml,application/xml; q=0.9,image/webp,/; q=0.8’,
‘accept-encoding’: ‘gzip, deflate, sdch, br’,
‘accept-language’: ‘zh-CN,zh; q=0.8,en; q=0.6,ja; q=0.4’ } }
ctx.response
{ status: 404, message: ‘Not Found’, header: {}, body: undefined }
可以看出,二者的结构,尤其是ctx.response的结构,和原生的request对象还是有很大区别的,ctx.response对象只是一个单纯的字符串,上面没有注册任何事件,这表示下面的使用方法是错误的
fs.createReadStream(“./1.txt”).pipe(ctx.response);
上面的代码会抛出形如
TypeError: dest.on is not a function
的错误,原因是单纯的字符串无法处理stream对象。
middleware express中的中间件
在介绍Koa中间件之前,我们暂时先把目光投向express,因为Koa中间件的设计思想大部分来自Connect,而express又是基于connect扩展而来的。
Express本身是由路由和中间件构成的框架,从本质上来说,express的运行就是在不断调用各种中间件。
中间件本质上是接收请求并且做出相应动作的函数,该函数通常接收req和res作为参数,以便对request和response进行操作,在web应用中,中间件是循环运行的,中间件的第三个参数一般写作next,表示下一个中间件。
中间件的功能
由于中间件仍然是一个函数,它可以做到Node代码能做到的任何事情,此外,还包括了修改请求和相应对象,终结请求-相应循环,以及调用下一个中间件等功能,这通常是通过next()方法来实现的。如果在某个个中间件中没有调用next()方法,则表示请求响应-循环到此为止,下一个中间件永远不会被执行。
在Express中使用中间件
Express 应用可使用如下几种中间件:
? 应用级中间件
? 路由级中间件
? 错误处理中间件
? 内置中间件
? 第三方中间件
上面是官网的分类,实际上这几个概念有一些重合之处
从 4.x 版本开始,, Express 已经不再依赖 Connect 了。除了 express.static(负责管理静态资源), Express 以前内置的中间件现在已经全部剥离(路由在express不被看做中间件)
与express不同,Koa没有任何内置的中间件,甚至连路由处理都没有包括在内,所有中间件都要通过第三方模块来实现,因此我们说比起express来,更像是Connect。
中间件的调用
无论是express还是koa,中间件的调用都是通过next()函数来执行的,当我们调用app.use()方法时,实际上在内部形成了一个中间件数组,next()方法负责调用数组中的下一个中间件。如果在一个中间件中没有调用next()方法,那么中间件的调用会中断,后续的中间件都不会被执行。
中间件的顺序执行
在Web开发中,我们通常希望能够顺序执行一些操作,例如当收到http请求后,首先向日志系统中写入数据,然后再进行路由处理,最后再进行数据库查询,这些应该是同步进行的。在Express和Koa中,表现为顺序调用某些中间件。在Express中,要实现中间件的顺序调用并不容易。
下面的代码定义了两个express中间件,和之前不同之处在于第二个中间件中调用了process.nextTrick(),表示这是一个异步操作
var app = require(‘express’)(); app.use(function(req,res, next){ next(); console.log("after next") }); app.use(function(req,res,next){ process.nextTick(function(){ console.log("before next"); next(); }); });

得到的运行结果是
after next
before next
这是因为在调用第一个next()后,第二个中间件内的process.nextTick由于是异步调用的,因此马上返回到第一个中间件,继续输出after next,然后中间件二的回调返回结果,输出before next
Koa中的中间件
Koa和express中间件最大的不同之处,在于Koa使用了async/await方法,可以方便地实现中间件的顺序调用,而express中不借助第三方模块的话,无法保证这一点。
借助async/await方法,事情变得简单了
var Koa = require("koa"); var app = new Koa(); app.use( async (ctx, next) =>{ await next(); console.log("after next") }); app.use( async (ctx, next) =>{ process.nextTick(function(){ console.log("before next"); next(); }); }); app.listen(3000);

使用Koa改写之后,代码输出:
before next after next
从本质上看,express和Koa其实区别并不大,所谓的async/await也不过是语法糖而已,但就是小小的语法糖简化了整个开发流程和代码量,仅这一点就够了。
本节的内容到此结束,下一节讲述路由以及静态文件处理的实现

    推荐阅读