koa-send 实现

前言

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

  • 直接被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');

附录

参考