乐趣区

关于koa:Koa入门教程2常用中间件

中间件执行流程

中间件的执行流程,能够用上面这张图片来活泼的阐明(图片应用了 Koa 2 的 async 语法):

对于 Koa 1 来说也相似,只是 async 函数换作 generator 函数,await 换作 yield 关键字。

对于前端程序员,能够把 yield 之前的代码认为是捕捉阶段,yield 之后的认为的冒泡阶段,从而了解多个中间件之间代码的执行流程。

路由中间件

路由个别属于业务代码,咱们个别放在其余根底中间件之后来注册。路由的基本原理就是判断 url path, 而后决定是否执行某个中间件逻辑。

简略实现能够相似这样:

const Koa = require('koa')
const app = new Koa()

app.use(function *(next) {if (this.path === '/home') {this.body = '首页'}
    else {yield next}
    console.log('这里会执行哦')
})
app.use(function *(next) {if (this.path === '/admin') {this.body = '治理端'}
})
app.listen(3000)

能够看到,对于不合乎本中间件的申请 path,就间接抛弃,并去执行下一个中间件。如果所有中间件都匹配不到,会返回 404(Koa 默认行为).

下面代码有一个问题,就是 “console.log” 会始终执行,要解决这个也很简略。因为对于路由中间件来说,所有逻辑都是匹配 path 的 if 判断外部的,所以对于这个不匹配的 else 代码,能够间接当做该 generator 的完结。能够在 yield 后面加 return 或这样批改:

app.use(function* (next) {if (this.path !== '/') return yield next;
  this.body = 'we are at home!';
})

koa-router

为了应答更简单的路由性能,咱们须要引入第三方的 koa-router 路由模块。不过 Koa1 须要应用 4.x 版本的。

Issue: You are using koa@1.x but koa-views@5.x needs koa@2 or above. If you are still at v1 please consider using koa-views@4.x. Note however, there are no updates supporting v1

npm i koa-router@4 -d

koa-router 裸露一个 Router 类,像 Vue.js 一样,只需创立一个 router 实例,就能够注册对应的路由规定。

var app = require('koa')();
var Router = require('koa-router');

var myRouter = new Router();

myRouter.get('/', function *(next) {this.response.body = 'Hello World!';});

app.use(myRouter.routes());

app.listen(3000);

从用法中显然能看进去,routers 办法返回的应该就是一个 generator 中间件函数,只是外部由 koa-router 进行了路由规定的解决和逻辑执行。开发者只需关注如何向 koa-router 对象上注册 解决中间件

koa-router 像少数路由一样反对很多 http 办法和匹配规定:

router.get()
router.post()
router.put()
router.del()
router.patch()

视图渲染中间件

Koa 有很多的中间件 比方 [koa-view](
https://github.com/queckezz/k…

该中间件反对多种模板引擎。

cookie

cookie 的获取和设置是 Koa 内置的 context 集成的能力,不须要中间件的参加。

this.cookies.get('cookieName')
this.cookies

koa-router

为了兼容 Koa1,咱们须要装置一个老一点的 koa-router

npm i koa-router@5.x

koa-logger

用于打印申请日志和耗时

npm i koa-logger@1 // 1.x 反对 Koa1

session

// 为了兼容 Koa1
npm i koa-session@3.x

该版本的 koa-session 只须要执行其导出函数并传入一个 app 对象即可应用

const session = require('koa-session')
app.use(session(app))
app.use(function * (next) {console.log(this.session.xxx)
})

koa-compress

默认的 Koa 利用,咱们察看下浏览器的 Response 响应的话,会发现尽管申请时浏览器携带了 Accept-Encoding: gzip, 但实际上响应外面并没有 Content-Encoding: gzip, 也就是说并没有压缩。

装置 koa-compress@1.x 之后,就能够让 Koa 默认开启对响应内容的压缩了:

app.use(require('koa-compress')())

大一点的文件才有成果哦,太小的话还比不上 ‘Content-Encoding’ 头所占的字节的话,就有点得失相当了。

koa-csrf

# 为了兼容 Koa1 请装置 2.x 版本
npm i koa-csrf@2.x

该模块的导出对象是一个函数,函数会创立一个中间件,你须要将他注册到 Koa 的 app 外面。应用形式如下:

app.use(session(app)) // koa-csrf 的机制要依赖 session 能力
app.use(csrf()) // 这是 koa1 的用法

koa-csrf 原理:

解决跨站申请伪造攻打,须要在客户端申请时携带一个机密的 token,这个 token 要确保只有服务器端晓得,而且用后即焚. 其思路是,一个用户在拜访页面时,服务端先把这个 csrf-token 搁置到页面中,而后页面再次发动 POST 申请时,页面须要带上这个 token,由服务端来校验是不是服务器颁发的 token.

回到 koa-csrf 这个模块,在每次申请周期中,koa-csrf 都会在它的中间件内生成一个秘钥 secret, 而后基于 secret 生成一个 csrf-token; 并把这个 csrf-token 挂在 ctx 上,把 secret 挂在 session 上(因为 secret 作为一个秘钥基于 session 能够针对一个独立用户,没必要每次都变). 咱们把 koa-csrf 中生成 token 过程的源码捡进去如下:

// 创立一个秘钥并放在 session 里,每次生成和校验 csrf 时都用这个 token
var secret = this.session.secret
        || (this.session.secret = tokens.secretSync())
// 摘录 tokens.secretSync 的实现如下:
Tokens.prototype.secretSync = function secretSync () {return uid.sync(this.secretLength) // 其实就是应用 uid-safe 模块生成一个固定长度的随机 uniqueId
}

有了 secret 秘钥了,再来看下 csrf-token 咋生成的:

// 基于上一步的秘钥 secret 来生成 csrf-token 放在 ctx 对象上
this._csrf = tokens.create(secret)
// csrf-token 的生成过程如下:
Tokens.prototype.create = function create (secret) {if (!secret || typeof secret !== 'string') {throw new TypeError('argument secret is required')
  }
  // 重点在这里。其中 rndm 模块仅仅就是用来生成 n 位数的随机字符串;而_tokenize 函数就是用来生成 csrf-token 的,其实现我摘录在上面
  return this._tokenize(secret, rndm(this.saltLength))
}

// tokenize 实现
Tokens.prototype._tokenize = function tokenize (secret, salt) {// csrf-token 的格局为: salt 随机字符串 + hash(salt+secret)
  return salt + '-' + hash(salt + '-' + secret)
}

至此,csrf-token 就生成了。接下来,你须要在 GET 申请的页面上,把 this.csrf 渲染到页面中。而后前端再次申请后端的 POST 接口时,须要带上那个 token。这样,POST 申请达到服务器时 koa-csrf 中间件就会在申请到来时优先进行校验。

校验规定曾经不言而喻了:

  1. 从前端的 query 或 body 或 cookie 中取出 _csrf 这个变量(csrf-token)
  2. 依照 csrf-token 的规定,取出 横线 后面的字符串作为 salt 随机串,取前面的作为 待校验的哈希[fehash]
  3. 从服务端的 session 中 (this.session.secret) 拿出秘钥 secret
  4. 应用与当初截然不同的 tokenize 函数算一下这个哈希:
        var result = hash('前端传来的 salt' + '服务端秘钥 secret')
  1. 比对本次算进去的 result 与 前端传来的 待校验 fehash 值 是否统一。不统一则阐明是伪造的申请。koa-csrf 中间件会间接跑错

那么,会不会存在黑客在两头网络窃取到某次申请的 token 后,再利用这个 token 来施行 CSRF 呢?这个实际上是无奈防止的,既然黑客能窃取到 http 报文(阐明申请被中间人劫持或站点被 XSS 注入),那黑客齐全能够窃取到 cookie 等信息,相当于齐全模仿了用户,这种状况下任何防备都没有作用了;只能说如果发现 IP 变了那就要求用户从新登录且切换 secret。

更多中间件

简直所有的网络应用所需的性能都有中间件提供。能够在官网 wiki 中看到中间件列表

中间件编写最佳实际

带参数的中间件

对于编写公共中间件的场景来说,更多的须要用户能自定义中间件中一些配置。此时须要反对用户对中间件进行配置。要实现可配置的中间件也简略,只须要写一个包装函数,返回一个 generator 的函数即可。例如咱们的日志中间件,能够容许用户自定义日志格局,则能够这样:

// 可自定义日志格局的中间件
const mylogger = function (format) {format = format || '{{method}} {{url}} - {{time}}'
    return function *(next) {const start = Date.now()
        yield next
        const ms = Date.now() - start
        console.log(format
            .replace('{{method}}', this.method)
            .replace('{{url}}', this.url)
            .replace('{{time}}', ms)
        )
    }
}
// 应用该中间件
app.use(mylogger('{{time}} - {{method}} : {{url}}'))

合并多个中间件

有时可能须要将多个中间件合并为一个。对于 Generator 来说,能够应用 .call(this. next) 的形式将他们合并。

const Koa = require('koa')
const app = new Koa()

function *a(next) {console.log('come a')
    yield next;
    console.log('end a')
}
  
function *b(next) {console.log('come b')
    yield next
    console.log('end b')
}

function *all(next) {console.log('come all')
    yield a.call(this, b.call(this, next));
    console.log('end all')
}

app.use(all)
app.listen(3000)

执行上述代码,控制台会输入:

come all
come a
come b
end b
end a
end all

你肯定比拟纳闷为什么多个 generator 函数通过 call(this, next) 是怎么做到如此合并执行的?其实实质上 Koa 的运作也是基于合并 middlware 来执行的。这里大略是这样的:

  1. 首先咱们须要晓得 a、b、all 都是 generator 函数;而 next 是 generator 对象
  2. a.call(this, b.call(this, next)) 相当于先执行 b.call(this, next) 创立 b 的 generator 对象。而后该对象会作为 next 参数去执行 a 函数,进而创立出 a 的 generator 对象
  3. 因而 a.call(this, b.call(this, next)) 的返回值是 a 函数的 generator 对象;且 a 函数中的 next 示意的是 b 函数的 generator 对象。这样执行这个表达式的返回值,便相当于执行:
function *a(next) {console.log('come a')
    yield (b 中间件 的 generator 对象);
    console.log('end a')
}
  1. 如此,all 这个 generator 函数就把各个中间件联结起来执行了。

上述过程相似于 koa-compse 模块的合并能力, 这里贴一个 compose 模块的实现(援用自阮一峰的 Koa 教程):

function compose(middleware){return function *(next){if (!next) next = noop();

    var i = middleware.length;

    while (i--) {next = middleware[i].call(this, next);
    }

    yield *next;
  }
}

function *noop(){}

以上就是中间件合并的原理了,合并后会返回一个新的 generator 函数。而 Koa 是如何应用 co 库把合并后的 generator 中间件函数运行起来的呢?这个就有点简单了,更具体的 middleware 合并 和 Koa 原理能够参考: qianlongo github

总结

本章节介绍了几个罕用中间件,如 koa-router 和 koa-view,并对中间件的合并和传参进行了简略介绍。基本上 Koa 的所有应用形式都曾经介绍结束,前面就是赤裸裸的实际了

退出移动版