Static 中间件
该中间件为 Vafast 提供了高性能的静态文件服务功能,支持智能缓存、ETag 验证、自定义头部和灵活的配置选项。
安装
安装命令:
bash
bun add @vafast/static基本用法
typescript
import { Server, createHandler } from 'vafast'
import { staticPlugin } from '@vafast/static'
import { join } from 'path'
// 创建静态文件路由
const staticRoutes = await staticPlugin({
assets: 'public',
prefix: '/static'
})
// 添加自定义路由
const customRoutes = [
{
method: 'GET',
path: '/',
handler: createHandler(() => {
return { message: 'Static file server is running' }
})
}
]
// 合并路由
const allRoutes = [...customRoutes, ...staticRoutes]
const server = new Server(allRoutes)
export default {
fetch: (req: Request) => server.fetch(req)
}配置选项
StaticPluginOptions
typescript
interface StaticPluginOptions {
/** 静态文件目录路径,默认:'public' */
assets?: string
/** URL 前缀,默认:'/public' */
prefix?: string
/** 静态路由文件数量限制,默认:1024 */
staticLimit?: number
/** 是否始终使用静态路由,默认:NODE_ENV === 'production' */
alwaysStatic?: boolean
/** 忽略的文件模式,默认:['.DS_Store', '.git', '.env'] */
ignorePatterns?: Array<string | RegExp>
/** 是否不需要文件扩展名,默认:false */
noExtension?: boolean
/** 是否启用 URI 解码,默认:false */
enableDecodeURI?: boolean
/** 路径解析函数,默认:Node.js resolve */
resolve?: (...pathSegments: string[]) => string
/** 自定义响应头部 */
headers?: Record<string, string>
/** 是否禁用缓存,默认:false */
noCache?: boolean
/** 缓存控制指令,默认:'public' */
directive?: 'public' | 'private' | 'must-revalidate' | 'no-cache' | 'no-store' | 'no-transform' | 'proxy-revalidate' | 'immutable'
/** 缓存最大年龄(秒),默认:86400 (24小时) */
maxAge?: number | null
/** 是否启用 index.html 服务,默认:true */
indexHTML?: boolean
}默认配置
typescript
const defaultOptions = {
assets: 'public', // 静态文件目录
prefix: '/public', // URL 前缀
staticLimit: 1024, // 文件数量限制
alwaysStatic: process.env.NODE_ENV === 'production', // 生产环境默认启用
ignorePatterns: ['.DS_Store', '.git', '.env'], // 忽略的文件
noExtension: false, // 需要文件扩展名
enableDecodeURI: false, // 不启用 URI 解码
resolve: resolveFn, // 使用 Node.js resolve
headers: {}, // 无自定义头部
noCache: false, // 启用缓存
directive: 'public', // 公共缓存
maxAge: 86400, // 24小时缓存
indexHTML: true // 启用 index.html
}使用模式
1. 基本静态文件服务
typescript
import { Server, createHandler } from 'vafast'
import { staticPlugin } from '@vafast/static'
const staticRoutes = await staticPlugin({
assets: 'public',
prefix: '/static'
})
const routes = [
{
method: 'GET',
path: '/',
handler: createHandler(() => {
return { message: 'Welcome to Static File Server' }
})
}
]
const server = new Server([...routes, ...staticRoutes])
export default {
fetch: (req: Request) => server.fetch(req)
}2. 生产环境优化
typescript
import { Server, createHandler } from 'vafast'
import { staticPlugin } from '@vafast/static'
const staticRoutes = await staticPlugin({
assets: 'dist',
prefix: '/assets',
alwaysStatic: true, // 生产环境使用静态路由
noCache: false, // 启用缓存
maxAge: 31536000, // 1年缓存
directive: 'public, immutable' // 不可变缓存
})
const server = new Server(staticRoutes)
export default {
fetch: (req: Request) => server.fetch(req)
}3. 自定义头部和缓存控制
typescript
import { Server, createHandler } from 'vafast'
import { staticPlugin } from '@vafast/static'
const staticRoutes = await staticPlugin({
assets: 'public',
prefix: '/static',
headers: {
'X-Served-By': 'Vafast Static Plugin',
'X-Version': '1.0.0'
},
noCache: false,
directive: 'public',
maxAge: 3600 // 1小时缓存
})
const server = new Server(staticRoutes)
export default {
fetch: (req: Request) => server.fetch(req)
}4. 忽略特定文件
typescript
import { Server, createHandler } from 'vafast'
import { staticPlugin } from '@vafast/static'
const staticRoutes = await staticPlugin({
assets: 'public',
prefix: '/static',
ignorePatterns: [
'.DS_Store',
'.git',
'.env',
'*.log',
'temp/*',
/\.(config|local)$/ // 使用正则表达式
]
})
const server = new Server(staticRoutes)
export default {
fetch: (req: Request) => server.fetch(req)
}5. 无扩展名支持
typescript
import { Server, createHandler } from 'vafast'
import { staticPlugin } from '@vafast/static'
const staticRoutes = await staticPlugin({
assets: 'public',
prefix: '/static',
noExtension: true, // 不需要文件扩展名
alwaysStatic: true // 必须启用
})
const server = new Server(staticRoutes)
export default {
fetch: (req: Request) => server.fetch(req)
}6. 混合路由配置
typescript
import { Server, createHandler } from 'vafast'
import { staticPlugin } from '@vafast/static'
// 创建静态文件路由
const staticRoutes = await staticPlugin({
assets: 'public',
prefix: '/static',
alwaysStatic: false, // 使用通配符路由
staticLimit: 500 // 降低限制以使用通配符
})
// 添加自定义路由
const customRoutes = [
{
method: 'GET',
path: '/',
handler: createHandler(() => {
return { message: 'Mixed routes server' }
})
},
{
method: 'GET',
path: '/api/status',
handler: createHandler(() => {
return { status: 'running', staticRoutes: staticRoutes.length }
})
}
]
const server = new Server([...customRoutes, ...staticRoutes])
export default {
fetch: (req: Request) => server.fetch(req)
}完整示例
typescript
import { Server, createHandler } from 'vafast'
import { staticPlugin } from '@vafast/static'
import { join } from 'path'
// 创建不同配置的静态文件路由
const publicStaticRoutes = await staticPlugin({
assets: 'public',
prefix: '/public',
alwaysStatic: true,
headers: {
'X-Served-By': 'Vafast Static Plugin',
'X-Category': 'Public Assets'
},
noCache: false,
directive: 'public',
maxAge: 86400 // 24小时
})
const assetsStaticRoutes = await staticPlugin({
assets: 'dist/assets',
prefix: '/assets',
alwaysStatic: true,
headers: {
'X-Served-By': 'Vafast Static Plugin',
'X-Category': 'Build Assets'
},
noCache: false,
directive: 'public, immutable',
maxAge: 31536000 // 1年
})
const docsStaticRoutes = await staticPlugin({
assets: 'docs',
prefix: '/docs',
alwaysStatic: false, // 使用通配符路由
staticLimit: 100, // 降低限制
enableDecodeURI: true, // 启用 URI 解码
headers: {
'X-Served-By': 'Vafast Static Plugin',
'X-Category': 'Documentation'
},
noCache: true, // 文档不缓存
indexHTML: true // 支持 index.html
})
// 定义自定义路由
const routes = [
{
method: 'GET',
path: '/',
handler: createHandler(() => {
return {
message: 'Vafast Static File Server',
version: '1.0.0',
endpoints: [
'GET /public/* - 公共静态文件(24小时缓存)',
'GET /assets/* - 构建资源(1年缓存)',
'GET /docs/* - 文档文件(无缓存)',
'GET /api/status - 服务器状态',
'GET /api/files - 文件统计'
]
}
})
},
{
method: 'GET',
path: '/api/status',
handler: createHandler(() => {
return {
status: 'running',
timestamp: new Date().toISOString(),
staticRoutes: {
public: publicStaticRoutes.length,
assets: assetsStaticRoutes.length,
docs: docsStaticRoutes.length
}
}
})
},
{
method: 'GET',
path: '/api/files',
handler: createHandler(async () => {
// 这里可以添加文件统计逻辑
return {
message: 'File statistics',
totalRoutes: publicStaticRoutes.length + assetsStaticRoutes.length + docsStaticRoutes.length,
categories: {
public: publicStaticRoutes.length,
assets: assetsStaticRoutes.length,
docs: docsStaticRoutes.length
}
}
})
}
]
// 合并所有路由
const allRoutes = [
...routes,
...publicStaticRoutes,
...assetsStaticRoutes,
...docsStaticRoutes
]
// 创建服务器
const server = new Server(allRoutes)
// 导出 fetch 函数
export default {
fetch: (req: Request) => server.fetch(req)
}
console.log('🚀 Vafast Static File Server 启动成功!')
console.log('📁 公共文件:/public/* (24小时缓存)')
console.log('🔧 构建资源:/assets/* (1年缓存)')
console.log('📚 文档文件:/docs/* (无缓存)')
console.log('📊 总路由数:', allRoutes.length)测试示例
typescript
import { describe, expect, it, beforeAll, afterAll } from 'bun:test'
import { Server, createHandler } from 'vafast'
import { staticPlugin } from '@vafast/static'
import { writeFile, mkdir, rm } from 'fs/promises'
import { join } from 'path'
import { tmpdir } from 'os'
describe('Vafast Static Plugin', () => {
let tempDir: string
let testFilePath: string
let testHtmlPath: string
beforeAll(async () => {
// 创建临时目录和测试文件
tempDir = join(tmpdir(), 'vafast-static-test-' + Date.now())
await mkdir(tempDir, { recursive: true })
// 创建测试文件
testFilePath = join(tempDir, 'test.txt')
await writeFile(testFilePath, 'Hello, Static File!')
// 创建测试 HTML 文件
testHtmlPath = join(tempDir, 'index.html')
await writeFile(testHtmlPath, '<html><body>Test HTML</body></html>')
})
afterAll(async () => {
// 清理临时文件
await rm(tempDir, { recursive: true, force: true })
})
it('should create static routes', async () => {
const routes = await staticPlugin({
assets: tempDir,
prefix: '/static',
alwaysStatic: true
})
expect(routes).toBeDefined()
expect(Array.isArray(routes)).toBe(true)
expect(routes.length).toBeGreaterThan(0)
})
it('should serve static files with correct paths', async () => {
const routes = await staticPlugin({
assets: tempDir,
prefix: '/static',
alwaysStatic: true
})
const app = new Server(routes)
// 测试访问静态文件
const res = await app.fetch(
new Request('http://localhost/static/test.txt')
)
expect(res.status).toBe(200)
const data = await res.text()
expect(data).toBe('Hello, Static File!')
})
it('should handle index.html correctly', async () => {
const routes = await staticPlugin({
assets: tempDir,
prefix: '/static',
alwaysStatic: true,
indexHTML: true
})
const app = new Server(routes)
// 测试访问目录根路径(应该返回 index.html)
const res = await app.fetch(new Request('http://localhost/static/'))
expect(res.status).toBe(200)
const data = await res.text()
expect(data).toContain('Test HTML')
})
it('should respect custom headers', async () => {
const customHeaders = {
'X-Custom-Header': 'custom-value',
'X-Another-Header': 'another-value'
}
const routes = await staticPlugin({
assets: tempDir,
prefix: '/static',
alwaysStatic: true,
headers: customHeaders
})
const app = new Server(routes)
const res = await app.fetch(
new Request('http://localhost/static/test.txt')
)
expect(res.status).toBe(200)
// 检查自定义头部
expect(res.headers.get('X-Custom-Header')).toBe('custom-value')
expect(res.headers.get('X-Another-Header')).toBe('another-value')
})
it('should handle caching correctly', async () => {
const routes = await staticPlugin({
assets: tempDir,
prefix: '/static',
alwaysStatic: true,
noCache: false,
directive: 'public',
maxAge: 3600
})
const app = new Server(routes)
const res = await app.fetch(
new Request('http://localhost/static/test.txt')
)
expect(res.status).toBe(200)
// 检查缓存头部
expect(res.headers.get('Cache-Control')).toContain('public')
expect(res.headers.get('Cache-Control')).toContain('max-age=3600')
expect(res.headers.get('Etag')).toBeDefined()
})
it('should handle no-cache option', async () => {
const routes = await staticPlugin({
assets: tempDir,
prefix: '/static',
alwaysStatic: true,
noCache: true
})
const app = new Server(routes)
const res = await app.fetch(
new Request('http://localhost/static/test.txt')
)
expect(res.status).toBe(200)
// 检查无缓存头部
expect(res.headers.get('Cache-Control')).toBeUndefined()
expect(res.headers.get('Etag')).toBeUndefined()
})
it('should handle wildcard routes when not alwaysStatic', async () => {
const routes = await staticPlugin({
assets: tempDir,
prefix: '/static',
alwaysStatic: false,
staticLimit: 1 // 强制使用通配符路由
})
const app = new Server(routes)
// 应该有一个通配符路由
const wildcardRoute = routes.find(route => route.path === '/static/*')
expect(wildcardRoute).toBeDefined()
// 测试通配符路由
const res = await app.fetch(
new Request('http://localhost/static/test.txt')
)
expect(res.status).toBe(200)
const data = await res.text()
expect(data).toBe('Hello, Static File!')
})
it('should ignore specified patterns', async () => {
const routes = await staticPlugin({
assets: tempDir,
prefix: '/static',
alwaysStatic: true,
ignorePatterns: ['test.txt']
})
// 被忽略的文件不应该有路由
const ignoredRoute = routes.find(route => route.path === '/static/test.txt')
expect(ignoredRoute).toBeUndefined()
})
it('should handle root prefix correctly', async () => {
const routes = await staticPlugin({
assets: tempDir,
prefix: '/', // 根前缀
alwaysStatic: true
})
const app = new Server(routes)
// 测试使用根前缀访问文件
const res = await app.fetch(new Request('http://localhost/test.txt'))
expect(res.status).toBe(200)
const data = await res.text()
expect(data).toBe('Hello, Static File!')
})
it('should handle 304 Not Modified responses', async () => {
const routes = await staticPlugin({
assets: tempDir,
prefix: '/static',
alwaysStatic: true,
noCache: false
})
const app = new Server(routes)
// 第一次请求
const res1 = await app.fetch(
new Request('http://localhost/static/test.txt')
)
expect(res1.status).toBe(200)
const etag = res1.headers.get('Etag')
expect(etag).toBeDefined()
// 第二次请求,带 If-None-Match 头部
const res2 = await app.fetch(
new Request('http://localhost/static/test.txt', {
headers: {
'If-None-Match': etag!
}
})
)
// 应该返回 304 Not Modified
expect(res2.status).toBe(304)
})
it('should work with custom routes', async () => {
const staticRoutes = await staticPlugin({
assets: tempDir,
prefix: '/static',
alwaysStatic: true
})
// 添加自定义路由
const customRoutes = [
{
method: 'GET',
path: '/',
handler: createHandler(() => {
return { message: 'Static server is running' }
})
}
]
const allRoutes = [...customRoutes, ...staticRoutes]
const app = new Server(allRoutes)
// 测试自定义路由
const customRes = await app.fetch(new Request('http://localhost/'))
expect(customRes.status).toBe(200)
const customData = await customRes.json()
expect(customData.message).toBe('Static server is running')
// 测试静态文件路由
const staticRes = await app.fetch(
new Request('http://localhost/static/test.txt')
)
expect(staticRes.status).toBe(200)
const staticData = await staticRes.text()
expect(staticData).toBe('Hello, Static File!')
})
it('should handle file not found correctly', async () => {
const routes = await staticPlugin({
assets: tempDir,
prefix: '/static',
alwaysStatic: true
})
const app = new Server(routes)
// 测试访问不存在的文件
try {
await app.fetch(
new Request('http://localhost/static/nonexistent.txt')
)
// 如果到这里,说明错误没有被正确处理
expect(true).toBe(false)
} catch (error) {
expect(error).toBeDefined()
}
})
})特性
- ✅ 高性能: 支持静态路由和通配符路由两种模式
- ✅ 智能缓存: 自动生成 ETag 和缓存控制头部
- ✅ 灵活配置: 支持自定义头部、缓存策略和忽略模式
- ✅ 文件扩展名: 可选的扩展名支持
- ✅ 索引文件: 自动服务 index.html 文件
- ✅ 路径安全: 防止目录遍历攻击
- ✅ 类型安全: 完整的 TypeScript 类型支持
- ✅ 易于集成: 无缝集成到 Vafast 应用
最佳实践
1. 生产环境配置
typescript
const staticRoutes = await staticPlugin({
assets: 'dist',
prefix: '/assets',
alwaysStatic: true, // 生产环境使用静态路由
noCache: false, // 启用缓存
maxAge: 31536000, // 1年缓存
directive: 'public, immutable' // 不可变缓存
})2. 开发环境配置
typescript
const staticRoutes = await staticPlugin({
assets: 'public',
prefix: '/static',
alwaysStatic: false, // 开发环境使用通配符路由
noCache: true, // 禁用缓存便于调试
maxAge: null // 无缓存年龄
})3. 安全配置
typescript
const staticRoutes = await staticPlugin({
assets: 'public',
prefix: '/static',
ignorePatterns: [
'.env',
'.git',
'*.log',
'temp/*',
/\.(config|local)$/
]
})4. 性能优化
typescript
const staticRoutes = await staticPlugin({
assets: 'public',
prefix: '/static',
alwaysStatic: files.length <= 1000, // 根据文件数量动态选择
staticLimit: 1000, // 设置合理的限制
headers: {
'X-Content-Type-Options': 'nosniff'
}
})5. 监控和调试
typescript
const staticRoutes = await staticPlugin({
assets: 'public',
prefix: '/static',
alwaysStatic: true
})
// 添加监控路由
const monitorRoutes = [
{
method: 'GET',
path: '/admin/static-stats',
handler: createHandler(() => {
return {
totalRoutes: staticRoutes.length,
mode: 'static',
timestamp: new Date().toISOString(),
cacheInfo: {
statCache: statCache.keys().length,
fileCache: fileCache.keys().length,
htmlCache: htmlCache.keys().length
}
}
})
}
]6. 错误处理和日志
typescript
import { createHandler } from 'vafast'
const staticRoutes = await staticPlugin({
assets: 'public',
prefix: '/static',
alwaysStatic: false, // 使用通配符路由以支持错误处理
staticLimit: 100
})
// 添加错误处理中间件
const errorHandlingRoutes = [
{
method: 'GET',
path: '/static/*',
handler: createHandler(async (req: Request) => {
try {
// 这里可以添加自定义的错误处理逻辑
const response = await handleStaticRequest(req)
return response
} catch (error) {
console.error('Static file error:', error)
if (error instanceof NotFoundError) {
return new Response('File not found', { status: 404 })
}
return new Response('Internal server error', { status: 500 })
}
})
}
]高级特性
1. 智能路由选择
中间件会根据文件数量和配置自动选择最优的路由模式:
typescript
// 文件数量 <= staticLimit (默认 1024) 时使用静态路由
const staticRoutes = await staticPlugin({
assets: 'public',
prefix: '/static',
alwaysStatic: false, // 让中间件自动选择
staticLimit: 500 // 降低阈值以使用通配符路由
})
// 文件数量 > staticLimit 时自动使用通配符路由
// 这样可以减少内存占用,提高启动速度2. 多层缓存系统
中间件内置了三层缓存系统:
typescript
// 文件状态缓存 (3小时 TTL)
const statCache = new Cache({
useClones: false,
checkperiod: 5 * 60, // 5分钟检查一次
stdTTL: 3 * 60 * 60, // 3小时过期
maxKeys: 250 // 最多缓存250个文件状态
})
// 文件路径缓存 (3小时 TTL)
const fileCache = new Cache({
useClones: false,
checkperiod: 5 * 60,
stdTTL: 3 * 60 * 60,
maxKeys: 250
})
// HTML 文件缓存 (3小时 TTL)
const htmlCache = new Cache({
useClones: false,
checkperiod: 5 * 60,
stdTTL: 3 * 60 * 60,
maxKeys: 250
})3. 智能 ETag 生成
中间件使用 MD5 哈希算法生成 ETag,支持 HTTP 304 响应:
typescript
// 自动生成 ETag
const etag = await generateETag(filePath)
// 支持 If-None-Match 头部
if (await isCached(headersRecord, etag, filePath)) {
return new Response(null, {
status: 304,
headers
})
}4. 跨平台路径处理
中间件自动处理不同操作系统的路径分隔符:
typescript
const URL_PATH_SEP = '/'
const isFSSepUnsafe = sep !== URL_PATH_SEP
// 自动转换路径分隔符
const pathName = isFSSepUnsafe
? prefix + relativePath.split(sep).join(URL_PATH_SEP)
: join(prefix, relativePath)注意事项
- 文件数量: 大量文件时建议使用通配符路由以减少内存占用
- 缓存策略: 根据文件类型和更新频率设置合适的缓存策略
- 安全考虑: 避免暴露敏感文件,使用 ignorePatterns 过滤
- 性能影响: 静态路由模式在启动时扫描所有文件,大目录可能影响启动时间
- 路径安全: 中间件已内置路径安全检查,防止目录遍历攻击
- 缓存清理: 中间件会自动清理过期的缓存项,无需手动管理
- 内存使用: 通配符路由模式内存占用更少,适合大文件目录
- 启动时间: 静态路由模式启动更快,但内存占用更多
版本信息
- 当前版本: 0.0.1
- Vafast 兼容性: >= 0.1.12
- Node.js 支持: >= 18.0.0
- Bun 支持: 完全支持
更新日志
查看完整的更新历史:CHANGELOG.md
最新特性
- ✅ 智能路由选择(静态路由 vs 通配符路由)
- ✅ 多层缓存系统(文件状态、路径、HTML)
- ✅ 智能 ETag 生成和 HTTP 304 支持
- ✅ 跨平台路径处理
- ✅ 完整的 TypeScript 类型支持
- ✅ 灵活的缓存策略配置
- ✅ 安全的文件访问控制
