中间件顺序

next() 方法把中间件分成了两部分,如果中间件不调用 next() 会怎样?

  1. const compose = require('../src/util/compose');
  2. const middleware = [];
  3. middleware.push(async (ctx, next) => {
  4. console.log('第 1 个中间件 next 前');
  5. await next();
  6. console.log('第 1 个中间件 next 后');
  7. });
  8. middleware.push(async (ctx, next) => {
  9. console.log('第 2 个中间件 next 前');
  10. // await next(); // 不调用 next()
  11. console.log('第 2 个中间件 next 后');
  12. });
  13. middleware.push(async (ctx, next) => {
  14. console.log('第 3 个中间件 next 前');
  15. await next();
  16. console.log('第 3 个中间件 next 后');
  17. });
  18. const ctx = {};
  19. compose(middleware)(ctx);

模块输出

  1. 1 个中间件 next
  2. 2 个中间件 next
  3. 2 个中间件 next
  4. 1 个中间件 next

第二个中间件没有调用 next() 后续的中间件不再调用,但前面中间件 next() 之后的代码还能按照预期执行,表现特征就像第三个中间件不存在,利用这个特性可以方便的提前结束响应,减少没必要的开销

功能模块中间件化

文件不存在、请求方法错误

当请求了非法的文件路径或者使用了 POST 方法请求,响应可以中断,没必要执行后面的缓存、压缩等功能,可以写一个最简单处理错误的中间件,放在中间件列表第一个
src/middleware/error.js

  1. const fs = require('fs');
  2. async function error(ctx, next) {
  3. const { req, res, filePath } = ctx;
  4. const { method, url } = req;
  5. if (method !== 'GET') {
  6. res.statusCode = 404;
  7. res.setHeader('Content-Type', 'text/html');
  8. res.end('请使用 GET 方法访问文件!');
  9. } else {
  10. try {
  11. fs.accessSync(filePath, fs.constants.R_OK);
  12. await next();
  13. } catch (ex) {
  14. res.statusCode = 404;
  15. res.setHeader('Content-Type', 'text/html');
  16. res.end(`${url} 文件不存在!`);
  17. }
  18. }
  19. }
  20. module.exports = error;

只有当读取文件正常的时候才执行 await next(); 否则直接终端响应

缓存

src/middleware/cache.js

  1. const fs = require('fs/promises'); // 需要 Node.js V14 以上版本
  2. const etag = require('etag');
  3. async function cache(ctx, next) {
  4. const { res, req, filePath } = ctx;
  5. const { headers } = req;
  6. const { maxAge, enableEtag, enableLastModified } = ctx.config;
  7. await next();
  8. const stats = require('fs').statSync(filePath);
  9. if (!stats.isFile()) {
  10. return;
  11. }
  12. if (maxAge) {
  13. res.setHeader('Cache-Control', `max-age=${maxAge}`);
  14. }
  15. if (enableEtag) {
  16. const reqEtag = headers['etag'];
  17. // 可以改成异步读取文件内容了,但实际应用同样不会这么做,一般有离线任务计算
  18. const content = await fs.readFile(filePath);
  19. const resEtag = etag(content);
  20. res.setHeader('ETag', resEtag);
  21. res.statusCode = reqEtag === resEtag ? 304 : 200;
  22. }
  23. if (enableLastModified) {
  24. const lastModified = headers['if-modified-since'];
  25. const stat = await fs.stat(filePath);
  26. const mtime = stat.mtime.toUTCString();
  27. res.setHeader('Last-Modified', mtime);
  28. res.statusCode = lastModified === mtime ? 304 : 200;
  29. }
  30. }
  31. module.exports = cache;

compose

其它几个中间件实现类似,程序入口拆掉了具体功能实现后会变得非常简单
src/index.js

  1. const http = require('http');
  2. const path = require('path');
  3. const compose = require('./util/compose');
  4. const defaultConf = require('./config');
  5. // middleware
  6. const error = require('./middleware/error');
  7. const serve = require('./middleware/serve');
  8. const compress = require('./middleware/compress');
  9. const cache = require('./middleware/cache');
  10. class StaticServer {
  11. constructor(options = {}) {
  12. this.config = Object.assign(defaultConf, options);
  13. }
  14. start() {
  15. const { port, root } = this.config;
  16. this.server = http.createServer((req, res) => {
  17. const { url } = req;
  18. // 准备中间件的执行环境
  19. const ctx = {
  20. req,
  21. res,
  22. filePath: path.join(root, url),
  23. config: this.config,
  24. };
  25. // 按顺序调用中间件
  26. compose([error, serve, compress, cache])(ctx);
  27. }).listen(port, () => {
  28. console.log(`Static server started at port ${port}`);
  29. });
  30. }
  31. stop() {
  32. this.server.close(() => {
  33. console.log(`Static server closed.`);
  34. });
  35. }
  36. }
  37. module.exports = StaticServer;

完整代码:https://github.com/Samaritan89/static-server/tree/v5