3.0.0 • Published 3 years ago

doio v3.0.0

Weekly downloads
55
License
ISC
Repository
-
Last release
3 years ago

doio

doio是运行于服务端的Web框架,是从titbit衍生的框架,一些集成的功能做了模块分离,并独立发布了扩展。通常doio会做一些新的尝试,不过仍然可以保证稳定。考虑使用的易用和功能完整度,你应该使用titbit,它既不是重型框架,也不简单过头,并且提供一个足够使用的功能集。

完全兼容titbit和titbit-loader的版本停留在了1.2.8。从2.0开始,完全进行了更新。

Node.js的Web开发框架,同时支持HTTP/1.1和HTTP/2协议, 提供了简洁却极具表现力的中间件机制。

核心功能:

  • 中间件模式
  • 路由命名和分组
  • 中间件分组和规则过滤
  • 开启守护进程:使用cluster模块
  • 自动解析body数据
  • 支持通过配置启用HTTP/1.1或是HTTP/2服务
  • 支持配置启用HTTPS服务(HTTP/2服务必须要开启HTTPS)
  • 限制请求数量
  • 限制一段时间内单个IP的最大访问次数
  • IP黑名单和IP白名单

!注意

请使用最新版本。

安装

npm i doio

最小示例

'use strict';

const doio = require('doio');

const app = new doio();

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

app.run(2022);

//run接口和http模块的listen接口参数一致。

获取URL参数和表单数据

'use strict';

const doio = require('doio');

var app = new doio();

var {router} = app;

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

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

app.run(2019);

路由参数

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);

任意路由

//使用 * 表示任意路径,/static后出现的任意路径都会被解析到c.param.startPath。

const doio = require('doio');

const app = new doio();

app.get('/static/*', async c => {
  let path = c.param.starPath;
  c.res.body = path;
});
app.run(2022);
请求/static/css/a.css
返回结果:
    css/a.css

通过c.param.starPath获取的路径并不带有static。

上传文件

默认会解析上传的文件,如果你真的需要自己来控制,或者是需要其他特殊的处理流程,可以通过在外面用一层中间件来改变。这在后面dataHandle部分有说明。

解析后的文件数据在c.files中存储,想知道具体结构请往下看。

'use strict';

const doio = require('doio');

var app = new doio();

//添加中间件过滤上传文件的大小,后面有中间件详细说明。
app.use(async (c, next) => {

  if (c.name !== 'upload-image') {
    return await next();
  }

  //解析后的文件在c.files中存储,通过getFile可以方便获取文件数据。
  let upf = c.getFile('image');
  if (!upf) {
    c.res.body = 'file not found';
    return ;
  } else if (upf.data.length > 2000000) {
    c.res.body = 'max file size: 2M';
    return ;
  }
  await next();

});

app.post('/upload', async c => {
  let f = c.getFile('image');
  try {
    
    //通过sha1生成不会冲突的文件名,extName是解析文件扩展名的助手函数。
    let fname = c.helper.sha1(`${Date.now()}${Math.random()}`) + c.helper.extName(f.filename);

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

app.run(2022);

c.files数据结构

{
  "image" : [
    {
      'content-type': CONTENT_TYPE,
      filename: ORIGIN_FILENAME,
      start : START,
      end   : END,
      length: LENGTH
    },
    ...
  ],

  "video" : [
    {
      'content-type': CONTENT_TYPE,  //文件类型
      filename: ORIGIN_FILENAME //原始文件名
      start : START, //ctx.rawBody开始的索引位置
      end   : END,   //ctx.rawBody结束的索引位置
      length: LENGTH,  //文件长度,字节数
    },
    ...
  ]
}

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

中间件

中间件是一个很有用的模式,不同语言实现起来多少还是有些区别的,这个框架采用了一个有些不同的设计,目前来说运行很好,而且它很快。

中间件图示:

npm.io

此框架的中间件设计是从titbit的设计中演变过来的,但是做了更多的精简,简洁却不失灵活,举重若轻,就像蚂蚁的力量,猎豹的速度。在设计层面上,并不是根据中间件函数的数组取出来动态封装,递归调用,而是在服务运行之前,已经确定了执行链条。

使用方式:

app.use(async (c, next) => {
    console.log('before');
    await next();
    console.log('after');
});

app.use(async (c, next) => {
    
    let start_time = Date.now();
    await next();
    let end_time = Date.now();

    console.log('运行时间:', end_time - start_time, 'ms');

});

使用use添加的中间件按照添加顺序执行,这更方便编码。

中间件参数

你可以通过use的第二个参数控制过滤条件,传递的是一个函数对象,如果返回false,则表示检测失败,此时不会执行此中间件,直接跳转到下一层,任何非false的返回值都表示通过。

示例

let m1 = async (c, next) => {
  console.log('m1 in');
  await next();
  console.log('m1 out');
};

//只针对POST请求才执行
let m1filter = (ctx) => {
  if (ctx.method !== 'POST') {
    return false;
  }
  return true;
};

app.use(m1, mifilter);

pre:在处理data事件以前

使用use添加的中间件在处理完body数据之后,这时候已经可以获取到解析后的body数据。而使用pre添加的中间件,在body数据接收以前,这时候还没有接收数据,在此之前的处理逻辑要尽可能快,通常来说你可以进行更多的验证工作以及动态设定最大接收请求体的数据量。这可以通过ctx.maxBody属性设置,以字节为单位。默认它就是初始化配置的maxBody设置。

//根据路由分组设定最大接收的body数据量

app.pre(async (c, next) => {
  if (c.group === 'admin') {
    //~30M
    c.maxBody = 30000000;
  } else {
    //~10M
    c.maxBody = 10000000;
  }
});

pre的参数和use完全相同。在这一层,你可以挂载自己的数据请求处理函数,如果你设置了ctx.box.dataHandle为函数对象,则会挂载此函数在data事件上,而不是默认的处理过程,其函数参数就是chunk,是传递过来的buffer数据,具体参考Node.js文档。比如,你需要处理非常大量的数据,但是不能保存到内存再统一处理,而是使用流,直接把数据写入到一个创建的可写流,这时候就能够通过在这一层做处理。只要rawBody为空,或者是消息头content-type不是上传文件的类型则在下一层的bodyparser就不会进行body的文件解析和其他处理。如果你使用了rawBody,则要把它最后设置为空字符串。

配置选项

应用初始化,完整的配置选项如下

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

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

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

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

    //开启HTTPS
    https       : false,

    http2   : false,

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

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

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

    debug     : false,

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

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

    // 请求处理的钩子函数,如果设定了,在请求开始,回调函数中会执行此函数,
    // 在这里你可以设定一些事件处理函数,最好仅做这些或者是其他执行非常快的任务,可以是异步的。
    //在http/1.1协议中,传递参数就是request,response以及protocol表示协议的字符串'http:' 或者 'https:'
    //在http/2协议中,传递stream参数。

    requestHandle : null,


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

请求上下文

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

    var ctx = {

      version : '1.1', //协议版本
      
      maxBody : 0, //最大body请求数据量

      method    : '', //请求类型

      ip      : '', //客户端IP

      host    : '', 
      
      port    : 0,
      
      protocol: '', //协议

      //实际的访问路径
      path    : '',

      name    : '', //对路由和请求的命名
      
      headers   : {},

      //实际执行请求的路径,是添加到路由模块的路径
      routepath   : '', 

      //路由参数
      param     : {},

      //url的querystring参数,就是url 的 ? 后面的参数
      query     : {},

      //请求体解析后的数据
      body    : {},

      //是否是上传文件的操作
      isUpload  : false,

      //路由分组
      group     : '',
      
      //原始body数据
      rawBody   : '',

      //body数据接收到的总大小
      bodyLength  : 0,

      //解析后的文件信息,实际的文件数据还在rawBody中,这里只记录信息。
      files     : {},

      // 指向实际请求的回调函数,就是通过app.get等接口添加的回调函数。
      // 你甚至可以在执行请求过程中,让它指向一个新的函数,这称为请求函数重定向。
      exec : null,

      //助手函数,包括aes加解密、sha1、sha256、sha512、格式化时间字符串、生成随机字符串等处理。
      helper    : helper,

      //要返回数据和编码的记录
      res : {
        body : '',
        encoding : 'utf8',
      },
  
      //http模块请求回调函数传递的参数被封装到此。
      //在http2协议中,没有这两项。
      request   : null,
      response  : null,

      //只有在http2模块才有此项。
      stream : null,
  
      //中间件执行时挂载到此处的值可以传递到下一层。
      box : {},

      //app运行时,最开始通过addService添加的服务会被此处的service引用。
      //这称为依赖注入,不必每次在代码里引入。
      service:null,
    };

    ctx.send = (d) => {
      ctx.res.body = d;
    };

    ctx.getFile = (name, ind = 0) => {
      if (ind < 0) {return ctx.files[name] || [];}
  
      if (ctx.files[name] === undefined) {return null;}
      
      if (ind >= ctx.files[name].length) {return null;}
  
      return ctx.files[name][ind];
    };
  
    ctx.setHeader = (name, val) => {
      ctx.response.setHeader(name, val);
    };
 
    ctx.status = (stcode = null) => {
      if (stcode === null) { return ctx.response.statusCode; }
      if(ctx.response) { ctx.response.statusCode = stcode; }
    };

    //上传文件时,写入数据到文件的助手函数。
    ctx.moveFile = async (upf, target) => {
      let fd = await new Promise((rv, rj) => {
        fs.open(target, 'w+', 0o644, (err, fd) => {
          if (err) { rj(err); }
          else { rv(fd); }
        });
      });

      return new Promise((rv, rj) => {
        fs.write(fd, ctx.rawBody, upf.start, upf.length, 
          (err,bytesWritten,buffer) => {
            if (err) { rj(err); }
            else { rv(bytesWritten); }
          });
      })
      .then(d => {
        return d;
      }, e => { throw e; })
      .finally(() => {
        fs.close(fd, (err) => {});
      });
    };

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

最后

  • 关于中间件

要改造成可以在运行时能够重新生成执行过程的方式也不困难,实际上,由于本身就是利用栈来生成的,只需要清理掉并重新运行加载函数生成即可。无论哪种实现方式,要做到运行时修改该都是可以很快解决的,但是这很危险,除非真的有必要,否则你还是在修改逻辑后,重新运行服务,或者你应该在中间件内部来解决一些动态调整的问题。

3.0.0

3 years ago

2.2.5

4 years ago

2.2.3

4 years ago

2.2.4

4 years ago

2.2.2

4 years ago

2.2.1

4 years ago

2.1.9

4 years ago

2.1.8

4 years ago

2.1.7

4 years ago

2.1.6

4 years ago

2.1.5

4 years ago

2.1.4

4 years ago

2.1.3

4 years ago

2.1.2

4 years ago

2.0.9

4 years ago

2.0.8

4 years ago

2.1.0

4 years ago

2.0.7

4 years ago

2.0.6

4 years ago

2.0.5

4 years ago

2.0.3

4 years ago

2.0.4

4 years ago

2.0.2

4 years ago

2.0.0-beta

4 years ago

2.0.1

4 years ago

2.0.0

4 years ago

1.2.8

4 years ago

1.2.7

4 years ago

1.2.6

4 years ago

1.2.5

4 years ago

1.2.4

4 years ago

1.2.3

4 years ago

1.2.2

4 years ago

1.2.0

4 years ago

1.1.7

4 years ago

1.2.1

4 years ago

1.1.6

4 years ago

1.1.1

4 years ago

1.1.0

4 years ago

1.1.5

4 years ago

1.1.4

4 years ago

1.1.3

4 years ago

1.1.2

4 years ago

1.1.0-beta5

4 years ago

1.1.0-beta4

4 years ago

1.1.0-beta3

4 years ago

1.1.0-beta2

4 years ago

1.1.0-beta

4 years ago

1.0.3

4 years ago

1.0.2

4 years ago

1.0.1

4 years ago

1.0.2-beta

4 years ago

1.0.1-beta

4 years ago

1.0.0

4 years ago