koa源码阅读,koa2入门笔记

By admin in 4858美高梅 on 2019年4月3日

koa源码阅读的第5篇,涉及到向接口请求方提供文件数量。

koa源码阅读[2]-koa-router

其3篇,有关koa生态中相比重要的二其中间件:koa-router

第一篇:koa源码阅读-0
第二篇:koa源码阅读-1-koa与koa-compose

条件准备:安装node.js,提议版本号>=7.6,不然需额外安装插件。
  • 直接设置 node.js :node.js官网地址
    https://nodejs.org
  • nvm管理多版本 node.js :可以用nvm 实行node版本实行管制
    • Mac 系统安装 nvm
      https://github.com/creationix/nvm\#manual-install
    • windows 系统设置 nvm
      https://github.com/coreybutler/nvm-windows
    • Ubuntu 系统装置 nvm
      https://github.com/creationix/nvm

  • 新建项目,使用npm init起头化,目录如下

├── app.js
├── package.json
  • 设置 koa,并将版本音信保存在 package.json 中

cnpm i koa -S

接上次挖的坑,对koa2.x有关的源码举行辨析 第一篇。
不得不说,koa是三个很轻量、很优雅的http框架,越发是在二.x过后移除了co的引入,使其代码变得更为清晰。

第一篇:koa源码阅读-0
第二篇:koa源码阅读-一-koa与koa-compose
第三篇:koa源码阅读-贰-koa-router

koa-router是什么

率先,因为koa是三个管理中间件的阳台,而注册三在那之中间件使用use来执行。
不论如何请求,都会将富有的中间件执行3遍(假如未有中途截至的话)
之所以,那就会让开发者很困扰,如若大家要做路由该怎么写逻辑?

app.use(ctx => {
  switch (ctx.url) {
    case '/':
    case '/index':
      ctx.body = 'index'
      break
    case 'list':
      ctx.body = 'list'
      break
    default:
      ctx.body = 'not found'
  }
})

 

实在,那样是五个简短的点子,可是一定不适用于大型项目,数11个接口通过三个switch来控制未免太繁琐了。
再者说请求大概只协助get或者post,以及这种方法并不能够很好的支撑U奔驰G级L中隐含参数的乞求/info/:uid
express中是不会有这般的难点的,自个儿已经提供了getpost等之类的与METHOD同名的函数用来注册回调:
express

const express = require('express')
const app = express()

app.get('/', function (req, res) {
  res.send('hi there.')
})

 

但是koa做了不少的精简,将众多逻辑都拆分出来作为独立的中间件来存在。
故而造成司空眼惯express项目搬迁为koa时,供给额外的设置1些中间件,koa-router应当说是最常用的二个。
所以在koa中则要求相当的安装koa-router来完成类似的路由成效:
koa

const Koa = require('koa')
const Router = require('koa-router')

const app = new Koa()
const router = new Router()

router.get('/', async ctx => {
  ctx.body = 'hi there.'
})

app.use(router.routes())
  .use(router.allowedMethods())

 

看起来代码确实多了1些,究竟将许多逻辑都从框架之中间转播移到了中间件中来处理。
也究竟为了有限协理3个粗略的koa框架所取舍的部分事物吗。
koa-router的逻辑确实要比koa的纷繁一些,能够将koa想象为2个商场,而koa-router则是中间2个地摊
koa仅供给确认保障市集的平安运转,而真正和消费者打交道真的是在内部摆摊的koa-router

一、认识middleware中间件

在HelloWorld的demo中,代码如下

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

app.use(async (ctx,next) => {
  await next()
  ctx.body = 'Hello World';
});

app.listen(3000);

它的功力是:每收到1个 http 请求,Koa 都会调用通过 app.use()
注册的 async 函数,同时为该函数字传送入 ctxnext
五个参数,最终给页面再次回到二个`Hello World’.

上述代码中,由 async 标记的函数称为『异步函数』,在异步函数中,能够用
await 调用另二个异步函数,asyncawait 那一个至关心爱抚要字将在 ES柒中引入。参数 ctx 是由 koa 传入的,大家能够通过它来访问 request
responsenextkoa 传入的即将处理的下2个异步函数。
这里的 async 函数正是我们所说的中间件,正是因为中间件的扩充性才使得
Koa 的代码不难利落。

下面大家简要介绍一下不翼而飞中间件的多少个参数。

  • ctx : ctx 作为上下文使用,包罗了中央的 ctx.request
    ctx.response。另外,还对 Koa
    内部壹些常用的品质大概措施做了代理操作,使得大家能够直接通过 ctx
    获取。比如,ctx.request.url 能够写成 ctx.url。 —
    除此而外,Koa 还约定了二当中间件的储存空间 ctx.state。通过
    state 能够储存一些数目,比如用户数量,版本新闻等。假使你选拔
    webpack 打包的话,能够采取中间件,将加载财富的秘籍作为 ctx.state
    的习性传入到 view 层,方便获取财富路径。

  • next : next 参数的作用是将处理的控制权转交给下1当中间件,而
    next()
    前面包车型大巴代码,将会在下2在那之中间件及末端的中间件(假设局地话)执行完毕后再进行。

之所以: 中间件的相继很关键!

// 按照官方示例
const Koa = require('koa')
const app = new Koa()

// 记录执行的时间
app.use(async (ctx, next) => {
  let stime = new Date().getTime()
  await next()
  let etime = new Date().getTime()
  ctx.response.type = 'text/html'
  ctx.response.body = '<h1>Hello World</h1>'
  console.log(`请求地址: ${ctx.path},响应时间:${etime - stime}ms`)
});

app.use(async (ctx, next) => {
  console.log('中间件1 doSoming')
  await next();
  console.log('中间件1 end')
})

app.use(async (ctx, next) => {
  console.log('中间件2 doSoming')
  await next();
  console.log('中间件2 end')
})

app.use(async (ctx, next) => {
  console.log('中间件3 doSoming')
  await next();
  console.log('中间件3 end')
})

app.listen(3000, () => {
  console.log('server is running at http://localhost:3000')
})

运作起来后,控制台显示:

server is running at http://localhost:3000

接下来打开浏览器,访问 http://localhost:3000,控制台呈现内容更新为:

server is running at http://localhost:3000
中间件1 doSoming
中间件2 doSoming
中间件3 doSoming
中间件3 end
中间件2 end
中间件1 end
请求地址: /,响应时间:2ms

从结果上得以见见,流程是一百年不遇的开辟,然后壹卓荦超伦的密闭,像是剥洋葱1样
—— 洋葱模型。

其余,就算三其中间件未有调用
await next(),会如何呢?答案是『前边的中间件将不会实施』。
假诺await next()末端未有中间件了,那么也将终结执行。

expresskoa同为一堆人展开开发,与express相比,koa来得特别的神工鬼斧。
因为express是一个大而全的http框架,内置了看似router等等的中间件举行拍卖。
而在koa中,则将看似意义的中间件全部摘了出去,早期koa内部是置于了koa-compose的,而现行反革命也是将其分了出去。
koa只保留叁个简约的中间件的组合,http请求的拍卖,作为一个成效性的中间件框架来存在,自己仅有微量的逻辑。
koa-compose则是作为整合中间件最为重大的一个工具、洋葱模型的实际达成,所以要将二者放在壹块儿来看。

拍卖静态文件是贰个累赘的作业,因为静态文件都是来源于于服务器上,肯定不能够松开全部权力让接口来读取。
种种路子的校验,权限的合作,都以需求思念到的地方。
koa-sendkoa-static即便扶助大家处理那一个繁琐事情的中间件。
koa-sendkoa-static的基础,可以在NPM的界面上看出,staticdependencies中涵盖了koa-send

koa-router的大体结构

koa-router的组织并不是很复杂,也就分了多少个文件:

.
├── layer.js
└── router.ja

 

layer重中之重是针对性有的音讯的包裹,重要路基由router提供:

tag desc
layer 信息存储:路径、METHOD、路径对应的正则匹配、路径中的参数、路径对应的中间件
router 主要逻辑:对外暴露注册路由的函数、提供处理路由的中间件,检查请求的URL并调用对应的layer中的路由处理

二、路由koa-router

路由是用以描述 URL 与处理函数之间的对应关系的。比如用户访问
http://localhost:3000/,那么浏览器就会显得 index
页面包车型地铁内容,纵然用户访问的是
http://localhost:3000/home,那么浏览器应该展现 home 页面包车型地铁始末。

要促成上述功能,借使不借助 koa-router
大概别的路由中间件,而是自身去处理路由,那么写法大概如下所示:

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

app.use(async (ctx, next) => {
    if (ctx.request.path === '/') {
        ctx.response.body = '<h1>index page</h1>';
    } else {
        await next();
    }
});
app.use(async (ctx, next) => {
    if (ctx.request.path === '/home') {
        ctx.response.body = '<h1>home page</h1>';
    } else {
        await next();
    }
});
app.use(async (ctx, next) => {
    if (ctx.request.path === '/404') {
        ctx.response.body = '<h1>404 Not Found</h1>';
    } else {
        await next();
    }
});

app.listen(3000, ()=>{
  console.log('server is running at http://localhost:3000')
})

诸如此类的写法可以处理大约的运用,然则,一旦要拍卖的 URL
多起来的话就会显得越发笨重。所以大家得以借助 koa-router
来更简单的完毕那一功效。
上面来介绍一下哪些科学的选用 koa-router

  • 安装 koa-router

cnpm i koa-router -S
  • 中央采取格局

假诺要在 app1.js 中使用 koa-router 来处理
URL,能够通过以下代码来落到实处:

const Koa = require('koa')
// 注意 require('koa-router') 返回的是函数:
const router = require('koa-router')()
const app = new Koa()

 // 添加路由
 router.get('/', async (ctx, next) => {
    ctx.response.body = `<h1>index page</h1>`
})

router.get('/home', async (ctx, next) => {
    ctx.response.body = '<h1>HOME page</h1>'
})

router.get('/404', async (ctx, next) => {
    ctx.response.body = '<h1>404 Not Found</h1>'
})

 // 调用路由中间件
 app.use(router.routes())

app.listen(3000, ()=>{
  console.log('server is running at http://localhost:3000')
})

因此地方的例证,大家得以见见和事先不使用 koa-router
的来得效果是同一的。可是使用了 koa-router
之后,代码稍微简化了有个别,而且少了 if 判断,还有省略了
await next()(因为尚未别的中间件供给执行,所以那边就先省略了)。

当然,除了 GET 方法,koa-router 也支撑处理别的的请求方法,比如:

//支持这种链式写法
router
  .get('/', async (ctx, next) => {
    ctx.body = 'Hello World!';
  })
  .post('/users', async (ctx, next) => {
    // ... 
  })
  .put('/users/:id', async (ctx, next) => {
    // ... 
  })
  .del('/users/:id', async (ctx, next) => {
    // ... 
  })
  .all('/users/:id', async (ctx, next) => {
    // ... 
  });

上述代码中有贰个all 方法。all
方法用于拍卖上述形式不能够合作的状态,只怕你不鲜明客户端发送的伏乞方法类型。比如有2个GET伸手,优先匹配和router.get方法中url平整平等的伏乞,假设匹配不到的话就协作router.all方法中url规则一样的伸手。
当呼吁都无法儿合作的时候,大家得以跳转到自定义的 404 页面,比如:

//这个放在路由的最后
router.all('/*', async (ctx, next) => {
  ctx.response.status = 404;
  ctx.response.body = '<h1>404 Not Found</h1>';
});

* 号是一种通配符,表示匹配任意
URL。那里的归来是壹种简化的写法,真实开支中,我们必定要去读取 HTML
文件只怕其它模板文件的始末,再响应请求。关于那有的的内容后边的章节中会详细介绍。

koa基本结构

.
├── application.js
├── request.js
├── response.js
└── context.js

 

关于koa整个框架的达成,也只是简单的拆分为了多个文本。

就象在上壹篇笔记中效仿的那样,创立了二个目的用来注册中间件,监听http服务,那个便是application.js在做的作业。
而框架的意义呢,便是在框架内,大家要服从框架的老实来做事情,同样的,框架也会提要求大家有些更易用的点子来让大家成功必要。
针对http.createServer回调的五个参数requestresponse拓展的贰次封装,简化1些常用的操作。
譬如说大家对Header的有些操作,在原生http模块中大概要那样写:

// 获取Content-Type
request.getHeader('Content-Type')

// 设置Content-Type
response.setHeader('Content-Type', 'application/json')
response.setHeader('Content-Length', '18')
// 或者,忽略前边的statusCode,设置多个Header
response.writeHead(200, {
  'Content-Type': 'application/json',
  'Content-Length': '18'
})

 

而在koa中得以如此处理:

// 获取Content-Type
context.request.get('Content-Type')

// 设置Content-Type
context.response.set({
  'Content-Type': 'application/json',
  'Content-Length': '18'
})

 

简化了部分对准requestresponse的操作,将这一个封装在了request.jsresponse.js文件中。
但还要那会带来3个行使上的麻烦,这样封装现在实际取得大概设置header变得层级更深,须求通过context找到requestresponse,然后才能展开操作。
所以,koa使用了node-delegates来进一步简化那几个步骤,将request.getresponse.set清①色代理到context上。
也便是说,代理后的操作是那样子的:

context.get('Content-Type')

// 设置Content-Type
context.set({
  'Content-Type': 'application/json',
  'Content-Length': '18'
})

 

这么就变得很显明了,获取Header,设置Header再也不会担心写成request.setHeader,一呵而就,通过context.js来整合request.jsresponse.js的行为。
同时context.js也会提供部分其余的工具函数,例如Cookie等等的操作。

application引入contextcontext中又结合了requestresponse的法力,多个公文的法力已经很清晰了:

file desc
applicaiton 中间件的管理、http.createServer的回调处理,生成Context作为本次请求的参数,并调用中间件
request 针对http.createServer -> request功能上的封装
response 针对http.createServer -> response功能上的封装
context 整合requestresponse的部分功能,并提供一些额外的功能

而在代码结构上,唯有application对外的koa是利用的Class的方法,其余多少个文件均是抛出1个平淡无奇的Object

4858美高梅 1

koa-router的周转流程

能够拿上面所抛出的为主例子来验证koa-router是怎么的多个实施流程:

const router = new Router() // 实例化一个Router对象

// 注册一个路由的监听
router.get('/', async ctx => {
  ctx.body = 'hi there.'
})

app
  .use(router.routes()) // 将该Router对象的中间件注册到Koa实例上,后续请求的主要处理逻辑
  .use(router.allowedMethods()) // 添加针对OPTIONS的响应处理,一些预检请求会先触发 OPTIONS 然后才是真正的请求

 

其他特色
  • 命名路由:在开发进度中大家能够基于路由名称和参数很便宜的生成路由
    URL

router.get('user', '/users/:id', async (ctx, next)=>{
  // ... 
});

router.url('user', 3);
// => 生成路由 "/users/3" 

router.url('user', { id: 3 });
// => 生成路由 "/users/3" 

router.use(async (ctx, next) {
  // 重定向到路由名称为 “sign-in” 的页面 
  ctx.redirect(ctx.router.url('sign-in'));
})

router.url 方法方便大家在代码中依据路由名称和参数(可选)去变通具体的
URL,而不用利用字符串拼接的格局去生成 URL 了。

  • 多中间件:koa-router
    也支撑单个路由多中间件的处理。通过这么些特点,大家能够为三个路由添加特殊的中间件处理。也得以把3个路由要做的政工拆分成多少个步骤去达成,当路由处理函数中有异步操作时,那种写法的可读性和可维护性更高。比如上边包车型大巴以身作则代码所示:

router.get(
    '/users/:id',
    async (ctx, next) => {
        ctx.body=`<h1>user:${ctx.params.id}</h1>`;
        ctx.user='xiaoming';
        next();
    },
    async (ctx, next) => {
        console.log(ctx.user);
        // 在这个中间件中再对用户信息做一些处理
        // => { id: 17, name: "Alex" }
    }
);
  • 嵌套路由:大家得以在选用中定义多个路由,然后把那些路由组成起来用,那样便于大家管理三个路由,也简化了路由的写法。

const Router=require('koa-router')

const forums = new Router();
const posts = new Router();

posts.get('/', async (ctx, next)=>{
    ctx.body=`fid:${ctx.params.fid}`
});
posts.get('/:pid', async (ctx, next)=>{
    ctx.body=`pid:${ctx.params.pid}`
});
forums.use('/forums/:fid/posts', posts.routes(), posts.allowedMethods());

// 可以匹配到的路由为 "/forums/123/posts" 或者 "/forums/123/posts/123"
app.use(forums.routes());
  • 路由前缀:通过 prefix
    那些参数,大家得以为一组路由添加统1的前缀,和嵌套路由接近,也有益大家管理路由和简化路由的写法。分歧的是,前缀是2个固定的字符串,不能够添加动态参数。

const Router=require('koa-router')
const router = new Router({
  prefix: '/users'
});

router.get('/', ...); // 匹配路由 "/users" 
router.get('/:id', ...); // 匹配路由 "/users/:id" 

相似在更新版本号的时候很有益于。

  • URL 参数:koa-router 也支撑参数,参数会被添加到 ctx.params
    中。参数也得以是1个正则表明式,这一个功能的贯彻是由此
    path-to-regexp 来完结的。原理是把 URL
    字符串转化成正则对象,然后再拓展正则匹配,从前的例子中的 *
    通配符正是1种正则表明式。

router.get('/:category/:title', function (ctx, next) {
  console.log(ctx.params);
  // => { category: 'programming', title: 'how-to-node' } 
});

拿七个完好无缺的流水生产线来解释

koa-send关键是用以更便于的处理静态文件,与koa-router等等的中间件区别的是,它并不是一向作为三个函数注入到app.use中的。
而是在1些中间件中进行调用,传入当前乞求的Context及文件对应的职位,然后完成效益。

始建实例时的局地政工

首先,在koa-router实例化的时候,是能够传递三个陈设项参数作为起初化的配置音讯的。
而是那些布局项在readme中只是简单的被描述为:

Param Type Description
[opts] Object  
[opts.prefix] String prefix router paths(路由的前缀)

告知大家能够加上1个Router登记时的前缀,也等于说若是依据模块化分,能够不要在种种路径匹配的前端都助长巨长的前缀:

const Router = require('koa-router')
const router = new Router({
  prefix: '/my/awesome/prefix'
})

router.get('/index', ctx => { ctx.body = 'pong!' })

// curl /my/awesome/prefix/index => pong!

 

P.S.
可是要牢记,如若prefix/末段,则路由的注册就能够省去前缀的/了,不然会冒出/再也的情事

实例化Router时的代码:

function Router(opts) {
  if (!(this instanceof Router)) {
    return new Router(opts)
  }

  this.opts = opts || {}
  this.methods = this.opts.methods || [
    'HEAD',
    'OPTIONS',
    'GET',
    'PUT',
    'PATCH',
    'POST',
    'DELETE'
  ]

  this.params = {}
  this.stack = []
}

 

看得出的唯有三个methods的赋值,不过在翻看了任何源码后,发现除了prefix还有1部分参数是实例化时传递进入的,可是不太清楚为啥文书档案中未有提到:

Param Type Default Description
sensitive Boolean false 是否严格匹配大小写
strict Boolean false 如果设置为false则匹配路径后边的/是可选的
methods Array[String] ['HEAD','OPTIONS','GET','PUT','PATCH','POST','DELETE'] 设置路由可以支持的METHOD
routerPath String null  

3、解析呼吁参数

当大家捕获到请求后,一般都要求把请求传递过来的数量解析出来。数据传递过来的不二等秘书籍相似有两种:

  • get请求,请求参数为URL路线后边以?千帆竞发的询问参数:如http://localhost:3000/home?id=1&name=hfimy。使用ctx.request.queryctx.request.querystring能够拿走到查询参数。不一致的是query回去的是指标,querystring重返的是字符串。

  router.get('/home', async(ctx, next) => {
    console.log('query:',ctx.request.query)
    console.log('querystring:',ctx.request.querystring)
    ctx.response.body = '<h1>HOME page</h1>'
  })

访问http://localhost:3000/home?id=1&name=hfimy,控制台出口如下

query: {id:'1',name:'hfimy'}
querystring: id=1&name=hfimy
  • get请求,请求参数放在URL途径里面,如http://localhost:3000/home/1/hfimy。那种情景下,koa-router会把请求参数解析在params对象上,通过ctx.params能够获得到这些目的。

  router.get('/home/:id/:name', async(ctx, next) => {
    console.log(ctx.params)
    ctx.response.body = '<h1>HOME page</h1>'
  })

访问http://localhost:3000/home/1/hfimy,控制台出口如下

{id:'1',name:'hfimy'}
  • post请求,请求参数放在body里面。当用 post
    形式请求时,大家会遇到二个标题:post 请求常常都会透过表单或 JSON
    情势发送,而任由 Node 还是 Koa,都 从未有过提供 解析 post
    请求参数的功效。那里,大家将引入多少个koa-bodyparser包,安装到位以往,我们须求在
    app.js 中引入中间件并动用:

  const Koa = require('koa')
  const router = require('koa-router')()
  const bodyParser = require('koa-bodyparser')
  const app = new Koa()

  app.use(bodyParser())

不管是通过表单提交依然以JSON花样发送,我们都足以因此ctx.request.body取获得提交的数量。

开创服务

首先,大家供给创制叁个http服务,在koa2.x中开创服务与koa1.x有点有个别差别,供给使用实例化的章程来举行创办:

const app = new Koa()

 

而在实例化的历程中,其实koa只做了少数的政工,创建了多少个实例属性。
将引入的contextrequest以及response通过Object.create拷贝的主意放置实例中。

this.middleware = [] // 最关键的一个实例属性

// 用于在收到请求后创建上下文使用
this.context = Object.create(context)
this.request = Object.create(request)
this.response = Object.create(response)

 

在实例化完结后,我们就要开始展览注册中间件来落到实处我们的作业逻辑了,上面也波及了,koa仅作为二个中间件的咬合以及呼吁的监听。
故此不会像express那样提供router.getrouter.post等等的操作,仅仅存在八个相比较像样http.createServeruse()
接下去的手续就是挂号中间件并监听3个端口号运维服务:

const port = 8000

app.use(async (ctx, next) => {
  console.time('request')
  await next()
  console.timeEnd('request')
})
app.use(async (ctx, next) => {
  await next()
  ctx.body = ctx.body.toUpperCase()
})

app.use(ctx => {
  ctx.body = 'Hello World'
})

app.use(ctx => {
  console.log('never output')
})

app.listen(port, () => console.log(`Server run as http://127.0.0.1:${port}`))

 

在翻看application.js的源码时,能够看到,暴光给外部的点子,常用的大约正是uselisten
三个用来加载中间件,另1个用来监听端口并运维服务。

而那八个函数实际上并不曾过多的逻辑,在use中仅仅是判定了传播的参数是不是为3个function,以及在贰.x版本针对Generator函数的有的奇异处理,将其转移为了Promise情势的函数,并将其push到构造函数中创设的middleware数组中。
其一是从1.x过渡到2.x的一个工具,在3.x本子将直接移除Generator的支持。
其实在koa-convert其间也是引用了cokoa-compose来拓展中间转播,所以也就不再赘述。

而在listen中做的工作就更简便易行了,只是简单的调用http.createServer来成立服务,并监听对应的端口之类的操作。
有二个细节在于,createServer中流传的是koa实例的另二个措施调用后的重回值callback,这几个方法才是实在的回调解和处理理,listen只是http模块的2个快速格局。
其一是为了局地用socket.iohttps要么局地别的的http模块来实行利用的。
也就表示,只就算足以提供与http模块一致的表现,koa都得以很有益于的联网。

listen(...args) {
  debug('listen')
  const server = http.createServer(this.callback())
  return server.listen(...args)
}

 

koa-send的GitHub地址

sensitive

假使设置了sensitive,则会以更严谨的合营规则来监听路由,不会忽视U卡宴L中的大小写,完全遵照注册时的来合作:

const Router = require('koa-router')
const router = new Router({
  sensitive: true
})

router.get('/index', ctx => { ctx.body = 'pong!' })

// curl /index => pong!
// curl /Index => 404

 

4、钦点静态能源目录

此间介绍贰个在koa中提供静态财富访问的第二方中间件:koa-static,用法与express中的express-static基本①致,钦点静态文件目录即可。1般在app.js同级目录下创办2个public目录,用来存放在静态文件。

...
const static = require('koa-static')
...
//注意,提供静态资源访问的中间件需要放在路由中间件的前面使用
app.use(static(path.resolve(__dirname, "./public")))

接纳koa-compose合并中间件

所以大家就来看望callback的实现:

callback() {
  const fn = compose(this.middleware)

  if (!this.listenerCount('error')) this.on('error', this.onerror)

  const handleRequest = (req, res) => {
    const ctx = this.createContext(req, res)
    return this.handleRequest(ctx, fn)
  }

  return handleRequest
}

 

在函数内部的第一步,正是要拍卖中间件,将四个数组中的中间件转换为我们想要的洋葱模型格式的。
此间就用到了相比较基本的koa-compose

实在它的成效上与co恍如,只可是把co处理Generator函数那部分逻辑全体去掉了,自个儿co的代码也正是壹两百行,所以精简后的koa-compose代码仅有4捌行。

我们驾驭,async函数实际上剥开它的语法糖现在是长那些样子的:

async function func () {
  return 123
}

// ==>

function func () {
  return Promise.resolve(123)
}
// or
function func () {
  return new Promise(resolve => resolve(123))
}

 

故而拿上述use的代码举例,实际上koa-composekoa源码阅读,koa2入门笔记。获得的是如此的参数:

[
  function (ctx, next) {
    return new Promise(resolve => {
      console.time('request')
      next().then(() => {
        console.timeEnd('request')
        resolve()
      })
    })
  },
  function (ctx, next) {
    return new Promise(resolve => {
      next().then(() => {
        ctx.body = ctx.body.toUpperCase()
        resolve()
      })
    })
  },
  function (ctx, next) {
    return new Promise(resolve => {
      ctx.body = 'Hello World'
      resolve()
    })
  },
  function (ctx, next) {
    return new Promise(resolve => {
      console.log('never output')
      resolve()
    })
  }
]

 

就像是在第多个函数中输出表示的那么,第多在那之中间件不会被实践,因为第四个中间件并从未调用next,所以完结类似那样的三个洋葱模型是很有趣的一件业务。
率先抛开不变的ctx不谈,洋葱模型的落到实处中央在于next的处理。
因为next是你进去下一层中间件的钥匙,唯有手动触发未来才会跻身下1层中间件。
下一场大家还须求确认保障next要在中间件执行完成后进行resolve,重回到上1层中间件:

return function (context, next) {
  // last called middleware #
  let index = -1
  return dispatch(0)
  function dispatch (i) {
    if (i <= index) return Promise.reject(new Error('next() called multiple times'))
    index = i
    let fn = middleware[i]
    if (i === middleware.length) fn = next
    if (!fn) return Promise.resolve()
    try {
      return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
    } catch (err) {
      return Promise.reject(err)
    }
  }
}

 

因而肯定了这两点以后,上面的代码就会变得很清晰:

  1. next用来进入下1在那之中间件
  2. next在近日中间件执行到位后会触发回调通告上二个中间件,而到位的前提是里面包车型客车中间件已经施行到位(resolved)

能够看出在调用koa-compose以往实际会回到三个自进行函数。
在实行函数的起来部分,判断当前中间件的下标来防患在1当中间件中屡屡调用next
因为一旦频仍调用next,就会招致下一在那之中间件的累累推行,那样就破坏了洋葱模型。

其次便是compose骨子里提供了二个在洋葱模型全体推行达成后的回调,八个可选的参数,实际上成效与调用compose后边的then拍卖未有太大分别。

以及上边提到的,next是跻身下2在那之中间件的钥匙,能够在那一个柯里化函数的使用上看出来:

Promise.resolve(fn(context, dispatch.bind(null, i + 1)))

 

将本身绑定了index参数后传出本次中间件,作为调用函数的第二个参数,也正是next,效果就像调用了dispatch(1),那样正是二个洋葱模型的贯彻。
fn的调用借使是一个async function,那么外层的Promise.resolve会等到中间的async执行resolve而后才会触发resolve,例如那样:

Promise.resolve(new Promise(resolve => setTimeout(resolve, 500))).then(console.log) // 500ms以后才会触发 console.log

 

P.S.
一个从koa1.x切换到koa2.x的暗坑,co会对数组举行分外处理,使用Promise.all展开包装,可是koa2.x从没这么的操作。
由此1旦在中间件中要针对性3个数组实行异步操作,一定要手动添加Promise.all,可能说等草案中的await*

// koa1.x
yield [Promise.resolve(1), Promise.resolve(2)]              // [1, 2]

// koa2.x
await [Promise.resolve(1), Promise.resolve(2)]              // [<Promise>, <Promise>]

// ==>
await Promise.all([Promise.resolve(1), Promise.resolve(2)]) // [1, 2]
await* [Promise.resolve(1), Promise.resolve(2)]             // [1, 2]

 

原生的公文读取、传输形式

Node中,假设应用原生的fs模块进行文件数量传输,大约是那样的操作:

const fs      = require('fs')
const Koa     = require('koa')
const Router  = require('koa-router')

const app     = new Koa()
const router  = new Router()
const file    = './test.log'
const port    = 12306

router.get('/log', ctx => {
  const data = fs.readFileSync(file).toString()
  ctx.body = data
})

app.use(router.routes())
app.listen(port, () => console.log(`Server run as http://127.0.0.1:${port}`))

 

或者用createReadStream代替readFileSync也是卓有作用的,分歧会在底下关系

以此不难的言传身教仅针对贰个文本进行操作,而只要大家要读取的文件是有很多个,甚至于或许是经过接口参数字传送递过来的。
因而很难保障这些文件一定是真心真意存在的,而且我们也许还索要加上一些权力设置,防止部分灵动文件被接口再次回到。

router.get('/file', ctx => {
  const { fileName } = ctx.query
  const path = path.resolve('./XXX', fileName)
  // 过滤隐藏文件
  if (path.startsWith('.')) {
    ctx.status = 404
    return
  }

  // 判断文件是否存在
  if (!fs.existsSync(path)) {
    ctx.status = 404
    return
  }

  // balabala

  const rs = fs.createReadStream(path)
  ctx.body = rs // koa做了针对stream类型的处理,详情可以看之前的koa篇
})

 

添加了种种逻辑判断现在,读取静态文件就变得安全不少,然则这也只是在四个router中做的处理。
假诺有两个接口都会进展静态文件的读取,势必会存在大气的重复逻辑,所以将其提炼为一个共用函数将是一个很好的选项。

strict

strictsensitive效益类似,也是用来设置让路径的万分变得尤其残忍,在暗中同意情状下,路径结尾处的/是可选的,若是打开该参数现在,要是在注册路由时尾部未有添加/,则格外的路由也决然不可见添加/结尾:

const Router = require('koa-router')
const router = new Router({
  strict: true
})

router.get('/index', ctx => { ctx.body = 'pong!' })

// curl /index  => pong!
// curl /Index  => pong!
// curl /index/ => 404

 

伍、常用中间件

收下请求,处理重临值

因此上面的代码,三个koa劳动已经算是运行起来了,接下去正是访问看效能了。
在吸收到二个呼吁后,koa会拿从前提到的contextrequestresponse来创建此番请求所运用的上下文。
koa1.x中,上下文是绑定在this上的,而在koa2.x是作为第二个参数字传送入进来的。
村办估摸恐怕是因为Generator不可能运用箭头函数,而async函数能够动用箭头函数导致的吧:) 纯属个人YY

一言以蔽之,大家通过上边提到的多个模块创制了多个呼吁所需的上下文,基本上是一通儿赋值,代码就不贴了,未有太多逻辑,就是有2个小细节相比有趣:

request.response = response
response.request = request

 

让两者之间产生了三个引用关系,既能够通过request获取到response,也得以透过response获取到request
并且那是1个递归的引用,类似那样的操作:

let obj = {}

obj.obj = obj

obj.obj.obj.obj === obj // true

 

并且如上文提到的,在context创立的长河中,将一大批判的requestresponse的性子、方法代理到了自小编,有趣味的可以团结翻看源码(望着有点晕):koa.js
|
context.js
这个delegate的兑现也究竟相比简单,通过取出原始的属性,然后存贰个引用,在自笔者的质量被触发时调用对应的引用,类似四个民间版的Proxy呢,期待后续能够采纳Proxy代替它。

下一场大家会将生成好的context用作参数字传送入koa-compose转变的洋葱中去。
因为不论是何种景况,洋葱肯定会重返结果的(出错与否),所以大家还亟需在最后有一个finished的拍卖,做一些近乎将ctx.body转换为多少开始展览输出之类的操作。

koa运用了大气的getset访问器来贯彻效益,例如最常用的ctx.body = 'XXX',它是来自responseset body
那应该是requestresponse中逻辑最复杂的三个主意了。
中间要拍卖很多东西,例如在body剧情为空时支持您改改请求的status code为20肆,并移除无用的headers
以及1旦未有手动钦定status code,会暗许内定为200
居然还会根据当前传遍的参数来判断content-type应该是html大概普通的text

// string
if ('string' == typeof val) {
  if (setType) this.type = /^\s*</.test(val) ? 'html' : 'text'
  this.length = Buffer.byteLength(val)
  return
}

 

以及还蕴藏针对流(Stream)的尤其处理,例如假诺要用koa完结静态财富下载的功用,也是足以一贯调用ctx.body展开赋值的,全体的东西都曾经在response.js中帮您处理好了:

// stream
if ('function' == typeof val.pipe) {
  onFinish(this.res, destroy.bind(null, val))
  ensureErrorHandler(val, err => this.ctx.onerror(err))

  // overwriting
  if (null != original && original != val) this.remove('Content-Length')

  if (setType) this.type = 'bin'
  return
}

// 可以理解为是这样的代码
let stream = fs.createReadStream('package.json')
ctx.body = stream

// set body中的处理
onFinish(res, () => {
  destory(stream)
})

stream.pipe(res) // 使response接收流是在洋葱模型完全执行完以后再进行的

 

onFinish用来监听流是不是得了、destory用来关闭流

别的的访问器基本上正是一些广阔操作的卷入,例如针对querystring的封装。
在选用原生http模块的景况下,处理UQashqaiL中的参数,是亟需本人引入额外的包进行拍卖的,最广泛的是querystring
koa也是在在那之中引入的该模块。
从而对外抛出的query大概是其一样子的:

get query() {
  let query = parse(this.req).query
  return qs.parse(query)
}

// use
let { id, name } = ctx.query // 因为 get query也被代理到了context上,所以可以直接引用

 

parse为parseurl库,用来从request中提出query参数

亦大概针对cookies的卷入,也是松手了最流行的cookies
在率先次接触get cookies时才去实例化Cookie目标,将那些麻烦的操作挡在用户看不到的地点:

get cookies() {
  if (!this[COOKIES]) {
    this[COOKIES] = new Cookies(this.req, this.res, {
      keys: this.app.keys,
      secure: this.request.secure
    })
  }
  return this[COOKIES]
}

set cookies(_cookies) {
  this[COOKIES] = _cookies
}

 

所以在koa中使用Cookie就如那样就足以了:

this.cookies.get('uid')

this.cookies.set('name', 'Niko')

// 如果不想用cookies模块,完全可以自己赋值为自己想用的cookie
this.cookies = CustomeCookie

this.cookies.mget(['uid', 'name'])

 

那是因为在get cookies中间有咬定,假若未有二个可用的库克ie实例,才会私下认可去实例化。

koa-send的方式

这就是koa-send做的事体了,提供了八个装进卓殊周全的拍卖静态文件的中间件。
那里是八个最基础的运用例子:

const path = require('path')
const send = require('koa-send')

// 针对某个路径下的文件获取
router.get('/file', async ctx => {
  await send(ctx, ctx.query.path, {
    root: path.resolve(__dirname, './public')
  })
})

// 针对某个文件的获取
router.get('/index', async ctx => {
  await send(ctx, './public/index.log')
})

 

一经我们的目录结构是那般的,simple-send.js为实践文书:

.
├── public
│   ├── a.log
│   ├── b.log
│   └── index.log
└── simple-send.js

 

使用/file?path=XXX就足以很轻易的拜会到public下的文书。
以及走访/index就能够得到/public/index.log文件的剧情。

methods

methods计划项存在的意思在于,要是大家有三个接口须求同时协理GETPOSTrouter.getrouter.post那般的写法必然是丑陋的。
所以大家兴许会想到利用router.all来简化操作:

const Router = require('koa-router')
const router = new Router()

router.all('/ping', ctx => { ctx.body = 'pong!' })

// curl -X GET  /index  => pong!
// curl -X POST /index  => pong!

 

那简直是太完善了,能够很自在的兑现大家的要求,可是如若再多实验一些任何的methods随后,狼狈的工作就时有爆发了:

> curl -X DELETE /index  => pong!
> curl -X PUT    /index  => pong!

 

那鲜明不是切合我们预料的结果,所以,在那种意况下,基于近日koa-router亟需开始展览如下修改来贯彻咱们想要的功力:

const Koa = require('koa')
const Router = require('router')

const app = new Koa()
// 修改处1
const methods = ['GET', 'POST']
const router = new Router({
  methods
})

// 修改处2
router.all('/', async (ctx, next) => {
  // 理想情况下,这些判断应该交由中间件来完成
  if (!~methods.indexOf(ctx.method)) {
    return await next()
  }

  ctx.body = 'pong!'
})

 

那般的两处改动,就能够完毕大家所愿意的效应:

> curl -X GET    /index  => pong!
> curl -X POST   /index  => pong!
> curl -X DELETE /index  => Not Implemented
> curl -X PUT    /index  => Not Implemented

 

自个儿个人觉得那是allowedMethods兑现的一个逻辑难点,可是恐怕是本人未有get到作者的点,allowedMethods中比较紧要的片段源码:

Router.prototype.allowedMethods = function (options) {
  options = options || {}
  let implemented = this.methods

  return function allowedMethods(ctx, next) {
    return next().then(function() {
      let allowed = {}

      // 如果进行了ctx.body赋值,必然不会执行后续的逻辑
      // 所以就需要我们自己在中间件中进行判断
      if (!ctx.status || ctx.status === 404) {
        if (!~implemented.indexOf(ctx.method)) {
          if (options.throw) {
            let notImplementedThrowable
            if (typeof options.notImplemented === 'function') {
              notImplementedThrowable = options.notImplemented() // set whatever the user returns from their function
            } else {
              notImplementedThrowable = new HttpError.NotImplemented()
            }
            throw notImplementedThrowable
          } else {
            ctx.status = 501
            ctx.set('Allow', allowedArr.join(', '))
          }
        } else if (allowedArr.length) {
          // ...
        }
      }
    })
  }
}

 

首先,allowedMethods是当做三个前置的中间件存在的,因为在回到的函数中先调用了next,其次才是本着METHOD的判断,而那样推动的叁个后果就是,固然大家在路由的回调中展开类似ctx.body = XXX的操作,实际上会修改本次请求的status值的,使之并不会化为404,而一筹莫展正确的触及METHOD检查的逻辑。
想要正确的触发METHOD逻辑,就须要团结在路由监听中手动判断ctx.method是或不是为我们想要的,然后在跳过当前中间件的推行。
而那1判定的步子实际上与allowedMethods中间件中的!~implemented.indexOf(ctx.method)逻辑完全是再次的,不太明了koa-router怎么会这么处理。

当然,allowedMethods是不可见作为二个放到中间件来存在的,因为三个Koa中只怕会挂在五个RouterRouter期间的安顿也许有差别,不可能确认保证全体的Router都和当下Router可处理的METHOD是同等的。
之所以,个人感觉methods参数的存在意义并不是十分大。。

①. 赶回json格式的数码

假定急需响应再次回到json数据,大家只必要安装响应数据类型为json格式,并把json数据挂载在响应体body上即可兑现再次来到json数据。

ctx.set("Content-Type", "application/json")
ctx.body = JSON.stringify(jsonData)

唯独如此每一回回来响应都要求写重复的代码,大家再一次引入1个koa-json中间件,它会活动将大家重返的多少转换为json格式。

const Koa = require('koa');
const json = require('koa-json');
const app = new Koa();

app.use(json());

app.use((ctx) => {
  ctx.body = { name: 'hfimy',age:23 };
});

$ GET /

{
  "name": "ht",
  "age": 23
}

洋葱模型执行到位后的壹些操作

koa的二个呼吁流程是那般的,先实施洋葱里边的兼具中间件,在实行到位未来,还会有一个回调函数。
该回调用来遵照中间件执行进度中所做的作业来控制回来给客户端什么数据。
拿到ctx.bodyctx.status那一个参数实行拍卖。
席卷前面提到的流(Stream)的拍卖都在此间:

if (body instanceof Stream) return body.pipe(res) // 等到这里结束后才会调用我们上边`set body`中对应的`onFinish`的处理

 

并且下面还有二个非同小可的处理,要是为false则不做其余处理,直接回到:

if (!ctx.writable) return

 

事实上这些也是response提供的3个访问器,这里边用来判断当前恳请是不是早已调用过end给客户端再次来到了数码,假使已经接触了response.end()以后,则response.finished会被置为true,相当于说,本次请求已经甘休了,同时访问器中还处理了多少个bug,请求已经再次来到结果了,可是依然未有关闭套接字:

get writable() {
  // can't write any more after response finished
  if (this.res.finished) return false

  const socket = this.res.socket
  // There are already pending outgoing res, but still writable
  // https://github.com/nodejs/node/blob/v4.4.7/lib/_http_server.js#L486
  if (!socket) return true
  return socket.writable
}

 

此地就有二个koaexpress对待的劣势了,因为koa运用的是二个洋葱模型,对于再次来到值,假如是选取ctx.body = 'XXX'来拓展赋值,那会造成最后调用response.end时在洋葱全体实施到位后再展开的,约等于下面所描述的回调中,而express哪怕在中间件中就能够Infiniti制支配曾几何时归来数据:

// express.js
router.get('/', function (req, res) {
  res.send('hello world')

  // 在发送数据后做一些其他处理
  appendLog()
})

// koa.js
app.use(ctx => {
  ctx.body = 'hello world'

  // 然而依然发生在发送数据之前
  appendLog()
})

4858美高梅, 

不过辛亏照旧足以经过一贯调用原生的response指标来进展发送数据的,当大家手动调用了response.end以后(response.finished === true),就代表最后的回调会一直跳过,不做任何处理。

app.use(ctx => {
  ctx.res.end('hello world')

  // 在发送数据后做一些其他处理
  appendLog()
})

 

异常处理

koa的全方位请求,实际上依然叁个Promise,所以在洋葱模型后面包车型地铁监听不仅仅有resolve,对reject也同样是有处理的。
期间任何壹环出bug都会招致持续的中间件以及前面等待回调的中间件终止,直接跳转到近年来的一个尤其处理模块。
所以,若是有近似接口耗费时间总结的中间件,一定要记得在try-catch中执行next的操作:

app.use(async (ctx, next) => {
  try {
    await next()
  } catch (e) {
    console.error(e)
    ctx.body = 'error' // 因为内部的中间件并没有catch 捕获异常,所以抛出到了这里
  }
})

app.use(async (ctx, next) => {
  let startTime = new Date()
  try {
    await next()
  } finally {
    let endTime = new Date() // 抛出异常,但是不影响这里的正常输出
  }
})

app.use(ctx => Promise.reject(new Error('test')))

 

P.S. 假设不行被抓走,则会继续执行后续的response

app.use(async (ctx, next) => {
  try {
    throw new Error('test')
  } catch (e) {
    await next()
  }
})

app.use(ctx => {
  ctx.body = 'hello'
})

// curl 127.0.0.1 
// > hello

 

假使协调的中间件未有捕获分外,就会走到暗中同意的格外处理模块中。
在暗中认可的突出模块中,基本上是对准statusCode的有个别甩卖,以及一些私下认可的一无所长展现:

const code = statuses[err.status]
const msg = err.expose ? err.message : code
this.status = err.status
this.length = Buffer.byteLength(msg)
this.res.end(msg)

 

statuses是多个第3方模块,包含各样http
code的音信: statuses

提出在最外层的中间件都要好做足够处理,因为暗中认可的失实提醒有些太丢人了(纯文本),本身处理跳转到至极处理页面会好有的,以及幸免有个别接口因为暗中认可的1贰分音讯导致解析战败。

koa-send提供的效果

koa-send提供了众多造福的选项,除去常用的root以外,还有大约小拾个的选项可供使用:

options type default desc
maxage Number 0 设置浏览器可以缓存的毫秒数
对应的HeaderCache-Control: max-age=XXX
immutable Boolean false 通知浏览器该URL对应的资源不可变,可以无限期的缓存
对应的HeaderCache-Control: max-age=XXX, immutable
hidden Boolean false 是否支持隐藏文件的读取
.开头的文件被称为隐藏文件
root String 设置静态文件路径的根目录,任何该目录之外的文件都是禁止访问的。
index String 设置一个默认的文件名,在访问目录的时候生效,会自动拼接到路径后边 (此处有一个小彩蛋)
gzip Boolean true 如果访问接口的客户端支持gzip,并且存在.gz后缀的同名文件的情况下会传递.gz文件
brotli Boolean true 逻辑同上,如果支持brotli且存在.br后缀的同名文件
format Boolean true 开启以后不会强要求路径结尾的//path/path/表示的是一个路径 (仅在path是一个目录的情况下生效)
extensions Array false 如果传递了一个数组,会尝试将数组中的所有item作为文件的后缀进行匹配,匹配到哪个就读取哪个文件
setHeaders Function 用来手动指定一些Headers,意义不大

routerPath

这些参数的留存。。感觉会招致有些很新奇的处境。
那即将聊起在登记完全中学间件以往的router.routes()的操作了:

Router.prototype.routes = Router.prototype.middleware = function () {
  let router = this
  let dispatch = function dispatch(ctx, next) {
    let path = router.opts.routerPath || ctx.routerPath || ctx.path
    let matched = router.match(path, ctx.method)
    // 如果匹配到则执行对应的中间件
    // 执行后续操作
  }
  return dispatch
}

 

因为大家其实向koa注册的是如此的1在那之中间件,在每便请求发送过来时,都会实施dispatch,而在dispatch中判断是不是命中某些router时,则会用到这么些布局项,那样的七个表达式:router.opts.routerPath || ctx.routerPath || ctx.pathrouter表示当前Router实例,约等于说,假设我们在实例化一个Router的时候,要是填写了routerPath,那会导致无论任何请求,都会先行利用routerPath来作为路由检查:

const router = new Router({
  routerPath: '/index'
})

router.all('/index', async (ctx, next) => {
  ctx.body = 'pong!'
})
app.use(router.routes())

app.listen(8888, _ => console.log('server run as http://127.0.0.1:8888'))

 

万一有诸如此类的代码,无论请求什么UPAJEROL,都会认为是/index来进展匹配:

> curl http://127.0.0.1:8888
pong!
> curl http://127.0.0.1:8888/index
pong!
> curl http://127.0.0.1:8888/whatever/path
pong!

 

2. 记录日志

log4js 是 Node.js 中二个成熟的记录日志的第贰方模块。

  • 日记分类
    :日志能够大概上分为访问日志和使用日志。访问日志一般记录客户端对品种的访问,首要是
    http
    请求。这一个多少属于运行数量,也足以反过来协助改进和升级换代网址的天性和用户体验;应用日志是项目中须要特殊标记和笔录的岗位打字与印刷的日志,包涵出现至极的情景,方便开发职员查询项目标周转景况和稳定
    bug 。应用日志包蕴了debuginfowarn
    error等级别的日记。

  • 日志等级:log4js 中的日志输出可分为如下捌个级次:

    ALL、TRACE、DEBUG、INFO、WARN、ERROR、FATAL、MARK、OFF

    ALL:输出全数的日记

    OFF:全体日志都不出口

    其余:输出级别相等大概高档其他日志。

在动用中根据级别记录了日志之后,能够服从钦赐级别输出超过钦定级其他日志。

log4js 官方简单示例

const log4js = require('log4js');
const logger = log4js.getLogger();
logger.level = 'debug';
logger.debug("Some debug messages");

运营该代码,能够在巅峰看到如下输出:

[2017-12-24T16:45:45.101] [DEBUG] default - Some debug messages

1段带有日期、时间、日志级别和调用 debug
方法时传出的字符串的文书日志。达成了简易的顶峰日志输出。

log4js 官方复杂示例

const log4js = require('log4js');
log4js.configure({
  appenders: { cheese: { type: 'file', filename: 'cheese.log' } },
  categories: { default: { appenders: ['cheese'], level: 'error' } }
});
const logger = log4js.getLogger('cheese');
logger.trace('Entering cheese testing');
logger.debug('Got cheese.');
logger.info('Cheese is Gouda.');
logger.warn('Cheese is quite smelly.');
logger.error('Cheese is too ripe!');
logger.fatal('Cheese was breeding ground for listeria.');

重国民党的新生活运动行,在当前目录下会变卦1个日记文件cheese.log文本,文件中有两条日志并记下了error及以上级别的新闻,如下

[2017-12-24T17:03:42.761] [ERROR] cheese - Cheese is too ripe!
[2017-12-24T17:03:42.761] [FATAL] cheese - Cheese was breeding ground for listeria.

我们能够透过自定义达成日志中间件,把logger对象挂载到ctx左右文中,从而在利用的别样地点都能够出口日志。

日记切割:当大家的类型在线上环境稳定运转后,访问量会越来越大,日志文件也会愈来愈大。日益增大的文本对查看和跟踪难点带来了诸多不便,同时叠加了服务器的下压力。即使能够遵从项目将日志分为多少个公文,但并不会有太大的改进。所以我们根据日期将日志文件进行剪切。比如:后天将日志输出到
task-2017-1贰-2肆.log 文件,后天会输出到 task-2017-12-25.log
文件。减小单个文件的轻重缓急不仅有益于开发人员根据日期排查难题,还有利于对日记文件进行搬迁。由此,我们修改日志类型为日期文件,遵照日期切割日志输出,以减小单个日志文件的深浅。如下,修改代码:

log4js.configure({
    appenders: {
        cheese: {
            type: 'dateFile', // 日志类型 
            filename: `log/task`,  // 输出的文件名
            pattern: '-yyyy-MM-dd.log',  // 文件名增加后缀
            alwaysIncludePattern: true   // 是否总是有后缀名
        }
    },
    categories: { default: { appenders: ['cheese'], level: 'error' } }
});

如此,在当前目录下会变卦多少个log目录,并转移一个task-2017-12-24.log日志文件。

除了log4js以外,还有万分不难的koa-logger日志中间件,直接在控制莱比锡输出

const logger = require('koa-logger')
const Koa = require('koa')

const app = new Koa()
app.use(logger())

redirect的注意事项

在原生http模块中开始展览302的操作(俗称重定向),须求这么做:

response.writeHead(302, {
  'Location': 'redirect.html'
})
response.end()
// or
response.statusCode = 302
response.setHeader('Location', 'redirect.html')
response.end()

 

而在koa中也有redirect的包裹,能够通过直接调用redirect函数来形成重定向,可是急需专注的是,调用完redirect随后并从未平昔触及response.end(),它仅仅是添加了二个statusCodeLocation而已:

redirect(url, alt) {
  // location
  if ('back' == url) url = this.ctx.get('Referrer') || alt || '/'
  this.set('Location', url)

  // status
  if (!statuses.redirect[this.status]) this.status = 302

  // html
  if (this.ctx.accepts('html')) {
    url = escape(url)
    this.type = 'text/html charset=utf-8'
    this.body = `Redirecting to <a href="${url}">${url}</a>.`
    return
  }

  // text
  this.type = 'text/plain charset=utf-8'
  this.body = `Redirecting to ${url}.`
}

 

一而再的代码还会继续执行,所以建议在redirect自此手动甘休近日的请求,也正是直接return,不然很有相当的大概率延续的statusbody赋值一点都不小概会招致某个古怪的难题。

app.use(ctx => {
  ctx.redirect('https://baidu.com')

  // 建议直接return

  // 后续的代码还在执行
  ctx.body = 'hello world'
  ctx.status = 200 // statusCode的改变导致redirect失效 
})

 

参数们的具体表现

有个别参数的搭配足以完成部分神奇的作用,有部分参数会潜移默化到Header,也有一对参数是用来优化质量的,类似gzipbrotli的选项。

koa-send的首要逻辑可以分成这几块:

  1. path途径有效性的反省
  2. gzip等压缩逻辑的使用
  3. 文本后缀、暗许入口文件的协作
  4. 读取文件数量

在函数的伊始部分有这么的逻辑:

const resolvePath = require('resolve-path')
const {
  parse
} = require('path')

async function send (ctx, path. opts = {}) {
  const trailingSlash = path[path.length - 1] === '/'
  const index = opts.index

  // 此处省略各种参数的初始值设置

  path = path.substr(parse(path).root.length)

  // ...

  // normalize path
  path = decode(path) // 内部调用的是`decodeURIComponent`
  // 也就是说传入一个转义的路径也是可以正常使用的

  if (index && trailingSlash) path += index

  path = resolvePath(root, path)

  // hidden file support, ignore
  if (!hidden && isHidden(root, path)) return
}

function isHidden (root, path) {
  path = path.substr(root.length).split(sep)
  for (let i = 0; i < path.length; i++) {
    if (path[i][0] === '.') return true
  }
  return false
}

 

巧用router帕特h达成转载作用

相同的,那些短路运算符一共有四个表明式,第三个的ctx则是方今恳求的上下文,约等于说,要是大家有三个早于routes施行的中间件,也得以开始展览赋值来修改路由判断所运用的URL

const router = new Router()

router.all('/index', async (ctx, next) => {
  ctx.body = 'pong!'
})

app.use((ctx, next) => {
  ctx.routerPath = '/index' // 手动改变routerPath
  next()
})
app.use(router.routes())

app.listen(8888, _ => console.log('server run as http://127.0.0.1:8888'))

 

诸如此类的代码也能够落实均等的作用。
实例化中流传的routerPath令人捉摸不透,但是在中间件中改变routerPath的这些依旧得以找到适合的现象,这一个能够简单的驾驭为转载的一种落成,转载的进程是对客户端不可知的,在客户端看来照旧访问的是初期的UEscortL,不过在中间件中改变ctx.routerPath能够很随意的使路由万分到大家想转载的地点去

// 老版本的登录逻辑处理
router.post('/login', ctx => {
  ctx.body = 'old login logic!'
})

// 新版本的登录处理逻辑
router.post('/login-v2', ctx => {
  ctx.body = 'new login logic!'
})

app.use((ctx, next) => {
  if (ctx.path === '/login') { // 匹配到旧版请求,转发到新版
    ctx.routerPath = '/login-v2' // 手动改变routerPath
  }
  next()
})
app.use(router.routes())

 

那般就落到实处了多少个简单易行的转化:

> curl -X POST http://127.0.0.1:8888/login
new login logic!

 

三. 错误处理

错误处理是利用健壮性子外关键的一片段。koa 里面提供了 error
事件,当产生错误时,能够由此监听该事件,对错误进行联合的拍卖。

const koa = require('koa');
const app = koa();
//当发生错误的时候可以将错误信息写入日志
app.on('error', (err,ctx)=>{
    if (process.env.NODE_ENV != 'test') {
        console.log(err.message);
        console.log(err);
        //ctx.logger.error(err)
    }
});   

那里我们引入3个koa-onerror中间件,优化错误处理音讯。

const fs = require('fs');
const koa = require('koa');
const onerror = require('koa-onerror');

const app = new koa();

onerror(app);

app.use(ctx => {
  // foo();
  ctx.body = fs.createReadStream('not exist');
});

小记

koa是贰个很有趣的框架,在阅读源码的进度中,其实也发现了有个别小标题:

  1. 多少人搭档保证一份代码,确实能够看到各人都有分化的编码风格,例如typeof val !== 'string''number' == typeof code,很鲜明的二种风格。233三
  2. delegate的调用格局在质量尤其多的时候并不是很为难,一大长串的链式调用,假若换来循环会更雅观一下

但是,koa照旧是一个很棒的框架,很吻合阅读源码来展开学习,这个都以局部小细节,无伤大雅。

总括一下koakoa-compose的作用:

  • koa 注册中间件、注册http服务、生成请求上下文调用中间件、处理中间件对上下文对象的操作、再次来到数据停止请求
  • koa-compose 将数组中的中间件集合转换为串行调用,并提供钥匙(next)用来跳转下1在那之中间件,以及监听next获得内部中间件执行完成的布告

路线检查

率先是判定传入的path是还是不是为一个索引,(结尾为/会被认为是1个索引)
设倘诺目录,并且设有多个得力的index参数,则会将index拼接到path后边。
也正是大致那样的操作:

send(ctx, './public/', {
  index: 'index.js'
})

// ./public/index.js

 

resolve-path 是一个用来拍卖途径的包,用来帮衬过滤壹些13分的路径,类似path//file/etc/XXX 那样的恶意路径,并且会再次回到处理后相对路径。

isHidden用来判定是还是不是必要过滤隐藏文件。
因为但凡是.始于的公文都会被认为隐藏文件,同理目录使用.始发也会被认为是东躲西藏的,所以就有了isHidden函数的完结。

实际笔者个人觉得这一个应用1个正则就能够化解的题材。。为啥还要分割为数组呢?

function isHidden (root, path) {
  path = path.substr(root.length)

  return new RegExp(`${sep}\\.`).test(path)
}

 

业已给社区付出了PR

注册路由的监听

上述全部是有关实例化Router时的一部分操作,上面就来说一下接纳最多的,注册路由相关的操作,最熟知的终将正是router.getrouter.post那么些的操作了。
但实际那么些也只是2个飞快格局罢了,在中间调用了来自Routerregister方法:

Router.prototype.register = function (path, methods, middleware, opts) {
  opts = opts || {}

  let router = this
  let stack = this.stack

  // support array of paths
  if (Array.isArray(path)) {
    path.forEach(function (p) {
      router.register.call(router, p, methods, middleware, opts)
    })

    return this
  }

  // create route
  let route = new Layer(path, methods, middleware, {
    end: opts.end === false ? opts.end : true,
    name: opts.name,
    sensitive: opts.sensitive || this.opts.sensitive || false,
    strict: opts.strict || this.opts.strict || false,
    prefix: opts.prefix || this.opts.prefix || '',
    ignoreCaptures: opts.ignoreCaptures
  })

  if (this.opts.prefix) {
    route.setPrefix(this.opts.prefix)
  }

  // add parameter middleware
  Object.keys(this.params).forEach(function (param) {
    route.param(param, this.params[param])
  }, this)

  stack.push(route)

  return route
}

 

该方法在诠释中标为了 private
可是在那之中的有个别参数在代码中各个地方都未曾体现出来,鬼知道为啥会留着那个参数,但既然存在,就须求驾驭她是为什么的

本条是路由监听的基本功措施,函数签名大概如下:

Param Type Default Description
path String/Array[String] 一个或者多个的路径
methods Array[String] 该路由需要监听哪几个METHOD
middleware Function/Array[Function] 由函数组成的中间件数组,路由实际调用的回调函数
opts Object {} 一些注册路由时的配置参数,上边提到的strictsensitiveprefix在这里都有体现

能够看出,函数大概正是落成了那般的流水生产线:

  1. 检查path是否为数组,假使是,遍历item举办调用自己
  2. 实例化三个Layer目的,设置有个别伊始化参数
  3. 设置针对某个参数的中间件处理(假如有的话)
  4. 将实例化后的靶子放入stack中存储

据此在介绍那多少个参数从前,简单的描述一下Layer的构造函数是很有不可缺少的:

function Layer(path, methods, middleware, opts) {
  this.opts = opts || {}
  this.name = this.opts.name || null
  this.methods = []
  this.paramNames = []
  this.stack = Array.isArray(middleware) ? middleware : [middleware]

  methods.forEach(function(method) {
    var l = this.methods.push(method.toUpperCase());
    if (this.methods[l-1] === 'GET') {
      this.methods.unshift('HEAD')
    }
  }, this)

  // ensure middleware is a function
  this.stack.forEach(function(fn) {
    var type = (typeof fn)
    if (type !== 'function') {
      throw new Error(
        methods.toString() + " `" + (this.opts.name || path) +"`: `middleware` "
        + "must be a function, not `" + type + "`"
      )
    }
  }, this)

  this.path = path
  this.regexp = pathToRegExp(path, this.paramNames, this.opts)
}

 

layer是肩负储存路由监听的新闻的,每一趟注册路由时的UEnclaveL,U福睿斯L生成的正则表达式,该U福特ExplorerL中存在的参数,以及路由对应的中间件。
统统交由Layer来存款和储蓄,重点须求关爱的是实例化进程中的那么些数组参数:

  • methods
  • paramNames
  • stack

methods仓库储存的是该路由监听对应的管事METHOD,并会在实例化的历程中针对METHOD举行高低写的转移。
paramNames因为用的插件难点,看起来不那么清楚,实际上在pathToRegExp个中会对paramNames其一数组实行push的操作,这么看可能会痛快一下pathToRegExp(path, &this.paramNames, this.opts),在拼接hash布局的门径参数时会用到这几个数组
stack储存的是该路由监听对应的中间件函数,router.middleware壹部分逻辑会重视于这么些数组

4. 视图view

能够依据选取的模版引擎来定义视图。上边简单介绍怎么着引入模板引擎

  • 行使ejs模板引擎:koa-ejs

const Koa = require('koa');
const render = require('koa-ejs');
const path = require('path');

const app = new Koa();
render(app, {
  root: path.join(__dirname, 'view'),
  layout: 'template',
  viewExt: 'html',
  cache: false,
  debug: true
});

app.use(async function (ctx) {
  await ctx.render('user');
});

app.listen(7001);
  • 选择xtemplate模板引擎:koa-xtpl

const path = require('path')
const Koa = require('koa')
const xtpl = require('koa-xtpl')
const app = new Koa()

// root 
app.use(xtpl(path.join(__dirname, 'views')))
// or options 
app.use(xtpl({
  root: path.join(__dirname, 'views'),
  extname: 'xtpl',
  commands: {}
}))

app.use(async ctx => {
  await ctx.render('demo', { title: new Date() })
})

app.listen(3000)
  • kow-views:能够自定义使用分化的模板

var views = require('koa-views');

// Must be used before any router is used
app.use(views(__dirname + '/views', {
  map: {
    html: 'underscore'
  }
}));

app.use(async function (ctx, next) {
  ctx.state = {
    session: this.session,
    title: 'app'
  };

  await ctx.render('user', {
    user: 'John'
  });
});

压缩的开启与公事夹的拍卖

在上头的那1坨代码执行完以往,大家就取得了1个实惠的途径,(假使是不行路径,resolvePath会直接抛出相当)
接下去做的作业正是反省是还是不是有可用的压缩文件使用,此处未有怎么逻辑,就是简单的exists操作,以及Content-Encoding的修改 (用于开启压缩)

后缀的10分:

if (extensions && !/\.[^/]*$/.exec(path)) {
  const list = [].concat(extensions)
  for (let i = 0; i < list.length; i++) {
    let ext = list[i]
    if (typeof ext !== 'string') {
      throw new TypeError('option extensions must be array of strings or false')
    }
    if (!/^\./.exec(ext)) ext = '.' + ext
    if (await fs.exists(path + ext)) {
      path = path + ext
      break
    }
  }
}

 

能够看出此间的遍历是一心依据我们调用send是传播的顺序来走的,并且还做了.标记的匹配。
也正是说那样的调用都是卓有功能的:

await send(ctx, 'path', {
  extensions: ['.js', 'ts', '.tsx']
})

 

假若在添加了后缀未来能够同盟到实在的文本,那么就觉着那是3个卓有成效的门路,然后开始展览了break的操作,也正是文书档案中所说的:First found is served.

在得了那有个别操作之后会开始展览目录的检查评定,判断当前路线是或不是为三个索引:

let stats
try {
  stats = await fs.stat(path)

  if (stats.isDirectory()) {
    if (format && index) {
      path += '/' + index
      stats = await fs.stat(path)
    } else {
      return
    }
  }
} catch (err) {
  const notfound = ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR']
  if (notfound.includes(err.code)) {
    throw createError(404, err)
  }
  err.status = 500
  throw err
}

 

path

在函数底部的拍卖逻辑,首假若为了扶助多路径的还要登记,若是发现第几个path参数为数组后,则会遍历path参数举行调用自个儿。
于是针对八个URL的相同路由得以这么来拍卖:

router.register(['/', ['/path1', ['/path2', 'path3']]], ['GET'], ctx => {
  ctx.body = 'hi there.'
})

 

那样完全是二个得力的装置:

> curl http://127.0.0.1:8888/
hi there.
> curl http://127.0.0.1:8888/path1
hi there.
> curl http://127.0.0.1:8888/path3
hi there.

 

6、代码目录结构

在真的的选用开发中,大家十分的小概将持有代码都写在app.js中,1般会将代码实行分层。

贰个小彩蛋

能够窥见2个很风趣的作业,借使发现日前路线是三个目录之后,并且无人不晓钦赐了format,那么还会再尝试拼接二回index
那正是下面所说的老大彩蛋了,当大家的public途径结构长得像这么的时候:

└── public
    └── index
        └── index # 实际的文件 hello

 

咱俩得以由此贰个简单易行的主意取获得最尾巴部分的文件数量:

router.get('/surprises', async ctx => {
  await send(ctx, '/', {
    root: './public',
    index: 'index'
  })
})

// > curl http://127.0.0.1:12306/surprises
// hello

 

此间就用到了上边的多少个逻辑处理,首先是trailingSlash的判断,如果以/结尾会拼接index,以及一旦当前path合营为是三个索引之后,又会拼接一回index
故而3个简约的/加上index的参数就足以一直得到到/index/index
贰个小小的的彩蛋,实际支出中应该很少会那样玩

methods

而关于methods参数,则默许认为是2个数组,固然是只监听贰个METHOD也必要传入1个数组作为参数,要是是空数组的话,纵然URL匹配,也会向来跳过,执行下一在那之中间件,那么些在持续的router.routes中会提到

壹. 别离路由

咱俩将具有的router抽离出来,在app.js同级目录创制1个router目录,并在index.js文本中揭露无遗接口,那样能够越发将相应的路由处理逻辑放在不一致的文本里。然后只要求在app.js中引入路由主文件,将app盛传即可

// app.js 
const router=require('./router/index')
...
router(app)

// router/index.js
const router = require('koa-router')()
module.export=(app)=>{
  router.get('/',async (ctx,next)=>{
    ...
    await next();
    ...
  })
  ...
  app.use(router.routes(),router.allowedMethods())

}

最终的读取文件操作

终极终于来到了文本读取的逻辑处理,首先就是调用setHeaders的操作。

因为通过下边包车型客车斑斑筛选,这里获得的path和你调用send时传入的path不是同七个门道。
但是倒也向来不供给非得在setHeaders函数中开始展览处理,因为能够看来在函数停止时,将实际的path回来了出来。
大家完全能够在send执行达成后再展开安装,至于官方readme中所写的and doing it after is too late because the headers are already sent.
以此不须要担心,因为koa的回到数据都以置于ctx.body中的,而body的剖析是在富有的中间件全部实践完事后才会议及展览开处理。
相当于说全部的中间件都履行完事后才会起始发送http请求体,从前设置Header都以卓有成效的。

if (setHeaders) setHeaders(ctx.res, path, stats)

// stream
ctx.set('Content-Length', stats.size)
if (!ctx.response.get('Last-Modified')) ctx.set('Last-Modified', stats.mtime.toUTCString())
if (!ctx.response.get('Cache-Control')) {
  const directives = ['max-age=' + (maxage / 1000 | 0)]
  if (immutable) {
    directives.push('immutable')
  }
  ctx.set('Cache-Control', directives.join(','))
}
if (!ctx.type) ctx.type = type(path, encodingExt) // 接口返回的数据类型,默认会取出文件后缀
ctx.body = fs.createReadStream(path)

return path

 

以及包括上面包车型地铁maxageimmutable都以在这里生效的,然则要留意的是,假设Cache-Control早就存在值了,koa-send是不会去掩盖的。

middleware

middleware则是一回路由真正实行的业务了,照旧是适合koa正式的中间件,能够有三个,依据洋葱模型的法子来推行。
这也是koa-router中最珍视的地点,能够让我们的一些中间件只在特定的URL时执行。
此地写入的四个中间件都是针对性该URL生效的。

P.S.
koa-router中,还提供了一个情势,叫做router.use,那一个会登记一个基于router实例的中间件

2. 分离controller层,新增2个controller文件夹,将router对应路由的工作处理逻辑提取出来,如下
// controller/home.js
module.export={
  index:async (ctx,next)=>{
    ...
  },
  home:async (ctx,next)=>{
    ctx.body='<h1>Home Page</h1>'
  }
}

// router/index.js
const router = require('koa-router')()
const HomeController = require('../controller/home')
module.export=(app)=>{
  router.get('/',HomeController.home)
  ...
  app.use(router.routes(),router.allowedMethods())

}

当前的代码结构目录已经比较清楚了,适用于以 node
作为中间层的类型。倘若想要把 node
作为真正的后端去操作数据库等,提出再分出壹层
service,用于拍卖数据层面包车型地铁交互,比如调用 model
处理数据库,调用第叁方接口等,而controller
里面只做一些简短的参数处理。

使用Stream与使用readFile的区别

在最后给body赋值的职位能够看出,是利用的Stream而不用是readFile,使用Stream进展传输能推动至少多个好处:

  1. 第二种格局,假如是大文件,在读取完结后会如今存放到内部存款和储蓄器中,并且toString是有长度限制的,假诺是一个伟大的文件,toString调用会抛出尤其的。
  2. 行使第二种办法进行读取文件,是要在方方面面包车型地铁数据都读取完毕后再回去给接口调用方,在读取数据的里边,接口都以地处Wait的动静,未有其余数据再次来到。

能够做三个像样那样的德姆o:

const http      = require('http')
const fs        = require('fs')
const filePath  = './test.log'

http.createServer((req, res) => {
  if (req.url === '/') {
    res.end('<html></html>')
  } else if (req.url === '/sync') {
    const data = fs.readFileSync(filePath).toString()

    res.end(data)
  } else if (req.url === '/pipe') {
    const rs = fs.createReadStream(filePath)

    rs.pipe(res)
  } else {
    res.end('404')
  }
}).listen(12306, () => console.log('server run as http://127.0.0.1:12306'))

 

第1走访首页http://127.0.0.1:12306/跻身3个空的页面 (首即使无心搞CORS了),然后在支配台调用四个fetch就能够收获如此的对待结果了:

4858美高梅 2
4858美高梅 3

可以看到在下行传输的光阴大概的还要,使用readFileSync的章程会大增必然时间的Waiting,而这些日子正是服务器在进展文件的读取,时间长短取决于读取的文件大小,以及机器的特性。

opts

opts则是用来设置有个别路由生成的配备规则的,包罗如下几个可选的参数:

Param Type Default Description
name String 设置该路由所对应的name,命名router
prefix String 非常鸡肋的参数,完全没有卵用,看似会设置路由的前缀,实际上没有一点儿用
sensitive Boolean false 是否严格匹配大小写,覆盖实例化Router中的配置
strict Boolean false 是否严格匹配大小写,如果设置为false则匹配路径后边的/是可选的
end Boolean true 路径匹配是否为完整URL的结尾
ignoreCaptures Boolean 是否忽略路由匹配正则结果中的捕获组
三. 分别中间件

除此以外,随着项目标附加,中间件的数据也愈发多,建议足以把拥有的中间件抽出来放在三个middleware文本夹下,不管是第2方中间件,依然自定义的中间件,统一放在此到处理。

koa-static

koa-static是一个基于koa-send的浅封装。
因为通过下边包车型地铁实例也能够看来,send格局必要自身在中间件中调用才行。
手动钦赐send对应的path等等的参数,这个也是属于重复性的操作,所以koa-static将那些逻辑进行了一回封装。
让大家得以经过平昔登记1当中间件来成功静态文件的拍卖,而不再须要关爱参数的读取之类的难点:

const Koa = require('koa')
const app = new Koa()
app.use(require('koa-static')(root, opts))

 

opts是透传到koa-send中的,只可是会利用第三个参数root来覆盖opts中的root
而且添加了壹些细节化的操作:

  • 默许添加三个index.html

    if (opts.index !== false) opts.index = opts.index || 'index.html'
    

     

  • 暗许只针对HEADGET两种METHOD

    if (ctx.method === 'HEAD' || ctx.method === 'GET') {
    // ...
    }
    

     

  • 添加二个defer挑选来决定是还是不是先实施此外中间件。
    如果deferfalse,则会先实施send,优先匹配静态文件。
    要不则会等到任何中间件先进行,分明其余中间件未有拍卖该请求才会去追寻对应的静态财富。
    只需点名root,剩下的做事付出koa-static,大家就无需关切静态能源应该怎么着处理了。

name

首先是name,首如果用于那多少个地点:

  1. 抛出万分时更有利的固定
  2. 可以透过router.url(<name>)router.route(<name>)收获到相应的router信息
  3. 在中间件执行的时候,name会被塞到ctx.routerName

    router.register(‘/test1’, [‘GET’], _ => {}, {
    name: ‘module’
    })

    router.register(‘/test2’, [‘GET’], _ => {}, {
    name: ‘module’
    })

    console.log(router.url(‘module’) === ‘/test1’) // true

    try {
    router.register(‘/test2’, [‘GET’], null, {

    name: 'error-module'
    

    })
    } catch (e) {
    console.error(e) // Error: GET error-module: middleware must be a function, not object
    }

 

尽管多少个router动用相同的命名,则通过router.url调用重回起首注册的那些:

// route用来获取命名路由
Router.prototype.route = function (name) {
  var routes = this.stack

  for (var len = routes.length, i=0; i<len; i++) {
    if (routes[i].name && routes[i].name === name) {
      return routes[i] // 匹配到第一个就直接返回了
    }
  }

  return false
}

// url获取该路由对应的URL,并使用传入的参数来生成真实的URL
Router.prototype.url = function (name, params) {
  var route = this.route(name)

  if (route) {
    var args = Array.prototype.slice.call(arguments, 1)
    return route.url.apply(route, args)
  }

  return new Error('No route found for name: ' + name)
}

 

4. view

提供视图,根据采取的模版引擎定义视图,能够通过render渲染后当做响应中央重临给前端,也能够定义一些谬误页面如404等。

小结

koa-sendkoa-static究竟七个11分轻量级的中间件了。
自身未有太复杂的逻辑,正是局地再度的逻辑被提炼成的中间件。
但是确实能够裁减过多常见支出中的义务量,能够令人更注意的青眼业务,而非这么些边边角角的效应。

跑题说下router.url的那三个事情

设若在类型中,想要针对有些URL展开跳转,使用router.url来生成path则是三个正确的抉择:

router.register(
  '/list/:id', ['GET'], ctx => {
    ctx.body = `Hi ${ctx.params.id}, query: ${ctx.querystring}`
  }, {
    name: 'list'
  }
)

router.register('/', ['GET'], ctx => {
  // /list/1?name=Niko
  ctx.redirect(
    router.url('list', { id: 1 }, { query: { name: 'Niko' } })
  )
})

// curl -L http://127.0.0.1:8888 => Hi 1, query: name=Niko

 

能够见到,router.url事实上调用的是Layer实例的url格局,该办法重如果用来拍卖生成时传播的有的参数。
源码地址:layer.js#L116
函数接收七个参数,paramsoptions,因为作者Layer实例是储存了对应的path等等的消息,所以params不怕储存的在途径中的一些参数的替换,options在现阶段的代码中,仅仅存在1个query字段,用来拼接search末尾的数码:

const Layer = require('koa-router/lib/layer')
const layer = new Layer('/list/:id/info/:name', [], [_ => {}])

console.log(layer.url({ id: 123, name: 'Niko' }))
console.log(layer.url([123, 'Niko']))
console.log(layer.url(123, 'Niko'))
console.log(
  layer.url(123, 'Niko', {
    query: {
      arg1: 1,
      arg2: 2
    }
  })
)

 

上述的调用格局都以可行的,在源码中有对应的拍卖,首先是本着多参数的判断,假设params不是三个object,则会以为是因而layer.url(参数, 参数, 参数, opts)那种措施来调用的。
将其更换为layer.url([参数, 参数], opts)形式的。
那时的逻辑仅供给处理三种意况了:

  1. 数组方式的参数替换
  2. hash花样的参数替换
  3. 无参数

以此参数替换指的是,一个URL会通过二个其3方的库用来处理链接中的参数部分,也正是/:XXX的这一片段,然后传入二个hash贯彻类似模版替换的操作:

// 可以简单的认为是这样的操作:
let hash = { id: 123, name: 'Niko' }
'/list/:id/:name'.replace(/(?:\/:)(\w+)/g, (_, $1) => `/${hash[$1]}`)

 

然后layer.url的拍卖就是为着将各样参数生成类似hash如此的结构,最后替换hash获取完整的URL

5. public静态文件目录以及log日志文件目录。

末段的利用结构如下:

4858美高梅 4

co3.png

prefix

上面实例化Layer的进程中近乎是opts.prefix的权重更高,可是随着在上面就有了一个判断逻辑举办调用setPrefix再也赋值,在翻遍了全方位的源码后发觉,那样绝无仅有的3个差距就在于,会有一条debug采用的是登记router时传入的prefix,而其余省方都会被实例化Router时的prefix所覆盖。

与此同时只要想要路由科学的运用prefix,则要求调用setPrefix,因为在Layer实例化的长河中关于path的积存正是来源于远传入的path参数。
而应用prefix前缀则供给手动触发setPrefix

// Layer实例化的操作
function Layer(path, methods, middleware, opts) {
  // 省略不相干操作
  this.path = path
  this.regexp = pathToRegExp(path, this.paramNames, this.opts)
}

// 只有调用setPrefix才会应用前缀
Layer.prototype.setPrefix = function (prefix) {
  if (this.path) {
    this.path = prefix + this.path
    this.paramNames = []
    this.regexp = pathToRegExp(this.path, this.paramNames, this.opts)
  }

  return this
}

 

以此在爆出给使用者的多少个点子中都有反映,类似的getset以及use
自然在文书档案中也提供了足以平素设置富有router前缀的章程,router.prefix
文书档案中就这么不难的报告你可以安装前缀,prefix在其间会循环调用全体的layer.setPrefix

router.prefix('/things/:thing_id')

 

不过在翻看了layer.setPrefix源码后才察觉那里实在是带有多个暗坑的。
因为setPrefix的落实是获得prefix参数,拼接到当前path的头部。
如此就会推动叁个题材,固然大家一再调用setPrefix会招致多次prefix外加,而非替换:

router.register('/index', ['GET'], ctx => {
  ctx.body = 'hi there.'
})

router.prefix('/path1')
router.prefix('/path2')

// > curl http://127.0.0.1:8888/path2/path1/index
// hi there.

 

prefix方法会叠加前缀,而不是覆盖前缀

7、运营安排

  • 运营:选用 nodemon 来代表 node
    以运维应用。当代码爆发变化时候,nodemon 会帮我们机关心重视启。

cnpm i nodemon -g
...
nodemon app.js
  • 安插:使用 pm二,pm二 是二个蕴涵负载均衡成效的Node应用的经过管理器。

cnpm i pm2 -g
...
pm2 start app.js
sensitive与strict

这俩参数没啥好说的,正是会覆盖实例化Router时所传递的那俩参数,效果都同壹。

end

end是一个很有意思的参数,这么些在koa-router中援引的任何模块中有反映到,path-to-regexp:

if (end) {
  if (!strict) route += '(?:' + delimiter + ')?'

  route += endsWith === '$' ? '$' : '(?=' + endsWith + ')'
} else {
  if (!strict) route += '(?:' + delimiter + '(?=' + endsWith + '))?'
  if (!isEndDelimited) route += '(?=' + delimiter + '|' + endsWith + ')'
}

return new RegExp('^' + route, flags(options))

 

endWith能够简不难单地驾驭为是正则中的$,也便是协作的结尾。
看代码的逻辑,差不多就是,即使设置了end: true,则不管任何情形都会在终极添加$意味着极度的末梢。
而如果end: false,则只有在同时安装了strict: false或者isEndDelimited: false时才会触发。
就此大家得以通过那八个参数来兑现UENVISIONL的混淆匹配:

router.register(
  '/list', ['GET'], ctx => {
    ctx.body = 'hi there.'
  }, {
    end: false,
    strict: true
  }
)

 

也正是说上述代码最终生成的用来匹配路由的正则表达式大概是那样的:

/^\/list(?=\/|$)/i

// 可以通过下述代码获取到正则
require('path-to-regexp').tokensToRegExp('/list/', {end: false, strict: true})

 

结尾的$是可选的,那就会造成,我们只要发送任何起初为/list的请求都会被那些中间件所获得到。

ignoreCaptures

ignoreCaptures参数用来安装是或不是供给再次来到URL中匹配的路子参数给中间件。
而一旦设置了ignoreCaptures后来那三个参数就会变为空对象:

router.register('/list/:id', ['GET'], ctx => {
  console.log(ctx.captures, ctx.params)
  // ['1'], { id: '1' }
})

// > curl /list/1

router.register('/list/:id', ['GET'], ctx => {
  console.log(ctx.captures, ctx.params)
  // [ ], {  }
}, {
  ignoreCaptures: true
})
// > curl /list/1

 

那些是在中间件执行期间调用了来自layer的七个办法赢得的。
率先调用captures获得具有的参数,若是设置了ignoreCaptures则会招致平昔回到空数组。
下一场调用params将注册路由时所生成的有着参数以及参数们其实的值传了进来,然后生成3个完全的hash注入到ctx对象中:

// 中间件的逻辑
ctx.captures = layer.captures(path, ctx.captures)
ctx.params = layer.params(path, ctx.captures, ctx.params)
ctx.routerName = layer.name
return next()
// 中间件的逻辑 end

// layer提供的方法
Layer.prototype.captures = function (path) {
  if (this.opts.ignoreCaptures) return []
  return path.match(this.regexp).slice(1)
}

Layer.prototype.params = function (path, captures, existingParams) {
  var params = existingParams || {}

  for (var len = captures.length, i=0; i<len; i++) {
    if (this.paramNames[i]) {
      var c = captures[i]
      params[this.paramNames[i].name] = c ? safeDecodeURIComponent(c) : c
    }
  }

  return params
}

// 所做的事情大致如下:
// [18, 'Niko'] + ['age', 'name']
// =>
// { age: 18, name: 'Niko' }

 

router.param的作用

上述是有关心册路由时的1对参数描述,能够看看在register中实例化Layer指标后并未直接将其放入stack中,而是进行了这么的二个操作之后才将其推入stack

Object.keys(this.params).forEach(function (param) {
  route.param(param, this.params[param])
}, this)

stack.push(route) // 装载

 

此间是用作添加针对有些URL参数的中间件处理的,与router.param双面关联性很强:

Router.prototype.param = function (param, middleware) {
  this.params[param] = middleware
  this.stack.forEach(function (route) {
    route.param(param, middleware)
  })
  return this
}

 

2者操作看似,前者用于对新增的路由监听添加全部的param中间件,而后者用于针对现有的全体路由添加param中间件。
因为在router.param中有着this.params[param] = XXX的赋值操作。
这么在继续的新增路由监听中,直接循环this.params就足以得到持有的中间件了。

router.param的操作在文书档案中也有介绍,文书档案地址
大体就是能够用来做1些参数校验之类的操作,可是因为在layer.param中有了1些新鲜的处理,所以我们不用顾虑param的执行顺序,layer会保证param肯定是早于重视这么些参数的中间件执行的:

router.register('/list/:id', ['GET'], (ctx, next) => {
  ctx.body = `hello: ${ctx.name}`
})

router.param('id', (param, ctx, next) => {
  console.log(`got id: ${param}`)
  ctx.name = 'Niko'
  next()
})

router.param('id', (param, ctx, next) => {
  console.log('param2')
  next()
})


// > curl /list/1
// got id: 1
// param2
// hello: Niko

 

最常用的get/post之类的火速形式

以及说完了上面的功底艺术register,大家得以来看下揭破给开发者的多少个router.verb方法:

// get|put|post|patch|delete|del
// 循环注册多个METHOD的快捷方式
methods.forEach(function (method) {
  Router.prototype[method] = function (name, path, middleware) {
    let middleware

    if (typeof path === 'string' || path instanceof RegExp) {
      middleware = Array.prototype.slice.call(arguments, 2)
    } else {
      middleware = Array.prototype.slice.call(arguments, 1)
      path = name
      name = null
    }

    this.register(path, [method], middleware, {
      name: name
    })

    return this
  }
})

Router.prototype.del = Router.prototype['delete'] // 以及最后的一个别名处理,因为del并不是有效的METHOD

 

令人大失所望的是,verb办法将大气的opts参数都砍掉了,暗中同意只留下了八个name字段。
只是很粗大略的处理了一晃命名name路由相关的逻辑,然后举办调用register形成操作。

router.use-Router内部的中间件

以及上文中也论及的router.use,能够用来注册二个中间件,使用use挂号中间件分为二种状态:

  1. 常见的中间件函数
  2. 将长存的router实例作为中间件传入
普通的use

这里是use格局的第二代码:

Router.prototype.use = function () {
  var router = this
  middleware.forEach(function (m) {
    if (m.router) { // 这里是通过`router.routes()`传递进来的
      m.router.stack.forEach(function (nestedLayer) {
        if (path) nestedLayer.setPrefix(path)
        if (router.opts.prefix) nestedLayer.setPrefix(router.opts.prefix) // 调用`use`的Router实例的`prefix`
        router.stack.push(nestedLayer)
      })

      if (router.params) {
        Object.keys(router.params).forEach(function (key) {
          m.router.param(key, router.params[key])
        })
      }
    } else { // 普通的中间件注册
      router.register(path || '(.*)', [], m, { end: false, ignoreCaptures: !hasPath })
    }
  })
}

// 在routes方法有这样的一步操作
Router.prototype.routes = Router.prototype.middleware = function () {
  function dispatch() {
    // ...
  }

  dispatch.router = this // 将router实例赋值给了返回的函数

  return dispatch
}

 

先是种是比较正规的章程,传入2个函数,1个可选的path,来拓展挂号中间件。
然则有1些要小心的是,.use('path')如此那般的用法,中间件无法独立存在,必须求有一个能够与之途径相匹配的路由监听存在:

router.use('/list', ctx => {
  // 如果只有这么一个中间件,无论如何也不会执行的
})

// 必须要存在相同路径的`register`回调
router.get('/list', ctx => { })

app.use(router.routes())

 

案由是那般的:

  1. .use.get都是依据.register来兑现的,不过.usemethods参数中传递的是四个空数组
  2. 在2个门路被匹配到时,会将享有匹配到的中游件取出来,然后检核查应的methods,如果length !== 0则会对最近匹配组标记3个flag
  3. 在履行中间件在此以前会先判断有没有其一flag,假使未有则表达该路线全体的中间件都未有安装METHOD,则会直接跳过进入别的流程(比如allowedMethod

    Router.prototype.match = function (path, method) {
    var layers = this.stack
    var layer
    var matched = {

    path: [],
    pathAndMethod: [],
    route: false
    

    }

    for (var len = layers.length, i = 0; i < len; i++) {

    layer = layers[i]
    
    if (layer.match(path)) {
      matched.path.push(layer)
    
      if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
        matched.pathAndMethod.push(layer)
    
        // 只有在发现不为空的`methods`以后才会设置`flag`
        if (layer.methods.length) matched.route = true
      }
    }
    

    }

    return matched
    }

    // 以及在routes中有那样的操作
    Router.prototype.routes = Router.prototype.middleware = function () {
    function dispatch(ctx, next) {

    // 如果没有`flag`,直接跳过
    if (!matched.route) return next()
    

    }

    return dispatch
    }

 

将别的router实例传递进入

可以看出,假诺选拔了router.routes()来格局来复用中间件,会遍历该实例的保有路由,然后设置prefix
并将修改完的layer生产到日前的router中。
那就是说今后将要注意了,在上面其实早就涉及了,LayersetPrefix是东拼西凑的,而不是覆盖的。
use是会操作layer对象的,所以这么的用法会导致前边的中间件路径也被改动。
再正是只要传入use的中间件已经注册在了koa中就会招致相同的中间件会执行三次(设若有调用next的话):

const middlewareRouter = new Router()
const routerPage1 = new Router({
  prefix: '/page1'
})

const routerPage2 = new Router({
  prefix: '/page2'
})

middlewareRouter.get('/list/:id', async (ctx, next) => {
  console.log('trigger middleware')
  ctx.body = `hi there.`
  await next()
})

routerPage1.use(middlewareRouter.routes())
routerPage2.use(middlewareRouter.routes())

app.use(middlewareRouter.routes())
app.use(routerPage1.routes())
app.use(routerPage2.routes())

 

就好像上述代码,实际上会有四个难题:

  1. 终极使得的访问路径为/page2/page1/list/1,因为prefix会拼接而非覆盖
  2. 当大家在中间件中调用next以后,console.log会两次三番输出一遍,因为有着的routes都以动态的,实际上prefix都被改动为了/page2/page1

一定要小心使用,不要以为那样的不二法门得以用来落实路由的复用

伸手的处理

以及,终于来到了最终一步,当一个呼吁来了之后,Router是何等处理的。
一个Router实例能够抛出两当中间件注册到koa上:

app.use(router.routes())
app.use(router.allowedMethods())

 

routes肩负重大的逻辑。
allowedMethods担负提供3个前置的METHOD反省立中学间件。

allowedMethods不要紧好说的,正是依据当前恳请的method开展的部分校验,并重返一些错误音讯。
而下边介绍的累累措施其实都以为了最后的routes服务:

Router.prototype.routes = Router.prototype.middleware = function () {
  var router = this

  var dispatch = function dispatch(ctx, next) {
    var path = router.opts.routerPath || ctx.routerPath || ctx.path
    var matched = router.match(path, ctx.method)
    var layerChain, layer, i

    if (ctx.matched) {
      ctx.matched.push.apply(ctx.matched, matched.path)
    } else {
      ctx.matched = matched.path
    }

    ctx.router = router

    if (!matched.route) return next()

    var matchedLayers = matched.pathAndMethod
    var mostSpecificLayer = matchedLayers[matchedLayers.length - 1]
    ctx._matchedRoute = mostSpecificLayer.path
    if (mostSpecificLayer.name) {
      ctx._matchedRouteName = mostSpecificLayer.name
    }

    layerChain = matchedLayers.reduce(function(memo, layer) {
      memo.push(function(ctx, next) {
        ctx.captures = layer.captures(path, ctx.captures)
        ctx.params = layer.params(path, ctx.captures, ctx.params)
        ctx.routerName = layer.name
        return next()
      })
      return memo.concat(layer.stack)
    }, [])

    return compose(layerChain)(ctx, next)
  };

  dispatch.router = this

  return dispatch
}

 

率先能够见见,koa-router还要还提供了一个别名middleware来兑现均等的功效。
以及函数的调用最后会再次回到二在那之中间件函数,那些函数才是确实被挂在到koa上的。
koa的中间件是纯粹的中间件,不管什么样请求都会进行所包括的中间件。
故而不建议为了选用prefix而创造四个Router实例,那会造成在koa上挂载多少个dispatch用来检查UPAJEROL是或不是顺应规则

进去中间件现在会进展UCRUISERL的论断,正是大家下面提到的能够用来做foraward落到实处的地方。
匹配调用的是router.match形式,虽说看似赋值是matched.path,而其实在match主意的贯彻中,里边全体是匹配到的Layer实例:

Router.prototype.match = function (path, method) {
  var layers = this.stack // 这个就是获取的Router实例中所有的中间件对应的layer对象
  var layer
  var matched = {
    path: [],
    pathAndMethod: [],
    route: false
  }

  for (var len = layers.length, i = 0; i < len; i++) {
    layer = layers[i]

    if (layer.match(path)) { // 这里就是一个简单的正则匹配
      matched.path.push(layer)

      if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
        // 将有效的中间件推入
        matched.pathAndMethod.push(layer)

        // 判断是否存在METHOD
        if (layer.methods.length) matched.route = true
      }
    }
  }

  return matched
}

// 一个简单的正则匹配
Layer.prototype.match = function (path) {
  return this.regexp.test(path)
}

 

而之所以会设有说判断是还是不是有ctx.matched来实行拍卖,而不是直接对这本性情进行赋值。
那是因为上面也论及过的,八个koa实例恐怕会登记多少个koa-router实例。
那就导致三个router实例的中间件执行完结后,后续恐怕还会有其余的router实例也命中了有个别URL,但是那样会保险matched始终是在增进的,而非每趟都会覆盖。

pathpathAndMethod都是match回到的八个数组,两者的界别在于path回来的是匹配ULacrosseL成功的多少,而pathAndMethod则是匹配U哈弗L且匹配到METHOD的数量

const router1 = new Router()
const router2 = new Router()

router1.post('/', _ => {})

router1.get('/', async (ctx, next) => {
  ctx.redirectBody = 'hi'
  console.log(`trigger router1, matched length: ${ctx.matched.length}`)
  await next()
})

router2.get('/', async (ctx, next) => {
  ctx.redirectBody = 'hi'
  console.log(`trigger router2, matched length: ${ctx.matched.length}`)
  await next()
})

app.use(router1.routes())
app.use(router2.routes())

// >  curl http://127.0.0.1:8888/
// => trigger router1, matched length: 2
// => trigger router2, matched length: 3

 

至于中间件的履行,在koa-router中也应用了koa-compose来归并洋葱:

var matchedLayers = matched.pathAndMethod

layerChain = matchedLayers.reduce(function(memo, layer) {
  memo.push(function(ctx, next) {
    ctx.captures = layer.captures(path, ctx.captures)
    ctx.params = layer.params(path, ctx.captures, ctx.params)
    ctx.routerName = layer.name
    return next()
  })
  return memo.concat(layer.stack)
}, [])

return compose(layerChain)(ctx, next)

 

那坨代码会在具有匹配到的中间件从前增加3个ctx属性赋值的中间件操作,相当于说reduce的实施会让洋葱模型对应的中间件函数数量最少X2
layer中恐怕包罗多个中间件,不要忘了middleware,那就是怎么会在reduce中使用concat而非push
因为要在每一当中间件执行从前,修改ctx为本次中间件触发时的片段音信。
席卷匹配到的U君越L参数,以及当前中间件的name等等的新闻。

[
  layer1[0], // 第一个register中对应的中间件1
  layer1[1], // 第一个register中对应的中间件2
  layer2[0]  // 第二个register中对应的中间件1
]

// =>

[
  (ctx, next) => {
    ctx.params = layer1.params // 第一个register对应信息的赋值  
    return next()
  },
  layer1[0], // 第一个register中对应的中间件1
  layer1[1], // 第一个register中对应的中间件2
  (ctx, next) => {
    ctx.params = layer2.params // 第二个register对应信息的赋值  
    return next()
  },
  layer2[0]  // 第二个register中对应的中间件1
]

 

routes最后,会调用koa-compose来合并reduce所生成的中间件数组,以及选拔了在此之前在koa-compose中涉嫌了的第壹个可选的参数,用来做洋葱执行到位后最后的回调解和处理理。


小记

至此,koa-router的沉重就早已做到了,完结了路由的挂号,以及路由的监听处理。
在阅读koa-router的源码进度中觉得很吸引:

  • 显著代码中已经落实的效果,为何在文书档案中就未有体现出来吧。
  • 若是文书档案中不写明能够这么来用,为何还要在代码中有对应的落到实处啊?

两个最简便的举例证明:

  1. 能够由此修改ctx.routerPath来实现forward功能,可是在文书档案中不会告诉你
  2. 能够通过router.register(path, ['GET', 'POST'])来火速的监听多个METHOD,但是register被标记为了@private

参考资料:

  • koa-router |
    docs
  • path-to-regexp |
    docs

演示代码在库房中的地点:learning-koa-router

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图
Copyright @ 2010-2019 美高梅手机版4858 版权所有