从一开始,Remix 就始终认为您拥有自己的服务器架构。这就是 Remix 建立在 Web Fetch API 之上的原因,并且可以通过内置或社区提供的适配器在任何现代 运行时 上运行。虽然我们认为拥有服务器为大多数应用程序提供了最佳的 UX/性能/SEO 等,但不可否认的是,在现实世界中确实存在大量对单页应用程序的有效用例
这就是我们在 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 应用中手动选择加入 SPA 模式,方法是在你的 Remix Vite 插件配置中设置 ssr: false
// 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
- 如果导出它们,构建将抛出错误
你只能从 SPA 模式下的 root.tsx
导出 HydrateFallback
- 如果从任何其他路由导出它,构建将抛出错误。
你不能从你的 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!)。
此迁移的第一步是让你的当前 React Router 应用在 vite
上运行,以便你拥有非 JS 代码(即 CSS、SVG 等)所需的任何插件。
如果你当前正在使用 BrowserRouter
一旦你使用 vite,你应该能够根据 本指南 中的步骤将你的 BrowserRouter
应用放入一个通配符 Remix 路由中。
如果你当前正在使用 RouterProvider
如果你当前正在使用 RouterProvider
,那么最好的方法是将你的路由移动到单独的文件中,并通过 route.lazy
加载它们
Component
导出(用于 RR)以及 default
导出(用于 Remix 最终使用)一旦你将所有路由都放在自己的文件中,你可以
app/
目录中loader
/action
函数重命名为 clientLoader
/clientAction
index.html
文件替换为一个 app/root.tsx
路由,该路由导出一个 default
组件和 HydrateFallback