前言

狭义中间件,请求/拦截 最显著的特征是

  • 直接被app.use()
  • 拦截请求
  • 操作响应

最典型的场景是 Koa.js 官方支持传输静态文件中间件的实现koa-send
主要实现场景流程是

  • 拦截请求,判断该请求是否请求本地静态资源文件
  • 操作响应,返回对应的静态文件文本内容或出错提示

    本节主要以官方的 koa-send 中间件为参考,实现了一个最简单的koa-end 实现,方便原理讲解和后续二次自定义优化开发。

实现步骤

  • step 01 配置静态资源绝对目录地址
  • step 02 判断是否支持隐藏文件
  • step 03 获取文件或者目录信息
  • step 04 判断是否需要压缩
  • step 05 设置HTTP头信息
  • step 06 静态文件读取

    实现源码

    demo源码
    https://github.com/chenshenhai/koajs-design-note/tree/master/demo/chapter-04-02

    1. ## 安装依赖
    2. npm i
    3. ## 执行 demo
    4. npm run start
    5. ## 最后启动chrome浏览器访问
    6. ## http://127.0.0.1:3000/index.html

    koa-send 源码解读

    1. const fs = require('fs');
    2. const path = require('path');
    3. const {
    4. basename,
    5. extname
    6. } = path;
    7. const defaultOpts = {
    8. root: '',
    9. maxage: 0,
    10. immutable: false,
    11. extensions: false,
    12. hidden: false,
    13. brotli: false,
    14. gzip: false,
    15. setHeaders: () => {}
    16. };
    17. async function send(ctx, urlPath, opts = defaultOpts) {
    18. const { root, hidden, immutable, maxage, brotli, gzip, setHeaders } = opts;
    19. let filePath = urlPath;
    20. // step 01: normalize path
    21. // 配置静态资源绝对目录地址
    22. try {
    23. filePath = decodeURIComponent(filePath);
    24. // check legal path
    25. if (/[\.]{2,}/ig.test(filePath)) {
    26. ctx.throw(403, 'Forbidden');
    27. }
    28. } catch (err) {
    29. ctx.throw(400, 'failed to decode');
    30. }
    31. filePath = path.join(root, urlPath);
    32. const fileBasename = basename(filePath);
    33. // step 02: check hidden file support
    34. // 判断是否支持隐藏文件
    35. if (hidden !== true && fileBasename.startsWith('.')) {
    36. ctx.throw(404, '404 Not Found');
    37. return;
    38. }
    39. // step 03: stat
    40. // 获取文件或者目录信息
    41. let stats;
    42. try {
    43. stats = fs.statSync(filePath);
    44. if (stats.isDirectory()) {
    45. ctx.throw(404, '404 Not Found');
    46. }
    47. } catch (err) {
    48. const notfound = ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR']
    49. if (notfound.includes(err.code)) {
    50. ctx.throw(404, '404 Not Found');
    51. return;
    52. }
    53. err.status = 500
    54. throw err
    55. }
    56. let encodingExt = '';
    57. // step 04 check zip
    58. // 判断是否需要压缩
    59. if (ctx.acceptsEncodings('br', 'identity') === 'br' && brotli && (fs.existsSync(filePath + '.br'))) {
    60. filePath = filePath + '.br';
    61. ctx.set('Content-Encoding', 'br');
    62. ctx.res.removeHeader('Content-Length');
    63. encodingExt = '.br';
    64. } else if (ctx.acceptsEncodings('gzip', 'identity') === 'gzip' && gzip && (fs.existsSync(filePath + '.gz'))) {
    65. filePath = filePath + '.gz';
    66. ctx.set('Content-Encoding', 'gzip');
    67. ctx.res.removeHeader('Content-Length');
    68. encodingExt = '.gz';
    69. }
    70. // step 05 setHeaders
    71. // 设置HTTP头信息
    72. if (typeof setHeaders === 'function') {
    73. setHeaders(ctx.res, filePath, stats);
    74. }
    75. ctx.set('Content-Length', stats.size);
    76. if (!ctx.response.get('Last-Modified')) {
    77. ctx.set('Last-Modified', stats.mtime.toUTCString());
    78. }
    79. if (!ctx.response.get('Cache-Control')) {
    80. const directives = ['max-age=' + (maxage / 1000 | 0)];
    81. if (immutable) {
    82. directives.push('immutable');
    83. }
    84. ctx.set('Cache-Control', directives.join(','));
    85. }
    86. const ctxType = encodingExt !== '' ? extname(basename(filePath, encodingExt)) : extname(filePath);
    87. ctx.type = ctxType;
    88. // step 06 stream
    89. // 静态文件读取
    90. ctx.body = fs.createReadStream(filePath);
    91. }
    92. module.exports = send;

    koa-send 使用

    1. const send = require('./index');
    2. const Koa = require('koa');
    3. const app = new Koa();
    4. // public/ 为当前项目静态文件目录
    5. app.use(async ctx => {
    6. await send(ctx, ctx.path, { root: `${__dirname}/public` });
    7. });
    8. app.listen(3000);
    9. console.log('listening on port 3000');

    附录

    参考

  • https://github.com/koajs/send