React Router&SSR
- [React Router](#React Router)
-
- create-react-router
- SSR端启动
- [V6版本的React Router](#V6版本的React Router)
React Router
create-react-router
pnpx create-react-router@latest --template remix-run/react-router/tutorials/address-book

pnpm run dev

目录结构:
tsconfig.json
javascript
{
"include": [
"**/*", //全局路径
"**/.server/**/*", //SSR
"**/.client/**/*", //CSR
".react-router/types/**/*"
],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"types": ["node", "vite/client"],
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"rootDirs": [".", "./.react-router/types"],
"baseUrl": ".",
"esModuleInterop": true,
"verbatimModuleSyntax": true,
"noEmit": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true
}
}
react-router.config.ts
xxx.config.xxx 这种文件是可以从根路径去解析的
typescript
import { type Config } from "@react-router/dev/config";
export default {
ssr: false, //ssr设置为false
} satisfies Config; //Config定义的类型
补充一
添加预加载,这样对后面的静态页面 about 就不会出现先加载后再展示页面了
pre-rendering
借助的是浏览器中的预渲染
通过预加载的方式找到/app/routes/about.tsx这个路径,这个资源因此也可以前置加载了
javascript
import { type Config } from "@react-router/dev/config";
export default {
ssr: false, //ssr设置为false
prerender: ['/about'] //预加载
} satisfies Config; //Config定义的类型

package.json
scripts
- dev:执行的是 react-router-dev,"dev": "react-router dev",
- build:传递的是环境的变量,可以看作是服务端渲染时的场景,"build": "cross-env NODE_ENV=production react-router build",
pnpm run build
会先进行打包构建,得到client,如果react-router.config.ts中的ssr为true,则有client和service两个包
这是ssr为false时:
这是ssr为true时:
servser端中的,index.js中是一些路由的配置
state状态,router路由 -> next 在ssr中是有一个同构
的概念, 客户端渲染的部分,服务端渲染的部分会有同步的依赖,这部分相同的依赖就有可能是 状态,路由
- start:这时是 serve,cross-env NODE_ENV=production react-router-serve ./build/server/index.js
dependencies
- @react-router/node
- @react-router/servce:react-router-serve的依赖
app
整体的根路径就是:root.tsx
root.tsx
介绍
由三部分构成:
- APP 渲染页面的结构,form表单
- Layout 布局,整体页面,html是在这里绘制的
- ErrorBoundary 目前还没用上
typescript
import type { Route } from "./+types/root";
这里 Route的根路径,算是最新版本的特性,指代当前 router的类型,
这里的 ./+types/root 使用的是 tsconfig.json中的 rootDirs[1]的值,完整是 .react-router/types/app/+types 这个路径
用的就是这个文件
javascript
export default function App() {
......
}
是前端应用中常见的绘制的部分
javascript
import appStylesHref from "./app.css?url";
这个也就是 app/app.css的部分,算是对应的同一型的,默认型的样式的格式
javascript
import {
Form,
Scripts,
ScrollRestoration,
isRouteErrorResponse,
} from "react-router";
import type { Route } from "./+types/root";
import appStylesHref from "./app.css?url";
export default function App() {
return (
<>
<div id="sidebar">
<h1>React Router Contacts</h1>
<div>
<Form id="search-form" role="search">
<input
aria-label="Search contacts"
id="q"
name="q"
placeholder="Search"
type="search"
/>
<div aria-hidden hidden={true} id="search-spinner" />
</Form>
<Form method="post">
<button type="submit">New</button>
</Form>
</div>
<nav>
<ul>
<li>
<a href={`/contacts/1`}>Your Name</a>
</li>
<li>
<a href={`/contacts/2`}>Your Friend</a>
</li>
</ul>
</nav>
</div>
</>
);
}
// The Layout component is a special export for the root route.
// It acts as your document's "app shell" for all route components, HydrateFallback, and ErrorBoundary
// For more information, see https://reactrouter.com/explanation/special-files#layout-export
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href={appStylesHref} />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
// The top most error boundary for the app, rendered when your app throws an error
// For more information, see https://reactrouter.com/start/framework/route-module#errorboundary
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
let message = "Oops!";
let details = "An unexpected error occurred.";
let stack: string | undefined;
if (isRouteErrorResponse(error)) {
message = error.status === 404 ? "404" : "Error";
details =
error.status === 404
? "The requested page could not be found."
: error.statusText || details;
} else if (import.meta.env.DEV && error && error instanceof Error) {
details = error.message;
stack = error.stack;
}
return (
<main id="error-page">
<h1>{message}</h1>
<p>{details}</p>
{stack && (
<pre>
<code>{stack}</code>
</pre>
)}
</main>
);
}
补充一
后面contacts.ts补充后,这里开始增加详情页
- 增加Outlet
javascript
import {
Form,
//详情页
Outlet,
Scripts,
ScrollRestoration,
isRouteErrorResponse,
} from "react-router";
- Outlet增加到页面
javascript
return (
<>
<div id="sidebar">
....
</div>
<div id="detail">
<Outlet />
</div>
</>
);
完成上面这两部后,页面上就成这样了:
补充二
目前从 contacts/1 切换到 contacts/2 是重定向的动作,通过刷新页面的方式,利用history导航进行页面跳转,希望不通过刷新页面的方式去做,这样要怎么做?
使用Link
- 引入Link
javascript
import {
Form,
Outlet,
//增加Link
Link,
Scripts,
ScrollRestoration,
isRouteErrorResponse,
} from "react-router";
- 写入Link
html
<nav>
<ul>
<li>
{/* 将这里原本的a标签替换成Link */}
<Link to={`/contacts/1`}>Your Name</Link>
</li>
<li>
<Link to={`/contacts/2`}>Your Friend</Link>
</li>
</ul>
</nav>
补充三
- 引入数据
javascript
// 通过mock接口请求,返回假数据
import { getContacts } from "./data";
- 导出一个方法,客户端请求加载
javascript
export async function clientLoader() {
// 将此方法的loader作为参数透传出去
const contacts = await getContacts();
return { contacts };
}
- 样式中修改
javascript
// 传递不同的加载器,针对不同加载器来加载不同数据
//type ComponentProps = T.CreateComponentProps<Info>,ComponentProps这个类型是客户端下发下来的,Info是路由的节点状态
export default function App({ loaderData }:Route.ComponentProps) {
//这里的contacts是自动解析出来的
const { contacts } = loaderData;
...
<nav>
{contacts.length ? (
<ul>
{contacts.map((contact) => (
<li key={contact.id}>
<Link to={`contacts/${contact.id}`}>
{contact.first || contact.last ? (
<>
{contact.first} {contact.last}
</>
) : (
<i>No Name</i>
)}
{contact.favorite ? (
<span>★</span>
) : null}
</Link>
</li>
))}
</ul>
) : (
<p>
<i>No contacts</i>
</p>
)}
</nav>
........
}

补充四
这里做的是客户端 渲染的事情,客户端渲染会出现一个问题,会有一个白屏渲染的过程,是客户端需要加载资源生成的,优化刷新白屏跳转要怎么做?
优化办法:
HydrateFallback:当页面初始化的时候,渲染之前能够做一个fallback
javascript
//router中默认能够解析到的
export function HydrateFallback() {
return (
<div id="loading-splash">
<div id="loading-splash-spinner" />
<p>Loading, please wait...</p>
</div>
);
}

补充五
添加 about 导航
javascript
export default function App({ loaderData }:Route.ComponentProps) {
...
<div id="sidebar">
<h1>
<Link to="about">React Router Contacts</Link>
</h1>
...
</div>
}
补充六
将sidebar内容提出来,App精简成这样,并且删除clientLoader方法:
javascript
export default function App() {
return <Outlet />
}
所有代码:
javascript
import {
Outlet,
Scripts,
ScrollRestoration,
isRouteErrorResponse,
} from "react-router";
import type { Route } from "./+types/root";
import { getContacts } from "./data";
import appStylesHref from "./app.css?url";
//router中默认能够解析到的
export function HydrateFallback() {
return (
<div id="loading-splash">
<div id="loading-splash-spinner" />
<p>Loading, please wait...</p>
</div>
);
}
// 传递不同的加载器,针对不同加载器来加载不同数据
//type ComponentProps = T.CreateComponentProps<Info>,ComponentProps这个类型是客户端下发下来的,Info是路由的节点状态
export default function App() {
return <Outlet />
}
// The Layout component is a special export for the root route.
// It acts as your document's "app shell" for all route components, HydrateFallback, and ErrorBoundary
// For more information, see https://reactrouter.com/explanation/special-files#layout-export
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href={appStylesHref} />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
// The top most error boundary for the app, rendered when your app throws an error
// For more information, see https://reactrouter.com/start/framework/route-module#errorboundary
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
let message = "Oops!";
let details = "An unexpected error occurred.";
let stack: string | undefined;
if (isRouteErrorResponse(error)) {
message = error.status === 404 ? "404" : "Error";
details =
error.status === 404
? "The requested page could not be found."
: error.statusText || details;
} else if (import.meta.env.DEV && error && error instanceof Error) {
details = error.message;
stack = error.stack;
}
return (
<main id="error-page">
<h1>{message}</h1>
<p>{details}</p>
{stack && (
<pre>
<code>{stack}</code>
</pre>
)}
</main>
);
}
补充七
- 引入模拟数据接口
javascript
import { createEmptyContact } from "./data";
- 导出数据
javascript
export async function action() {
const contact = await createEmptyContact()
return {
contact
}
}
routes.ts
安装 @react-router/dev 和 @types/react-router 两个插件
typescript
import type { RouteConfig } from "@react-router/dev/routes";
//后补充的代码
import {route} from '@react-router/dev/routes'
//export default [] satisfies RouteConfig;
// 针对 contacts/1,contacts/2 这种格式,匹配到 routes/contacts.tsx这个文件
export default [
route('contacts/:contactId','routes/contacts.tsx')
] satisfies RouteConfig;
补充一
引入home.tsx
- 引入index
javascript
import {index,route} from '@react-router/dev/routes'
- 导出home.tsx
javascript
export default [
index('routes/home.tsx'),
route('contacts/:contactId','routes/contacts.tsx')
] satisfies RouteConfig;
补充二
添加about路由
javascript
import type { RouteConfig } from "@react-router/dev/routes";
import {index,route} from '@react-router/dev/routes'
export default [
index('routes/home.tsx'),
route('contacts/:contactId', 'routes/contacts.tsx'),
//about路由添加
route('about','routes/about.tsx')
] satisfies RouteConfig;
补充三
额外增加新定义的想表达的页面
layout页面=左侧sidebar+右侧home/contacts/about
about页面=并列的关系,需要layout布局改变
- 引入layout
javascript
import {index,layout,route} from '@react-router/dev/routes'
- 修改路由
javascript
export default [
layout('layouts/sidebar.tsx', [
index('routes/home.tsx'),
route('contacts/:contactId', 'routes/contacts.tsx'),
]),
route('about','routes/about.tsx')
] satisfies RouteConfig;
补充四
javascript
export default [
layout('layouts/sidebar.tsx', [
index('routes/home.tsx'),
route('contacts/:contactId', 'routes/contacts.tsx'),
]),
route('about', 'routes/about.tsx'),
route('contacts/:contactId/edit','routes/edit-contact.tsx')
] satisfies RouteConfig;
增加 edit 路由
routes
contacts.tsx
创建 routes/contacts.tsx 文件
从router 6开始,就开始刻意将路由
和视图
绑定,由于 路由在客户端和服务端渲染都是能够共用的,除此之外,jsx的部分大多数也是能够共用的,因此认定 客户端渲染 和 服务端渲染,只是一种渲染模式,渲染内容是完全一致的。
javascript
import { Form } from "react-router";
import type { ContactRecord } from "../data";
export default function Contact() {
//初始的值,针对于输入框内补充对应用户的基本的信息
const contact = {
first: "Your",
last: "Name",
avatar: "https://placecats.com/200/200",
twitter: "your_handle",
notes: "Some notes",
favorite: true,
};
return (
// 布局的返回,使用jsx的方式,有数据则返回,没有数据则做一个兜底
<div id="contact">
<div>
<img
alt={`${contact.first} ${contact.last} avatar`}
key={contact.avatar}
src={contact.avatar}
/>
</div>
<div>
<h1>
{contact.first || contact.last ? (
<>
{contact.first} {contact.last}
</>
) : (
<i>No Name</i>
)}
<Favorite contact={contact} />
</h1>
{contact.twitter ? (
<p>
<a
href={`https://twitter.com/${contact.twitter}`}
>
{contact.twitter}
</a>
</p>
) : null}
{contact.notes ? <p>{contact.notes}</p> : null}
<div>
<Form action="edit">
<button type="submit">Edit</button>
</Form>
{/* form表单,是否要删除的操作 */}
<Form
action="destroy"
method="post"
onSubmit={(event) => {
const response = confirm(
"Please confirm you want to delete this record."
);
if (!response) {
event.preventDefault();
}
}}
>
<button type="submit">Delete</button>
</Form>
</div>
</div>
</div>
);
}
function Favorite({
contact,
}: {
contact: Pick<ContactRecord, "favorite">;
}) {
const favorite = contact.favorite; //favorite的传参
return (
// 是否喜爱的一个操作
<Form method="post">
<button
aria-label={
favorite
? "Remove from favorites"
: "Add to favorites"
}
name="favorite"
value={favorite ? "false" : "true"}
>
{favorite ? "★" : "☆"}
</button>
</Form>
);
}
这样子以后,路径 contacts/1 就不会报404了
补充一
- 请求数据
javascript
import { getContact, type ContactRecord } from "../data";
// params 就是 id的入参
export async function loader({params}):Route.LoaderArgs {
const contact = await getContact(params.contactId)
// 兜底,找不到名字的话就返回404
if (!contact) {
throw new Response('Could not find contact',{status:404})
}
return { contact };
}
- 获取数据,显示数据
javascript
export default function Contact({ loaderData }:Route.ComponentProps) {
//初始的值,针对于输入框内补充对应用户的基本的信息
/* const contact = {
first: "Your",
last: "Name",
avatar: "https://placecats.com/200/200",
twitter: "your_handle",
notes: "Some notes",
favorite: true,
}; */
const {contact}=loaderData
return (
...)
}

home.tsx
创建 routes/home.tsx 文件
javascript
export default function Home() {
return (
<p id="index-page">
This is a demo for React Router.
<br />
Check out{" "}
<a href="https://reactrouter.com">
the docs at reactrouter.com
</a>
.
</p>
);
}

about.tsx
创建 routes/about.tsx 文件
做静态页面,没有包含动态数据展示和请求处理
javascript
import { Link } from "react-router";
export default function About() {
return (
<div id="about">
<Link to="/">← Go to demo</Link>
<h1>About React Router Contacts</h1>
<div>
<p>
This is a demo application showing off some of the
powerful features of React Router, including
dynamic routing, nested routes, loaders, actions,
and more.
</p>
<h2>Features</h2>
<p>
Explore the demo to see how React Router handles:
</p>
<ul>
<li>
Data loading and mutations with loaders and
actions
</li>
<li>
Nested routing with parent/child relationships
</li>
<li>URL-based routing with dynamic segments</li>
<li>Pending and optimistic UI</li>
</ul>
<h2>Learn More</h2>
<p>
Check out the official documentation at{" "}
<a href="https://reactrouter.com">
reactrouter.com
</a>{" "}
to learn more about building great web
applications with React Router.
</p>
</div>
</div>
);
}

edit-contact.tsx
在routes目录中新增edit-contact.tsx文件,也要注册到routes中
javascript
import { Form,redirect } from "react-router";
import type { Route } from "./+types/edit-contact";
import { getContact,updateContact } from "../data";
export async function action({
params,
request,
}: Route.ActionArgs) {
const formData = await request.formData();
// React内置的绑定
const updates = Object.fromEntries(formData);
await updateContact(params.contactId, updates);
// 重定向到当前的数据
return redirect(`/contacts/${params.contactId}`);
}
export async function loader({ params }: Route.LoaderArgs) {
const contact = await getContact(params.contactId);
if (!contact) {
throw new Response("Not Found", { status: 404 });
}
return { contact };
}
export default function EditContact({
loaderData,
}: Route.ComponentProps) {
const { contact } = loaderData;
return (
<Form key={contact.id} id="contact-form" method="post">
<p>
<span>Name</span>
<input
aria-label="First name"
defaultValue={contact.first}
name="first"
placeholder="First"
type="text"
/>
<input
aria-label="Last name"
defaultValue={contact.last}
name="last"
placeholder="Last"
type="text"
/>
</p>
<label>
<span>Twitter</span>
<input
defaultValue={contact.twitter}
name="twitter"
placeholder="@jack"
type="text"
/>
</label>
<label>
<span>Avatar URL</span>
<input
aria-label="Avatar URL"
defaultValue={contact.avatar}
name="avatar"
placeholder="https://example.com/avatar.jpg"
type="text"
/>
</label>
<label>
<span>Notes</span>
<textarea
defaultValue={contact.notes}
name="notes"
rows={6}
/>
</label>
<p>
<button type="submit">Save</button>
<button type="button">Cancel</button>
</p>
</Form>
);
}

增加action方法后:
data.ts
默认的数据,后面还有许多测试数据
layouts
sidebar.tsx
创建 app/layouts目录
创建 layouts/sidebar.tsx文件
javascript
import { Outlet } from "react-router";
export default function SidebarLayout() {
return <Outlet />;
}
补充一
将 root.tsx 文件的 App 内容复制到这里
全部内容:
javascript
import { Outlet } from "react-router";
import { Link,Form } from "react-router";
import { getContacts } from "../data";
import type { Route } from "../+types/root";
export async function clientLoader() {
// 将此方法的loader作为参数透传出去
const contacts = await getContacts();
return { contacts };
}
export default function SidebarLayout({ loaderData }:Route.ComponentProps) {
const { contacts } = loaderData;
return (
<>
<div id="sidebar">
<h1>React Router Contacts</h1>
<div>
<Form id="search-form" role="search">
<input
aria-label="Search contacts"
id="q"
name="q"
placeholder="Search"
type="search"
/>
<div aria-hidden hidden={true} id="search-spinner" />
</Form>
<Form method="post">
<button type="submit">New</button>
</Form>
</div>
<nav>
{contacts.length ? (
<ul>
{contacts.map((contact) => (
<li key={contact.id}>
<Link to={`contacts/${contact.id}`}>
{contact.first || contact.last ? (
<>
{contact.first} {contact.last}
</>
) : (
<i>No Name</i>
)}
{contact.favorite ? (
<span>★</span>
) : null}
</Link>
</li>
))}
</ul>
) : (
<p>
<i>No contacts</i>
</p>
)}
</nav>
</div>
<div id="detail">
<Outlet />
</div>
</>
)
}
about 页面就不在 sidebar 里了
补充二
数据添加,结合roots补充七
javascript
import { createEmptyContact } from "../data";
export async function action() {
const contact = await createEmptyContact()
return {
contact
}
}

SSR端启动
- 开放 ssr:
react-router.config.ts:
typescript
import { type Config } from "@react-router/dev/config";
export default {
ssr: true, //ssr设置为false
prerender: ['/about'] //预加载
} satisfies Config; //Config定义的类型
- 打包构建
pnpm run build

- 启动
pnpm run start

- 打开链接
V6版本的React Router
最新版本的router,如果没有SSR的场景,只是为了渲染,只是使用路由基本的显示声明的话,其实完全没必要升级的
除了 hashRouter,memoryRouter外,还需要熟悉的几个hooks:
useNavigate:重定向
useLocation:location.href 获取可以通过这个方法获取
useMatch:像contacts/:id 可以通过useMatch的方式去获取
React Router for API
在V7之前,VueRouter 和 React Router 是很相像的
这里看 react-router-demo的这个例子:
pnpm i
pnpm run start
App.test.tsx报错则删除这个文件
-
index.tsx文件的路由这里使用的是hashRouter,你可以使用createBrowserRouter等
-
路由使用声明式的方式定义的
-
memoryRouter是从内存中读取的,不会关联到这边url上
-
browserRouter是通过一个非hash值,是比较符合直觉的
这里使用browserRouter
在hello中,使用的是match能够获取数据

- 要使用memoryRouter的话,需要进行一些配置
createMemoryRouter
createMemoryRouter github
hashRouter也是调用的createRouter,唯一的区别是,它传递window参数的时候,使用window中的hash值传递。createBrowserRouter也是一样,调用的createRouter函数