【koa】静态资源访问,以及条件请求(Conditional Requests)

实现静态资源访问

实现可配置的静态资源中间件存在几个需要注意的问题:

  • 首先是 HEADGET,在 HTTP 请求中,它们具有相同的语义。

  • 是否等待下游中间件返回后访问静态资源,这作为一个可选项暴露在参数中。

  • 注意不要覆盖已有的处理结果,

    if (ctx.body != null || ctx.status !== 404) return;
    
  • 恶意行为,使用 resolve-path 包来避免恶意访问。

    path = resolvePath(config.root, ctx.path.replace(new RegExp('^/'), ''));
    

    正则替换掉绝对路径 /,用于不触发这个包的绝对路径异常。

  • 访问的默认文件,作为参数暴露。触发时机是:当前访问的是目录。

  • 是否进行 304 响应(Conditional Requests),作为参数暴露。使用 Cache-ControlLast-ModifiedIf-Modified-Since 机制实现。

    仔细来讲,Cache-Control 头用来向客户端响应,告知是否允许缓存,置为 maxAge=0no-cache

    然后将 Last-Modified 设置为 stats.mtime.toUTCString()

    HTTP 协议要求客户端在下次访问时带上 If-Modified-Since,用来想服务端提示上次响应时服务端给的时间。

    在服务端可以这样实现相关逻辑:

    let modifiedTime = ctx.get('If-Modified-Since');
    if (modifiedTime) {
        if (stats.mtime.toUTCString() === modifiedTime) {
            return ctx.status = 304;
        }
    }
    

参考 @koajs/static@koajs/send

全部代码:

const fs = require('fs');
const _path = require('path');
const resolvePath = require('resolve-path');

/**
 * opts:
 *  - index = 'index.html' 默认文件
 *  - bubbling = false 是否在冒泡阶段处理
 *  - cache = false 启用客户端缓存,进行 304 响应
 */
module.exports = (root, opts = {}) => {
    let config = {
        root: _path.resolve(root),
        index: opts.index ?? 'index.html',
        bubbling: opts.defer ?? false,
        cache: opts.cache ?? false,
    };

    return async (ctx, next) => {
        if (ctx.method !== 'HEAD' && ctx.method !== 'GET') return;
        if (config.bubbling) {
            // 等待其后中间件处理
            await next();
        }

        // 已有中间件处理则返回 
        if (ctx.body != null || ctx.status !== 404) return;

        // 拼接、检测恶意行为
        let path;
        try {
            path = resolvePath(config.root, ctx.path.replace(new RegExp('^/'), ''));
        } catch (error) {
            return next();
        }

        // 获取 stat
        let stats;
        try {
            stats = fs.statSync(path);
            if (stats.isDirectory()) {
                // 是目录且启用了 index 则加上 index
                if (config.index) {
                    path += `\\${config.index}`
                    stats = fs.statSync(path);
                }
            }
        } catch (error) {
            // 出错,交给下游处理
            return next();
        }

        ctx.set('Content-Length', stats.size);
        if (config.cache) {
            ctx.set('Last-Modified', stats.mtime.toUTCString());
            ctx.set('Cache-Control', 'no-cache');

            let modifiedTime = ctx.get('If-Modified-Since');
            if (modifiedTime) {
                if (stats.mtime.toUTCString() === modifiedTime) {
                    return ctx.status = 304;
                }
            }
        }

        ctx.type = _path.extname(path);
        ctx.body = fs.createReadStream(path);
    }
}
实现 ETag

ETag 是上述 Conditional Requests 的新方案,在 HTTP/1.1 标准中定义。

其本质思想是针对任何(主要是静态资源)响应实体进行 hash,拿到一个数据摘要,作为响应头 ETag。客户端被要求实现:每次请求都带着最新的 ETag

然后服务端通过比对进行响应。

这个方案的性能会差一点,但是解决了先前的弊端,例如 Last-Modified 精确到秒,对于频繁更改的响应实体无法有效频繁响应。

可以使用 @jshttp/etag ,同时 koa 实现了 etag 的对比逻辑,其上下文对象的 fresh 属性的 getter 实现如下:

/**
   * Check if the request is fresh, aka
   * Last-Modified and/or the ETag
   * still match.
   *
   * @return {Boolean}
   * @api public
   */

get fresh() {
    const method = this.method;
    const s = this.ctx.status;

    // GET or HEAD for weak freshness validation only
    if ('GET' !== method && 'HEAD' !== method) return false;

    // 2xx or 304 as per rfc2616 14.26
    if ((s >= 200 && s < 300) || 304 === s) {
        return fresh(this.header, this.response.header);
    }

    return false;
},

参考 @koajs/conditional-get@koajs/etag

全部代码:

module.exports = (opts) => {
    return async (ctx, next) => {
        await next();

        // 2xx statusCode
        if ((ctx.status / 100 | 0) !== 2) return;

        let entity = await require('./util/getEntity')(ctx.body);
        if (entity) {
            ctx.etag = require('etag')(entity, opts);
            if (ctx.fresh) {
                ctx.body = null;
                ctx.status = 304;
            }
        }
    };
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

高厉害

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值