返回

Firebase Hosting根路径/重写Cloud Function (绕开index.html)

vue.js

Firebase Hosting 根路径 / 重写踩坑:强制 Cloud Function 接管,绕开 index.html

搞 SPA 迁移 Nuxt SSR,用 Firebase Hosting 部署,结果根路径 / 的重写规则死活不生效?明明给 /migrated-route 这种指定路径配 Function 跑得好好的,怎么一到根路径就拉胯了呢?

具体场景是这样的:手头有个 Vue 2 老项目 (SPA),正一步步往 Nuxt 3 (SSR) 迁移。用的是 Firebase Hosting。新迁移好的路由,比如 /migrated-route,想让它走 Cloud Function (nuxtServer) 来实现 SSR,配置如下:

// firebase.json
{
  "hosting": {
    // ... 其他配置
    "rewrites": [
      {
        "source": "/migrated-route",
        "function": "nuxtServer" // 这个能正常工作
      }
      // ... 其他规则
    ]
  }
}

上面这个 /migrated-route 的重写是没问题的,访问它时,请求会交给 nuxtServer 这个 Cloud Function 处理。

按理说,根路径 / 也应该照葫芦画瓢:

// firebase.json (尝试修改)
{
  "hosting": {
    // ... 其他配置
    "rewrites": [
      {
        "source": "/migrated-route",
        "function": "nuxtServer"
      },
      {
        "source": "/", // 尝试给根路径也加上
        "function": "nuxtServer"
      }
      // ... 其他规则
    ]
  }
}

但这样一搞,访问网站根目录 (your-app.web.app/),发现请求根本没到 nuxtServer 那里去,Firebase Hosting 还是直接返回了老 Vue 2 项目构建出来的 index.html 文件。这到底是咋回事?怎么才能让根路径 / 也由 nuxtServer 来伺服 Nuxt 生成的 HTML 呢?

问题出在哪?

Firebase Hosting 处理请求挺实在的:它会先瞅瞅你配置的 public 目录(通常是 distpublic)下有没有完全匹配 请求路径的静态文件。

要是真有,比如你根目录下放了个 index.html,那用户访问 / 时,Firebase 就直接把这个 index.html 甩出去了,压根儿不看 rewrites 里关于 / 的规则。这就是优先级问题:静态文件 > 重写规则 (rewrites)

对于 /migrated-route 这种路径,你的 public 目录下通常没有一个叫 migrated-route 的文件或者 migrated-route/index.html,所以 Firebase 找不到匹配的静态文件,这才轮到 rewrites 规则上场,把请求转发给 nuxtServer

而根路径 / 比较特殊,它默认会寻找 index.html。一旦你在 public 目录下放了 index.html(这对 SPA 应用来说是标配),Firebase 就找到了能直接响应 / 请求的文件,于是 rewrites 里针对 / 的规则就被“短路”了。

怎么搞定?

明白了原因,解决思路就清晰了:得想办法让 Firebase 在处理根路径 / 时,“找不到”那个碍事的 index.html,从而让 rewrites 规则有机会介入。

这里提供几个方案,核心都是围绕怎么处理那个 index.html

方案一:重命名大法 (推荐)

这个方法最直接,也是原问题提问者最终采用的方案,效果不错。

原理

核心思想就是:既然 Firebase 默认找 index.html,那我就不叫 index.html

  1. 把原来 Vue 2 SPA 的入口文件 public/index.html (或者你构建输出目录下的 index.html) 改个名,比如改成 public/app.html
  2. 这样一来,当请求 / 时,Firebase 在 public 目录下找不到 index.html 了。
  3. 此时,rewrites 里针对 / 的规则就能生效,请求被顺利交给 nuxtServer 处理,实现了根路径由 Nuxt SSR 渲染的目标。
  4. 但是,那些还没迁移的、仍然是 Vue 2 的 SPA 路由怎么办?它们也需要一个入口 HTML 文件。所以,我们需要修改 firebase.json 里的“兜底”规则,让所有其他 未匹配到的请求(包括那些老的 SPA 路由)都指向我们改名后的 app.html

操作步骤

  1. 重命名 HTML 文件:
    找到你的 Vue 2 项目的 public 目录(或者是 Webpack/Vite 配置的 publicDir),把里面的 index.html 文件重命名为 app.html

    # 假设你的静态文件放在 public 目录
    mv public/index.html public/app.html
    
  2. 修改 Vue CLI 配置 (或其他构建工具):
    你需要告诉 Vue CLI(或其他构建工具,如 Vite)现在入口 HTML 文件叫 app.html 了。如果你用的是 Vue CLI 3+,修改 vue.config.js 文件:

    // vue.config.js
    module.exports = {
      // ... 其他配置
      indexPath: 'app.html' // 告诉 Webpack 生成的 HTML 文件名叫 app.html
    };
    

    如果你用的是 Vite,查阅 Vite 文档,修改相应的配置项(通常是在 vite.config.jsbuild.rollupOptions.input 里指定入口)。

  3. 修改 firebase.json
    现在来调整 firebase.jsonrewrites 规则。

    // firebase.json
    {
      "hosting": {
        "public": "dist", // 假设你的构建输出目录是 dist
        "ignore": [
          "firebase.json",
          "**/.*",
          "**/node_modules/** "
        ],
        "rewrites": [
          {
            "source": "/migrated-route", // 已迁移的路由指向 Nuxt Function
            "function": "nuxtServer"
          },
          {
            "source": "/", // 根路径也指向 Nuxt Function
            "function": "nuxtServer"
          },
          // ... 可能还有其他迁移后的路由规则 ...
    
          {
            "source": "**", // 重要的兜底规则!
            "destination": "/app.html" // 所有其他未匹配的请求,都 fallback 到 SPA 的入口 app.html
          }
        ]
      },
      "functions": {
        "source": "functions"
      }
    }
    

    重点解释一下最后的 {"source": "** ", "destination": "/app.html"} 规则:

    • "source": "**": 这是一个通配符,匹配所有** 未能**被前面规则匹配到的请求路径。
    • "destination": "/app.html": 将这些请求(比如 /old-feature, /user/profile 等这些还未迁移的 Vue 2 路由)内部重写到 /app.html。浏览器地址栏的 URL 不变,但 Firebase 会返回 app.html 的内容。app.html 加载后,Vue Router 会接管,根据 URL 显示对应的 SPA 页面。

    这样配置后:

    • 访问 / -> 找不到 index.html -> 命中 "source": "/" -> 交给 nuxtServer
    • 访问 /migrated-route -> 命中 "source": "/migrated-route" -> 交给 nuxtServer
    • 访问 /some-old-vue2-route -> 未命中前面的规则 -> 命中 "source": "**" -> 返回 app.html 内容,由 Vue Router 处理。

进阶技巧

  • 通用性: 这个方法不局限于 Vue CLI,任何构建 SPA 的工具(Vite, Rollup 等)都可以用,关键是找到修改输出 HTML 文件名的配置项。
  • cleanUrls 配置: 如果你在 firebase.json 中设置了 "cleanUrls": true,Firebase 会自动帮你处理 URL 末尾的 .html 扩展名,所以访问 /app 也会被视为对 /app.html 的请求(虽然在这个场景下我们主要是用 ** rewrite 来指向它)。
  • 注意部署: 确保你部署到 Firebase Hosting 的是包含 app.html 并且 firebase.json 更新后的版本。

方案二:曲线救国 - 使用重定向 (Redirect)

如果你非常不想改 index.html 的名字(可能有特殊原因),可以考虑用 Firebase Hosting 的 redirects 功能。

原理

这个方法是让 Firebase 对根路径 / 请求执行一个服务器端重定向 。用户访问 / 时,Firebase 不提供内容,而是直接返回一个 3xx 重定向状态码,告诉浏览器去访问另一个由 nuxtServer 处理的路径,例如 /home

操作步骤

  1. 确保 Nuxt 能处理目标路径:
    你需要确保你的 nuxtServer Cloud Function 能够处理一个新的、专门用作重定向目标的路径,比如 /home。这个路径应该返回你希望在根路径展示的 Nuxt SSR 页面。

  2. 修改 firebase.json
    firebase.jsonhosting 部分添加 redirects 配置。

    // firebase.json
    {
      "hosting": {
        "public": "dist",
        // ... 其他配置
        "redirects": [
          {
            "source": "/",
            "destination": "/home", // 重定向到 Nuxt 处理的路径
            "type": 301 // 或者 302,取决于你是否希望搜索引擎认为这是永久重定向
          }
        ],
        "rewrites": [
          {
            "source": "/migrated-route",
            "function": "nuxtServer"
          },
          {
            "source": "/home", // 确保 /home 由 Nuxt Function 处理
            "function": "nuxtServer"
          },
          // ... 其他迁移后的路由规则 ...
    
          {
            "source": "**", // SPA 的 fallback 规则仍然需要
            "destination": "/index.html" // 这里还是用 index.html,因为我们没改名
          }
        ]
      },
      // ... functions 配置 ...
    }
    

优缺点

  • 优点: 不需要修改 index.html 文件名或构建配置。
  • 缺点:
    • 用户访问根路径时会经历一次额外的 HTTP 重定向,有轻微的性能开销。
    • 地址栏 URL 会从 / 变成 /home,可能不是期望的用户体验。
    • 对 SEO 可能有影响(取决于 type 是 301 还是 302,以及你如何处理 canonical 标签等)。
    • 你需要专门为这个重定向目标创建一个 Nuxt 能处理的路由 (/home)。

方案三:挪个窝 - 修改 Hosting 根目录 public

这个方案思路是:既然根目录下的 index.html 碍事,那我把 SPA 的所有文件都挪到 Hosting 根目录的一个子目录里去。

原理

修改 firebase.json 里的 hosting.public 配置,让它指向一个子目录,比如 dist/spa。然后,把原来构建到 dist 目录的所有文件(包括 index.html)都放到 dist/spa 下。这样,Firebase Hosting 的物理根目录 (相对于配置的 public 目录)就变了,原来的 / 路径下就没有 index.html 了,rewrites 规则就能生效。

操作步骤

  1. 调整构建输出:
    修改你的构建配置(如 vue.config.jsoutputDir),让 Vue 2 SPA 项目构建到 dist/spa 子目录。

    // vue.config.js
    module.exports = {
      // ... 其他配置
      outputDir: 'dist/spa' // 修改输出目录
    };
    
  2. 修改 firebase.json
    更新 hosting.public 指向这个新的子目录,并相应调整 rewrites 中的 SPA fallback 路径。

    // firebase.json
    {
      "hosting": {
        "public": "dist/spa", // Hosting 的根现在是 dist/spa
        "ignore": [
          "firebase.json",
          "**/.*",
          "**/node_modules/** ",
          "../functions/**" // 可能需要忽略上一层的 functions 目录
        ],
        "rewrites": [
          {
            "source": "/migrated-route",
            "function": "nuxtServer"
          },
          {
            "source": "/",
            "function": "nuxtServer"
          },
          // ... 其他迁移后的路由规则 ...
    
          {
            "source": "**", // Fallback 规则
            "destination": "/index.html" // 注意:这里的 /index.html 是相对于 public 目录 (即 dist/spa) 的
          }
        ]
      },
      // ... functions 配置 ...
    }
    

    部署时,确保 dist/spa 目录被正确部署。

优缺点

  • 优点: 文件结构上可能更清晰地分离了 SPA 和(可能存在的)其他根目录下的静态文件(如果有的话)。
  • 缺点:
    • 需要修改构建输出路径,可能影响本地开发服务器或 CI/CD 流程。
    • firebase.jsonpublic 路径和 rewritesdestination 路径需要小心处理,它们都是相对于新的 public 目录 (dist/spa) 的。
    • 感觉不如方案一直接。

总的来说,方案一(重命名大法) 是解决这个特定问题的最常用且直接有效的办法。它巧妙地利用了 Firebase Hosting 的处理逻辑,通过移除默认的 index.html 冲突,让针对根路径 /rewrite 规则得以执行,同时通过调整 ** fallback 规则保证了老 SPA 路由的正常运作。操作简单,影响范围可控,推荐优先尝试。