Skip to content
On this page

Koa静态文件服务

Koa提供了多种方式来处理静态文件服务。本指南介绍如何使用Koa提供静态文件服务,包括图片、CSS、JavaScript等文件。

基础静态文件服务

安装koa-static中间件

bash
npm install koa-static

基础用法

javascript
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 port 3000');

项目结构

project/
├── app.js
├── public/
│   ├── css/
│   │   ├── style.css
│   │   └── responsive.css
│   ├── js/
│   │   ├── app.js
│   │   └── utils.js
│   ├── images/
│   │   ├── logo.png
│   │   └── background.jpg
│   └── favicon.ico

高级配置选项

配置选项详解

javascript
const serve = require('koa-static');

app.use(serve(path.join(__dirname, 'public'), {
  // 设置最大缓存时间(毫秒)
  maxage: 1000 * 60 * 60 * 24 * 7, // 7天
  
  // 设置缓存控制头
  cacheControl: 'public, max-age=604800', // 7天
  
  // 设置Last-Modified头
  lastModified: true,
  
  // 设置ETag头
  etag: true,
  
  // 是否允许范围请求
  range: true,
  
  // 是否隐藏以.开头的文件
  hidden: false,
  
  // 设置索引文件
  index: 'index.html',
  
  // 是否重定向到带斜杠的目录URL
  redirect: true,
  
  // 设置自定义内容类型
  setHeaders: (res, path, stats) => {
    // 为特定文件类型设置特殊头
    if (path.endsWith('.js')) {
      res.setHeader('Content-Type', 'application/javascript');
    }
  }
}));

缓存配置

javascript
// 不同类型文件的不同缓存策略
const conditionalServe = (ctx, next) => {
  const ext = require('path').extname(ctx.path);
  
  if (ext === '.js' || ext === '.css') {
    // 长期缓存静态资源
    ctx.set('Cache-Control', 'public, max-age=31536000'); // 1年
  } else if (ext === '.html') {
    // HTML文件不缓存
    ctx.set('Cache-Control', 'no-cache, no-store, must-revalidate');
  } else {
    // 其他文件中等缓存
    ctx.set('Cache-Control', 'public, max-age=86400'); // 1天
  }
  
  return serve(path.join(__dirname, 'public'))(ctx, next);
};

app.use(conditionalServe);

多目录静态服务

多个静态目录

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

const app = new Koa();

// 提供多个静态目录
app.use(serve(path.join(__dirname, 'public')));
app.use(serve(path.join(__dirname, 'assets')));
app.use(serve(path.join(__dirname, 'uploads')));

// 注意:目录顺序很重要,前面的目录优先匹配

带前缀的静态服务

javascript
const mount = require('koa-mount');

// 将静态文件挂载到特定路径
app.use(mount('/public', serve(path.join(__dirname, 'public'))));
app.use(mount('/assets', serve(path.join(__dirname, 'assets'))));
app.use(mount('/uploads', serve(path.join(__dirname, 'uploads'))));

// 现在可以通过以下路径访问:
// /public/style.css
// /assets/app.js
// /uploads/image.jpg

高级静态文件处理

条件静态文件服务

javascript
// 根据条件提供不同的静态文件
const conditionalStatic = async (ctx, next) => {
  if (ctx.path.startsWith('/admin')) {
    // 管理员静态文件
    await serve(path.join(__dirname, 'admin-static'))(ctx, next);
  } else {
    // 普通用户静态文件
    await serve(path.join(__dirname, 'public'))(ctx, next);
  }
};

app.use(conditionalStatic);

带认证的静态文件

javascript
// 需要认证的静态文件
const protectedStatic = async (ctx, next) => {
  // 检查用户是否已认证
  if (!ctx.state.user || !ctx.state.user.isAuthenticated) {
    ctx.status = 401;
    ctx.body = 'Authentication required';
    return;
  }
  
  // 检查用户是否有权限访问该文件
  if (ctx.path.startsWith('/private') && !ctx.state.user.hasPrivateAccess) {
    ctx.status = 403;
    ctx.body = 'Forbidden';
    return;
  }
  
  await serve(path.join(__dirname, 'protected'))(ctx, next);
};

app.use(protectedStatic);

文件压缩和优化

配合压缩中间件

bash
npm install koa-compress
javascript
const compress = require('koa-compress');

// 先应用压缩中间件
app.use(compress({
  threshold: 2048, // 大于2KB的文件才压缩
  gzip: {
    flush: require('zlib').constants.Z_SYNC_FLUSH
  },
  deflate: {
    flush: require('zlib').constants.Z_SYNC_FLUSH
  },
  br: false // 禁用Brotli压缩
}));

// 再应用静态文件服务
app.use(serve(path.join(__dirname, 'public'), {
  maxage: 1000 * 60 * 60 * 24 * 7 // 7天缓存
}));

安全考虑

防止目录遍历攻击

javascript
// koa-static本身已经防止了目录遍历攻击
// 但可以添加额外的安全层
const path = require('path');

const secureServe = async (ctx, next) => {
  // 验证路径是否安全
  const requestedPath = path.normalize(ctx.path);
  
  // 确保请求的路径在允许的目录内
  const allowedPath = path.join(__dirname, 'public');
  const resolvedPath = path.resolve(allowedPath, requestedPath);
  
  if (!resolvedPath.startsWith(path.resolve(allowedPath))) {
    ctx.status = 403;
    ctx.body = 'Forbidden';
    return;
  }
  
  await serve(path.join(__dirname, 'public'))(ctx, next);
};

app.use(secureServe);

限制文件类型

javascript
// 只允许特定类型的文件
const allowedExtensions = ['.js', '.css', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.woff', '.woff2', '.ttf'];

const filteredServe = async (ctx, next) => {
  const ext = path.extname(ctx.path).toLowerCase();
  
  if (!allowedExtensions.includes(ext)) {
    ctx.status = 403;
    ctx.body = 'File type not allowed';
    return;
  }
  
  await serve(path.join(__dirname, 'public'))(ctx, next);
};

app.use(filteredServe);

性能优化

响应头优化

javascript
const optimizedServe = serve(path.join(__dirname, 'public'), {
  maxage: 1000 * 60 * 60 * 24 * 365, // 1年缓存
  etag: true,
  lastModified: true,
  setHeaders: (res, filepath) => {
    const ext = path.extname(filepath);
    
    // 为不同类型的文件设置不同的响应头
    switch (ext) {
      case '.js':
        res.setHeader('Content-Type', 'application/javascript');
        res.setHeader('Cache-Control', 'public, max-age=31536000');
        break;
      case '.css':
        res.setHeader('Content-Type', 'text/css');
        res.setHeader('Cache-Control', 'public, max-age=31536000');
        break;
      case '.png':
      case '.jpg':
      case '.jpeg':
      case '.gif':
        res.setHeader('Cache-Control', 'public, max-age=31536000');
        break;
      default:
        res.setHeader('Cache-Control', 'public, max-age=86400');
    }
  }
});

app.use(optimizedServe);

虚拟路径映射

javascript
// 使用koa-static-cache提供更高级的缓存功能
const staticCache = require('koa-static-cache');

app.use(staticCache(path.join(__dirname, 'public'), {
  maxAge: 365 * 24 * 60 * 60, // 1年
  gzip: true, // 自动gzip压缩
  usePrecompiledGzip: true, // 使用预压缩文件
  alias: {
    '/favicon.ico': '/images/favicon.ico'
  }
}));

错误处理

静态文件错误处理

javascript
// 静态文件错误处理
app.use(async (ctx, next) => {
  try {
    await next();
    
    // 如果没有找到文件且状态为404
    if (ctx.status === 404 && ctx.path.startsWith('/static/')) {
      ctx.status = 404;
      ctx.body = 'File not found';
    }
  } catch (err) {
    if (err.code === 'ENOENT') {
      // 文件不存在
      ctx.status = 404;
      ctx.body = 'File not found';
    } else {
      // 其他错误
      console.error('Static file error:', err);
      ctx.status = 500;
      ctx.body = 'Internal Server Error';
    }
  }
});

app.use(serve(path.join(__dirname, 'public')));

实际应用示例

完整的静态文件服务配置

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

const app = new Koa();

// 压缩中间件
app.use(compress({
  threshold: 2048
}));

// 静态文件服务配置
const staticOptions = {
  maxage: 1000 * 60 * 60 * 24 * 30, // 30天缓存
  hidden: false,
  index: 'index.html',
  defer: false,
  gzip: true,
  setHeaders: (res, path, stats) => {
    // 设置安全头
    res.setHeader('X-Content-Type-Options', 'nosniff');
    
    // 根据文件类型设置内容安全策略
    const ext = path.extname(path);
    if (ext === '.html') {
      res.setHeader('Content-Security-Policy', "default-src 'self'");
    }
  }
};

// 挂载不同类型的静态资源
app.use(mount('/assets', serve(path.join(__dirname, 'assets'), staticOptions)));
app.use(mount('/images', serve(path.join(__dirname, 'images'), {
  ...staticOptions,
  maxage: 1000 * 60 * 60 * 24 * 365 // 图片缓存1年
})));
app.use(mount('/uploads', serve(path.join(__dirname, 'uploads'), {
  ...staticOptions,
  maxage: 1000 * 60 * 60 * 24 * 7 // 上传文件缓存7天
})));

// 默认静态文件服务
app.use(serve(path.join(__dirname, 'public'), staticOptions));

app.listen(3000);

CDN和版本控制

javascript
// 使用文件哈希进行版本控制
const fs = require('fs');
const crypto = require('crypto');

const getFileHash = (filePath) => {
  const content = fs.readFileSync(filePath);
  return crypto.createHash('md5').update(content).digest('hex').substring(0, 8);
};

// 在模板中使用带版本号的静态资源
const staticHelper = {
  assetPath: (filename) => {
    const filePath = path.join(__dirname, 'public', filename);
    if (fs.existsSync(filePath)) {
      const hash = getFileHash(filePath);
      return `${filename}?v=${hash}`;
    }
    return filename;
  }
};

// 在模板中使用
// <script src="/js/app.js?v=abc123ef"></script>

通过合理配置静态文件服务,可以显著提高应用性能并确保安全性。