Appearance
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>
通过合理配置静态文件服务,可以显著提高应用性能并确保安全性。