中间件顺序
next() 方法把中间件分成了两部分,如果中间件不调用 next() 会怎样?
const compose = require('../src/util/compose');const middleware = [];middleware.push(async (ctx, next) => {console.log('第 1 个中间件 next 前');await next();console.log('第 1 个中间件 next 后');});middleware.push(async (ctx, next) => {console.log('第 2 个中间件 next 前');// await next(); // 不调用 next()console.log('第 2 个中间件 next 后');});middleware.push(async (ctx, next) => {console.log('第 3 个中间件 next 前');await next();console.log('第 3 个中间件 next 后');});const ctx = {};compose(middleware)(ctx);
模块输出
第 1 个中间件 next 前第 2 个中间件 next 前第 2 个中间件 next 后第 1 个中间件 next 后
第二个中间件没有调用 next() 后续的中间件不再调用,但前面中间件 next() 之后的代码还能按照预期执行,表现特征就像第三个中间件不存在,利用这个特性可以方便的提前结束响应,减少没必要的开销
功能模块中间件化
文件不存在、请求方法错误
当请求了非法的文件路径或者使用了 POST 方法请求,响应可以中断,没必要执行后面的缓存、压缩等功能,可以写一个最简单处理错误的中间件,放在中间件列表第一个
src/middleware/error.js
const fs = require('fs');async function error(ctx, next) {const { req, res, filePath } = ctx;const { method, url } = req;if (method !== 'GET') {res.statusCode = 404;res.setHeader('Content-Type', 'text/html');res.end('请使用 GET 方法访问文件!');} else {try {fs.accessSync(filePath, fs.constants.R_OK);await next();} catch (ex) {res.statusCode = 404;res.setHeader('Content-Type', 'text/html');res.end(`${url} 文件不存在!`);}}}module.exports = error;
只有当读取文件正常的时候才执行 await next(); 否则直接终端响应
缓存
src/middleware/cache.js
const fs = require('fs/promises'); // 需要 Node.js V14 以上版本const etag = require('etag');async function cache(ctx, next) {const { res, req, filePath } = ctx;const { headers } = req;const { maxAge, enableEtag, enableLastModified } = ctx.config;await next();const stats = require('fs').statSync(filePath);if (!stats.isFile()) {return;}if (maxAge) {res.setHeader('Cache-Control', `max-age=${maxAge}`);}if (enableEtag) {const reqEtag = headers['etag'];// 可以改成异步读取文件内容了,但实际应用同样不会这么做,一般有离线任务计算const content = await fs.readFile(filePath);const resEtag = etag(content);res.setHeader('ETag', resEtag);res.statusCode = reqEtag === resEtag ? 304 : 200;}if (enableLastModified) {const lastModified = headers['if-modified-since'];const stat = await fs.stat(filePath);const mtime = stat.mtime.toUTCString();res.setHeader('Last-Modified', mtime);res.statusCode = lastModified === mtime ? 304 : 200;}}module.exports = cache;
compose
其它几个中间件实现类似,程序入口拆掉了具体功能实现后会变得非常简单
src/index.js
const http = require('http');const path = require('path');const compose = require('./util/compose');const defaultConf = require('./config');// middlewareconst error = require('./middleware/error');const serve = require('./middleware/serve');const compress = require('./middleware/compress');const cache = require('./middleware/cache');class StaticServer {constructor(options = {}) {this.config = Object.assign(defaultConf, options);}start() {const { port, root } = this.config;this.server = http.createServer((req, res) => {const { url } = req;// 准备中间件的执行环境const ctx = {req,res,filePath: path.join(root, url),config: this.config,};// 按顺序调用中间件compose([error, serve, compress, cache])(ctx);}).listen(port, () => {console.log(`Static server started at port ${port}`);});}stop() {this.server.close(() => {console.log(`Static server closed.`);});}}module.exports = StaticServer;
