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 配置参考:
- 保留命名空间(
/__/*) - 配置的
redirects - 真实静态内容(该路径有真实文件)
- 配置的
rewrites(第一条匹配source的规则胜出) - 自定义 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 目标就只有 destination、function、run 三种。两个真正可行的方案:
- 用部署 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;只有
301redirect 才告诉搜索引擎页面搬了。 - 函数超过 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 GB | 10 GB(约 360 MB/天) | 无(硬上限) | 允许 |
| Blaze(按量付费) | 10 GB 免费,之后 $0.026/GB | 10 GB 免费,之后 $0.15/GB | 按用量 | 允许 |
数据来自 Firebase 定价页;以精确数字下决策前请再核对。
FAQ
- rewrite 和 redirect 有什么区别?:rewrite 返回别的内容,而地址栏 URL 保持不变。redirect 返回
301或302,浏览器跳到新 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,哪怕函数允许更长时间。把长任务挪到后台任务或队列里。