中间件顺序
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');
// middleware
const 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;