codecamp

H3 静态资源服务

把本地目录作为静态资源(HTML、图片、CSS、JS)对外提供,serveStatic 一个工具就够了。它不直接读盘——你提供「怎么读」和「怎么取元数据」两个回调,剩下的交给它。

基本用法:必须 return

import { H3, serveStatic } from "h3";

const app = new H3();

app.use("/public/**", (event) => {
  // 关键:必须 return,否则中间件形同虚设,请求会穿透过去
  return serveStatic(event, {
    getContents: (id) => {
      // TODO:根据 id 读文件内容,返回 Buffer/string 或 undefined
    },
    getMeta: (id) => {
      // TODO:根据 id 取元数据,返回 { size, mtime } 或 undefined
    },
  });
});

getContents 和 getMeta 两个回调职责分离,是有意为之:

  • getMeta 只取大小与修改时间,H3 用它回 HEAD 请求、算 etag、发 304 Not Modified——不必真的读文件内容。
  • getContents 才真正读文件。

这样对 HEAD 请求和大文件都更省。

完整示例:基于 Node fs

import { stat, readFile } from "node:fs/promises";
import { join } from "node:path";
import { H3, serve, serveStatic } from "h3";

const app = new H3();

app.use("/public/**", (event) => {
  return serveStatic(event, {
    indexNames: ["/index.html"], // 目录请求回落到 index.html
    getContents: (id) => readFile(join("public", id)), // 读文件内容
    getMeta: async (id) => {
      const stats = await stat(join("public", id)).catch(() => {});
      if (stats?.isFile()) {
        return { size: stats.size, mtime: stats.mtimeMs }; // 供 etag / 304 使用
      }
      // 文件不存在返回 undefined,H3 会按 404 处理
    },
  });
});

serve(app); // 启动监听

size 和 mtime 被用来生成 etag;当客户端带着匹配的 If-None-Match / If-Modified-Since 来时,H3 返回 304 Not Modified,避免重复传输未改动的文件。

路径穿越防护:谁负责?

路径穿越防护是 serveStatic 与你的后端共同承担的,先说清楚分工:

  • serveStatic 自身会拒绝非规范的 pathname——出现多个前导斜杠、\、或 %25 嵌套转义的点段(如 //private/x、/pub/%252e%252e/private/x)时直接返回 404,根本不会进到 getContents。
  • 但它刻意把 %2f / %5c 这类编码分隔符保留在传给 getContents / getMeta 的 id 里(与 event.url.pathname 同源、同分段结构)。所以后端绝不能先 decodeURIComponent(id) 再拼路径——那样会把一个透明段重新变成真正的 /,绕过上面的 use("/public/**") 守卫。
  • 后端真正要负责的,是对 id 解析出的绝对路径做作用域校验:确认它落在资源根目录之内。

import { stat, readFile } from "node:fs/promises";
import { join, resolve, normalize } from "node:path";

app.use("/public/**", (event) => {
  return serveStatic(event, {
    getContents: (id) => {
      // 关键:不要 decodeURIComponent(id)——serveStatic 保留 %2f/%5c 正是为了防穿越
      const full = resolve("public", "." + normalize(id).replace(/^\/+/, "/"));
      const root = resolve("public");
      if (!full.startsWith(root + "/") && full !== root) {
        return undefined; // 越界,当文件不存在处理
      }
      return readFile(full);
    },
    getMeta: async (id) => {
      const full = resolve("public", "." + normalize(id).replace(/^\/+/, "/"));
      const root = resolve("public");
      if (!full.startsWith(root + "/") && full !== root) return undefined;
      const stats = await stat(full).catch(() => {});
      return stats?.isFile() ? { size: stats.size, mtime: stats.mtimeMs } : undefined;
    },
  });
});

⚠️ 直接用 join("public", id) 而不校验,等于把路径穿越的口子敞开。恶意请求 /public/../../etc/passwd 在 join 之后会逃出 public。务必在拼完路径后做一次「是否在根目录内」的判定。

结合 routeRules 加缓存头

静态资源通常想带长缓存。与其在每个 getMeta 里手设,不如用第 3 章讲过的路由规则,在 routeRules 里对 /public/** 统一加 cache 头,由 H3 在响应阶段自动补上——serveStatic 与路由规则是叠加生效的:

const app = new H3({
  routeRules: {
    // 对 /public 下所有资源统一加 1 小时强缓存
    "/public/**": { cache: { maxAge: 60 * 60 } },
  },
});

这样 serveStatic 负责读文件、routeRules 负责加头,职责清晰,也不用在每个请求里重复设。

💡 当静态资源体量很大、或需要边缘分发时,直接用对象存储 / CDN 比自己 serveStatic 更合适——把 /public/** 用 proxy 或反向代理指到 CDN 即可,H3 只管动态部分。

H3 会话选项与过期
H3 流式响应
温馨提示
下载编程狮App,免费阅读超1000+编程语言教程
取消
确定
目录

关闭

MIP.setData({ 'pageTheme' : getCookie('pageTheme') || {'day':true, 'night':false}, 'pageFontSize' : getCookie('pageFontSize') || 20 }); MIP.watch('pageTheme', function(newValue){ setCookie('pageTheme', JSON.stringify(newValue)) }); MIP.watch('pageFontSize', function(newValue){ setCookie('pageFontSize', newValue) }); function setCookie(name, value){ var days = 1; var exp = new Date(); exp.setTime(exp.getTime() + days*24*60*60*1000); document.cookie = name + '=' + value + ';expires=' + exp.toUTCString(); } function getCookie(name){ var reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)'); return document.cookie.match(reg) ? JSON.parse(document.cookie.match(reg)[2]) : null; }