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

## 安装依赖
npm i

## 执行 demo
npm run start

## 最后启动chrome浏览器访问
##  http://127.0.0.1:3000/index.html

koa-send 源码解读

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 path
    if (/[\.]{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 = 500
    throw 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;

koa-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');

附录

参考

results matching ""

    No results matching ""