react-router
-
为什么一个项目只有一个html文件?可不可以多几个html文件?
之前我的一个朋友问我这个问题,我的回答是不清楚,没遇到过需要多个html文件的需求。
这显然是忘记了“路由”,准确来说是客户端路由。client side routing 的目的就是解决请求多个html文件的需求,因为每次请求过来的html文件都需要现渲染,用户体验很差,而使用路由则不需要刷新页面并更新内容。路由并不是向web服务器请求html文件,而是本地渲染UI。
React Router 有一些概念:
全貌了解
假设你构建了如下路由:
const root = ReactDOM.createRoot(
document.getElementById("root")
);
root.render(
<BrowserRouter>
<Routes>
<Route path="/" element={<App />}>
<Route index element={<Home />} />
<Route path="teams" element={<Teams />}>
<Route path=":teamId" element={<Team />} />
<Route path="new" element={<NewTeamForm />} />
<Route index element={<LeagueStandings />} />
</Route>
</Route>
<Route element={<PageLayout />}>
<Route path="/privacy" element={<Privacy />} />
<Route path="/tos" element={<Tos />} />
</Route>
<Route path="contact-us" element={<Contact />} />
</Routes>
</BrowserRouter>
);
你开始渲染你的app
<BrowserRouter>创建了一个history,并且将初始的location放入state中,然后订阅URL的变化<Routes>递归它的子路由以构建一个route config,与location一起匹配这些路由,生成路由匹配数组,数组中每个成员都代表当前路径分别匹配的组件,但是只渲染第一个匹配成员- 如果在渲染某个组件时遇到了
<Outlet/>,则会接着渲染匹配数组中的下一个成员 - 如果用户的交互有导致url改变,比如点击链接、登录跳转、编辑后跳转…..
history便会通知<BrowserRouter>,然后重新渲染,即再重复一次该流程
Router
Picking a Router
在React Router v6中建议使用 data APIs
createBrowserRoutercreateMemoryRoutercreateHashRoutercreateStaticRouter
Elements style routes 使用 data APIs
对于一些使用非data APIs的老项目而言,更新很简单,这些老项目配置routes的方式是Elements的形式,而dataAPIs接受参数为routes array,只需要使用createRoutesFromElements 即可
import {
createBrowserRouter,
createRoutesFromElements,
Route,
RouterProvider,
} from "react-router-dom";
const router = createBrowserRouter(
**createRoutesFromElements(
<Route path="/" element={<Root />}>
<Route path="dashboard" element={<Dashboard />} />
{/* ... etc. */}
</Route>
)**
);
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
-
为什么非要使用data APIs?有什么好处吗
只有data APIs产生的routes才能使用 data router,如
route.action route.errorElement route.loader.….
一般使用 createBrowserRouter 即可,如果是不能使用完整URL的情况则使用 createHashRouter
routes
全貌
routes 可以说是 React Router 总重要的部分了。它们将 URL segments 与 components 、data loading 、data mutation 结合。
routes 简单地说就是传给 router 创建函数的对象们
来看看对 route 对象的定义
interface RouteObject {
path?: string;
index?: boolean;
children?: React.ReactNode;
caseSensitive?: boolean;
id?: string;
loader?: LoaderFunction;
action?: ActionFunction;
element?: React.ReactNode | null;
Component?: React.ComponentType | null;
errorElement?: React.ReactNode | null;
ErrorBoundary?: React.ComponentType | null;
handle?: RouteObject["handle"];
shouldRevalidate?: ShouldRevalidateFunction;
lazy?: LazyRouteFunction<RouteObject>;
}
✨ 路径区分大小写
- Path
对于一条 URL 而言,可以被分成多段匹配结果,比如 /temps/rocket/123 可以匹配到 /temps /temps/rocket /temps/rocket/123 ,而每一结果中的 URL片段就是 path,也就指向一个组件,也就是说例子匹配到了三个组件
- Layout Routes
没有 path,但是有 chidren 的route
- Dynamic Segments
以 : 开头的 Path Segments 就是 Dynamic Segments
在路径匹配阶段,它们会被解析,然后以“params”的形式传给 router APIs,比如 loader action useParams
- Optional Segments
以 ?结尾的 Path Segments 就是 Optional Segments
它们是可选的,就是对于最终的 URL 而言这段片段可以跳过
无论静态片段还是动态片段都可以是可选片段
action
action 的作用就是将 data mutation 的工作交给了 route
调用 action 的方法是任何发送给 route 的 non-get submission
// forms
<Form method="post" action="/songs" />;
<fetcher.Form method="put" action="/songs/123/edit" />;
// imperative submissions
let submit = useSubmit();
submit(data, {
method: "delete",
action: "/songs/123",
});
fetcher.submit(data, {
method: "patch",
action: "/songs/123/edit",
});
request
action function 会接受参数 request,它是 Fetch Request 的实例,一般是使用其中的 formData
<Route
action={async ({ request }) => {
let formData = await request.formData();
// ...
}}
/>
errorElement
只要是在渲染该路由的 loading action component 时抛出了错误,就会转而渲染路由对应的 errorElement,如果路由没有指定 errorElement ,则会冒泡到父级路由
errorElement 其实也是一个组件,既然是想编写一个跟错误有关的组件,那肯定需要知道错误是什么,使用 useRouteError Hook 即可
loader
loader 让路由对应组件渲染前能够拿到所需数据
一般是 loader function 内请求数据,返回数据,组件内使用 useLoaderData Hook 拿到数据
request
loader function 的任务是获取数据,为什么还会接受参数 request 呢
以超链接为例
<a
href={props.to}
onClick={(event) => {
event.preventDefault();
navigate(props.to);
}}
/>
如果没有js,默认行为是发送get请求给web服务器,而 React Router 阻止了默认行为,并传递 request 给 loader function
最常见的用法就是创建一个 URL 并读取它的 URLSearchParams,比如搜索栏:搜索的目的就是根据条件筛选数据来显示,所以搜索行为肯定需要再次触发 loader function
function loader({ request }) {
const url = new URL(request.url);
const searchTerm = url.searchParams.get("q");
return searchProducts(searchTerm);
}
Components
Form
GET submissons
Form 的默认请求方法是 get,而 get 请求就相当于一次普通的导航(navigation)比如用户点击超链接。
在进行 GET 请求时,参数可以通过 URL 的查询字符串(query string)来传递。查询字符串是 URL 中的一部分,以 "?" 开头,然后是一系列以 "&" 分隔的键值对,例如:
http://example.com/api/users?name=John&age=30
在上面的例子中,查询字符串是 "?name=John&age=30",其中包含两个键值对,分别是 name=John 和 age=30。
Form 的默认行为是,发出请求时会序列化表单内容,添加到 URLSearchParams 对象中,然后与action指定路径合并,形成完整的URL,发送 get 请求
而接受 request 的函数可通过 new URL(request.url).searchParams.get(’q’) 的方式拿到查询字符串
Mutation submissions
除 get 以外的请求方式都成为 Mutation submissions ,而它们的 request 会被传递给 action function ,form 的prop action 指定路由的 action function。
表单内容会被序列化为 FormData 放进 request 中
useSubmit Hook 配合 Form 组件使用,即编程角度的提交
utilities
defer
-
为什么要用 defer
当匹配到路由时,需要等待其 loader function 执行完毕才能渲染组件,如果 loader function 执行时间过长,则会出现页面尝试无响应的状态
这时如果给出正在加载之类的提示信息,则会提高用户体验
但是往常的解决办法是在组件内使用
[isLoading,setIsLoading] = useState(true)这样 state 控制 jsx 的形式,然而组件根本不能渲染,这个方案就不可能实现或者让父级路由,使用 useNavigation Hook,指定判断路由对应路径,这样过于复杂
const navigation = useNavigation(); navigation.state === ‘loading’ ? navigation.location.pathname === ‘yourPath’ ? ‘loading’ : <Out/> : <Out/>而 defer 配合
<React.Suspense/> <Awiat/>,则可以在 loader function 执行过程中渲染组件中不需要用到 loader 返回 data 的部分,以及需要使用 data 的部分渲染 <React.Suspense/> prop fallback 的内容。如下例,loader 执行过程中,会展示 Loading…. 和 lalala
export async function loader() { const res = new Promise((resolve, reject) => { setTimeout(() => { resolve('klns'); }, 3000); }) return defer({ res: res }); } export function Profile() { const data = useLoaderData(); return ( <div className="profile"> <React.Suspense fallback={<p>Loading...</p>} > <Await resolve={data.res} errorElement={ <p>Error loading!</p> } > {(res) => ( <div>{res}</div> )} </Await> </React.Suspense> <p>lallala</p> </div> ) }