一、Koa介绍
Koa是一个类似于Express的Web开发框架,创始人也是同一个人。它的主要特点是,使用了ES6的Generator函数,进行了架构的重新设计。也就是说,Koa的原理和内部结构很像Express,但是语法和内部结构进行了升级。
官方faq有这样一个问题:”为什么koa不是Express 4.0?“,回答是这样的:”Koa与Express有很大差异,整个设计都是不同的,所以如果将Express 0按照这种写法升级到4.0,就意味着重写整个程序。所以,我们觉得创造一个新的库,是更合适的做法。“
1、示例
安装好nodejs,创建项目文件,按照如下操作引入koa。
1 | #初始化项目 |
创建一个js文件
1 | // 导入koa模块 |
运行文件,浏览器即可访问。
app.use方法用于向middleware数组添加Generator函数。
listen方法指定监听端口,并启动当前应用。它实际上等同于下面的代码。
1 | ar http = require('http'); |
参数ctx是由koa传入的封装了request和response的变量,next是koa传入的将要处理的下一个异步函数,然后,设置response的Content-Type和内容。
由async标记的函数称为异步函数,在异步函数中,可以用await调用另一个异步函数,这两个关键字将在ES7中引入。
引入包方式:
命令 npm install koa@2.0.0;
项目的package.json中的 dependencies引入,在使用命令 npm install即会吧项目的包都安装好!
直接用命令node app.js在命令行启动程序,或者用npm start启动。npm start命令会让npm执行定义在package.json文件中的start对应命令:
1 | "scripts": { |
2、级联
在上述的代码中,node每收到一个http请求,koa就会调用通过app.use()注册的async函数,并传入ctx和next参数。
这里的await next()的作用是:处理函数的执行顺序。
koa把很多中间件组成一个处理链,每个async函数做自己任务,然后用await next()来调用下一个async函数。
就是停止该中间件,继续执行下一个符合请求的中间件(‘downstrem’),然后控制权再逐级返回给上层中间件(‘upstream’)。
例如,4个中间件组成处理链,依次请求,打印日志,记录处理时间,输出HTML:
1 | const Koa = require('koa'); |
运行后访问,后台打印出结果。
1 | 请求数据 |
如果一个没有调用await next(),后续的代码将不再执行了。
3、配置
应用配置是 app 实例属性,目前支持的配置项如下:
1 | app.env 默认为 NODE_ENV or "development" |
app.listen(…)
Koa 应用并非是一个 1-to-1 表征关系的 HTTP 服务器。 一个或多个Koa应用可以被挂载到一起组成一个包含单一 HTTP 服务器的大型应用群。
如下为一个绑定3000端口的简单 Koa 应用,其创建并返回了一个 HTTP 服务器,为 Server#listen() 传递指定参数(参数的详细文档请查看nodejs.org)。
1 | const Koa = require('koa'); |
同时支持 HTTPS 和 HTTPS,或者在多个端口监听同一个应用。
1 | const http = require('http'); |
app.callback()
返回一个适合 http.createServer() 方法的回调函数用来处理请求。
app.use(function)
为应用添加指定的中间件,详情请看 Middleware
app.keys=
设置签名cookie密钥。
该密钥会被传递给KeyGrip, 当然,您也可以自己生成 KeyGrip. 例如:
1 | app.keys = ['im a newer secret', 'i like turtle']; |
在进行cookie签名时,只有设置 signed 为 true 的时候,才会使用密钥进行加密:
1 | ctx.cookies.set('name', 'tobi', { signed: true }); |
app.context
app.context是从中创建ctx的原型。 可以通过编辑app.context向ctx添加其他属性。当需要将ctx添加到整个应用程序中使用的属性或方法时,这将会非常有用。这可能会更加有效(不需要中间件)和/或更简单(更少的require()),而不必担心更多的依赖于ctx,这可以被看作是一种反向模式。
例如,从ctx中添加对数据库的引用:
1 | app.context.db = db(); |
注:ctx上的很多属性是被限制的,在app.context只能通过使用Object.defineProperty()来编辑这些属性(不推荐)。可以在 https://github.com/koajs/koa/issues/652上查阅
已安装的APP沿用父级的ctx和配置。因此,安装的应用程序只是一组中间件。
4、错误处理
默认情况下Koa会将所有错误信息输出到 stderr, 除非 app.silent 是true.当err.status是404或者err.expose时,默认错误处理程序也不会输出错误。要执行自定义错误处理逻辑,如集中式日志记录,您可以添加一个”错误”事件侦听器:
1 | app.on('error', err => { |
如果错误发生在 请求/响应 环节,并且其不能够响应客户端时,Contenxt 实例也会被传递到 error 事件监听器的回调函数里。
1 | app.on('error', (err, ctx) => { |
当发生错误但仍能够响应客户端时(比如没有数据写到socket中),Koa会返回一个500错误(Internal Server Error)。 无论哪种情况,Koa都会生成一个应用级别的错误信息,以便实现日志记录等目的。
二、Context(上下文)
Koa Context 将 node 的 request 和 response 对象封装在一个单独的对象里面,其为编写 web 应用和 API 提供了很多有用的方法。
context 在每个 request 请求中被创建,在中间件中作为接收器(receiver)来引用,或者通过 this 标识符来引用:
1 | app.use(async ctx => { |
许多 context 的访问器和方法为了便于访问和调用,简单的委托给他们的 ctx.request 和 ctx.response 所对应的等价方法, 比如说 ctx.type 和 ctx.length 代理了 response 对象中对应的方法,ctx.path 和 ctx.method 代理了 request 对象中对应的方法。
具体API见https://www.koajs.com.cn/
三、Request和Response
Koa Request 对象是对 node 的 request、response进一步抽象和封装,提供了日常 HTTP 服务器开发中一些有用的功能。
详细api见https://www.koajs.com.cn/
四、处理URL
通常情况下需要对不同的URL调用不同的处理函数,这样才能返回不同的结果。
1 | app.use(async (ctx, next) => { |
但是我们可以借助中间件来处理不同的URL调用不同的处理函数。
koa-router
koa-router这个middleware负责处理URL映射。
在中增加,之后执行npm install执行安装模块。
创建文件,写入代码
1 | const Koa = require('koa'); |
处理post请求
router.get(‘/path’, async fn)处理的是get请求;router.post(‘/path’, async fn)处理post请求。
post请求通常会发送一个表单,或者JSON,它作为request的body发送,但无论是Node.js提供的原始request对象,还是koa提供的request对象,都不提供解析request的body的功能!所以,需要引入另一个middleware来解析原始request请求,然后,把解析后的参数,绑定到ctx.request.body中。
步骤:
1 | 1、package.json中添加依赖项:"koa-bodyparser": "3.2.0"然后使用npm install安装。 |
1 | const Koa = require('koa'); |
在之前的示例中都是全部写在app.js里,每加一个URL,就需要修改app.js。随着URL越来越多,app.js就会越来越长。如果把URL处理函数集中分类写到其他js文件,让app.js自动导入所有处理URL的函数,代码一分离比较清楚:
新建目录结构:
把路由信息分别写在不同的文件:
hello.js
1 | var fn_hello = async (ctx, next) => { |
index.js
1 |
|
app.js
1 | const Koa = require('koa'); |
启动后测试没问题,这里app.js中整理下代码如下:
1 | const Koa = require('koa'); |
Controller Middleware
我们把扫描controllers目录和创建router的代码从app.js中提取出来,作为一个简单的middleware使用,命名为controller.js:
1 | const fs = require('fs'); |
app中简化为:
1 | const Koa = require('koa'); |
经过重新整理后的代码具有非常好的模块化,所有处理URL的函数按功能组存放在controllers目录,只需要不断往这个目录下加东西就可以了,app.js保持不变。
五、Nunjucks
模板引擎
模板引擎就是基于模板配合数据构造出字符串输出的一个组件。比如下面的函数就是一个模板引擎:
1 | function examResult (data) { |
如果我们输入数据如下:
1 | examResult({ |
该模板引擎把模板字符串里面对应的变量替换以后,就可以得到以下输出:
小明同学一年级期末考试语文78分,数学87分,位于年级第999名。
模板引擎最常见的输出就是输出网页,也就是HTML文本。当然,也可以输出任意格式的文本,比如Text,XML,Markdown等等。
有同学要问了:既然JavaScript的模板字符串可以实现模板功能,那为什么我们还需要另外的模板引擎?
因为JavaScript的模板字符串必须写在JavaScript代码中,要想写出新浪首页这样复杂的页面,是非常困难的。
输出HTML有几个特别重要的问题需要考虑:
转义
对特殊字符要转义,避免受到XSS攻击。比如,如果变量name的值不是小明,而是小明,模板引擎输出的HTML到了浏览器,就会自动执行恶意JavaScript代码。
格式化
对不同类型的变量要格式化,比如,货币需要变成12,345.00这样的格式,日期需要变成2016-01-01这样的格式。
简单逻辑
模板还需要能执行一些简单逻辑,比如,要按条件输出内容,需要if实现如下输出:
1 | {{ name }}同学, |
所以,我们需要一个功能强大的模板引擎,来完成页面输出的功能。
Nunjucks
Nunjucks是Mozilla开发的一个纯JavaScript编写的模板引擎,既可以用在Node环境下,又可以运行在浏览器端。但是,主要还是运行在Node环境下,因为浏览器端有更好的模板解决方案,例如MVVM框架。
如果你使用过Python的模板引擎jinja2,那么使用Nunjucks就非常简单,两者的语法几乎是一模一样的,因为Nunjucks就是用JavaScript重新实现了jinjia2。
从上面的例子我们可以看到,虽然模板引擎内部可能非常复杂,但是使用一个模板引擎是非常简单的,因为本质上我们只需要构造这样一个函数:
1 | function render(view, model) { |
其中,view是模板的名称(又称为视图),因为可能存在多个模板,需要选择其中一个。model就是数据,在JavaScript中,它就是一个简单的Object。render函数返回一个字符串,就是模板的输出。
下面我们来使用Nunjucks这个模板引擎来编写几个HTML模板,并且用实际数据来渲染模板并获得最终的HTML输出。
创建项目:
package.json中引入:”nunjucks”: “2.4.2”
模板引擎是可以独立使用的,并不需要依赖koa。
在app.js中编写代码如下:
1 | const nunjucks = require('nunjucks'); |
创建env需要的参数可以查看文档获知。我们用autoescape = opts.autoescape && true这样的代码给每个参数加上默认值,最后使用new nunjucks.FileSystemLoader(‘views’)创建一个文件系统加载器,从views目录读取模板。
我们编写一个hello.html模板文件,放到views目录下,内容如下:
Hello
然后,我们就可以用下面的代码来渲染这个模板:
1 | var s = env.render('hello.html', { name: '小明' }); |
运行获得输出如下:
1 | "D:\Program Files\nodejs\node.exe" D:\mydoc\NodeJs\koa-study-demo\Demo-03\app.js |
咋一看,这和使用JavaScript模板字符串没啥区别嘛。不过,试试:
1 | var s = env.render('hello.html', { name: '<script>alert("小明")</script>' }); |
获得输出如下:
1 | <h1>Hello <script>alert("小明")</script></h1> |
这样就避免了输出恶意脚本。
此外,可以使用Nunjucks提供的功能强大的tag,编写条件判断、循环等功能,例如:
1 | <!-- 循环输出名字 --> |
Nunjucks模板引擎最强大的功能在于模板的继承。
仔细观察各种网站可以发现,网站的结构实际上是类似的,头部、尾部都是固定格式,只有中间页面部分内容不同。如果每个模板都重复头尾,一旦要修改头部或尾部,那就需要改动所有模板。
更好的方式是使用继承。先定义一个基本的网页框架base.html:
1 | <html> |
base.html定义了三个可编辑的块,分别命名为header、body和footer。子模板可以有选择地对块进行重新定义:
1 | {% extends 'base.html' %} |
然后,我们对子模板进行渲染:
1 |
输出HTML如下:
1 | <html> |
性能
对于模板渲染本身来说,速度是非常非常快的,因为就是拼字符串嘛,纯CPU操作。
性能问题主要出现在从文件读取模板内容这一步。这是一个IO操作,在Node.js环境中,我们知道,单线程的JavaScript最不能忍受的就是同步IO,但Nunjucks默认就使用同步IO读取模板文件。
但是Nunjucks会缓存已读取的文件内容,也就是说,模板文件最多读取一次,就会放在内存中,后面的请求是不会再次读取文件的,只要我们指定了noCache: false这个参数。
在开发环境下,可以关闭cache,这样每次重新加载模板,便于实时修改模板。在生产环境下,一定要打开cache,这样就不会有性能问题。
Nunjucks也提供了异步读取的方式,但是这样写起来很麻烦,有简单的写法我们就不会考虑复杂的写法。保持代码简单是可维护性的关键。
实现简单编写View实现
处理静态文件
我们把所有静态资源文件全部放入/static目录,目的就是能统一处理静态文件。在koa中,我们需要编写一个middleware,处理以/static/开头的URL。
编写middleware
处理静态文件的middleware,static-files.js的文件:
1 | const path = require('path'); |
staticFiles是一个普通函数,它接收两个参数:URL前缀和一个目录,然后返回一个async函数。这个async函数会判断当前的URL是否以指定前缀开头,如果是,就把URL的路径视为文件,并发送文件内容。如果不是,这个async函数就不做任何事情,而是简单地调用await next()让下一个middleware去处理请求。
mz提供的API和Node.js的fs模块完全相同,但fs模块使用回调,而mz封装了fs对应的函数,并改为Promise。这样,我们就可以非常简单的用await调用mz的函数,而不需要任何回调。
所有的第三方包都可以通过npm官网搜索并查看其文档:https://www.npmjs.com/
最后,这个middleware使用起来也很简单,在app.js里加一行代码:
1 | let staticFiles = require('./static-files'); |
注意:也可以去npm搜索能用于koa2的处理静态文件的包并直接使用。
集成Nunjucks
集成Nunjucks实际上也是编写一个middleware,这个middleware的作用是给ctx对象绑定一个render(view, model)的方法,这样,后面的Controller就可以调用这个方法来渲染模板了。
我们创建一个templating.js来实现这个middleware:
1 | const nunjucks = require('nunjucks'); |
在这个middleware中,主要的是templating,给ctx绑定render函数,就继续调用下一个middleware。
使用的时候,我们在app.js添加如下代码:
1 | const isProduction = process.env.NODE_ENV === 'production'; |
isProduction,它判断当前环境是否是production环境。如果是,就使用缓存,如果不是,就关闭缓存。在开发环境下,关闭缓存后,我们修改View,可以直接刷新浏览器看到效果,否则,每次修改都必须重启Node程序,会极大地降低开发效率。
全局变量process中定义了一个环境变量env.NODE_ENV,开发的时候,环境变量应该设置为’development’,而部署到服务器时,环境变量应该设置为’production’。
注意:生产环境上必须配置环境变量NODE_ENV = ‘production’,而开发环境不需要配置,实际上NODE_ENV可能是undefined,所以判断的时候,不要用NODE_ENV === ‘development’。
类似的,我们在使用上面编写的处理静态文件的middleware时,也可以根据环境变量判断:
1 | if (! isProduction) { |
在生产环境下,静态文件是由部署在最前面的反向代理服务器(如Nginx)处理的,Node程序不需要处理静态文件。而在开发环境下,我们希望koa能顺带处理静态文件,否则,就必须手动配置一个反向代理服务器,这样会导致开发环境非常复杂。
编写View
运行
再检查一下app.js里的middleware的顺序:
第一个middleware是记录URL以及页面执行时间:
1 | app.use(async (ctx, next) => { |
第二个middleware处理静态文件:
1 | if (! isProduction) { |
第三个middleware解析POST请求:
1 | app.use(bodyParser()); |
第四个middleware负责给ctx加上render()来使用Nunjucks:
1 | app.use(templating('view', { |
最后一个middleware处理URL路由:
1 | app.use(controller()); |
现在,在VS Code中运行代码,不出意外的话,在浏览器输入localhost:3000/,可以看到首页内容:
koa-index
直接在首页登录,如果输入正确的Email和Password,进入登录成功的页面:
koa-admin-ok
如果输入的Email和Password不正确,进入登录失败的页面:
koa-admin-failed
怎么判断正确的name和Password?目前我们在admin.js中是这么判断的:
1 | if (name === 'admin' && password === '111111') { |
扩展
注意到ctx.render内部渲染模板时,Model对象并不是传入的model变量,而是:
Object.assign({}, ctx.state || {}, model || {})
首先,model || {}确保了即使传入undefined,model也会变为默认值{}。Object.assign()会把除第一个参数外的其他参数的所有属性复制到第一个参数中。第二个参数是ctx.state || {},这个目的是为了能把一些公共的变量放入ctx.state并传给View。
例如,某个middleware负责检查用户权限,它可以把当前用户放入ctx.state中:
1 | app.use(async (ctx, next) => { |
这样就没有必要在每个Controller的async函数中都把user变量放入model中。