React Router v7 已發布。 查看文件
SPA 模式
本頁內容

SPA 模式

從一開始,Remix 的觀點一直是您擁有您的伺服器架構。這就是為什麼 Remix 是建立在 Web Fetch API 之上,並且可以透過內建或社群提供的配接器在任何現代執行環境上運作。雖然我們相信擁有伺服器可以為大多數應用程式提供最佳的 UX/效能/SEO 等,但不可否認的是,在現實世界中,單頁應用程式 (Single Page Application) 存在許多有效的用例。

  • 您不想管理伺服器,並且偏好透過 Github Pages 或其他 CDN 上的靜態檔案部署您的應用程式
  • 您不想執行 Node.js 伺服器
  • 您想要將 React Router 應用程式遷移到 Remix
  • 您正在開發一種無法在伺服器端渲染的特殊嵌入式應用程式
  • 「您的老闆根本不在乎 SPA 架構的 UX 上限,也不會給您的開發團隊時間/能力來重新架構事情」- Kent C. Dodds

這就是為什麼我們在 2.5.0 (RFC) 中新增了對 SPA 模式 的支援,它大量建立在 客戶端資料 API 之上。

SPA 模式需要您的應用程式使用 Vite 和 Remix Vite 外掛程式

什麼是 SPA 模式?

SPA 模式基本上就像您使用 createBrowserRouter/RouterProvider 擁有自己的 React Router + Vite 設定,但同時還有一些額外的 Remix 好處

  • 基於檔案的路由(或透過 routes() 基於設定的路由)
  • 透過 route.lazy 自動進行基於路由的程式碼分割
  • <Link prefetch> 支援預先擷取路由模組
  • 透過 Remix <Meta>/<Links> API 進行 <head> 管理

SPA 模式告訴 Remix 您不打算在執行階段執行 Remix 伺服器,並且希望在建置時產生靜態的 index.html 檔案,而您只會使用 客戶端資料 API 來進行資料載入和變更。

index.html 是從您 root.tsx 路由中的 HydrateFallback 元件產生。用於產生 index.html 的初始「渲染」將不包含比根更深的任何路由。這確保如果將您的 CDN/伺服器設定為這樣做,則可以為 / 以外的路徑(例如,/about)提供/水合 index.html 檔案。

使用方式

您可以透過使用儲存庫中的 SPA 模式範本快速開始

npx create-remix@latest --template remix-run/remix/templates/spa

或者,您可以透過在 Remix Vite 外掛程式設定中將 ssr: false 設定為手動選擇加入 SPA 模式。

// vite.config.ts
import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    remix({
      ssr: false,
    }),
  ],
});

開發

在 SPA 模式下,您的開發方式與傳統 Remix SSR 應用程式相同,實際上您會使用正在執行的 Remix 開發伺服器以啟用 HMR/HDR。

npx remix vite:dev

生產環境

當您在 SPA 模式下建置應用程式時,Remix 將呼叫 / 路由的伺服器處理常式,並將呈現的 HTML 儲存在與您的用戶端資源一起的 index.html 檔案中(預設為 build/client/index.html)。

npx remix vite:build

預覽

您可以使用 vite preview 在本機預覽生產環境建置。

npx vite preview

vite preview 並非設計為生產環境伺服器使用。

部署

要部署,您可以使用您選擇的任何 HTTP 伺服器來提供您的應用程式。伺服器應設定為從單一根 /index.html 檔案提供多個路徑(通常稱為「SPA 回退」)。如果伺服器不直接支援此功能,則可能需要其他步驟。

舉一個簡單的例子,您可以使用 sirv-cli

npx sirv-cli build/client/ --single

或者,如果您是透過 express 伺服器提供服務(儘管在這種情況下,您可能只想考慮直接在 SSR 模式下執行 Remix 😉)。

app.use("/assets", express.static("build/client/assets"));
app.get("*", (req, res, next) =>
  res.sendFile(
    path.join(process.cwd(), "build/client/index.html"),
    next
  )
);

對 div 而非完整文件進行水合作用

如果您不想對完整的 HTML document 進行水合作用,您可以選擇使用 SPA 模式,並僅對文件的子區段(例如 <div id="app">)進行水合作用,只需做一些小的變更。

1. 新增 index.html 檔案

由於 Remix 不會呈現 HTML 文件,因此您需要在 Remix 之外提供該 HTML。最簡單的方法是保留 app/index.html 文件,其中包含一個佔位符,您可以在建置時用 Remix 呈現的 HTML 取代該佔位符,以產生最終的 index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>My Cool App!</title>
  </head>
  <body>
    <div id="app"><!-- Remix SPA --></div>
  </body>
</html>

<!-- Remix SPA --> HTML 註解是我們將用 Remix HTML 取代的部分。

由於空白在 DOM/VDOM 樹中具有意義,因此請務必不要在其周圍和周圍的 div 中包含任何空格,否則您會遇到 React 水合作用問題。

2. 更新 root.tsx

更新您的根路由以僅呈現 <div id="app"> 的內容。

export function HydrateFallback() {
  return (
    <>
      <p>Loading...</p>
      <Scripts />
    </>
  );
}

export default function Component() {
  return (
    <>
      <Outlet />
      <Scripts />
    </>
  );
}

3. 更新 entry.server.tsx

在您的 app/entry.server.tsx 檔案中,您會想要取得 Remix 呈現的 HTML 並將其插入到您的靜態 app/index.html 檔案佔位符中。您也會想要停止像預設 entry.server.tsx 檔案那樣預先附加 <!DOCTYPE html> 宣告,因為該宣告應該在您的 app/index.html 檔案中)。

import fs from "node:fs";
import path from "node:path";

import type { EntryContext } from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import { renderToString } from "react-dom/server";

export default function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext
) {
  const shellHtml = fs
    .readFileSync(
      path.join(process.cwd(), "app/index.html")
    )
    .toString();

  const appHtml = renderToString(
    <RemixServer context={remixContext} url={request.url} />
  );

  const html = shellHtml.replace(
    "<!-- Remix SPA -->",
    appHtml
  );

  return new Response(html, {
    headers: { "Content-Type": "text/html" },
    status: responseStatusCode,
  });
}

如果您的應用程式中目前沒有 app/entry.server.tsx 檔案,您可能需要執行 npx remix reveal

4. 更新 entry.client.tsx

更新 app/entry.client.tsx 以對 <div id="app"> 而非文件進行水合作用。

import { RemixBrowser } from "@remix-run/react";
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";

startTransition(() => {
  hydrateRoot(
    document.querySelector("#app"),
    <StrictMode>
      <RemixBrowser />
    </StrictMode>
  );
});

如果您的應用程式中目前沒有 app/entry.client.tsx 檔案,您可能需要執行 npx remix reveal

注意事項/警告

  • SPA 模式僅在使用 Vite 和 Remix Vite 外掛程式時才有效。

  • 您不能使用伺服器 API,例如 headersloaderaction - 如果您匯出它們,則建置會擲回錯誤。

  • 您只能從您的 root.tsx 匯出 HydrateFallback 在 SPA 模式中 - 如果您從任何其他路由匯出,則建置會擲回錯誤。

  • 您不能從您的 clientLoader/clientAction 方法呼叫 serverLoader/serverAction,因為沒有正在執行的伺服器 - 如果呼叫它們,則會擲回執行階段錯誤。

伺服器建置

請務必注意,Remix SPA 模式是透過在建置期間於伺服器上執行根路由的「預先呈現」來產生您的 index.html 檔案。

  • 這表示當您建立 SPA 時,您仍然會有「伺服器建置」和「伺服器呈現」步驟,因此您需要小心使用參考僅限於用戶端方面的相依性,例如 documentwindowlocalStorage 等。
  • 一般來說,解決這些問題的方法是從 entry.client.tsx 匯入任何僅限於瀏覽器的程式庫,這樣它們就不會出現在伺服器建置中。
  • 否則,您通常可以使用 React.lazy 或來自 remix-utils<ClientOnly> 元件來解決這些問題。

CJS/ESM 相依性問題

如果您在使用應用程式相依性時遇到 ESM/CJS 問題,您可能需要使用 Vite ssr.noExternal 選項,將某些相依性包含在您的伺服器套件中。

import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";

export default defineConfig({
  plugins: [
    remix({
      ssr: false,
    }),
    tsconfigPaths(),
  ],
  ssr: {
    // Bundle `problematic-dependency` into the server build
    noExternal: ["problematic-dependency"],
  },
  // ...
});

這些問題通常是由於相依性的發佈程式碼對於 CJS/ESM 配置不正確。透過在 ssr.noExternal 中包含特定的相依性,Vite 會將該相依性捆綁到伺服器建置中,並有助於避免在執行伺服器時發生執行階段匯入問題。

如果您有相反的用例,並且您特別想要將相依性保持在套件外部,則可以使用相反的 ssr.external 選項。

從 React Router 遷移

我們也期望 SPA 模式有助於人們將現有的 React Router 應用程式遷移到 Remix 應用程式(無論是否為 SPA!)。

此遷移的第一步是在 vite 上執行您目前的 React Router 應用程式,以便您擁有非 JS 程式碼(即 CSS、SVG 等)所需的任何外掛程式。

如果您目前使用 BrowserRouter

一旦您使用 vite,您應該能夠將您的 BrowserRouter 應用程式放入 此指南中的步驟所述的 catch-all Remix 路由中。

如果您目前使用 RouterProvider

如果您目前使用 RouterProvider,則最佳方法是將您的路由移至個別檔案,並透過 route.lazy 載入它們。

  • 根據 Remix 檔案慣例命名這些檔案,以簡化遷移至 Remix(SPA)的過程。
  • 將您的路由元件匯出為具名 Component 匯出(對於 RR),以及 default 匯出(供 Remix 最終使用)。

一旦您將所有路由置於它們自己的檔案中,您就可以

  • 將這些檔案移至 Remix app/ 目錄中。
  • 啟用 SPA 模式。
  • 將所有 loader/action 函數重新命名為 clientLoader/clientAction
  • 將您的 React Router index.html 檔案取代為匯出 default 元件和 HydrateFallbackapp/root.tsx 路由。
文件和範例以 MIT