静态文件中间件
本章只讲三件事:
useStaticFiles()如何提供文件useDefaultFiles()如何处理目录默认页useFileServer()如何把两者组合起来
最小示例
cangjie
import soulsoft_web_hosting.*
import soulsoft_web_staticfiles.*
main(args: Array<String>): Int64 {
let builder = WebHost.createBuilder(args)
let app = builder.build()
// 一次启用默认文件和静态文件服务
app.useFileServer()
app.run()
return 0
}默认情况下:
- 文件根目录来自
environment.webRootPath - 常见情况下就是项目下的
wwwroot useFileServer()会先注册useDefaultFiles(),再注册useStaticFiles()
一个典型目录结构如下:
text
demo/
├─ src/
│ └─ main.cj
└─ wwwroot/
├─ index.html
├─ hello.txt
└─ assets/
└─ app.css三个入口
useStaticFiles()
只负责“把请求路径映射到具体文件并输出响应”,不会主动查找目录默认页。
cangjie
import soulsoft_web_staticfiles.*
import soulsoft_web_staticfiles.fileProviders.*
// GET /hello.txt -> wwwroot/hello.txt
app.useStaticFiles { options =>
// 指定静态文件目录
options.fileProvider = PhysicalFileProvider("wwwroot")
}useDefaultFiles()
只负责“目录路径重写”或“补 / 重定向”,本身不输出文件内容。
cangjie
import soulsoft_web_staticfiles.*
import soulsoft_web_staticfiles.fileProviders.*
app.useDefaultFiles { options =>
// 指定默认文件查找目录
options.fileProvider = PhysicalFileProvider("wwwroot")
}它处理的是这类路径:
/docs-> 如果目录存在且命中默认文件,先301到/docs//docs/-> 重写为/docs/index.html
useFileServer()
这是组合入口。默认等价于:
cangjie
app.useDefaultFiles()
app.useStaticFiles()如果配置的是 FileServerOptions,requestPath 和 fileProvider 会同时共享给这两个中间件。
常见用法
提供根目录静态资源
cangjie
import soulsoft_web_staticfiles.*
import soulsoft_web_staticfiles.fileProviders.*
app.useStaticFiles { options =>
// 访问 /hello.txt 时从 wwwroot/hello.txt 读取
options.fileProvider = PhysicalFileProvider("wwwroot")
}挂到指定前缀
cangjie
import soulsoft_web_http.*
import soulsoft_web_staticfiles.*
import soulsoft_web_staticfiles.fileProviders.*
app.useStaticFiles { options =>
// 只有 /static 开头的请求才进入静态文件中间件
options.requestPath = PathString("/static")
options.fileProvider = PhysicalFileProvider("wwwroot")
}这时:
GET /static/hello.txt会命中wwwroot/hello.txtGET /hello.txt不会命中这个中间件
默认文件 + 静态文件
cangjie
import soulsoft_web_staticfiles.*
import soulsoft_web_staticfiles.fileProviders.*
// 先处理目录默认页
app.useDefaultFiles { options =>
options.fileProvider = PhysicalFileProvider("wwwroot")
}
// 再输出实际文件内容
app.useStaticFiles { options =>
options.fileProvider = PhysicalFileProvider("wwwroot")
}上面的行为是:
GET /-> 如果存在index.html,返回首页GET /docs-> 先重定向到/docs/GET /docs/-> 重写为/docs/index.html,再由静态文件中间件输出
扩展内容类型
cangjie
import soulsoft_web_staticfiles.*
import soulsoft_web_staticfiles.fileProviders.*
app.useStaticFiles { options =>
let provider = ContentTypeProvider()
// 给自定义扩展名补充映射
provider.mappings[".cj"] = "text/plain"
options.fileProvider = PhysicalFileProvider("wwwroot")
options.contentTypeProvider = provider
}允许未知类型
cangjie
import soulsoft_web_staticfiles.*
import soulsoft_web_staticfiles.fileProviders.*
app.useStaticFiles { options =>
options.fileProvider = PhysicalFileProvider("wwwroot")
// 未知扩展名也允许输出
options.serveUnknownFileTypes = true
// 未知类型统一使用这个 Content-Type
options.defaultContentType = "application/octet-stream"
}默认情况下,未知扩展名不会被提供。
写入响应前追加响应头
cangjie
import soulsoft_web_http.*
import soulsoft_web_staticfiles.*
import soulsoft_web_staticfiles.fileProviders.*
app.useStaticFiles { options =>
options.fileProvider = PhysicalFileProvider("wwwroot")
// 在正文写入前补充缓存头
options.onPrepareResponse = { fileContext =>
fileContext.context.response.headers.cacheControl = ["public, max-age=604800"]
}
}组合多个文件目录
cangjie
import std.collection.*
import soulsoft_web_staticfiles.*
import soulsoft_web_staticfiles.fileProviders.*
let provider = CompositeFileProvider([
PhysicalFileProvider("wwwroot"),
PhysicalFileProvider("assets")
] as Collection<IFileProvider>)
app.useStaticFiles { options =>
// 按注册顺序查找,第一个命中的文件直接返回
options.fileProvider = provider
}中间件顺序
静态文件是否会被终结点“抢先处理”,取决于顺序。
StaticFileMiddleware 在运行时会先检查 context.getEndpoint():
- 如果当前请求已经匹配到终结点,它直接跳过
- 如果还没有终结点,它才尝试读文件
所以有两种常见写法。
终结点优先
cangjie
app.useRouting()
app.useStaticFiles()
app.mapGet("/hello.txt") { ctx =>
ctx.response.write("dynamic")
}这时如果 /hello.txt 已被路由匹配成终结点,静态文件中间件会跳过,返回动态结果。
静态文件优先
cangjie
app.useStaticFiles()
app.mapGet("/hello.txt") { ctx =>
ctx.response.write("dynamic")
}这时静态文件中间件先运行,只要命中物理文件,就不会再继续走到后面的终结点。
时序图
下面是 useDefaultFiles() + useStaticFiles() 组合时的典型链路:
执行流程
useStaticFiles() 做什么
静态文件中间件只有在下面条件同时满足时才会尝试输出文件:
- 当前请求还没有匹配到
Endpoint - 请求方法是
GET或HEAD - 请求路径命中了
requestPath - 能解析出
Content-Type,或者显式允许未知类型 - 文件确实存在
少一项都直接 next(context)。
useDefaultFiles() 做什么
默认文件中间件只做目录判断:
- 当前请求还没有匹配到
Endpoint - 请求方法是
GET或HEAD - 请求路径命中了
requestPath - 对应目录存在
- 目录下命中了默认文件名列表
命中后分两种情况:
- 路径缺少尾斜杠且开启了
redirectToAppendTrailingSlash:返回301 - 路径已经带
/:把请求路径改写为默认文件,再交给后续静态文件中间件
静态文件响应了什么
命中文件后,静态文件中间件会自动处理这些 HTTP 语义:
Content-TypeContent-LengthETagLast-ModifiedAccept-RangesIf-MatchIf-None-MatchIf-Modified-SinceIf-Unmodified-SinceRangeIf-Range
因此它不只是“把文件读出来”,还会根据请求头返回:
200 OK206 Partial Content304 Not Modified412 Precondition Failed416 Requested Range Not Satisfiable
一个推荐写法
如果你的目标是“有默认首页、静态资源挂在 /assets、动态路由优先”,可以这样写:
cangjie
import soulsoft_web_http.*
import soulsoft_web_hosting.*
import soulsoft_web_routing.*
import soulsoft_web_staticfiles.*
import soulsoft_web_staticfiles.fileProviders.*
let builder = WebHost.createBuilder(args)
let app = builder.build()
// 先做路由匹配,让终结点有机会优先命中
app.useRouting()
// 目录访问走默认页,例如 /assets/docs/ -> /assets/docs/index.html
app.useFileServer { options =>
options.requestPath = PathString("/assets")
options.fileProvider = PhysicalFileProvider("wwwroot")
}
// 这里的动态终结点会优先于静态文件
app.mapGet("/assets/version") { ctx =>
ctx.response.write("1.0.0")
}这个顺序的含义很明确:
- 路由先决定当前请求是否已经命中终结点
- 静态文件只处理还没命中终结点的请求
- 默认文件和静态文件共享同一个前缀与文件提供器