24.0.3 • Published 19 days ago

titbit v24.0.3

Weekly downloads
1,239
License
ISC
Repository
-
Last release
19 days ago

npm.io

titbit

titbit是运行于服务端的Web框架,最开始是为了在教学中方便开发而设计,也用在一些业务系统上。它绝对算不上重型框架,但是也不简单过头。

关于类型和TypeScript的支持 如果关于ECMAScript对类型系统的提案能够通过,则以后可以直接在JavaScript中使用类型,而无需考虑支持TS。 如果后续此提案没有通过,再考虑支持。

参考连接:JS的类型提案

有bug或是疑惑请提交issue或者发送私信。

它非常快,无论是路由查找还是中间件执行过程。

因为github无法正常显示图片,并且github服务访问太慢以及其他问题,建议使用gitee(码云)查看文档。

码云地址

Wiki中有相关主题的说明:Wiki

Node.js的Web开发框架,同时支持HTTP/1.1和HTTP/2协议, 提供了强大的中间件机制。

核心功能:

  • 请求上下文设计屏蔽接口差异。

  • 中间件模式。

  • 路由分组和命名。

  • 中间件按照路由分组执行。中间件匹配请求方法和路由来执行。

  • 开启守护进程:使用cluster模块。

  • 显示子进程负载情况。

  • 默认解析body数据。

  • 支持通过配置启用HTTP/1.1或是HTTP/2服务。兼容模式,允许同时支持HTTP/2和HTTP/1.1。

  • 支持配置启用HTTPS服务(HTTP/2服务必须要开启HTTPS)。

  • 限制请求数量。

  • 限制一段时间内单个IP的最大访问次数。

  • IP黑名单和IP白名单。

  • 在cluster模式,监控子进程超出最大内存限制则重启。

  • 可选择是否开启自动负载模式:根据负载创建新的子进程处理请求,并在空闲时恢复初始状态。

  • 可控制子进程最大内存占用,并在超出时自动重启,可控制必须重启的最大内存以及当超出某一个数值但只有连接数为0的时候才会重启的内存。

  • 默认设定和网络安全相关的配置,避免软件服务层面的DDOS攻击和其他网络安全问题。

!注意

请尽可能使用最新版本。titbit会先查找路由再进行请求上下文对象的创建,如果没有发现路由,则不会创建请求上下文对象。 这是为了避免无意义的操作,也会有其他一些错误或恶意请求的检测处理,错误状态码涉及到404和400,因此若需要在这个过程中控制返回的错误信息,需要通过初始化选项中的notFound和badRequest进行设置即可,默认的它们只是一条简短的文本信息。

安装

npm i titbit

同样可以通过yarn安装:

yarn add titbit

兼容性

从最初发展到后来一段时间内,都尽可能保证大版本的兼容性。中间经历过多次比较大的演进,有时候次版本号演进也会有不兼容更新。从21.5+版本以后,只有大版本更新可能会有一些不兼容的更新,并给出不兼容项,请注意文档和Wiki。之后的两个小版本号更新都不会体现不兼容的更新。(在此之前,次要版本号仍然可以保证兼容性)

·重要版本改进

最小示例

'use strict'

const titbit = require('titbit')

const app = new titbit()

app.run(1234)

当不填加路由时,titbit默认添加一个路由:

/*

浏览器访问会看到一个非常简单的页面,这仅仅是为了方便最开始的了解和访问文档,它不会对实际开发有任何影响。

添加一个路由

'use strict'

const titbit = require('titibit')

const app = new titbit()


app.get('/', async c => {
  //data类型为string|Buffer。可以设置c.res.encoding为返回数据的编码格式,默认为'utf8'。
  c.res.body = 'success'
})

//默认监听0.0.0.0,参数和原生接口listen一致。
app.run(1234)

路由和请求类型

HTTP的起始行给出了请求类型,也被称为:请求方法。目前的请求方法:

GET POST PUT DELETE OPTIONS  TRACE HEAD PATCH

最常用的是前面5个。对于每个请求类型,router中都有同名但是小写的函数进行路由挂载。为了方便调用,在初始化app后,可以使用app上同名的快捷调用。(框架层面仅支持这些。)

示例:

'use strict';

const titbit = require('titibit');

const app = new titbit({
  debug: true
});

app.get('/', async c => {
  c.res.body = 'success';
});

app.get('/p', async c => {
  c.res.body = `${c.method} ${c.routepath}`;
});

app.post('/', async c => {
  //返回上传的数据
  c.res.body = c.body;
});

app.put('/p', async c => {
  c.res.body = {
    method : c.method,
    body : c.body,
    query : c.query
  };
});

//默认监听0.0.0.0,参数和原生接口listen一致。
app.run(8080);

获取URL参数

  • URL中的查询字符串(?后面a=1&b=2形式的参数)解析到c.query中。

  • 表单提交的数据解析到c.body中。

表单对应的content-type为application/x-www-form-urlencoded

'use strict';

const titbit = require('titbit');

let app = new titbit({
    debug: true
})

app.get('/q', async c => {
  //URL中?后面的查询字符串解析到query中。
  c.res.body = c.query; //返回JSON文本,主要区别在于header中content-type为text/json
})

app.post('/p', async c => {
  //POST、PUT提交的数据保存到body,如果是表单则会自动解析,否则只是保存原始文本值,
  //可以使用中间件处理各种数据。
  c.res.body = c.body;
})

app.run(2019);

获取POST提交的数据

提交请求体数据的请求是POST、PUT。在前端页面中,一般是表单提交,或者是异步请求。

  • 表单提交的数据解析到c.body中。

表单对应的content-type为application/x-www-form-urlencoded

异步请求的数据很多时候content-type是applicaiton/json

以上两种类型,对应的c.body都是一个object。

'use strict'

const titbit = require('titbit')

let app = new titbit({debug: true})

app.post('/p', async c => {
  //POST、PUT提交的数据保存到body,如果是表单则会自动解析为object,
  //可以使用中间件处理各种数据。
  c.send(c.body)
});

app.run(2019)

关于content-type

application/x-www-form-urlencoded

基本的表单类型会解析到c.body,是一个JS对象。

text/*

若content-type是text/*,就是text/开头的类型,比如text/json,框架层面不做解析处理,仅仅是把上传数据以utf8编码的格式转换成字符串赋值给c.body。后续的处理开发者自行决定。

multipart/form-data;boundary=xxx

若content-type是上传文件类型则默认会解析,解析后的文件对象放在c.files中,可以通过c.getFile获取。

application/json

这种类型会进行JSON.parse解析。

其他类型

若content-type是其他类型,则默认只是让c.body指向c.rawBody,即为最原始的Buffer数据。

框架层面提供基本的核心的支持,其他类型需要开发处理或者是使用扩展。

要比较容易使用,也要留出足够的空间给开发者处理,你可以完全抛弃框架默认的body解析,通过初始化选项parseBody设置为false关闭它。也可以在这基础上,进行扩展处理。

body解析模块本质上是一个中间件,这样设计的目的就是为了方便扩展和替换。

send函数返回数据

send函数就是对c.res.body的包装,其实就是设置了c.res.body的值。并且支持第二个参数,作为状态码,默认为0,表示采用模块自身的默认状态码,Node.js中http和http2默认状态码为200。

app.get('/', async c => {
  c.send('success')
})

app.get('/randerr', async c => {
  let n = parseInt(Math.random() * 10)
  if (n >= 5) {
    c.send('success')
  } else {
    //返回404状态码
    /*
      等效于:
        c.status(404)
        c.res.body = 'not found'
    */
   //你可以在v22.4.6以上的版本使用链式调用。
    c.status(404).send('not found')
  }
})

app.run(1234)

链式调用

在v22.4.6版本开始,可以对setHeader、status、sendHeader使用链式调用。

app.get('/', async c => {

  c.setHeader('content-type', 'text/plain; charset=utf-8')
    .setHeader('x-server', 'nodejs server')
    .status(200)
    .send(`${Date.now()} Math.random()}`)

})

路由参数

app.get('/:name/:id', async c => {
  //使用:表示路由参数,请求参数被解析到c.param
  let username = c.param.name;
  let uid = c.param.id;
  c.res.body = `${username} ${id}`;
});
app.run(8000);

任意路径参数

* 表示任意路径,但是必须出现在路由最后。

app.get('/static/*', async c => {
  //*表示的任意路径解析到c.param.starPath
  let spath = c.param.starPath

  c.send(spath)
})

路由查找规则


从v23.5.9开始,优化了路由查找过程。主要是对带参数路由和带 * 的路由进行了严格的顺序控制,而不是按照添加顺序进行匹配。

采用之前的版本开发的应用仍然不受影响,不存在兼容性问题。更严格的顺序减少了冲突的可能。

路由查找策略:

  1. 普通字符串路径。
  2. 带参数路由,参数少的路由会先匹配。
  3. 带 * 的路由,按照最长到最短的模式匹配。
示例:
存在路由: /x/y/:id  /x/y/* /x/*  /x/:key/:id

/x/y/123 先匹配 /x/y/:id,不会继续匹配。

/x/y/123/345 先匹配到 /x/y/*,不会继续匹配。

/x/q/123 会匹配到 /x/:key/:id。

/x/a.jpg 会匹配到 /x/*,其他路由都无法匹配。

/x/static/images/a.jpg 会匹配到 /x/*,其他路由都无法匹配。

分组添加路由

'use strict'

const titbit = require('../lib/titbit.js')

const app = new titbit({
  debug: true
})

//中间件函数
let mid_timing = async (c, next) => {
  console.time('request')
  await next()
  console.timeEnd('request')
}

//group返回值可以使用use、pre添加中间件。
// /api同时会添加到路由的前缀。
app.group('/api', route => {
  route.get('/test', async c => {
    c.send('api test')
  })

  route.get('/:name', async c => {
    c.send(c.param)
  })
})

//添加中间件到对应分组
app.use(
  async (c, next) => {
    console.log(c.method, c.headers)
    await next()
  }, {group: '/sub'}
).group('/sub', route => {
  route.get('/:id', async c => {
    c.send(c.param.id)
  })
})

//测试 不符合 路由规则,所以不会作为路径的前缀。
app.group('测试', route => {
  route.get('/test', async c => {
    console.log(c.group, c.name)
    c.send('test ok')
  }, 'test')
})

app.run(1234)

以上这种方式在指定多个中间件的时候会有些复杂,可以使用middleware方法。参考以下示例。

给分组和子分组指派中间件

'use strict'

const titbit = require('../lib/titbit.js')

const app = new titbit({
  debug: true
})

//中间件函数
let mid_timing = async (c, next) => {
  console.time('request')
  await next()
  console.timeEnd('request')
}

let sub_mid_test = async (c, next) => {
  console.log('mid test start')
  await next()
  console.log('mid test end')
}

//group返回值可以使用use、pre、middleware添加中间件。
// /api同时会添加到路由的前缀。
app.middleware([mid_timing])
  .group('/api', route => {
      route.get('/test', async c => {
        c.send('api test')
      })

      route.get('/:name', async c => {
        c.send(c.param)
      })

      //子分组 /sub启用中间件sub_mid_test,同时,子分组会启用上一层的所有中间件。
      route.middleware([sub_mid_test])
        .group('/sub', sub => {
            sub.get('/:key', async c => {
              c.send(c.param)
            })
        })
  })

app.run(1234)

分组支持嵌套调用,但是层级不能超过9。通常超过3层的嵌套分组就是有问题的,需要重新设计。

这个功能,其实不如titbit-loader扩展的自动加载机制方便易用,但是在实际情况中。有各种各样的需求。并且有时候不得不利用单文件做服务,同时还要能够兼顾框架本身的路由和中间件分组的优势,还要能够方便的编写逻辑明确,结构清晰的代码,才设计了middleware、group的接口功能。

以上路由指派分组的功能是非侵入式的,它不会影响已有代码,也不会和titbit-loader冲突。

!! 复杂的路由处理函数应该放在单独的模块中,使用一个统一的自动化加载函数来完成。


上传文件

默认会解析上传的文件,你可以在初始化服务的时候,传递parseBody选项关闭它,关于选项后面有详细的说明。 解析后的文件数据在c.files中存储,具体结构在后面有展示。

'use strict'

const titbit = require('titbit')

const app = new titbit()

app.post('/upload', async c => {
  
  let f = c.getFile('image')

  //此函数是助手函数,makeName默认会按照时间戳生成名字,extName解析文件的扩展名。
  //let fname = `${c.ext.makeName()}${c.ext.extName(f.filename)}`

  //根据原始文件名解析扩展名并生成时间戳加随机数的唯一文件名。

  let fname = c.ext.makeName(f.filename)

  try {
    c.res.body = await c.moveFile(f, fname)
  } catch (err) {
    c.res.body = err.message
  }
  
}, 'upload-image'); //给路由命名为upload-image,可以在c.name中获取。

app.run(1234)

c.files数据结构

这种结构是根据HTTP协议上传文件时的数据构造设计的,HTTP协议允许同一个上传名有多个文件,所以要解析成一个数组。而使用getFile默认情况只返回第一个文件,因为多数情况只是一个上传名对应一个文件。

对于前端来说,上传名就是你在HTML中表单的name属性:<input type="file" name="image"> image是上传名,不要把上传名和文件名混淆。

{
  image : [
    {
      'content-type': CONTENT_TYPE,
      //23.2.6以上可用,是content-type的别名,方便程序访问
      type: CONTENT_TYPE,
      filename: ORIGIN_FILENAME,
      start : START,
      end   : END,
      length: LENGTH,
      rawHeader: HEADER_DATA,
      headers: {...}
    },
    ...
  ],

  video : [
    {
      'content-type': CONTENT_TYPE,
      //23.2.6以上可用,是content-type的别名,方便程序访问
      type: CONTENT_TYPE,
      filename: ORIGIN_FILENAME,
      start : START,
      end   : END,
      length: LENGTH,
      rawHeader: HEADER_DATA,
      headers: {...}
    },
    ...
  ]
}

c.getFile就是通过名称索引,默认索引值是0,如果是一个小于0的数字,则会获取整个文件数组,没有返回null。

body最大数据量限制

'use strict'

const titbit = require('titbit')

const app = new titbit({
  //允许POST或PUT请求提交的数据量最大值为将近20M。
  //单位为字节。
  maxBody: 20000000
})

//...

app.run(1234)

中间件

中间件是一个很有用的模式,不同语言实现起来多少还是有些区别的,但是本质上没有区别。中间件的运行机制允许开发者更好的组织代码,方便实现复杂的逻辑需求。事实上,整个框架的运行机制都是中间件模式。

中间件图示:

npm.io

此框架的中间件在设计层面上,按照路由分组区分,也可以识别不同请求类型,确定是否执行还是跳过到下一层,所以速度非常快,而且多个路由和分组都具备自己的中间件,相互不冲突,也不会有无意义的调用。参考形式如下:

/*
  第二个参数可以不填写,表示全局开启中间件。
  现在第二个参数表示:只对POST请求方法才会执行,并且路由分组必须是/api。
  基于这样的设计,可以保证按需执行,不做太多无意义的操作。
*/
app.add(async (c, next) => {
    console.log('before');
    await next();
    console.log('after');
}, {method: 'POST', group: '/api'});

使用add添加的中间件是按照添加顺序逆序执行,这是标准的洋葱模型。为了提供容易理解的逻辑,提供use接口添加中间件,使用use添加的中间件按照添加顺序执行。不同的框架对实现顺序的逻辑往往会不同,但是顺序执行更符合开发者习惯。

建议只使用use来添加中间件:

//先执行
app.use(async (c, next) => {
  let start_time = Date.now()
  await next()
  let end_time = Date.now()
  console.log(end_time - start_time)
})

//后执行
app.use(async (c, next) => {
  console.log(c.method, c.path)
  await next()
})

//use可以级联: app.use(m1).use(m2)
//在21.5.4版本以后,不过这个功能其实根本不重要
//因为有titbit-loader扩展,实现的功能要强大的多。

titbit完整的流程图示

npm.io

需要知道的是,其实在内部,body数据接收和解析也都是中间件,只是刻意安排了顺序,分出了pre和use接口。

中间件参数

使用use或者pre接口添加中间件,还支持第二个参数,可以进行精确的控制,传递选项属性:

  • group 路由分组,表示针对哪个分组执行。

  • method 请求方法,可以是字符串或数组,必须大写。

  • name 请求名称,表示只针对此请求执行。

示例:

app.get('/xyz', async c => {
  //...
  //路由分组命名为proxy
}, {group: 'proxy'})

app.use(proxy, {
  method : ['PUT', 'POST', 'GET', 'DELETE', 'OPTIONS'],
  //针对路由分组proxy的请求执行。
  group : 'proxy'
})

pre 在接收body数据之前

使用pre接口添加的中间件和use添加的主要区别就是会在接收body数据之前执行。可用于在接收数据之前的权限过滤操作。其参数和use一致。

为了一致的开发体验,你可以直接使用use接口,只需要在选项中通过pre指定:

let setbodysize = async (c, next) => {
    //设定body最大接收数据为~10k。
    c.maxBody = 10000;
    await next();
};

//等效于app.pre(setbodysize);
app.use(setbodysize, {pre: true});

使用pre可以进行更复杂的处理,并且可以拦截并不执行下一层,比如titbit-toolkit扩展的proxy模块利用这个特性直接实现了高性能的代理服务,但是仅仅作为框架的一个中间件。其主要操作就是在这一层,直接设置了request的data事件来接收数据,并作其他处理,之后直接返回。

根据不同的请求类型动态限制请求体大小

这个需求可以通过pre添加中间件解决:

const app = new titbit({
  //默认最大请求体 ~10M 限制。
  maxBody: 10000000
})

app.pre(async (c, next) => {

  let ctype = c.headers['content-type'] || ''

  if (ctype.indexOf('text/') === 0) {
    //50K
    c.maxBody = 50000
  } else if (ctype.indexOf('application/') === 0) {
    //100K
    c.maxBody = 100000
  } else if (ctype.indexOf('multipart/form-data') < 0) {
    //10K
    c.maxBody = 10000
  }

  await next()

}, {method: ['POST', 'PUT']})

这些参数若同时出现在文件里会显得很复杂,维护也不方便,但是功能很强,所以若要交给程序自动完成则可以大大简化编码的工作。

完整的项目结构搭建,请配合使用titbit-loader,此扩展完成了路由、模型的自动加载和中间件自动编排。titbit-loader

HTTPS

'use strict'

const Titbit = require('titbit')

//只需要传递证书和密钥文件所在路径
const app = new Titbit({
    // './xxx.pem'文件也可以
    cert: './xxx.cert',
    key: './xxx.key'
})

app.run(1234)

同时支持HTTP/2和HTTP/1.1(兼容模式)

兼容模式是利用ALPN协议,需要使用HTTPS才可以,所以必须要配置证书和密钥。

'use strict'

const Titbit = require('titbit')

//只需要传递证书和密钥文件所在路径
const app = new Titbit({
    cert: './xxx.cert',
    key: './xxx.key',
    //启用http2并允许http1,会自动启用兼容模式
    http2: true,
    allowHTTP1: true
})

app.run(1234)

配置选项

应用初始化,完整的配置选项如下,请仔细阅读注释说明。

  {
    //此配置表示POST/PUT提交表单的最大字节数,也是上传文件的最大限制。
    maxBody   : 8000000,

    //最大解析的文件数量
    maxFiles      : 12,

    daemon        : false, //开启守护进程

    /*
      开启守护进程模式后,如果设置路径不为空字符串,则会把pid写入到此文件,可用于服务管理。
    */
    pidFile       : '',

    //是否开启全局日志,true表示开启,这时候会把请求信息输出或者写入到文件
    globalLog: false,

    //日志输出方式:stdio表示输出到终端,file表示输出到文件
    logType : 'stdio',

    //正确请求日志输出的文件路径
    logFile : '',

    //错误请求日志输出的文件路径
    errorLogFile : '',

    //日志文件最大条数
    logMaxLines: 50000,

    //最大历史日志文件数量
    logHistory: 50,

    //自定义日志处理函数
    logHandle: null,

    //开启HTTPS
    https       : false,

    http2   : false,

    allowHTTP1: false,

    //HTTPS密钥和证书的文件路径,如果设置了路径,则会自动设置https为true。
    key   : '',
    cert  : '',

    //服务器选项都写在server中,在初始化http服务时会传递,参考http2.createSecureServer、tls.createServer
    server : {
      handshakeTimeout: 8192, //TLS握手连接(HANDSHAKE)超时
      //sessionTimeout: 350,
    },

    //设置服务器超时,毫秒单位,在具体的请求中,可以再设置请求的超时。
    timeout   : 15000,

    debug     : false,

    //忽略路径末尾的 /
    ignoreSlash: true,

    //启用请求限制
    useLimit: false,

    //最大连接数,0表示不限制
    maxConn : 1024,

    //单个IP单位时间内的最大连接数,0表示不限制
    maxIPRequest: 0,

    //单位时间,默认为1秒
    unitTime : 1,
    
    //展示负载信息,需要通过daemon接口开启cluster集群模式
    loadMonitor : true,

    //负载信息的类型,text 、json、--null
    //json类型是给程序通信使用的,方便接口开发
    loadInfoType : 'text',

    //负载信息的文件路径,如果不设置则输出到终端,否则保存到文件
    loadInfoFile : '',

    //404要返回的数据
    notFound: 'Not Found',
    
    //400要返回的数据
    badRequest : 'Bad Request',

    //控制子进程最大内存使用量的百分比参数,范围从-0.42 ~ 0.36。基础数值是0.52,所以默认值百分比为80%。
    memFactor: 0.28,

    //url最大长度
    maxUrlLength: 2048,

    //请求上下文缓存池最大数量。
    maxpool: 4096,

    //子进程汇报资源信息的定时器毫秒数。
    monitorTimeSlice: 640,

    //在globalLog为true时,全局日志是否记录真实的IP地址,主要用在反向代理模式下。
    realIP: false,

    //允许的最大querystring参数个数。
    maxQuery: 12,

    //是否启用strong模式,启用后会使用process处理rejectionHandled 和 uncaughtException事件,
    //并捕获一些错误:TypeError,ReferenceError,RangeError,AssertionError,URIError,Error。
    strong: false,

    //快速解析querystring,多个同名的值会仅设置第一个,不会解析成数组。
    fastParseQuery: false,
    
    //是否自动解码Query参数,会调用decodeURIComponent函数。
    autoDecodeQuery: true,

    //在multipart格式中,限制单个表单项的最大长度。
    maxFormLength: 1000000,

    /* 错误处理函数,此函数统一收集服务运行时出现的
          tlsClientError、服务器error、secureConnection错误、clientError、运行时的抛出错误。
      errname是一个标记错误信息和出现位置的字符串,统一格式为--ERR-CONNECTION--、--ERR-CLIENT--这种形式。

      通常Node.js抛出错误会有code和message等信息方便识别和排查,也不排除有抛出错误没有code的情况,
        errname可用可不用,但是参数会进行传递。
      通过配置选项传递自定函数即可实现自定义错误收集和处理方式。
    */
    errorHandle: (err, errname) => {
      this.config.debug && console.error(errname, err)
    },

    //最大负载率百分比,默认为75表示当CPU使用率超过75%,则会自动创建子进程。
    //必须通过autoWorker开启自动负载模式才有效。
    maxLoadRate: 75,

    //http2协议的http2Stream超时,若不设置,-1表示和timeout一致。
    streamTimeout: -1,

    //请求超时时间,此超时时间是请求总的时间,主要是为了应对恶意请求。
    //比如,发出大量请求,每个请求每秒发送一个字节,空闲超时不会起作用,则可以长期占有服务器资源。
    //在大量请求时,正常用户无法访问,此攻击属于DDOS。
    requestTimeout: 100000,

  };
  // 对于HTTP状态码,在这里仅需要这两个,其他很多是可以不必完整支持,并且你可以在实现应用时自行处理。
  // 因为一旦能够开始执行,就可以通过运行状态返回对应的状态码。
  // 而在这之前,框架还在为开始执行洋葱模型做准备,不过此过程非常快。

请求上下文

请求上下文就是一个封装了各种请求数据的对象。通过这样的设计,把HTTP/1.1 和 HTTP/2协议的一些差异以及Node.js版本演进带来的一些不兼容做了处理,出于设计和性能上的考虑,对于HTTP2模块,封装请求对象是stream,而不是http模块的IncomingMessage和ServerResponse(封装对象是request和response)。

请求上下文属性和基本描述

属性描述
version协议版本,字符串类型,为'1.1' 或 '2'。
major协议主要版本号,1、2、3分别表示HTTP/1.1 HTTP/2 HTTP/3(目前还没有3)。
maxBody支持的最大请求体字节数,数字类型,默认为初始化时,传递的选项maxBody的值,可以在中间件中根据请求自动设定。
method请求类型,GET POST等HTTP请求类型,大写字母的字符串。
host服务的主机名,就是request.headers.host的值。
protocol协议字符串,不带冒号,'https'、'http'。
path具体请求的路径。
routepath实际执行请求的路由字符串。
queryurl传递的参数。
param路由参数。
files上传文件保存的信息。
bodybody请求体的数据,具体格式需要看content-type,一般为字符串或者对象,也可能是buffer。
port客户端请求的端口号。
ip客户端请求的IP地址,是套接字的地址,如果使用了代理服务器,需要检测x-real-ip或是x-forwarded-for消息头获取真正的IP。
headers指向request.headers。
isUpload是否为上传文件请求,此时就是检测消息头content-type是否为multipart/form-data格式。
name路由名称,默认为空字符串。
group路由分组,默认为空字符串。
replyHTTP/1.1协议,指向response,HTTP/2 指向stream。
requestHTTP/1.1 就是http模块request事件的参数IncomingMessage对象,HTTP/2 指向stream对象。
box默认为空对象,可以添加任何属性值,用来动态传递给下一层组件需要使用的信息。
service用于依赖注入的对象,指向app.service。
res一个对象包括encoding、body属性,用来暂存返回数据的编码和具体数据。
ext提供了一些助手函数,具体参考wiki。
send函数,用来设置res.body的数据并支持第二个参数作为状态码,默认状态码为200。
moveFile函数,用来移动上传的文件到指定路径。
status函数,设置状态码。
setHeader函数,设置消息头。
removeHeader函数,移除等待发送的消息头。
getFile函数,获取上传的文件信息,其实就是读取files属性的信息。
sendHeader函数,用于http2发送消息头,setHeader只是缓存了设置的消息头。对于http/1.1来说,为了保持代码一致,只是一个空函数。
user给用户登录提供一个标准属性,默认之为null。
pipe函数,流式响应数据,示例:await ctx.setHeader('content-type', 'text/html').pipe('./index.html')

注意:send函数只是设置ctx.res.body属性的值,在最后才会返回数据。和直接进行ctx.res.body赋值没有区别,只是因为函数调用如果出错会更快发现问题,而设置属性值写错了就是添加了一个新的属性,不会报错但是请求不会返回正确的数据。

依赖注入

请求上下文中有一项是service,指向的是app.service。当初始化app后,一切需要开始就初始化好的数据、实例等都可以挂载到app.service。

'use strict';

const titbit = require('titbit');

var app = new titbit({
  debug: true
});

//有则会覆盖,没有则添加。
app.addService('name', 'first');
app.addService('data', {
  id : 123,
  ip : '127.0.0.1'
});

/*
这可能看不出什么作用,毕竟在一个文件中,直接访问变量都可以,如果要做模块分离,就变得非常重要了。
*/
app.get('/info', async c => {

  c.res.body = {
    name : c.service.name,
    data : c.service.data
  };

});

app.run(1234);

扩展请求上下文

如果需要给请求上下文的对象添加扩展支持,可以通过app实例的httpServ.context实现。此属性是请求上下文的构造函数。

示例:

'use strict'
const titbit = require('titbit')
const app = new titbit({
    debug: true
})

//this即表示请求上下文
app.httpServ.context.prototype.testCtx = function () {
    console.log(this.method, this.path)
}

app.get('/test', async ctx => {
    ctx.testCtx()
})

app.run(1234)

app.isMaster和app.isWorker

Node.js在v16.x版本开始,cluster模块推荐使用isPrimary代替isMaster,不过isMaster仍然是可用的,在titbit初始化app实例之后,app上有两个getter属性:isMaster和isWorker。作用和cluster上的属性一致,其目的在于:

  • 在代码中不必再次编写const cluster = require('cluster')。

  • 屏蔽未来cluster可能的不兼容更改,增强代码兼容性。

daemon和run

run接口的参数为:port、host。host默认为0.0.0.0。还可以是sockPath,就是.sock文件路径,本质上是因为http的listen接口支持。使用.sock,host就被忽略了。

daemon的前两个参数和run一致,支持第三个参数是一个数字,表示要使用多少个子进程处理请求。默认为0,这时候会自动根据CPU核心数量创建子进程。之后,会保持子进程数量的稳定,在子进程意外终止后会创建新的子进程补充。

cluster模式,最多子进程数量不会超过CPU核心数量的2倍。

示例:

//host默认为0.0.0.0,端口1234
app.run(1234)

//监听localhost,只能本机访问
app.run(1234, 'localhost')

//使用两个子进程处理请求,host默认为0.0.0.0
app.daemon(1234, 2)

//使用3个子进程处理请求
app.daemon(1234, 'localhost', 3)

日志

框架本身提供了全局日志功能,当使用cluster模式时(使用daemon接口运行服务),使用初始化选项globoalLog可以开启全局日志,并且可以指定日志文件,在单进程模式,会把日志输出到终端,此时利用输出重定向和错误输出重定向仍然可以把日志保存到文件。

注意:只有使用daemon运行,采用cluster模式,才可以把日志保存到文件,run运行后的单进程仅仅是输出到屏幕,可以利用IO重定向保存到文件。

除了保存到文件和输出到终端进行调试,还可以利用logHandle选项设置自己的日志处理函数。

设置了logHandle,logFile和errorLogFile会失效,具体请看代码。

示例:

const titbit = require('titbit')

const app = new titbit({
  debug: true,
  //全局日志开启
  globalLog: true,

  //表示输出到文件,默认为stdio表示输出到终端。
  logType: 'file'

  //返回状态码为2xx或者3xx
  logFile : '/tmp/titbit.log',

  //错误的日志输出文件,返回状态码4xx或者5xx
  errorLogFile: '/tmp/titbit-error.log',

  //自定义处理函数,此时logFile和errorLogFile会失效。
  //接收参数为(worker, message)
  //worker具体参考cluster的worker文档
  /*
    msg为日志对象,属性:
      {
        type : '_log',
        success : true,
        log : '@ GET | https://localhost:2021/randst | 200 | 2020-10-31 20:27:7 | 127.0.0.1 | User-Agent'
      }
  */
  logHandle : (w, msg) => {
    console.log(w.id, msg)
  }

})

app.daemon(1234, 3)

使用中间件的方式处理日志和全局日志并不冲突,而如果要通过中间件进行日志处理会无法捕获没有路由返回404的情况,因为框架会先查找路由,没有则会返回。这时候,不会有请求上下文的创建,直接返回请求,避免无意义的操作。

而且,这样的方式其实更加容易和cluster模式结合,因为在内部就是利用master和worker的通信机制实现的。

消息事件处理

基于message事件,在daemon模式(基于cluster模块),提供了一个setMsgEvent函数用于获取子进程发送的事件消息并进行处理。

这要求worker进程发送的消息必须是一个对象,其中的type属性是必需的,表示消息事件的名称。其他字段的数据皆可以自定义。

使用方式如下:

const titbit = require('titbit')
const cluster = require('cluster')

const app = new titbit({
  debug: true,
  loadInfoFile: '/tmp/loadinfo.log'
})

if (cluster.isMaster) {
  app.setMsgEvent('test-msg', (worker, msg, handle) => {
    //子进程中会通过message事件收到消息
    worker.send({
      id : worker.id,
      data : 'ok'
    })

    console.log(msg)
  })
} else {
  //接收worker.send发送的消息
  process.on('message', msg => {
    console.log(msg)
  })

  setIneterval(() => {
    process.send({
      type : 'test-msg',
      pid : process.pid,
      time : (new Date()).toLocaleString()
    })
  }, 1000)

}

比较麻烦的地方在于,worker进程发送消息比较复杂,在22.4.0版本开始,提供了一个send方法用于快速发送消息。只有在worker进程中才会发送给master进程,所以不必额外进行worker进程检测。

app.send 和 app.workerMsg

现在让我们来改写上面代码的worker进程发送消息的部分:

const titbit = require('titbit')

const app = new titbit({
  debug: true,
  loadInfoFile: '/tmp/loadinfo.log'
})

//master进程注册消息事件类型,worker进程不会执行。
app.setMsgEvent('test-msg', (worker, msg, handle) => {
  //子进程中会通过message事件收到消息
  worker.send({
    id : worker.id,
    data : 'ok'
  })

  console.log(msg)
})

//只有worker进程才会监听。
app.workerMsg(msg => {
  console.log(msg)
})

cluster.isWorker
    &&
setInterval(() => {
  //只有worker会执行。
  app.send('test-msg', {
    pid: process.pid,
    time: (new Date).toLocaleString()
  })

}, 1000)

app.daemon(1234, 2)

自动调整子进程数量

通过daemon传递的参数作为基本的子进程数量,比如:

//使用2个子进程处理请求。
app.daemon(1234, 2)

如果需要自动根据负载创建子进程,并在负载空闲时终止进程,维持基本的数量,可以使用autoWorker接口来设置一个最大值,表示最大允许多少个子进程处理请求,这个值必须要比基本的子进程数量大才会生效。

//最大使用9个子进程处理请求。
app.autoWorker(9)

//...

app.daemon(1234, 2)

当负载过高时,会自动创建子进程,并且在空闲一段时间后,会自动终止连接数量为0的子进程,恢复到基本的数值。

此功能在v21.9.6+版本可用。但是请尽可能使用最新版本,此功能在后续版本经历几次升级改进,提高了稳定性和性能,保证在严苛的业务逻辑上仍然能够提供稳定的服务支持。


strong模式

通过strong选项可以开启strong模式,此模式会监听uncaughtException和unhandledRejection事件,保证程序稳定运行。最简单的情况,你只需要给strong设置为true即可。

strong模式的所有功能都可以通过process模块自行实现,此处只是简化了处理方式而已。

'use strict';

const titbit = require('titbit');

setTimeout(() => {
  //在定时器的循环里抛出异常
  throw new Error(`test error`)
}, 2000);

const app = new titbit({
    //调试模式,输出错误信息。
    debug: true,
    //开启strong模式
    strong: true
});

app.run(1234);

默认情况下,strong模式会捕获以下错误:

'TypeError', 'ReferenceError', 'RangeError', 'AssertionError', 'URIError', 'Error'

但是,你可能需要自定义处理方式,这可以通过给strong传递object类型的选项来实现。

//核心代码示例
const app = new titbit({
    //调试模式,输出错误信息。
    debug: true,
    //开启strong模式
    strong: {
      //静默行为,不会输出错误。
      quiet: true,
      //自定义错误处理函数
      errorHandle: (err, errname) => {
        //....
      },

      //要捕获的错误有哪些
      catchErrors: [
        'TypeError', 'URIError', 'Error', 'RangeError'
      ]

    }
});

同时运行http和https?

请注意这是打问号的,你最好不要在正式环境这样做,如果你已经开启了https,则不需要http,而且前端应用有些功能在不启用https是无法使用的。

如果你需要这样功能,也许是用于测试,那么你可以这样做:

'use strict'

const Titbit = require('titbit')
const http = require('node:http')
const https = require('https')

const app = new Titbit({
    //启用调试
    debug: true,
})

//以下都是http/1.1的服务,若要同时支持http2,需要启用http2服务并兼容http1,若有需要请使用titbit-httpc扩展。

//这种情况下,你需要自己设定相关事件的监听处理。

let http_server = http.createServer(app.httpServ.onRequest())
let https_server = https.createServer(app.httpServ.onRequest())

http_server.listen(2025)
https_server.listen(2026)

需要注意的是,这种情况无法再去支持http2,但是你可以使用http2去兼容http1。

其他

  • titbit在运行后,会有一个最后包装的中间件做最终的处理,所以设置c.res.body的值就会返回数据,默认会检测一些简单的文本类型并自动设定content-type(text/plain,text/html,application/json)。注意这是在你没有设置content-type的情况下进行。

  • 默认会限制url的最大长度,也会根据硬件情况设定一个最大内存使用率。

  • 这一切你都可以通过配置选项或是中间件来进行扩展和重写,既有限制也有自由。

  • 它很快,并且我们一直在都在关注优化。如果你需要和其他对比测试,请都添加多个中间件,并且都添加上百个路由,然后测试对比。

  • 提供了一个sched函数用来快速设置cluster模式的调度方式,支持参数为'rr'或'none',本质就是设置cluster.schedulingPolicy的值。

框架在初始化会自动检测内存大小并设定相关上限,你可以在初始化后,通过更改secure中的属性来更改限制,这需要你使用daemon接口,也就是使用master管理子进程的模式。

'use strict'

const Titbit = require('titbit');

let app = new Titbit();

/*
 以下操作可以通过选项memFactor控制,请参考上文的配置选项部分。
 */

//最大内存设定为600M,但是只有在连接数为0时才会自动重启。
app.secure.maxmem = 600_000_000;

//必须要重启的最大内存上限设定为900M,注意这是总的内存使用,包括你用Buffer申请的内存。
//这个值一般要比maxmem大,当内存使用超过maxmem设置的值,
//但是连接不为0,这时候如果继续请求超过diemem设置的值,则会直接重启进程。
app.secure.diemem = 900_000_000;

//最大内存使用设置为800M,这个就是程序运行使用的内存,但是不包括Buffer申请的内存。
app.secure.maxrss = 800_000_000;

app.get('/', async c => {
  c.send('ok');
})

app.daemon(8008, 2);

注意,这需要你开启loadMonitor选项,这是默认开启的,除非你设置为false

在服务始化时,会根据系统的可用内存来进行自动的设置,除非你必须要自己控制,否则最好是使用默认的配置。

24.0.3-beta3

19 days ago

24.0.3-beta2

19 days ago

24.0.3-beta5

19 days ago

24.0.3-beta4

19 days ago

24.0.3-beta

19 days ago

24.0.3

19 days ago

24.0.2-beta

22 days ago

24.0.1-beta

22 days ago

24.0.1-beta2

22 days ago

24.0.1-beta3

22 days ago

24.0.2

22 days ago

24.0.1

22 days ago

24.0.0-beta

24 days ago

24.0.0

23 days ago

23.6.6

4 months ago

23.6.5

4 months ago

23.6.5-beta6

4 months ago

23.6.5-beta5

4 months ago

23.6.5-beta4

4 months ago

23.6.3

4 months ago

23.6.5-beta2

4 months ago

23.6.5-beta3

4 months ago

23.6.5-beta

4 months ago

23.6.3-beta2

4 months ago

23.6.3-beta

4 months ago

23.6.2

4 months ago

23.6.2-beta

4 months ago

23.6.1

4 months ago

23.6.0

5 months ago

23.5.9

5 months ago

23.5.9-beta

5 months ago

23.5.7

5 months ago

23.5.6

5 months ago

23.5.8

5 months ago

23.5.8-beta

5 months ago

23.5.8-beta2

5 months ago

23.5.6-beta

5 months ago

23.5.2

5 months ago

23.5.5

5 months ago

23.5.2-beta

5 months ago

23.5.3-beta

5 months ago

23.4.0

6 months ago

23.4.2

6 months ago

23.4.1

6 months ago

23.3.4-beta2

6 months ago

23.3.4-beta3

6 months ago

23.5.1

6 months ago

23.5.0

6 months ago

23.3.6-beta2

6 months ago

23.4.0-beta11

6 months ago

23.4.0-beta10

6 months ago

23.3.2-beta

6 months ago

23.4.1-beta

6 months ago

23.2.5-beta

6 months ago

23.2.6-beta

6 months ago

23.3.2-beta2

6 months ago

23.4.0-beta

6 months ago

23.5.1-beta

6 months ago

23.3.2-beta4

6 months ago

23.3.2-beta3

6 months ago

23.3.6-beta

6 months ago

23.5.0-beta3

6 months ago

23.5.0-beta4

6 months ago

23.5.0-beta2

6 months ago

23.5.0-beta

6 months ago

23.3.4-beta

6 months ago

23.5.1-beta4

6 months ago

23.5.1-beta2

6 months ago

23.5.1-beta3

6 months ago

23.2.6-beta2

6 months ago

23.2.6-beta3

6 months ago

23.2.6-beta4

6 months ago

23.2.6

6 months ago

23.2.5

6 months ago

23.4.0-beta3

6 months ago

23.4.0-beta2

6 months ago

23.3.1

6 months ago

23.3.0

6 months ago

23.3.3

6 months ago

23.3.2

6 months ago

23.3.5

6 months ago

23.3.4

6 months ago

23.3.1-beta

6 months ago

23.4.0-beta9

6 months ago

23.4.0-beta8

6 months ago

23.4.0-beta5

6 months ago

23.4.0-beta4

6 months ago

23.4.0-beta7

6 months ago

23.4.0-beta6

6 months ago

23.2.4

7 months ago

23.2.3

8 months ago

23.2.4-beta

7 months ago

23.2.4-beta3

7 months ago

23.2.4-beta2

7 months ago

23.2.4-beta5

7 months ago

23.2.4-beta4

7 months ago

23.2.1-beta1

1 year ago

23.2.2-beta1

1 year ago

23.1.9-beta

1 year ago

23.2.2-beta

1 year ago

23.2.1-beta

1 year ago

23.1.9

1 year ago

23.2.2

1 year ago

23.2.1

1 year ago

23.2.0

1 year ago

23.1.6-beta

2 years ago

23.1.3-beta3

2 years ago

23.1.3-beta2

2 years ago

23.1.8-beta

1 year ago

23.1.6-beta2

2 years ago

23.1.6-beta3

2 years ago

23.1.7-beta4

1 year ago

23.1.7-beta2

1 year ago

23.1.5-beta

2 years ago

23.1.7-beta3

1 year ago

23.1.5-beta2

2 years ago

23.1.3

2 years ago

23.1.5

2 years ago

23.1.4

2 years ago

23.1.7

1 year ago

23.1.6

2 years ago

23.1.8

1 year ago

23.1.7-beta

1 year ago

23.1.3-beta

2 years ago

23.0.8-beta

2 years ago

23.1.2-beta

2 years ago

23.1.1-beta6

2 years ago

23.0.4-beta

2 years ago

23.0.7-beta2

2 years ago

23.1.1-beta

2 years ago

23.1.1-beta2

2 years ago

23.1.1-beta3

2 years ago

23.1.1-beta4

2 years ago

23.1.1-beta5

2 years ago

23.0.7-beta

2 years ago

23.0.4-beta2

2 years ago

23.0.4-beta6

2 years ago

23.0.4-beta7

2 years ago

23.0.4-beta4

2 years ago

23.0.4-beta5

2 years ago

23.0.4

2 years ago

23.0.6

2 years ago

23.0.5

2 years ago

23.0.8

2 years ago

23.0.7

2 years ago

23.0.9

2 years ago

23.1.2

2 years ago

23.1.1

2 years ago

23.1.0

2 years ago

23.0.9-beta

2 years ago

23.0.3-beta

2 years ago

23.0.3

2 years ago

23.0.3-beta2

2 years ago

23.0.1-beta

2 years ago

23.0.2-beta2

2 years ago

23.0.2

2 years ago

23.0.1

2 years ago

23.0.2-beta

2 years ago

22.8.2

2 years ago

23.0.0

2 years ago

22.7.9

2 years ago

22.7.8

2 years ago

22.8.1

2 years ago

22.8.0

2 years ago

22.8.1-beta

2 years ago

22.7.7

2 years ago

22.7.6

2 years ago

22.7.5

2 years ago

22.7.3

2 years ago

22.7.3-beta

2 years ago

22.7.6-beta

2 years ago

22.7.2

2 years ago

22.7.2-beta

2 years ago

22.7.1-beta

3 years ago

22.7.1

3 years ago

22.6.9

3 years ago

22.6.8

3 years ago

22.6.7

3 years ago

22.6.6

3 years ago

22.6.5

3 years ago

22.6.4

3 years ago

22.6.3

3 years ago

22.6.2

3 years ago

22.6.1

3 years ago

22.6.0

3 years ago

22.6.2-beta10

3 years ago

22.6.2-beta11

3 years ago

22.6.2-beta12

3 years ago

22.6.2-beta13

3 years ago

22.6.2-beta14

3 years ago

22.6.2-beta15

3 years ago

22.6.2-beta16

3 years ago

22.7.0

3 years ago

22.6.8-beta

3 years ago

22.6.8-beta6

3 years ago

22.6.8-beta4

3 years ago

22.6.8-beta3

3 years ago

22.6.8-beta2

3 years ago

22.6.5-beta

3 years ago

22.6.3-beta2

3 years ago

22.6.2-beta5

3 years ago

22.6.2-beta7

3 years ago

22.6.2-beta8

3 years ago

22.6.2-beta2

3 years ago

22.6.2-beta3

3 years ago

22.6.2-beta4

3 years ago

22.6.2-beta9

3 years ago

22.6.5-beta3

3 years ago

22.6.5-beta2

3 years ago

22.6.5-beta5

3 years ago

22.6.5-beta4

3 years ago

22.6.5-beta7

3 years ago

22.6.5-beta6

3 years ago

22.6.9-beta

3 years ago

22.6.2-beta

3 years ago

22.6.9-beta3

3 years ago

22.6.9-beta2

3 years ago

22.6.7-beta

3 years ago

22.6.0-beta

3 years ago

22.6.0-beta2

3 years ago

22.7.0-beta

3 years ago

22.6.3-beta

3 years ago

22.6.7-beta3

3 years ago

22.6.7-beta2

3 years ago

22.6.7-beta8

3 years ago

22.6.7-beta5

3 years ago

22.6.7-beta4

3 years ago

22.6.7-beta7

3 years ago

22.6.7-beta6

3 years ago

22.5.9-beta

3 years ago

22.5.9

3 years ago

22.5.8-beta

3 years ago

22.5.8

3 years ago

22.5.7-beta2

3 years ago

22.5.7-beta

3 years ago

22.5.7

3 years ago

22.5.6-beta

3 years ago

22.5.6-beta2

3 years ago

22.5.6-beta3

3 years ago

22.5.6

3 years ago

22.5.5-beta

3 years ago

22.5.5-beta3

3 years ago

22.5.5-beta2

3 years ago

22.5.3-beta11

3 years ago

22.5.3-beta10

3 years ago

22.5.3-beta12

3 years ago

22.5.3-beta7

3 years ago

22.5.3-beta8

3 years ago

22.5.3-beta5

3 years ago

22.5.3-beta6

3 years ago

22.5.5

3 years ago

22.5.4

3 years ago

22.5.3-beta9

3 years ago

22.5.3

3 years ago

22.5.3-beta

3 years ago

22.5.3-beta3

3 years ago

22.5.3-beta2

3 years ago

22.5.2-beta

3 years ago

22.5.2

3 years ago

22.5.1

3 years ago

22.5.0-beta

3 years ago

22.5.0

3 years ago

22.4.9

3 years ago

22.4.8

3 years ago

22.4.6

3 years ago

22.4.5

3 years ago

22.4.5-beta2

3 years ago

22.4.4-beta2

3 years ago

22.4.4-beta3

3 years ago

22.4.5-beta

3 years ago

22.4.4

3 years ago

22.4.2-beta

3 years ago

22.4.3-beta2

3 years ago

22.4.4-beta

3 years ago

22.4.3

3 years ago

22.4.2

3 years ago

22.4.3-beta

3 years ago

22.4.0-beta

3 years ago

22.4.1

3 years ago

22.4.0

3 years ago

22.3.6

3 years ago

22.3.6-beta3

3 years ago

22.3.6-beta2

3 years ago

22.3.6-beta5

3 years ago

22.3.6-beta4

3 years ago

22.3.6-beta6

3 years ago

22.3.6-beta

3 years ago

22.3.5-beta3

3 years ago

22.3.5-beta2

3 years ago

22.3.5-beta

3 years ago

22.3.5

3 years ago

22.3.4

3 years ago

22.3.3

3 years ago

22.3.4-beta

3 years ago

22.3.2

3 years ago

22.3.2-beta

3 years ago

22.3.2-beta2

3 years ago

22.3.1-beta2

3 years ago

22.3.1-beta3

3 years ago

22.3.1-beta

3 years ago

22.3.0

3 years ago

22.3.1

3 years ago

22.3.0-beta

3 years ago

22.2.8-beta

3 years ago

22.2.9

3 years ago

22.2.8

3 years ago

22.2.7-beta

3 years ago

22.2.7

3 years ago

22.2.6-beta

3 years ago

22.2.6

3 years ago

22.2.4-beta

3 years ago

22.2.4-beta2

3 years ago

22.2.5

3 years ago

22.2.4

3 years ago

22.2.3

3 years ago

22.2.4-beta4

3 years ago

22.2.4-beta3

3 years ago

22.2.2

3 years ago

22.2.2-beta

3 years ago

22.2.1-beta

3 years ago

22.2.1

3 years ago

22.2.0

3 years ago

22.1.8-beta2

3 years ago

22.1.9

3 years ago

22.1.8

3 years ago

22.1.7

3 years ago

22.1.8-beta

3 years ago

22.1.6

3 years ago

22.1.5

3 years ago

22.1.4-beta2

3 years ago

22.1.4-beta3

3 years ago

22.1.4

3 years ago

22.1.3

3 years ago

22.1.4-beta

3 years ago

22.1.2

3 years ago

22.1.1

3 years ago

22.1.1-beta

3 years ago

22.1.0

3 years ago

22.0.9

3 years ago

22.0.8

3 years ago

22.0.7

3 years ago

22.0.6

3 years ago

22.0.5

3 years ago

22.0.4

3 years ago

22.0.3

3 years ago

22.0.2

3 years ago

22.0.1

3 years ago

22.0.0

3 years ago

22.0.2-beta

3 years ago

22.0.1-beta

3 years ago

21.10.9

3 years ago

21.10.9-beta2

3 years ago

21.10.9-bet2

3 years ago

21.10.9-beta

3 years ago

21.10.8

3 years ago

21.10.7

3 years ago

21.10.6-beta3

3 years ago

21.10.6

3 years ago

21.10.6-beta2

3 years ago

21.10.6-beta

3 years ago

21.10.4-beta3

3 years ago

21.10.4-beta2

3 years ago

21.10.5

3 years ago

21.10.4

3 years ago

21.10.4-beta

3 years ago

21.10.3

3 years ago

21.10.2

3 years ago

21.10.2-beta3

3 years ago

21.10.2-beta4

3 years ago

21.10.2-beta

3 years ago

21.10.0-beta

3 years ago

21.10.1

3 years ago

21.10.0

3 years ago

21.9.10

3 years ago

21.9.9

3 years ago

21.9.7-beta

3 years ago

21.9.8

3 years ago

21.9.7

3 years ago

21.9.6

3 years ago

21.9.6-beta6

3 years ago

21.9.6-beta7

3 years ago

21.9.6-beta4

3 years ago

21.9.6-beta3

3 years ago

21.9.6-beta

3 years ago

21.9.6-beta2

3 years ago

21.9.5

3 years ago

21.9.5-beta

3 years ago

21.9.4

3 years ago

21.9.3

3 years ago

21.9.2

3 years ago

21.9.1

3 years ago

21.9.0

3 years ago

21.8.9

3 years ago

21.8.8

3 years ago

21.8.7

3 years ago

21.8.6

3 years ago

21.8.5

3 years ago

21.8.4

3 years ago

21.8.3

3 years ago

21.8.2

3 years ago

21.8.1

3 years ago

21.8.0

3 years ago

21.7.8

3 years ago

21.7.7

3 years ago

21.7.6

3 years ago

21.7.5

3 years ago

21.7.4

3 years ago

21.7.3

3 years ago

21.7.2

3 years ago

21.7.1

3 years ago

21.7.0

3 years ago

21.6.4

3 years ago

21.6.3

3 years ago

21.6.2

3 years ago

21.6.1

4 years ago

21.5.9

4 years ago

21.6.0

4 years ago

21.5.8

4 years ago

21.5.7

4 years ago

21.5.6

4 years ago

21.5.5

4 years ago

21.5.4

4 years ago

21.5.3

4 years ago

21.5.2

4 years ago

21.5.1

4 years ago

21.5.0

4 years ago

21.5.0-beta

4 years ago

21.4.9

4 years ago

21.4.8

4 years ago

21.4.7

4 years ago

21.4.6

4 years ago

21.4.5

4 years ago

21.4.4

4 years ago

21.4.3

4 years ago

21.4.2

4 years ago

21.4.0

4 years ago

21.4.1

4 years ago

21.3.7

4 years ago

21.3.6

4 years ago

21.3.5

4 years ago

21.3.4

4 years ago

21.3.3

4 years ago

21.3.2

4 years ago

21.3.1

4 years ago

21.3.0

4 years ago

21.2.8

4 years ago

21.2.7

4 years ago

21.2.6

4 years ago

21.2.4

4 years ago

21.2.5

4 years ago

21.2.3

4 years ago

21.2.2

4 years ago

21.2.0

4 years ago

21.1.5

4 years ago

21.1.4

4 years ago

21.1.3

4 years ago

21.1.3-beta

4 years ago

21.1.2

4 years ago

21.1.1

4 years ago

21.1.0

4 years ago

21.0.2

4 years ago

21.0.1

4 years ago

21.0.4

4 years ago

21.0.3

4 years ago

20.10.2-beta

4 years ago

21.0.0

4 years ago

21.0.6

4 years ago

21.0.5

4 years ago

21.0.7

4 years ago

20.10.2

4 years ago

20.10.3

4 years ago

21.0.0-beta

4 years ago

20.10.1

4 years ago

20.10.0

4 years ago

20.9.2

4 years ago

20.9.1

4 years ago

20.9.0

4 years ago

20.8.3

4 years ago

20.8.1

4 years ago

20.8.2

4 years ago

20.6.71

4 years ago

20.5.141

4 years ago

20.5.133

4 years ago

20.5.131

4 years ago

20.5.132

4 years ago

20.5.51

4 years ago

20.5.24

4 years ago

20.5.23

4 years ago

20.5.22

4 years ago

20.5.21

4 years ago

20.5.12

4 years ago

20.5.11

4 years ago

20.4.303

4 years ago

20.4.302

4 years ago

20.4.292

4 years ago

20.4.293

4 years ago

20.4.301

4 years ago

20.4.291

4 years ago

20.4.281

4 years ago

6.3.1

4 years ago

6.2.1

4 years ago

6.2.0

4 years ago

6.2.3

4 years ago

6.2.2

4 years ago

6.3.0

4 years ago

6.1.6

4 years ago

6.1.7

4 years ago

6.1.4

4 years ago

6.1.5

4 years ago

6.1.3

4 years ago

6.1.2

4 years ago

6.1.1

4 years ago

6.1.0

4 years ago

6.0.4

4 years ago

6.0.3

4 years ago

6.0.2

4 years ago

6.0.1

4 years ago

6.0.1-beta

4 years ago

5.3.4

4 years ago

5.3.3

4 years ago

5.3.2

4 years ago

5.3.1

4 years ago

5.3.1-beta

4 years ago

5.3.0-beta

4 years ago

5.2.2

4 years ago

5.2.1

4 years ago

5.2.0

4 years ago

5.1.7

4 years ago

5.1.6

4 years ago

5.1.5

4 years ago

5.1.4

4 years ago

5.1.3

4 years ago

5.1.2

4 years ago

5.1.1

4 years ago

5.1.0

4 years ago

5.0.3

4 years ago

5.0.2

4 years ago

5.0.1

4 years ago

5.0.0

4 years ago

5.0.0-beta

4 years ago

4.2.4

4 years ago

4.2.3

4 years ago

4.2.2

4 years ago

4.2.1

4 years ago

4.2.0

4 years ago

4.1.9

4 years ago

4.1.8

4 years ago

4.1.7

5 years ago

4.1.6

5 years ago

4.1.5

5 years ago

4.1.4

5 years ago

4.1.3

5 years ago

4.1.2

5 years ago

4.1.1

5 years ago

4.1.0

5 years ago

4.0.1

5 years ago

4.0.0

5 years ago

3.5.0

5 years ago

3.4.9

5 years ago

3.4.8

5 years ago

3.4.7

5 years ago

3.4.6

5 years ago

3.4.5

5 years ago

3.4.4

5 years ago

3.4.3

5 years ago

3.4.2

5 years ago

3.4.1

5 years ago

3.4.0

5 years ago

3.3.5

5 years ago

3.3.4

5 years ago

3.3.3

5 years ago

3.3.2

5 years ago

3.3.1

5 years ago

3.3.0

5 years ago

3.2.3

5 years ago

3.2.2

5 years ago

3.2.1

5 years ago

3.2.0

5 years ago

3.1.10

5 years ago

3.1.9

5 years ago

3.1.8

5 years ago

3.1.7

5 years ago

3.1.6

5 years ago

3.1.5

5 years ago

3.1.4

5 years ago

3.1.3

5 years ago

3.1.2

5 years ago

3.1.1

5 years ago

3.1.0

5 years ago

3.0.4

5 years ago

3.0.3

5 years ago

3.0.2

5 years ago

3.0.1

5 years ago

2.0.0

5 years ago

1.3.8

5 years ago

1.3.7

5 years ago

1.3.6

5 years ago

1.3.5

5 years ago

1.3.4

5 years ago

1.3.3

5 years ago

1.3.2

5 years ago

1.3.1

5 years ago

1.3.0

5 years ago

1.2.3

5 years ago

1.2.2

5 years ago

1.2.1

5 years ago

1.1.0

5 years ago

1.0.0

5 years ago