4、静态资源服务:koa-static 中间件使用、静态资源目录配置、缓存策略与性能优化

说实话,很多新手用 Koa2 做后端时,第一个卡住的地方就是静态资源。明明 HTML 写好了,CSS 也引用了,结果浏览器一片空白。我刚开始也踩过这个坑——后来才发现,Koa 默认压根不处理静态文件,你得主动告诉它:「嘿,帮我把这些文件吐出去」。

这一章,我们就来搞定这件事。我会从 koa-static 的基本用法讲起,再到目录配置、缓存策略,最后聊聊性能优化。嗯,都是实战中摸爬滚打出来的经验。

4.1 koa-static 中间件:让静态文件「活」起来

koa-static 是 Koa 生态里最常用的静态资源中间件。它的核心作用就一句话:把服务器上的文件,原封不动地返回给客户端

安装很简单:

npm install koa-static

基本用法:

const Koa = require('koa');
const serve = require('koa-static');
const path = require('path');

const app = new Koa();

// 将 public 目录下的文件作为静态资源
app.use(serve(path.join(__dirname, 'public')));

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000');
});

你看,就这么几行。现在你访问 http://localhost:3000/style.css,只要 public 目录下有这个文件,它就会乖乖返回。

💡 我的习惯:我会把静态资源放在项目根目录的 public/static/ 下。这样一眼就能找到,团队协作时也清晰。

4.2 静态资源目录配置:不止一个目录

实际项目中,静态资源往往不止一个目录。比如图片放在 images/,CSS 放在 css/,JS 放在 js/。你当然可以分别配置,但更优雅的做法是统一管理。

我个人习惯这样:

const staticPath = path.join(__dirname, 'public');
app.use(serve(staticPath, {
  // 配置选项
}));

然后在 public 下建子目录:

public/
├── css/
│   └── style.css
├── js/
│   └── app.js
├── images/
│   └── logo.png
└── index.html

访问时直接写路径就行:

<link rel="stylesheet" href="/css/style.css">
<script src="/js/app.js"></script>
<img src="/images/logo.png" alt="logo">

为什么要这样?因为浏览器请求静态资源时,路径越短越好。你想想看,如果每个资源都带上一长串路径,不仅难看,还容易出错。

⚠️ 注意:千万不要把 node_modules 或 .git 这类目录暴露出去。我曾经见过有人把整个项目根目录设为静态资源目录,结果源码全被扒走了。记住:只暴露你打算公开的目录

4.3 缓存策略:让浏览器少跑几趟

静态资源最大的特点是什么?不常变。一个 CSS 文件,可能几周才更新一次。如果每次请求都从服务器重新拉取,那太浪费了。

koa-static 默认会帮你设置一些缓存头,但我们可以做得更好。

4.3.1 强缓存 vs 协商缓存

缓存类型 原理 优点 缺点
强缓存 通过 Cache-ControlExpires 头,告诉浏览器「这个文件在 XX 秒内别来找我」 零请求,速度最快 文件更新后,浏览器可能还在用旧版本
协商缓存 通过 Last-ModifiedETag,让浏览器问服务器「文件变了吗?」 文件没变时不返回内容,节省带宽 每次都要发请求,有网络开销

实际项目中,我通常这样搭配:

  • 对于版本号明确的资源(比如 app.abc123.js),用强缓存,设置一年有效期。
  • 对于不确定版本的文件(比如 logo.png),用协商缓存,让浏览器每次来问一下。

4.3.2 在 koa-static 中配置缓存

app.use(serve(staticPath, {
  maxage: 30 * 24 * 60 * 60 * 1000, // 30 天,单位毫秒
  setHeaders: (res, path, stats) => {
    // 对 CSS/JS 文件设置更长的缓存
    if (path.endsWith('.css') || path.endsWith('.js')) {
      res.setHeader('Cache-Control', 'public, max-age=31536000');
    }
    // 对 HTML 文件不缓存
    if (path.endsWith('.html')) {
      res.setHeader('Cache-Control', 'no-cache');
    }
  }
}));

这里有个细节:maxage 是全局设置,setHeaders 可以针对不同文件类型做精细化控制。我建议你至少区分一下 HTML 和其他资源——HTML 通常需要实时更新,而 CSS/JS 可以放心缓存。

🔑 关键点:缓存策略的核心是「平衡」。缓存太久,用户看不到更新;缓存太短,服务器压力大。我的经验是:带 hash 的文件用强缓存,不带 hash 的用协商缓存

4.4 性能优化:让静态资源飞起来

缓存只是性能优化的一部分。下面这几个技巧,我在项目中屡试不爽。

4.4.1 启用 gzip 压缩

文本文件(CSS、JS、HTML)压缩后体积能减少 70% 以上。koa-static 本身不提供压缩功能,但可以搭配 koa-compress 中间件:

const compress = require('koa-compress');

app.use(compress({
  filter: (contentType) => /text|javascript|css/i.test(contentType),
  threshold: 2048, // 小于 2KB 的文件不压缩
  gzip: {
    flush: require('zlib').constants.Z_SYNC_FLUSH
  }
}));

app.use(serve(staticPath));

注意顺序:压缩中间件要放在静态资源中间件之前。否则先返回了文件,再压缩就来不及了。

4.4.2 使用 CDN 或反向代理

Node.js 处理静态文件的性能,说实话不如 Nginx。我在生产环境中,通常会在 Koa 前面加一层 Nginx 做反向代理,专门处理静态资源。

Nginx 配置示例:

location /static/ {
  alias /path/to/your/public/;
  expires 30d;
  add_header Cache-Control "public, immutable";
}

这样 Koa 只负责动态接口,静态资源全交给 Nginx。性能直接翻倍。

4.4.3 文件指纹与版本控制

前面提到带 hash 的文件名,这里展开说说。构建工具(如 Webpack、Vite)会在文件名中加入内容 hash:

app.8a3b9c.js
style.d4e5f6.css

内容变了,hash 就变。这样你可以放心设置超长缓存,用户永远拿到最新版本。

💡 避坑指南:我曾经犯过一个错误——给所有静态资源都设置了 1 年缓存,结果更新了 CSS 后,用户死活看不到新样式。后来才意识到,没有 hash 的文件千万别设太长的缓存。血的教训啊。

4.4.4 减少不必要的文件读取

koa-static 每次请求都会去磁盘读取文件。如果文件很多,磁盘 I/O 会成为瓶颈。我的建议:

  • 合并小文件:把多个 CSS 合并成一个,减少请求次数。
  • 使用内存缓存:对于高频访问的小文件,可以缓存到内存中。
  • 避免目录遍历:不要用 serve('.') 这种写法,指定具体目录。

4.5 实战:一个完整的静态资源配置

最后,给你一个我常用的配置模板,直接拿去用:

const Koa = require('koa');
const serve = require('koa-static');
const compress = require('koa-compress');
const path = require('path');

const app = new Koa();
const staticPath = path.join(__dirname, 'public');

// 1. 先压缩
app.use(compress({
  threshold: 2048,
  gzip: { flush: require('zlib').constants.Z_SYNC_FLUSH }
}));

// 2. 再处理静态资源
app.use(serve(staticPath, {
  maxage: 30 * 24 * 60 * 60 * 1000, // 30 天
  setHeaders: (res, filePath) => {
    // HTML 不缓存
    if (filePath.endsWith('.html')) {
      res.setHeader('Cache-Control', 'no-cache');
      return;
    }
    // 带 hash 的文件,缓存一年
    if (/[a-f0-9]{8,}\.(js|css)$/.test(filePath)) {
      res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
    }
  }
}));

app.listen(3000);

嗯,这套配置我用了两年多,线上没出过问题。你拿过去稍微改改路径就能用。

静态资源服务,说白了就是「把文件交给浏览器」。但怎么交、交得快不快、交得对不对,这里面门道不少。希望这一章能帮你少走弯路。下一章我们聊聊更进阶的话题——你猜是什么?