Skip to content

静态文件

serveStatic 将本地目录挂载为可公开访问的静态资源路径,支持浏览器缓存协议(ETag / Last-Modified)和 HTTP Range 请求(视频流、大文件断点续传)。

快速开始

cangjie
import ace_web.*
import ace_web.static_file.*

let app = App()

// 将 ./public 目录挂载到 /static 前缀
app.use(serveStatic("./public", "/static"))

访问 GET /static/logo.png 将映射到磁盘 ./public/logo.png

参数说明

cangjie
func serveStatic(
    dir    : String,            // 本地目录路径(相对或绝对)
    prefix : String = "/",      // URL 前缀,默认挂载到根路径
    options: StaticOptions = StaticOptions()
): Middleware

StaticOptions 配置项:

字段类型默认值说明
indexString"index.html"目录访问时的默认文件
etagBooltrue是否生成并校验 ETag
lastModifiedBooltrue是否输出 Last-Modified
maxAgeMsInt640Cache-Control: max-age 秒数(毫秒转换)
dotfilesString"ignore"点文件处理:"ignore" / "allow" / "deny"
spaFallbackBoolfalse开启 SPA 回退,404 时返回 index.html

声明式注册(@Middleware)

在框架宏层可用 @Middleware 将静态文件中间件纳入 IoC 管理:

cangjie
import ace_framework.*
import ace_web.static_file.*

@Middleware[order: 5]
class StaticFilesMiddleware <: MiddlewareProvider {
    @Value["static.dir"]
    var dir: String = "./public"

    @Value["static.prefix"]
    var prefix: String = "/assets"

    public func middleware(): Middleware {
        serveStatic(dir, prefix, StaticOptions(maxAgeMs: 86400_000))
    }
}

order 值越小越先执行。静态文件中间件建议排在鉴权中间件之前,避免不必要的鉴权消耗。

Range 请求(断点续传 / 视频流)

serveStatic 自动处理 Range: bytes=start-end 请求,无需额外配置:

GET /assets/video.mp4 HTTP/1.1
Range: bytes=0-1048575

响应:

HTTP/1.1 206 Partial Content
Content-Range: bytes 0-1048575/52428800
Content-Length: 1048576
Accept-Ranges: bytes

视频播放

HTML5 <video> 标签依赖 Range 请求实现拖动进度条。确保静态文件中间件在路由之前注册,或使用独立端口/路径前缀避免被其他中间件拦截。

缓存头

  • ETag:基于文件内容哈希,客户端再次请求时携带 If-None-Match,内容未变返回 304 Not Modified
  • Last-Modified:基于文件修改时间,配合 If-Modified-Since 头使用。
  • Cache-Control:通过 maxAgeMs 控制客户端强缓存时长。
cangjie
// 静态资源缓存 7 天
serveStatic("./dist", "/", StaticOptions(maxAgeMs: 7 * 86400_000))

开发环境禁用缓存

开发时建议将 maxAgeMs 设为 0,避免修改后浏览器仍读缓存。可通过配置文件区分环境:

toml
# config/dev.toml
[static]
maxAgeMs = 0

# config/prod.toml
[static]
maxAgeMs = 604800000   # 7 天

SPA 回退(单页应用)

前端 SPA(React/Vue 等)使用 History 路由时,刷新页面会请求服务端不存在的路径,此时需将请求回退到 index.html

cangjie
app.use(serveStatic(
    "./dist",
    "/",
    StaticOptions(
        spaFallback: true,
        maxAgeMs: 86400_000
    )
))

开启 spaFallback 后,所有未命中静态文件的 GET 请求(且非 API 路径)均返回 ./dist/index.html

API 路径冲突

若 API 与前端资源共用同一端口,请确保 API 路由在静态文件中间件之前注册,否则 API 路径可能被 SPA 回退拦截,返回 index.html 而非 JSON 响应。

cangjie
app.use(apiRouter.routes())   // 先注册 API 路由
app.use(serveStatic("./dist", "/", StaticOptions(spaFallback: true)))  // 再注册静态

完整示例

cangjie
import ace_web.*
import ace_web.static_file.*
import ace_router.*
import ace_http.*

main(): Unit {
    let app = App()
    let api = Router("/api")

    api.get("/health") { ctx =>
        ctx.json("""{"status":"ok"}""")
    }

    // API 优先
    app.use(api.routes())

    // 静态文件(含 SPA 回退)
    app.use(serveStatic(
        "./dist",
        "/",
        StaticOptions(spaFallback: true, maxAgeMs: 86400_000)
    ))

    listen(app, "0.0.0.0", 8080)
}

基于 Apache-2.0 许可证发布