核心功能:
- 订阅和操作history堆栈
- 将URL 与 router 匹配
- 渲染与 router相匹配的UI
1. 概念定义
- URL:地址栏中的URL;
- Location:由React Router基于浏览器内置的window.location对象封装而成的特定对象,它代表“用户在哪里”,基本代表了URL;
- Location State:不在URL中,但代表了Location的状态;
- History Stack:随着用户操作导航,浏览器会保留location的堆栈,可以通过返回前进按钮操作;
- Client Side Routing (CSR) :一个纯 HTML 文档可以通过history stack来链接到其他文档,CSR使我的能够操作浏览器历史堆栈,而无需向服务器发出文档请求;
- History:一个object,它允许 React Router 订阅 URL 中的更改,并提供 API 以编程方式操作浏览器历史堆栈;
- History Action :包括POP, PUSH, 或者 REPLACE
-
- push:将新的入口添加到history stack(点击链接或者navigation)
- replace:代替当前的堆栈信息,而不是新push
- pop:当用户点击后推或者前进按钮
- Segment :/ 字符之间的 URL 或 path pattern部分。例如,“/users/123”有两个segment;
- Path Pattern:看起来像 URL,但可以具有用于将 URL 与路由匹配的特殊字符,例如动态段 ("/users/:userId") 或通配符 ("/docs/*")。它们不是 URL,它们是 React Router 将匹配的模式。
- Dynamic Segment:动态的path pattern,例如,/users/:userId 将会匹配 /user/123;
- URL Params : 动态段匹配的 URL 的解析值;
- Router :使所有其他组件和hooks工作的有状态的最高层的组件;
- Route Config:将当前路径进行匹配,通过排序和匹配创建一个树状的routes对象;
- Route:通常具有 { path, element } 或 的路由元素。path是 pattern。当路径模式与当前 URL 匹配时展示;
- Route Element: 也就是 , 读取该元素的 props 以创建路由;
- Nested Routes: 因为路由可以有子路由,并且每个路由通过segment定义 URL 的一部分,所以单个 URL 可以匹配树的嵌套“分支”中的多个路由。这可以通过outlet、relative links等实现自动布局嵌套;
- Relative links:不以 / 开头的链接,继承渲染它们的最近路径。在无需知道和构建整个路径的情况下,就可以实现更深层的url macth;
- Match:当路由匹配 URL 时保存信息的对象,例如匹配的 url params和path name;
- Matches:与当前位置匹配的路由数组,此结构用于nested routes;
- Parent Route:带有子路由的父路由节点;
- Outlet: 匹配match中的下一个匹配项的组件;
- Index Route :当没有path时,在父路由的outlet中匹配;
- Layout Route: 专门用于在特定布局内对子路由进行分组;
2. history 和 location
React Router 的前提是:它必须能够订阅浏览器history stack 中的更改;
浏览器在用户浏览时维护自己的历史堆栈。这就是后退和前进按钮的工作方式。在传统网站(没有 JavaScript 的HTML 文档)中,每次用户单击链接、提交表单或单击后退和前进按钮时,浏览器都会向服务器发出请求。
例如,假设用户:
- 点击 /dashboard的链接;
- 点击 /accounts 的链接;
- 点击 /customers/123 的链接;
- 点击后退按钮;
- 点击指向 /dashboard 的链接;
history stack是如何的?
- /dashboard
- /dashboard, /accounts
- /dashboard, /accounts, /customers/123
- /dashboard, /accounts, /customers/123
- /dashboard, /accounts, /dashboard
2.1 history. object
通过客户端路由(CSR),我们可以通过代码操纵浏览器历史记录栈。
例如,可以写一些这样的代码来改变URL,而不需要浏览器向服务器发出请求的默认行为
<a
href="/contact"
onClick={(event) => {
// 阻止默认事件
event.preventDefault();
// push 并将 URL转想/contact
window.history.pushState({}, undefined, "/contact");
}}
/>
以上代码会修改URL,但不会渲染任何UI的变化,我们需要监听变化,并通过代码修改页面UI
window.addEventListener("popstate", () => {
// URL changed!
});
但此类事件只在点击前进后退按钮才生效,对window.history.pushState 或者 window.history.replaceState 无效
因此,React Router使用 history 对象来监听事件的变化,如POP,PUSH,或者REPLACE
let history = createBrowserHistory();
history.listen(({ location, action }) => {
// this is called whenever new locations come in
// the action is POP, PUSH, or REPLACE
});
在开发环境中,我们不需要关心history object,这些在React Router 底层实现了,React Router提供监听history stack变化,最终在URL变化时更新其状态,并重新渲染。
2.2 Location
React Router 声明了自己的location模块,大致为
{
pathname: "/bbq/pig-pickins",
search: "?campaign=instagram",
hash: "#menu",
state: null,
key: "aefz24ie"
}
pathname、search、hash大致同window.location一致,三者拼接起来等同于URL
location.pathname + location.search + location.hash;
// /bbq/pig-pickins?campaign=instagram#menu
注意:我们可以使用 urlSearchParams 来获取对应的 Search内容
// given a location like this:
let location = {
pathname: "/bbq/pig-pickins",
search: "?campaign=instagram&popular=true",
hash: "",
state: null,
key: "aefz24ie",
};
// we can turn the location.search into URLSearchParams
let params = new URLSearchParams(location.search);
params.get("campaign"); // "instagram"
params.get("popular"); // "true"
params.toString(); // "campaign=instagram&popular=true",
- location state
You may have wondered why the window.history.pushState() API is called "push state". State? Aren't we just changing the URL? Shouldn't it be history.push? Well, we weren't in the room when the API was designed, so we're not sure why "state" was the focus, but it is a cool feature of browsers nonetheless.
// 通过pushState注入堆栈,goback()时,退出一层堆栈
window.history.pushState("look ma!", undefined, "/contact");
window.history.state; // "look ma!"
// user clicks back
window.history.state; // undefined
// user clicks forward
window.history.state; // "look ma!"
可以将location.state 当作跟URL变动而变动的属性,只是一般用于开发者使用
在React Router中,我们可以通过Link 或者Navigate 来设置state,并使用useLocation获取state
<Link to="/pins/123" state={{ fromDashboard: true }} />;
let navigate = useNavigate();
navigate("/users/123", { state: partialUser });
let location = useLocation();
location.state;
- location key
一般用于定位滚动距离,或者客户端数据缓存等,因为每个堆栈都有唯一的key值,可以通过Map或者localStorage来标识指定的堆栈信息。
// 根据location.key缓存数据
let cache = new Map();
function useFakeFetch(URL) {
let location = useLocation();
let cacheKey = location.key + URL;
let cached = cache.get(cacheKey);
let [data, setData] = useState(() => {
// initialize from the cache
return cached || null;
});
let [state, setState] = useState(() => {
// avoid the fetch if cached
return cached ? "done" : "loading";
});
useEffect(() => {
if (state === "loading") {
let controller = new AbortController();
fetch(URL, { signal: controller.signal })
.then((res) => res.json())
.then((data) => {
if (controller.signal.aborted) return;
// set the cache
cache.set(cacheKey, data);
setData(data);
});
return () => controller.abort();
}
}, [state, cacheKey]);
useEffect(() => {
setState("loading");
}, [URL]);
return data;
}
3. 匹配
在初始渲染时,当历史堆栈发生变化时,React Router 会将位置与您的路由配置进行匹配,以提供一组要渲染的匹配项
<Routes>
<Route path="/" element={<App />}>
<Route index element={<Home />} />
<Route path="teams" element={<Teams />}>
<Route path=":teamId" element={<Team />} />
<Route path=":teamId/edit" element={<EditTeam />} />
<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>
// 对应的routes,可以使用 useRoutes(routesGoHere)获取
let routes = [
{
element: <App />,
path: "/",
children: [
{
index: true,
element: <Home />,
},
{
path: "teams",
element: <Teams />,
children: [
{
index: true,
element: <LeagueStandings />,
},
{
path: ":teamId",
element: <Team />,
},
{
path: ":teamId/edit",
element: <EditTeam />,
},
{
path: "new",
element: <NewTeamForm />,
},
],
},
],
},
{
element: <PageLayout />,
children: [
{
element: <Privacy />,
path: "/privacy",
},
{
element: <Tos />,
path: "/tos",
},
],
},
{
element: <Contact />,
path: "/contact-us",
},
];
- 匹配参数 & routes 排序
上述路由config为
[
"/",
"/teams",
"/teams/:teamId",
"/teams/:teamId/edit",
"/teams/new",
"/privacy",
"/tos",
"/contact-us",
];
/teams/:teamId 可以匹配 /teams/123 或者 /teams/aaa
针对 / teams/new,有 "/teams/:teamId"、 "/teams/new", 匹配,V6支持相对智能的匹配,在匹配时,React Router 会根据所有的segment、静态segment、动态segment、通配符模式等进行排序,并选择最具体的匹配项
- 路由匹配
<Route path=":teamId" element={<Team/>}/>
等价于
{
pathname: "/teams/firebirds", // 匹配出与此路由匹配的URL部分
params: {
teamId: "firebirds" // 用来存储对应关系
},
route: {
element: <Team />,
path: ":teamId"
}
}
因为routes时树状结构,因此,一个单一的URL可以匹配所有的树中的“分支”
/teams/firebirds
// 针对下列路由
<Routes>
<Route path="/" element={<App />}>
<Route index element={<Home />} />
<Route path="teams" element={<Teams />}>
<Route path=":teamId" element={<Team />} />
<Route path=":teamId/edit" element={<EditTeam />} />
<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>
// 匹配出的routes为:
[
{
pathname: "/",
params: null,
route: {
element: <App />,
path: "/",
},
},
{
pathname: "/teams",
params: null,
route: {
element: <Teams />,
path: "teams",
},
},
{
pathname: "/teams/firebirds",
params: {
teamId: "firebirds",
},
route: {
element: <Team />,
path: ":teamId",
},
},
];
4. 渲染
将把位置与你的路由配置相匹配,得到一组匹配的内容,然后像这样呈现一个React元素树。
// 假设代码为
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>
);
// 匹配 /teams/firebirds
<App>
<Teams>
<Team />
</Teams>
</App>
- outlets
很像slot, 应该在父路由元素中使用以呈现子路由元素,以此让嵌套的子路由展示,当匹配到子路由的路径后,会展示,或不展示
function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
{/* This element will render either <DashboardMessages> when the URL is
"/messages", <DashboardTasks> at "/tasks", or null if it is "/"
*/}
<Outlet />
</div>
);
}
function App() {
return (
<Routes>
<Route path="/" element={<Dashboard />}>
<Route
path="messages"
element={<DashboardMessages />}
/>
<Route path="tasks" element={<DashboardTasks />} />
</Route>
</Routes>
);
}
- index routes
// /teams/firebirds
<Route path="teams" element={<Teams />}>
<Route path=":teamId" element={<Team />} />
<Route path="new" element={<NewTeamForm />} />
<Route index element={<LeagueStandings />} />
</Route>
<App>
<Teams>
<Team />
</Teams>
</App>
// /teams
<App>
<Teams>
<LeagueStandings />
</Teams>
</App>
index routes 会将父route的outlet渲染出来,一般会在持久化导航的父路由节点上展示默认的子路由信息
- layout routes
<Routes>
<Route path="/" element={<App />}>
<Route index element={<Home />} />
<Route path="teams" element={<Teams />}>
<Route path=":teamId" element={<Team />} />
<Route path=":teamId/edit" element={<EditTeam />} />
<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>
加入要匹配/privacy,会匹配到的结果为:
<App>
<PageLayout>
<Privacy />
</PageLayout>
</App>
实际上,layout routes (布局路由),本身不参与匹配,但其子route参与,如果不这样实现,上述代码会很冗余
<Routes>
<Route path="/" element={<App />}>
<Route index element={<Home />} />
<Route path="teams" element={<Teams />}>
<Route path=":teamId" element={<Team />} />
<Route path=":teamId/edit" element={<EditTeam />} />
<Route path="new" element={<NewTeamForm />} />
<Route index element={<LeagueStandings />} />
</Route>
</Route>
<Route
path="/privacy"
element={
<PageLayout>
<Privacy />
</PageLayout>
}
/>
<Route
path="/tos"
element={
<PageLayout>
<Tos />
</PageLayout>
}
/>
<Route path="contact-us" element={<Contact />} />
</Routes>
5. 导航函数
可以使用useNavigate方法
let navigate = useNavigate();
useEffect(() => {
setTimeout(() => {
navigate("/logout");
}, 30000);
}, []);
要注意,不要随意使用navigate,这样会增加程序的复杂性
<li onClick={() => navigate("/somewhere")} />
// 使用 <Link to="sonewhere" />
6. 数据获取
let location = useLocation();
let urlParams = useParams();
let [urlSearchParams] = useSearchParams();