前文
花了两三天时间学习了最新的React Router V6官方文档,在这里归纳一下主要知识点。官网是全英文的技术文档,若有错误请大家指正。
主要概念
React Router不只是将url匹配到一个组件:它建立了一个多功能的用户接口映射到URL,相比日常的简单使用,它有着更多概念需要去了解。
我们将从三个主要方面去了解React Router如何工作的。
-
订阅和操作history栈
-
URL和routes匹配
-
渲染route UI
定义
在前后端框架中,对路由都有着许多不同的定义,在不同的环境下 也会有着不同的意思。
-
URL:地址栏中的URL
-
Location:React Router的特有对象,基于浏览器内置的window.location,代表着“用户在哪里”,它是与URL相关的对象,但包含了更多的信息
-
Location State:location的状态值,没有编码在url中。类似于hash、seartch参数(参数写在url中),区别在于存储在浏览器内存中且不可见
-
History Stack:浏览器通过追踪location栈来跳转。点击回退按钮按住不动就能看到历史记录栈
-
Client Side Routing(CSR):客户端路由可以让开发者在不请求服务端的情况下,操作浏览器的历史记录栈
-
History Action:POP、PUSH、REPLACE的一种,用户可以通过这些操作跳转到其他的URL
-
Push:当点击一个链接或者强制跳转就会将一个新的url push 到History Stack
-
Replace:替换掉当前的url
-
Pop:用户点击回退或者前进按钮发生的行为
-
Segment:分隔符/
-
Path Pattern:带有特殊字符用于匹配路由的表达式
-
Dynamic Segment:动态的path pattern,可以匹配多个值,比如/users/:userId可以配对/users/123,/users/223
-
URL Params:从匹配成功的url上解析的值
-
Router:带有状态的最高层级的组件,其他路由组件和hook基于它工作
-
Route:Route对象或者Route元素,格式可为{ path,element}或者,path是一个path表达式,当前url和path匹配,则会渲染对应的element
-
Nested Routes:嵌套路由
-
Relative Links:相对路径
-
Match:当路由配对后生成的对象,包含着匹配的路由信息如UR参数和path
-
Matchs: 和当前location匹配的一组路由或者route config分支,也是嵌套路由的结构
-
Parent Route:带有子路由的路由
-
Outlet:在所有配对的Matchs中渲染下一个match
-
Index Route:没有path的子路由
-
Layout Route:没有path的Parent Route,专用于形成带有特殊样式的子路由组
History和Location
React Route发挥作用前,必须订阅发生在浏览器history stack的变化。
当用户使用浏览器导航的时候,浏览器便维护着自己的history stack
例子,当用户
1.点击link到/lodashboard
2.点击link到/accounts
3.点击link到/customers/123
4.点击回退按钮
5.点击link到/dashboard
history stack如下
1./dashboard
2./dashboard,/accounts
3./dashboard,/accounts,/customers/123
4./dashboard,/accounts,/customers/123(这里有疑惑,点击回退应该是触发了pop为./dashboard,/accounts)
5./dashboard,/accounts,/dashboard
History对象
在客户端路由中,开发者操作浏览器的history stack
如下例子
<a
href="/contact"
onClick={(event) => {
// stop the browser from changing the URL and requesting the new document
event.preventDefault();
// push an entry into the browser history stack and change the URL
window.history.pushState({}, undefined, "/contact");
}}
/>
================说明:不要在Raect路由中直接使用window.history.pushState============
以上代码改变了URL但是并没有改变UI,我们需要写另一段代码来改变某些地方的状态,已使得当前的页面改变UI
通过pop事件监听
window.addEventListener("popstate", () => {
// URL changed!
});
但是pop事件只能用于前进或者回退按钮,没有事件与window.history.pushState和window.history.replaceState相关
这时候就需要React Route的特殊对象history大显身手了,它可在触发history action的时候监听url变化
let history = createBrowserHistory();
history.listen(({ location, action }) => {
...
})
不用专门设置hisory对象,这是Router的事情。它设置了这些对象,订阅了history stack的变化,并当URL变化的时候改变状态,使得可以重新渲染正确的UI界面。
Location对象
浏览器有一个window.location对象,它提供了url的信息和改变它的方法
// URL: https://reactrouter.com/docs/en/v6/getting-started/concepts#locations
window.location.pathname //getting-started/concepts
window.location.hash; // #location
window.location.reload(); // 刷新
React Router也有location对象,看起来更简单
{
pathname: "/bbq/pig-pickins",
search: "?campaign=instagram",
hash: "#menu",
state: null,
key: "aefz24ie"
}
state和key是React Router特有的,其它属性也存在window.location中
- Location Pathname
URL中路由匹配成功的那一部分字段
https://example.com/teams/hotspurs的pathname就是/teams/hotspurs
- Location Search
在React Router中又被称为URLSearchParams
let location = {
pathname: "/bbq/pig-pickins",
search: "?campaign=instagram&popular=true",
hash: "",
state: null,
key: "aefz24ie",
};
let searchParams = new URLSearchParams(location.search);
searchParams.get('campaign') // "instagram"
searchParams.get('popular') // "true"
params.toString();
- Location State
浏览器允许我们通过pushState持久化存储信息
当用户点击back,history的state值会变为之前的
window.history.pushState('look ma!', undefined, '/contact');
window.history.state; // 'look ma'
// user clicks back
window.history.state // undefined
一个好的Location用例需要做到以下几点
-
告知下一个页面,用户来自哪里并分配到不同的UI界面。比较常见的应用例子:在旧版INS上,如果用户单击了网络视图上的一个项目,就将记录显示在弹窗上,如果是直接通过URL访问,则记录显示在用户当前页面
-
将部分记录发送给下一个界面,便于它立即渲染部分数据,并获取到剩下的数据
设置location的两种方式Link和navigate
<Link to='/users/123' state={{ fromDashboard: true }} />
useNavigate('/users/123',{ state: partialUser })
可以通过useLocation获取它
let location = useLocation();
location.state;
- Location Key
每个location都有一个唯一的key,这对于基于位置的滚动管理、客户端数据缓存等情况非常有用。
有了唯一的key,可以建立一个纯对象new Map()或者locationStorage存储信息
例如,一个非常基础的客户端数据缓存可以通过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(() => {
return cached || null;
})
// 用缓存数据即为done,否则为loading
let [state, setState] = useState(() =>{
return cached ? 'done' : 'loading'
})
useEffect(() => {
if(state === 'loading') {
// 请求数据,并保存在cache中
fetch(URL).then(res =>{
cache.set(cacheKey, data);
setData(data);
})
}
},[state, cacheKey])
useEffect(() => {
setState("loading")
},[URL])
}
Matching匹配
在初始化渲染,history stack改变时候,React Router将会根据你的router config匹配location,并且生成一系列matches
Defining Routes
route config以树的形式展现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>
生成的树形对象
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)配置路由结构
Match Params
:teamId这种片段,就是用来动态匹配path的片段
如/team/123,/team/abc都可以匹配/team/:teamId,teamId可以是123,也可以是abc
Ranking Routes
收集所有的route config中的path匹配式子
可以得到以下的内容
[ "/", "/teams", "/teams/:teamId", "/teams/:teamId/edit", "/teams/new", "/privacy", "/tos", "/contact-us",];
那么URL :/teams/new可以匹配哪些路由呢?
答案是
/teams/new /teams/:teamId
在这里React Route必须选择其中一个。这需要我们去为routes设置优先级来得到预期的结果
在React Router,它会根据分段数、静态分段、动态分段、star segment等对你的路由进行排序,并选择最具体的匹配。
所以这里会选择**/teams/new**
Pathless Routes
<Route index element={<Home />} />
<Route element={<PageLayout />} />
这些路由都没有path,Home是index路由, PageLayout是layout路由
Route Matches
当一个route匹配一个URL后,它可由一个match对象表示
路由匹配如
<Route path=":teamId" element={<Team />} />
得到的对象如下
{
pathname: '/teams/firebirds',
params:{
teamId: 'firebirds'
}
route: {
element: <Team />,
path: ':teamId'
}
}
pathname是URL匹配路由的那部分
params是动态分段匹配后解析的值,注意:这里的itemId变成了对象属性,值变成了对象的值
因为routes是一个树,一个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 Route将会根据这些路由生成一个matches对象组
[
{
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",
},
},
];
Rendering渲染
观察下如果的app入口是这样的
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为例子,将会将location与route config**匹配,得到一组matches集合
<App>
<Teams>
<Team />
</Teams>
</App>
Outlets
Routes会默认渲染第一个元素(在我们案例中是App)
下一个配对元素是Teams,App可以用Outlet设置Teams的渲染位置
Outlet指的是下一个路由渲染位置
function App() {
return (
<div>
<GlobalNav />
<Outlet />
<GlobalFooter />
</div>
);
}
Index Routes
/teams的route config如下
<Route path="teams" element={<Teams />}>
<Route path=":teamId" element={<Team />} />
<Route path="new" element={<NewTeamForm />} />
<Route index element={<LeagueStandings} />
</Route>
如果URL是/teams/friebirds
matches树结构是
<App>
<Teams>
<Team />
</Teams>
</App>
如果URL是/teams,matches树结构是
<App>
<Teams>
<LeagueStandings />
</Teams>
</App>
为什么会有LeagueStandings?因为它是一个index route,当URL是父路由的路径,index route会作为父路由的Outlet
如果你没有匹配任何的子路由路径,那么Outlet将什么都不渲染。
<App>
<Teams />
</App>
为了避免页面空白,需要一个index route作为默认的路由页面
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>
PageLayout并没有参与路由匹配(虽然其子节点参与)。它存在的目的是让多个子路由界面都使用用于的布局样式。如果不去设置它,就需要在应用中手动设置它们
Navigating导航
React Router中有两种导航方式
-
Link
-
navigate
Link
导航的主要方式,允许用户点击后改变URL。React Router会阻止浏览器的默认行为,并且将一个新的入口添加到history栈。location改变后,新的匹配路由也会渲染。
嵌套路由不只是渲染了布局;它们也允许"相对links"。
如之前的teams路由
<Route path="teams" element={<Teams />}>
<Route path=":teamId" element={<Team />} />
</Route>
组件可以这样渲染Links
<Link to="psg" />
<Link to="new" />
和它们对应的Links的完整路径是/teams/psg和/teams/new,它们继承了已经渲染的路由路径,使得你不必关心其他多余的路由路径是什么。
导航函数
这个函数是在useNavigate中返回的,方便在纯js中使用
let navigate = useNavigate();
useEffect(() => {
setTimeout(() => {
navigate("/logout");
}, 30000);
}, []);
在使用navigate和Link之间是需要权衡的,官方并不推荐这样做
<li onClick={() => navigate("/somewhere")} />
除了Links和form,很少的交互需要改变URL,因为它带来了是否可访问和用户期望这些问题的复杂性。
Data Access数据获取
最后,应用需要从React Router中获取一些信息来建立完整的UI,为此,React Router提供了一些Hooks
let location = useLocation();
let urlParams = useParams();
let [urlSearchParams] = useSearchParams();
Review总结回顾
综上所述,通过一个例子分析React Route的生成和渲染过程
- 以这种方式渲染你的app
-
创建了一个history对象,将初始location放入state中,并订阅URL
-
.递归子路由来建立route config,根据location匹配路由,建立了一些路由matches并且渲染第一个匹配的路由元素
-
在每个父路由中渲染Outlet
-
outlet会渲染下一个即将匹配的路由
-
用户点击链接Link
-
Link调用navigate()
-
history改变url并且通知
-
再渲染,并回到第二步