React Router 概念
1. History 和 Locations
在 React Router 执行任何操作之前,它必须能够订阅浏览器历史堆栈的变化。
当用户在网站上导航时,浏览器会维护它们自己的历史堆栈。 这就是为什么浏览器的后退和前进按钮可以工作的原因。 在传统的网站(没有 JavaScript 的 HTML 文档)中,浏览器会在用户点击链接、提交表单或单击后退和前进按钮时向服务器发出请求。
例如,考虑以下操作:
- 点击链接到/dashboard
- 点击链接到/accounts
- 点击链接到/customers/123
- 点击后退按钮
- 点击链接到/dashboard
历史堆栈的变化将如下所示,其中粗体条目表示当前的 URL:
- /dashboard
- /dashboard, /accounts
- /dashboard, /accounts, /customers/123
- /dashboard, /accounts, /customers/123
- /dashboard, /accounts, /dashboard
2. History Object:
客户端路由可以让开发人员以编程方式操纵浏览器历史堆栈。 例如,我们可以编写以下代码来更改 URL,而不会造成浏览器默认行为,即发出到服务器的请求:
<a
href="/contact"
onClick={(event) => {
// 阻止浏览器更改URL和请求新文档
event.preventDefault();
// 将一个条目推入浏览器历史堆栈并更改URL
window.history.pushState({}, undefined, "/contact");
}}
/>这段代码更改了 URL,但对于用户界面(UI)没有任何影响。 需要编写更多的代码,在某个地方更改一些状态,以便使 UI 更改为 "/contact" 页面。 问题在于,浏览器不提供一种方法来“侦听 URL”并订阅此类更改。
并不完全正确。可以通过 pop 事件来监听 URL 的更改:
window.addEventListener("popstate", () => {
// URL changed!
});但是这仅在用户点击后退或前进按钮时触发。
当调用window.history.pushState或window.history.replaceState时,没有事件的触发。
这就是 React Router 专用的 history 对象发挥作用的地方。 **它提供了一种监听 URL 更改的方式**,无论是 push,pop 还是 replace 操作,都可以监听到。
let history = createBrowserHistory();
history.listen(({ location, action }) => {
// 每当出现新位置时,就会调用此函数
// 行动(PUSH,POP or REPLACE)
});应用程序不需要设置自己的 history 对象--这是<Router>的工作。
它设置了一个这样的对象,订阅了历史堆栈的变化,并在 URL 更改时更新其状态。
这会导致应用程序重新渲染并显示正确的用户界面(UI)。
它需要在 state 上**放置一个位置(location)**,其他所有工作都从这个单一的对象中进行。
3. Locations
在浏览器中,通过 window.location 对象可以获取 URL 的相关信息,并且该对象有一些方法可以修改 URL:
window.location.pathname; // /getting-started/concepts/
window.location.hash; // #location
window.location.reload(); // 强制刷新页面,并从服务器获取最新内容
// 还有很多其他属性和方法...在 React Router 应用程序中通常不会直接使用 window.location。
React Router 提供了 location 这一概念,这个概念基于 window.location,但要简单得多。
{
pathname: "/bbq/pig-pickins",
search: "?campaign=instagram",
hash: "#menu",
state: null,
key: "aefz24ie"
}其中前三个 { pathname, search, hash } 就像 window.location 返回的对应信息一样。
而后面的两个属性 { state, key } 则是 React Router 特有的。
Location Pathname
这是 URL 中 origin 后面的部分,因此对于 https://example.com/teams/hotspurs,路径名称 (pathname) 是 /teams/hotspurs。
这是与路由匹配唯一的位置部分。
Location Search
人们对 URL 的这一部分使用很多不同的术语:
- 位置搜索(location search)
- 搜索参数(search params)
- URL 搜索参数(URL search params)
- 查询字符串(query string)
在 React Router 中,称其为“Location Search”。 然而,Location Search 是 URLSearchParams 序列化后的版本。 因此有时也可以称其为"URL search params"。
例如:给定这样一个 location:
let location = {
pathname: "/bbq/pig-pickins",
search: "?campaign=instagram&popular=true",
hash: "",
state: null,
key: "aefz24ie",
};我们可以将 location.search 转换为 URLSearchParams:
let params = new URLSearchParams(location.search);
params.get("campaign"); // "instagram"
params.get("popular"); // "true"
params.toString(); // "campaign=instagram&popular=true"当精度不重要时,常见地通过交替使用这些术语来引用序列化的字符串版本为“search”,而引用解析版本为“search params”。
Location Hash
URL 中的哈希标记表示当前页面上的滚动位置。 在 window.history.pushState API 被引入之前,Web 开发人员仅使用 URL 的哈希部分进行客户端路由,它是我们可以在不向服务器发出新请求的情况下操作的唯一部分。 但是,今天我们可以将其用于设计目的。
Location State
浏览器允许通过向 pushState 传递一个值来持久化关于导航的信息。 当用户点击后退时,history.state 上的值会改变为之前“push”的值。
window.history.pushState("look ma!", undefined, "/contact");
window.history.state; // "look ma!"
// 用户点击后退
window.history.state; // undefined
// 用户点击前进
window.history.state; // "look ma!"在 React Router 应用中,不需要直接读取 history.state
React Router 利用了这个浏览器特性,稍微抽象了一下,并将值显示在 location 上,而不是历史记录上。
可以将 location.state 看作是 location.hash 或 location.search,
只不过它不会将值放入 URL 中,而是隐藏起来。
使用location.state的用例包括:
告诉下一页用户来自哪里,并分支 UI。
从列表中向下一个屏幕发送部分记录,以便它可以立即呈现部分数据,然后再获取其余数据。
可以通过
<Link>或 navigate 设置location.state:
<Link to="/pins/123" state={{ fromDashboard: true }} />;
let navigate = useNavigate();
navigate("/users/123", { state: partialUser });下一个页面上,可以使用 useLocation 访问它:
let location = useLocation();
location.state;**location.state 值将被序列化**,因此类似new Date()这样的东西将被转换为字符串。
Location Key
每个 Location 都有一个唯一的 key。 这对于高级情况非常有用,例如基于位置的滚动管理、客户端数据缓存等。 由于每个新位置都有一个唯一的 key,因此可以构建抽象对象,在其中存储信息, 如 plain object 、new Map() ,甚至 locationStorage。
例如,一个非常基本的客户端数据缓存可以按 Location Key(和取回 URL)存储值,并在用户单击时跳过获取数据:
let cache = new Map();
function useFakeFetch(URL) {
let location = useLocation();
let cacheKey = location.key + URL;
let cached = cache.get(cacheKey);
let [data, setData] = useState(() => {
// 从缓存初始化
return cached || null;
});
let [state, setState] = useState(() => {
// 如果缓存,避免读取
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;
// 设置缓存
cache.set(cacheKey, data);
setData(data);
});
return () => controller.abort();
}
}, [state, cacheKey]);
useEffect(() => {
setState("loading");
}, [URL]);
return data;
}4. 匹配
在初始渲染时,当历史堆栈发生变化时,React Router 会将 location 与路由配置匹配,以提供一组要渲染的匹配项。
5. 定义 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> 组件通过其 props.children 递归,剥离它们的 props,并生成像这样的对象:
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",
},
];实际上,使用钩子useroutes(routesgohere)替代<routes>。这就是<routes>正在做的所有事情。
路由可以定义多个段,例如::teamid/edit,亦或仅有一个像:teamid。 路由配置中一个分支下的所有部分都会被添加在一起,以创建路由的最终路径模式。
6. 匹配参数
注意:teamId 段。这就是路径模式的动态段(动态路由),这意味着它不静态匹配 URL(实际字符),而是动态匹配它。
任何值都可以填写:teamId。两者/teams/123 或/teams/cupcakes 都会匹配。
调用解析值 URL 参数。
因此,在这种情况下,teamId 参数将是 "123" 或 "cupcakes"。
7. 对路由排序
在我们的路由配置中,如果将所有分支的段加起来,我们将得到以下路径模式,这些模式是应用程序响应的:
[
"/",
"/teams",
"/teams/:teamId",
"/teams/:teamId/edit",
"/teams/new",
"/privacy",
"/tos",
"/contact-us",
]考虑 URL /teams/new。在列表中哪个模式与 URL 匹配?
没错,有两个!
/teams/new/teams/:teamId
React Router 在这里必须做出决策,只能选择一个。
许多路由器(包括客户端和服务器端)会按照定义顺序处理模式。
首先匹配成功则胜出。在这种情况下,我们将匹配/并呈现
查看这些模式,直觉上知道我们希望/teams/new 与 URL /teams/new 匹配。
这是完全匹配!React Router 也知道这一点。
匹配时,它将根据 段数、静态段、动态段、星形模式等对路由进行排名,并选择最具体的匹配。
8. 没有 path 的路由:
奇怪路由:
<Route index element={<Home />} />
<Route index element={<LeagueStandings />} />
<Route element={<PageLayout />} />它们甚至没有路径,它们怎么能成为路由呢?
这就是 React Router 中 "route" 这个词被比较宽泛地使用的地方。
<Home/> 和 <LeagueStandings/> 是索引路由(index routes),
而 <PageLayout/> 则是布局路由(layout route)。 这两者与路由匹配并没有太多关系。
9. 路由匹配
当路由与 URL 匹配时,它由一个匹配对象表示。
对于 <Route path=":teamId" element={<Team/>}/> 进行的匹配如下所示:
{
pathname: "/teams/firebirds",
params: {
teamId: "firebirds"
},
route: {
element: <Team />,
path: ":teamId"
}
}pathname 保存与此路由匹配的 URL 片段(在案例中是全部 URL)。 params 保存与动态路径参数相关联的解析值。
注意,params 对象键直接映射到片段的名称:
:teamId变为params.teamId。
由于是一棵树,单个 URL 可以匹配整个分支。
比如,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>React Router 会根据这些路由和 URL 创建一组匹配项,以便渲染与路由嵌套相匹配的嵌套 UI。
[
{
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",
},
},
];10. 渲染
最终的概念是渲染。考虑应用程序的入口这样:
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 路径为例。
<Routes> 将会匹配路由配置中的location, 得到一组匹配项,然后呈现像这样的 React 元素树:
<App>
<Teams>
<Team />
</Teams>
</App>在父级路由元素内呈现的每个匹配项都是一个非常强大的抽象。 大多数网站和应用程序都共享这个特征:每个页面子部分都是盒子中的盒子,每个子盒子包含一个导航部分,该部分更改页面的子部分。
react-router v5 就是这样做的
11. Outlets
Outlets(插座)是 React Router v6 中的一个重要概念,用于在路由中处理嵌套组件。
每个 Route 表示一个 URL 匹配模式,当 URL 与之匹配时将渲染相应的组件。
但如果有多层嵌套,如何在父级和子级之间传递渲染控制呢? 这就需要用到 Outlet 组件。
在下面的代码中,Routes 组件会自动渲染第一个匹配的组件(此例中为 App),
而 Teams 组件需要在其父级 App 组件中加入 Outlet 组件才能被正确渲染。
function App() {
return (
<div>
<GlobalNav />
<Outlet />
<GlobalFooter />
</div>
);
}Outlet 组件将始终呈现下一个匹配组件。
这意味着 <Teams> 组件也需要一个 Outlet 来呈现其子级 <Team/>。
例如,当 URL 为 /teams/firebirds/edit 时,使用 Outlet 的元素树将如下所示:
<App>
<Teams>
<EditTeam />
</Teams>
</App>Outlet 组件将根据 URL 匹配路由,动态渲染与父级组件匹配的子级组件。这使得处理复杂的嵌套组件变得简单而直观。
12. 索引路由
在路由配置中设置/teams 路径,如果 url 为/teams/firebirds,则元素树将为:
<app>
<teams>
<team />
</teams>
</app>但是如果 url 为/teams,则元素树将为:
<app>
<teams>
<leaguestandings />
</teams>
</app>什么是<route index element={<leaguestandings>}/>?
它之所以出现在这里,是因为它是一个索引路由。
索引路由在其父级路由的路径上渲染。
这样想一下,如果不在子路由的路径上,<outlet>就不会在 ui 中呈现任何内容:
<app>
<teams />
</app>如果左侧有所有团队的列表,那么空的 outlet 意味着右侧有一个空白页! ui 需要用索引路由来填充这个空间。
另一种思考索引路由的方式是,当父路由匹配但其子路由没有匹配时,它是默认的子路由。
根据用户界面的不同,可能不需要索引路由,但如果父级路由中存在任何持续导航,当用户尚未点击其中任何一个项目时,很可能需要索引路由来填充空白的屏幕空间。
13. Layout Routes
这是尚未匹配的路由配置的一部分:/privacy. 再看一下路由配置:
<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>渲染出来的元素树如下所示:
<PageLayout>
<Privacy />
</PageLayout>不要忘记在想要呈现子路由元素的布局中添加一个
<Outlet>。使用{children}将无法按预期工作。
PageLayout 路由实际上有些奇怪.
称其为布局路由(Layout Routes),因为它根本不参与匹配(虽然它的子元素会). 它只是存在为了更简单地在相同的布局包裹多个子路由。 如果我们不允许这样做,那么你将不得不用两种不同的方式处理布局:有时你的路由为您处理,有时您需要手动进行大量布局组件在整个应用程序中的重复:
可以这样做,但是更建议使用布局路由:
<Routes>
{/* ...略 */}
<Route
path="/privacy"
element={
<PageLayout>
<Privacy />
</PageLayout>
}
/>
<Route
path="/tos"
element={
<PageLayout>
<Tos />
</PageLayout>
}
/>
<Route path="contact-us" element={<Contact />} />
</Routes>14. 导航
当 url 改变时,称之为“导航”。
在 react router 中有两种导航方式:
<Link>navigate
link 组件
这是最常用的导航方法。
使用<link> 组件可让用户在单击链接时更改 url。
react router 将阻止浏览器的默认行为,并告诉浏览历史记录栈推入一个新条目。
location 发生变化后,新链接将渲染。
但是,这些链接仍然可以访问:
仍然呈现 <a href>,因此所有默认的辅助功能都可以得到满足(如键盘、焦点、seo 等)
如果右键或按下 command/ctrl+ 单击以“在新标签页中打开”,则不会阻止浏览器的默认行为。
嵌套路由不仅仅是关于呈现布局;它们还可以实现 “相对链接”。
之前的「teams」路由:
<route path="teams" element={<teams />}>
<route path=":teamid" element={<team />} />
</route><teams/> 组件可以像这样呈现链接:
<link to="psg" />
<link to="new" />它链接到的完整路径将是 /teams/psg 和 /teams/new。
它们继承了它所呈现的路由。因此,你的路由组件不必真正了解应用程序中的其他路由。 大量链接只需深入一个段落即可。你可以重新排列整个路由配置,这些链接仍然可以正常工作。这在开始构建网站并且设计和布局会发生变化时非常有价值。
15. 导航函数
此功能是从 useNavigate 钩子返回的,它允许作为程序员在任何时候更改 URL。
例如,可以在超时后更改 URL:
let navigate = useNavigate();
useEffect(() => {
setTimeout(() => {
navigate("/logout");
}, 30000);
}, []);或者在表单提交之后:
<form onSubmit={event => {
event.preventDefault();
let data = new FormData(event.target)
let urlEncoded = new URLSearchParams(data)
navigate("/create", { state: urlEncoded })
}}>类似于 <Link>,navigate 也可以使用嵌套的“to”值。
navigate("psg");与链接和表单不同,除非有很好的理由需要更改 URL,否则很少使用导航函数,因为它会引入较大的无障碍性和用户预期方面的复杂性。
16. 访问数据:
最后,一个应用程序将要问 React Router 要求一些信息以便构建完整的 UI。
为此,React Router 有一堆钩子。
let location = useLocation();
let urlParams = useParams();
let [urlSearchParams] = useSearchParams();17. 总结:
让我们从头开始把所有内容整合起来!
渲染应用:
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> );<BrowserRouter>创建一个历史记录,将初始location放入状态中,并订阅 URL。<Routes>递归其子路由以构建路由配置,与location进行匹配,创建一些路由匹配,并呈现第一个匹配的路由元素。在每个父级路由中呈现
<Outlet/>。<Outlet/>会呈现路由匹配中的下一个元素。用户点击链接
该链接调用
navigate()历史记录更改 URL 并通知
<BrowserRouter><BrowserRouter>重新呈现,重新从 2 开始