静态文件服务器

本指南介绍在 AdonisJS 应用中提供静态文件。你将学习如何:

  • 安装并配置静态文件中间件
  • 理解何时使用静态文件而非编译后的资源
  • 为最佳性能配置缓存、ETag 和 HTTP 请求头
  • 为安全性控制对点文件的访问
  • 为特定文件类型设置自定义请求头
  • 将静态文件复制到生产构建中

概述

静态文件服务器让你可以直接从文件系统提供文件,而无需为每个文件创建路由处理器。这对于不需要处理的资源至关重要,如 favicon、robots.txt 文件、用户上传内容或可下载的 PDF。

如果没有静态文件服务器,你需要为想要提供的每个文件创建单独的路由。这会迅速变得难以维护:

start/routes.ts
// 没有静态中间件 —— 繁琐且容易出错
router.get('/favicon.ico', async ({ response }) => {
  return response.download('public/favicon.ico')
})
router.get('/robots.txt', async ({ response }) => {
  return response.download('public/robots.txt')
})
router.get('/images/logo.png', async ({ response }) => {
  return response.download('public/images/logo.png')
})
// ... 可能有数百个路由

有了静态中间件,public 目录中的所有文件都会自动可用。该中间件在你的路由被访问之前拦截 HTTP 请求。如果存在与请求路径匹配的文件,它会使用适当的 HTTP 请求头(用于缓存和性能)提供该文件。如果文件不存在,请求会像往常一样继续到你的路由处理器。

Warning

AdonisJS 静态文件服务器在开发期间很方便,但在生产环境中,你应该优先通过反向代理(Nginx、Caddy、Traefik、Apache)或 CDN 来提供静态文件。这些工具是专为静态文件交付而构建的,比 Node.js 进程提供更好的性能、缓存和压缩。这让你的 AdonisJS 服务器可以专注于处理动态请求。

有关推荐的生产环境设置,请参阅 部署指南

AdonisJS 中的关键区别:public 目录中的文件按原样提供,不进行任何处理,而 resources 目录中的文件由你的资源打包器(如 Vite)处理。将已经是最终形式的文件放在 public 中。

安装

@adonisjs/static 包已通过 web 起步套件预配置。如果你使用的是不同的起步套件,可以手动安装和配置它。

使用以下命令安装并配置该包:

node ace add @adonisjs/static
查看 add 命令执行的步骤
  1. 使用检测到的包管理器安装 @adonisjs/static 包。

  2. adonisrc.ts 文件中注册以下服务提供者。

    {
      providers: [
        // ...其他 providers
        () => import('@adonisjs/static/static_provider')
      ]
    }
  1. 创建 config/static.ts 文件。

  2. start/kernel.ts 文件中注册以下中间件。

    server.use([
      () => import('@adonisjs/static/static_middleware')
    ])

配置

静态中间件的配置存储在 config/static.ts 文件中。

config/static.ts
import { defineConfig } from '@adonisjs/static'

const staticServerConfig = defineConfig({
  enabled: true,
  etag: true,
  lastModified: true,
  dotFiles: 'ignore',
})

export default staticServerConfig
enabled

enabled 属性允许你在不将中间件从中间件栈移除的情况下临时禁用它。这在调试或测试不同配置时很有用。将其设置为 false 以停止提供静态文件,同时保持中间件已注册。

{
  enabled: true
}
etag

etag 属性控制服务器是否为缓存验证生成 ETag 。ETag 帮助浏览器确定其缓存的文件版本是否仍然有效,而无需再次下载。

当浏览器请求一个它已缓存的文件时,它会发送 ETag 值。如果文件没有更改,服务器会以 304 Not Modified 状态响应,节省带宽。默认启用此选项,并且通常应在生产环境中保持启用。

{
  etag: true
}
lastModified

lastModified 属性启用 Last-Modified 请求头。服务器使用文件系统中文件的修改时间( stat.mtime 属性)作为请求头值。

浏览器可以将此请求头与 ETag 一起用于缓存验证。与 ETag 一样,默认启用此选项。

{
  lastModified: true
}
dotFiles

dotFiles 属性定义如何处理以圆点开头的文件请求(如 .env.gitignore)。你可以设置三个值之一:'ignore'(默认)、'deny''allow'

'ignore' 选项假装点文件不存在,并返回 404 状态码。出于安全考虑,这是推荐的设置。'deny' 选项使用 403 状态码显式拒绝访问。'allow' 选项像处理任何其他文件一样提供点文件。

{
  dotFiles: 'ignore' // 推荐
}
Warning

dotFiles 设置为 'allow' 可能会在 .env.git 目录被意外放入 public 文件夹时暴露敏感文件。推荐使用 'ignore' 设置(默认)。它返回 404 响应,就像文件不存在一样,防止信息泄露。

如果你需要为域名验证提供特定文件(如用于 SSL 证书的 .well-known/acme-challenge),请创建一个不带前导点的子目录,并将你的验证工具配置为使用该路径。

acceptRanges

acceptRanges 属性允许浏览器恢复中断的下载,而不是从头开始。启用后,服务器会向响应添加 Accept-Ranges 请求头。这对于视频或软件下载等大文件特别有用。该属性默认为 true

{
  acceptRanges: true
}
cacheControl

cacheControl 属性启用 Cache-Control 请求头。此请求头告诉浏览器和 CDN 在检查更新之前应缓存文件多长时间。启用后,你可以使用 maxAgeimmutable 属性来微调缓存行为。

{
  cacheControl: true
}
maxAge

maxAge 属性为 Cache-Control 请求头设置 max-age 指令。这告诉浏览器在检查更新之前可以缓存文件多长时间。你可以以毫秒数指定值,或使用时间表达式字符串如 '30 mins''1 day''1 year'

{
  cacheControl: true,
  maxAge: '30 days'
}
immutable

immutable 属性向 Cache-Control 请求头添加 immutable 指令。这告诉浏览器该文件在其缓存生命周期内永远不会更改,从而允许更激进的缓存。

将此用于带有版本化或哈希文件名的文件(如 app-v2.cssbundle-abc123.js)。默认情况下,immutable 是禁用的。

{
  cacheControl: true,
  maxAge: '1 year',
  immutable: true
}
Tip

immutable 指令仅在同时设置了 maxAge 时才有效。如果你启用了 immutable 而没有设置 maxAge,浏览器会忽略它。这防止了在没有明确过期时间的情况下的意外长期缓存。

::::

headers

headers 属性接受一个函数,该函数为特定文件返回自定义 HTTP 请求头。该函数接收文件路径作为第一个参数, 文件状态 对象作为第二个参数。这允许你根据文件类型、大小或其他属性设置不同的请求头。

该函数应返回一个对象,其中键是请求头名称,值是请求头值。如果函数返回 undefined 或不返回任何内容,则不会为该文件添加额外的请求头。

{
  headers: (path, stats) => {
    /**
     * 为 .mc2 文件设置自定义内容类型
     * 因为默认情况下它们无法被识别
     */
    if (path.endsWith('.mc2')) {
      return {
        'content-type': 'application/octet-stream'
      }
    }

    /**
     * 为 HTML 文件添加安全请求头
     * 以防止 XSS 攻击
     */
    if (path.endsWith('.html')) {
      return {
        'X-Content-Type-Options': 'nosniff',
        'X-Frame-Options': 'DENY'
      }
    }
  }
}

提供静态文件

一旦注册了中间件,你就可以在 public 目录中创建文件,并使用其文件路径在浏览器中访问它们。例如,./public/css/style.css 文件可以在 http://localhost:3333/css/style.css 访问。

以下是生产环境中典型的 public 目录的样子:

public/
├── favicon.ico           # 浏览器标签页图标
├── robots.txt           # 搜索引擎爬取指令
├── sitemap.xml          # 面向搜索引擎的 SEO 站点地图
├── images/
   ├── logo.png         # 静态徽标(不需要优化)
   └── og-image.jpg     # 社交媒体预览图
├── downloads/
   ├── user-guide.pdf   # 可下载文档
   └── terms.pdf        # 法律文档
└── uploads/
    └── avatars/         # 用户上传的头像

这些文件中的每一个都可以在其对应的 URL 访问:

  • http://localhost:3333/favicon.ico
  • http://localhost:3333/images/logo.png
  • http://localhost:3333/downloads/user-guide.pdf

public 目录与 resources 目录

理解何时使用 public 目录与 resources 目录,对于正确组织应用的资源至关重要。

对于已经是最终形式且不需要任何处理的文件,请使用 public 目录:

  • Favicon 和应用图标
  • robots.txtsitemap.xml 文件
  • 不需要优化的静态图片(徽标、图标)
  • 可下载文件(PDF、ZIP 压缩包、可执行文件)
  • 你想按原样提供的第三方 JavaScript 库
  • 用户上传的内容(头像、文档)

对于需要编译或优化的文件,请配合使用资源打包器的 resources 目录:

  • 需要编译的 CSS/SCSS 文件
  • 需要转译的现代 JavaScript/TypeScript
  • 受益于优化和响应式变体的图片
  • 需要版本控制或缓存清除哈希的资源
  • 任何应该由 Vite 或你的构建管道处理的文件
public 中的源文件不会被编译
public/styles/main.scss  # 这不会被编译
public/app.ts            # 这不会被转译
resources 中的源文件会被处理
resources/css/main.scss  # 由 Vite 编译
resources/js/app.ts      # 由 Vite 转译
Tip

一个常见的错误是将源文件(如 .scss 或现代 .ts)放在 public 目录中,并期望它们被编译。静态中间件会原样提供文件,而不进行任何处理。需要编译的源文件应该放在 resources 目录中,并由 Vite 或你的资源打包器处理。

将静态文件复制到生产环境

当你运行 node ace build 命令时,public 目录中的文件会自动复制到 build 文件夹。这确保了你的静态文件在生产环境中与编译后的应用代码一同可用。

复制 public 文件的规则在 adonisrc.ts 文件中定义:

adonisrc.ts
{
  metaFiles: [
    {
      pattern: 'public/**',
      reloadServer: false
    }
  ]
}

pattern 属性使用 glob 语法来匹配 public 目录中的所有文件。reloadServer: false 设置表示在开发期间对这些文件的更改不需要重启开发服务器。

如果你在开发服务器运行时向 public 目录添加文件,你无需重启。静态中间件会立即提供它们。但是,如果你修改了 config/static.ts 文件,你需要重启服务器以使配置更改生效。

另请参阅