狭义中间件,请求/拦截 最显著的特征是
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
## 安装依赖npm i## 执行 demonpm run start## 最后启动chrome浏览器访问## http://127.0.0.1:3000/index.html
const fs = require('fs');const path = require('path');const {basename,extname} = path;const defaultOpts = {root: '',maxage: 0,immutable: false,extensions: false,hidden: false,brotli: false,gzip: false,setHeaders: () => {}};async function send(ctx, urlPath, opts = defaultOpts) {const { root, hidden, immutable, maxage, brotli, gzip, setHeaders } = opts;let filePath = urlPath;// step 01: normalize path// 配置静态资源绝对目录地址try {filePath = decodeURIComponent(filePath);// check legal pathif (/[\.]{2,}/ig.test(filePath)) {ctx.throw(403, 'Forbidden');}} catch (err) {ctx.throw(400, 'failed to decode');}filePath = path.join(root, urlPath);const fileBasename = basename(filePath);// step 02: check hidden file support// 判断是否支持隐藏文件if (hidden !== true && fileBasename.startsWith('.')) {ctx.throw(404, '404 Not Found');return;}// step 03: stat// 获取文件或者目录信息let stats;try {stats = fs.statSync(filePath);if (stats.isDirectory()) {ctx.throw(404, '404 Not Found');}} catch (err) {const notfound = ['ENOENT', 'ENAMETOOLONG', 'ENOTDIR']if (notfound.includes(err.code)) {ctx.throw(404, '404 Not Found');return;}err.status = 500throw err}let encodingExt = '';// step 04 check zip// 判断是否需要压缩if (ctx.acceptsEncodings('br', 'identity') === 'br' && brotli && (fs.existsSync(filePath + '.br'))) {filePath = filePath + '.br';ctx.set('Content-Encoding', 'br');ctx.res.removeHeader('Content-Length');encodingExt = '.br';} else if (ctx.acceptsEncodings('gzip', 'identity') === 'gzip' && gzip && (fs.existsSync(filePath + '.gz'))) {filePath = filePath + '.gz';ctx.set('Content-Encoding', 'gzip');ctx.res.removeHeader('Content-Length');encodingExt = '.gz';}// step 05 setHeaders// 设置HTTP头信息if (typeof setHeaders === 'function') {setHeaders(ctx.res, filePath, stats);}ctx.set('Content-Length', stats.size);if (!ctx.response.get('Last-Modified')) {ctx.set('Last-Modified', stats.mtime.toUTCString());}if (!ctx.response.get('Cache-Control')) {const directives = ['max-age=' + (maxage / 1000 | 0)];if (immutable) {directives.push('immutable');}ctx.set('Cache-Control', directives.join(','));}const ctxType = encodingExt !== '' ? extname(basename(filePath, encodingExt)) : extname(filePath);ctx.type = ctxType;// step 06 stream// 静态文件读取ctx.body = fs.createReadStream(filePath);}module.exports = send;
const send = require('./index');const Koa = require('koa');const app = new Koa();// public/ 为当前项目静态文件目录app.use(async ctx => {await send(ctx, ctx.path, { root: `${__dirname}/public` });});app.listen(3000);console.log('listening on port 3000');