Firebase Hosting rewrite 怎么用,什么时候别用

Firebase Hosting 的 rewrite 能把 URL 映射到 Cloud Function、Cloud Run 或 SPA。截至 2026 年 6 月真正能用的三种模式、新的 functionId 写法,以及每种的隐藏陷阱。

rewrite 是 Firebase Hosting 里最常被滥用的功能。用对了,SPA 能跑、/api/* 命中 Cloud Function;用错了,要么静默把站的 SEO 拍扁、返回空白页,要么部署明明成功却报错——因为写法早就变了,老的扁平写法已经定位不到 region。本文讲清楚截至 2026 年 6 月真正能用的三种模式、当前的 functionId 对象写法,以及每种背后的陷阱。

一句话结论

  • Firebase Hosting 的匹配顺序是固定的:保留命名空间 → redirects → 真实静态文件 → rewrites → 404。只有当路径对不上任何真实文件时,rewrite 才会触发。
  • 真正有用的 rewrite 目标只有三种:destination(本地文件,给 SPA 用)、function(Cloud Function)、run(Cloud Run 服务)。没有任何 site 字段能把另一个 Hosting 站点挂到某个路径下。
  • 截至 2026 年 6 月,官方文档里 function 的写法是对象形式:{ "functionId": "api", "region": "us-central1" }。老的扁平写法 "function": "api" 加同级 "region" 已属遗留写法,是部署成功后报「function not found」的头号原因。
  • 重命名或搬迁 URL 要用 redirects"type": 301,不要用 rewrite。rewrite 不改 URL,搜索引擎根本看不到这次变更。
  • Hosting 有 60 秒请求超时。函数跑超过 60 秒就返回 HTTP 504,哪怕你把函数本身的超时调得更长也没用。

rewrite 到底做了什么

firebase.json 里的 rewrite 告诉 Firebase CDN:当请求匹配路径 X 时,用 Y 的响应来返回,且地址栏 URL 不变。Y 可以是本地静态文件、Cloud Function 或 Cloud Run 服务。rewrite 在真实静态文件之后、404 兜底之前生效,所以它接住所有「磁盘上没有对应文件」的 URL。

完整匹配顺序,参见 Firebase Hosting 配置参考

  1. 保留命名空间(/__/*
  2. 配置的 redirects
  3. 真实静态内容(该路径有真实文件)
  4. 配置的 rewrites(第一条匹配 source 的规则胜出)
  5. 自定义 404,然后是默认 404

这个顺序里藏着两个事实,能解释大多数「rewrite 不生效」的反馈。第一,真实文件永远赢过 rewrite,所以只要 /about.html 存在,** 的 rewrite 就盖不住它。第二,rewrite 自上而下匹配、第一条命中的胜出,所以一条宽泛的 ** 放在具体的 /api/** 前面,就会把 API 路径吞掉。

三种真正能用的模式

1. SPA 模式(让客户端路由回落到 index.html)

只在有客户端路由的应用里用(React Router、Vue Router、SvelteKit 的 SPA 模式)。多页静态站千万别这么干——它会把每个 URL 都变成同一份 HTML,把 SEO 彻底毁掉。

{
  "hosting": {
    "public": "dist",
    "rewrites": [
      { "source": "**", "destination": "/index.html" }
    ]
  }
}

陷阱:** 同样匹配 /sitemap.xml/robots.txt,于是爬虫来抓 sitemap 时拿回的是 index.html。由于 Firebase 在 rewrite 之前先返回真实文件,只要 sitemap.xml 实际存在于 dist 里,就会被直接命中。出问题的情况是你的构建没生成这些文件。要么在构建时生成、让它们落到磁盘上,要么加一条 negation 让通配符跳过它们:

{
  "source": "!/@(sitemap.xml|robots.txt)",
  "destination": "/index.html"
}

2. 动态后端模式(Cloud Function 或 Cloud Run)

把 rewrite 限定到一个前缀。绝不要把整个 ** 指给 function:那会让静态资源也走后端,慢、要计费、还完全没必要。注意截至 2026 年 6 月 function 的当前对象写法:

{
  "hosting": {
    "public": "dist",
    "rewrites": [
      { "source": "/api/v1/**", "function": { "functionId": "api", "region": "us-central1" } },
      { "source": "/og/**",      "run": { "serviceId": "og-image", "region": "us-west1" } }
    ]
  }
}

region 必须和函数或服务实际部署的区域一致。当函数只部署在一个区域时,CLI 会从源码自动推断 region;但若部署到多个区域,rewrite 里就必须显式写 region。这里最大的坑就是遗留写法:老的扁平 "function": "api" 加同级 "region" 字段——这正是 2024 年前大多数教程的样子,也正是 rewrite 部署得干干净净却报「function not found」的头号原因。改用 functionId 对象形式。

上线后端 rewrite 之前,还有两个限制值得钉死在脑子里:

  • 60 秒超时。 参见 Cloud Functions on Hosting 文档,Hosting 强制 60 秒请求超时。函数需要超过 60 秒时返回 HTTP 504,无论你给函数本身设了多长的超时。长任务该放进后台任务或队列,而不是挂在 Hosting rewrite 后面。
  • 2nd-gen 函数的 pinTag "pinTag": true 让函数版本与你的静态部署锁在一起,于是回滚 Hosting 也会一并回滚它所服务的函数。该选项仅对 Cloud Functions(2nd gen)可用。

3. 该用 redirect,别用 rewrite(重命名或搬迁 URL)

这一类最常被搞错。如果 URL 真的变了,你需要搜索引擎更新索引,而这要靠一个真正的 301。rewrite 会保留旧 URL,从不发出搬迁信号。用 redirects 块:

{
  "hosting": {
    "redirects": [
      { "source": "/blog/:slug", "destination": "/articles/:slug", "type": 301 },
      { "source": "/old/**",     "destination": "/new/:0",         "type": 301 }
    ]
  }
}

redirect 在真实文件和 rewrite 之前匹配,所以 301 永远胜出。:slug 捕获一个路径段;:0 是被 ** 匹配到的那段。

想把另一个站点挂到某路径下怎么办?

一个常见需求是把独立的文档站或状态页挂在主域名的 /docs/** 下。Firebase Hosting 没有任何 rewrite 字段能按名字指向另一个 Hosting 站点。合法的 rewrite 目标就只有 destinationfunctionrun 三种。两个真正可行的方案:

  • 用部署 target 配置多个独立 Hosting 站点。 多站点模型用的是一个 hosting 配置数组,每个站点带自己的 target,挂在各自的站点或自定义域名上(比如子域名 docs.yourapp.com),而不是同一域名下的某个路径。
  • 走 Cloud Run 代理。 如果你确实要在同一域名下用 /docs/**,就把这个前缀指向一个 Cloud Run 服务,由它去拉取并返回上游内容。代理逻辑和成本都归你。
{
  "hosting": [
    { "target": "app",  "public": "app/dist",  "rewrites": [{ "source": "**", "destination": "/index.html" }] },
    { "target": "docs", "public": "docs/dist" }
  ]
}

部署并验证

部署后,用无痕窗口 + curl 验证,免得缓存和登录态把你带偏:

firebase deploy --only hosting

# 真实静态文件直接命中(rewrite 不触发)
curl -sI https://yourdomain.com/about.html | head -1
# HTTP/2 200

# API 前缀命中函数
curl -sI https://yourdomain.com/api/v1/health | head -1
# HTTP/2 200

# 老 URL 返回真正的 301,带 Location 头
curl -sI https://yourdomain.com/blog/foo | grep -E 'HTTP|location'
# HTTP/2 301
# location: https://yourdomain.com/articles/foo

前 24 小时盯日志。如果爬虫在你的后端 rewrite 上拉静态资源,说明规则太宽:

firebase functions:log --only api --lines 200 | grep -E '\.(png|css|js|ico)' | head
# 有任何输出 = 通配符匹配到了不该匹配的资源

容易踩的坑

  • 扁平 function 写法。 "function": "api" 加同级 "region" 是遗留写法;能部署,但调用时定位不到 region,直接 404。改用 { "functionId": "api", "region": "..." }
  • 多页站做 ** -> /index.html 所有 URL 都变成同一份 HTML,Google 只索引一页,其余全丢。
  • ** 指给函数。 图片、CSS、JS 全走后端。改成限定前缀。
  • 该 redirect 的地方用了 rewrite。 rewrite 保留 URL;只有 301 redirect 才告诉搜索引擎页面搬了。
  • 函数超过 60 秒。 Hosting 在 60 秒超时并返回 504,无视函数自身的超时设置。
  • cleanUrls 不一致。 "cleanUrls": true/about.html/about 提供服务,针对 .html 写的 rewrite 或 redirect 可能永远匹配不到。

成本与限制(截至 2026 年 6 月)

Firebase Hosting 和 Firebase 其余部分共用同样的两档计费。免费的 Spark 方案允许商用,含 10 GB 存储内容和每月 10 GB 数据传输(约 360 MB/天)。按量付费的 Blaze 方案保留这些免费额度,超出后按 $0.026/GB 存储、$0.15/GB 传输计费。后端 rewrite 还要在此之上叠加 Cloud Functions 或 Cloud Run 的费用;** 指给函数之所以贵,正是因为它给每一次资源请求都计了费。

方案Hosting 存储每月传输超额商用
Spark(免费)10 GB10 GB(约 360 MB/天)无(硬上限)允许
Blaze(按量付费)10 GB 免费,之后 $0.026/GB10 GB 免费,之后 $0.15/GB按用量允许

数据来自 Firebase 定价页;以精确数字下决策前请再核对。

FAQ

  • rewrite 和 redirect 有什么区别?:rewrite 返回别的内容,而地址栏 URL 保持不变。redirect 返回 301302,浏览器跳到新 URL。页面真的搬家了就用 redirect,因为只有它会更新搜索引擎索引。
  • 为什么干净部署后函数 rewrite 报「function not found」?:几乎都是遗留扁平写法或 region 对不上。截至 2026 年 6 月用 { "functionId": "api", "region": "us-central1" },并让 region 与函数部署区域一致。参见 Firebase function not found
  • rewrite 到 Cloud Function 能传 query 吗?:能。query string 自动转发,被 ** 匹配到的路径段会出现在 request URL 里。
  • rewrite 的顺序重要吗?:重要。Firebase 自上而下用第一条匹配的 source,所以具体规则必须排在 ** 兜底规则前面。「不生效」的 rewrite 通常是被前一条规则、或被同路径的真实静态文件挡住了。逐项排查见 rewrite 没触发
  • 能把另一个 Firebase Hosting 站点挂到 /docs/** 这样的路径下吗?:不能。没有 site 这个 rewrite 字段。用多站点部署 target 挂在独立站点或子域名上,或者用一个 Cloud Run 服务把该路径代理出去。
  • 为什么挂在 rewrite 后面的长函数会失败?:Hosting 把请求上限设在 60 秒,超过就返回 504,哪怕函数允许更长时间。把长任务挪到后台任务或队列里。

相关阅读

标签: #独立开发 #Firebase #部署 / 托管 #排查