从一开始,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 插件配置中设置 ssr: false
来手动选择在你的 Remix+Vite 应用中启用 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 插件 时有效
你不能使用诸如 headers
、loader
和 action
之类的服务器 API——如果你导出它们,构建将抛出错误
你只能从你的 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)方面发挥作用!
此迁移的第一步是让你的当前 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
文件替换为导出一个 default
组件和 HydrateFallback
的 app/root.tsx
路由