從一開始,Remix 的觀點一直是您擁有您的伺服器架構。這就是為什麼 Remix 是建立在 Web Fetch API 之上,並且可以透過內建或社群提供的配接器在任何現代執行環境上運作。雖然我們相信擁有伺服器可以為大多數應用程式提供最佳的 UX/效能/SEO 等,但不可否認的是,在現實世界中,單頁應用程式 (Single Page Application) 存在許多有效的用例。
這就是為什麼我們在 2.5.0 (RFC) 中新增了對 SPA 模式 的支援,它大量建立在 客戶端資料 API 之上。
SPA 模式基本上就像您使用 createBrowserRouter
/RouterProvider
擁有自己的 React Router + Vite 設定,但同時還有一些額外的 Remix 好處
routes()
基於設定的路由)route.lazy
自動進行基於路由的程式碼分割<Link prefetch>
支援預先擷取路由模組<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
)
);
如果您不想對完整的 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 取代的部分。
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,例如 headers
、loader
和 action
- 如果您匯出它們,則建置會擲回錯誤。
您只能從您的 root.tsx
匯出 HydrateFallback
在 SPA 模式中 - 如果您從任何其他路由匯出,則建置會擲回錯誤。
您不能從您的 clientLoader
/clientAction
方法呼叫 serverLoader
/serverAction
,因為沒有正在執行的伺服器 - 如果呼叫它們,則會擲回執行階段錯誤。
請務必注意,Remix SPA 模式是透過在建置期間於伺服器上執行根路由的「預先呈現」來產生您的 index.html
檔案。
document
、window
、localStorage
等。entry.client.tsx
匯入任何僅限於瀏覽器的程式庫,這樣它們就不會出現在伺服器建置中。React.lazy
或來自 remix-utils
的 <ClientOnly>
元件來解決這些問題。如果您在使用應用程式相依性時遇到 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
選項。
我們也期望 SPA 模式有助於人們將現有的 React Router 應用程式遷移到 Remix 應用程式(無論是否為 SPA!)。
此遷移的第一步是在 vite
上執行您目前的 React Router 應用程式,以便您擁有非 JS 程式碼(即 CSS、SVG 等)所需的任何外掛程式。
如果您目前使用 BrowserRouter
一旦您使用 vite,您應該能夠將您的 BrowserRouter
應用程式放入 此指南中的步驟所述的 catch-all Remix 路由中。
如果您目前使用 RouterProvider
如果您目前使用 RouterProvider
,則最佳方法是將您的路由移至個別檔案,並透過 route.lazy
載入它們。
Component
匯出(對於 RR),以及 default
匯出(供 Remix 最終使用)。一旦您將所有路由置於它們自己的檔案中,您就可以
app/
目錄中。loader
/action
函數重新命名為 clientLoader
/clientAction
。index.html
檔案取代為匯出 default
元件和 HydrateFallback
的 app/root.tsx
路由。