“我正在参加「掘金·启航计划」”
你可能想知道React Router到底是如何做的,如何帮助你构建app,router的准确定义。这些都需要深入去了解路由的基础概念。React Router不仅仅局限在绑定url和组件,而是将所有ui都统一映射到URL的艺术。下面我们从几个方面进行深入:
- 操纵 history stack
- URL 匹配 routes
- 渲染嵌套UI基于route matches
定义
明确的定义还是很重要的,有些名词在前后端框架中是有不同含义的。下面就是写常用的名词:
- URL - 地址栏中的URL,有些人会把URL跟route混淆,但在React Router(以下简称RR)中还是有区别的
- Location - 属于RR的特定对象,基于浏览器的
window.location,表明用户当前所处的位置。 - Location State - 随着location一起存储的状态,不想hash和params会在URL中显示,默认会隐式的保存到浏览器内存中。
- History Stack - 用户浏览网页时记录下的历史堆栈
- 客户端路由(CSR) - 指的是脱离服务端,浏览器端来处理相关路由跳转
- History - History对象可以让RR来订阅路由变更,也支持通过API控制浏览器堆栈(History Stack)
- History Action -
POP,PUSH,REPLACE。 基本上用户切换路由就是基于这三个动作,动作名称也很简单,基本就是对stack的操作。 - Segment - URL中使用
/分割的部分,举个例子"/users/123" 就有2段。 - Path Pattern - 路由匹配模式指的是URL对应的匹配规则。就像动态segment(
"/users/:userId") 或是 star segments ("/docs/*") - Dynamic Segment - 动态segment用以匹配动态路由,比如
/users/:userId就会匹配/users/123 - URL Params - dynamic segment解析出的参数,比如上面就是{userId: 123}
- Router - 包含状态的顶级组件
- Route配置 - 用户的路由配置,用以和当前url进行匹配。
- Route - 路由组件,核心包含path和对应的element。path就是上面的path pattern
- Route Element - 或者说
<Route>,该元素的props被用来创建route - Nested Routes - 因为routes可以包含children,每个route又只包含URL的一部分,因此一个独立URL可以同时匹配到多个级联routes。
- Relative links - 链接中没有以
/开头的链接都会继承最近的路由路径,这样会比较方便构建深层URL而无需知道完整路径。 - Match - 是一个对象,保存着匹配到的url参数。
- Matches - Match数组,一般出现在嵌套路由匹配中。
- Parent Route - 包含子路由的路由
- Outlet - 渲染最新匹配到的组件
- Index Route - 在父路由的outlet中没有path字段的子路由
- Layout Route - parent route没有配置path,将一些子路由包含进group的路由
History 和 Location
RR首先必须要做的事情就是监听history stack的变化。
History对象
在CSR中,开发者可以手动操作history stack,举个例子
<a
href="/contact"
onClick={(event) => {
// 阻止浏览器默认行为
event.preventDefault();
// 手动push
window.history.pushState({}, undefined, "/contact");
}}
/>
上面只是演示用的,实际使用RR中可千万别直接调用window.history.pushState。
上面的代码只是改了URL,但是对于UI其实没什么作用。那如何使得UI随着URL一起变化呢?浏览器可没有提供某种方式来让我们监听URL变化,出了pop 事件:
window.addEventListener("popstate", () => {
// URL 变了
});
但是这个pop事件只有用户点了前进后退才会触发,对于push和replace是没有对应event的。这时候就是RR中history对象干的事了,提供了监听路由变化的方式,包含上面三种类型:
let history = createBrowserHistory();
history.listen(({ location, action }) => {
});
Apps中不需要自己去初始化history对象,都在<Router>里面做好了:监听路由变化,初始化对象,更新URL state。
Location
浏览器有个location对象window.location,提供一些URL信息和方法:
window.location.pathname
window.location.hash;
window.location.reload();
// 等等
在RR中,你也不用单独调用window.location,有个对应的location对象,示例如下:
{
pathname: "/bbq/pig-pickins",
search: "?campaign=instagram",
hash: "#menu",
state: null,
key: "aefz24ie"
}
前面三个参数{ pathname, search, hash } 就是 window.location里的信息。而后面两个{ state, key }就是RR中特有的。
Location Search
对于search有多种讲法:
- location search
- search params
- URL search params
- query string
反正在RR中我们统一使用"location search",这个基于URLSearchParams做序列化处理。
let location = {
pathname: "/bbq/pig-pickins",
search: "?campaign=instagram&popular=true",
hash: "",
state: null,
key: "aefz24ie",
};
let params = new URLSearchParams(location.search);
params.get("campaign"); // "instagram"
params.get("popular"); // "true"
params.toString(); // "campaign=instagram&popular=true",
Location State
你可能好奇为啥window.history.pushState()这个api叫push state,不是就改了URL么,为什么不叫history.push。虽然我们也不清楚,但是这个特性还是蛮不错的,能够让我们存储路由状态:
window.history.pushState("look ma!", undefined, "/contact");
window.history.state; // "look ma!"
// 点击返回
window.history.state; // undefined
// 点击前进
window.history.state; // "look ma!"
我们包装了一下,把这个state存到了location中。你可以通过<Link> 和 navigate来设置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()等参数,还是会被转换成string
Location Key 每个location都配置了个unique 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;
}
路由匹配
在首次渲染和history stack变更后,RR会把location匹配路由配置来给出对应的matchs,用以渲染。
定义路由
<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来生成路由配置:
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>,也可以使用useRoutes(routesGoHere)来代替。
匹配参数
比如:teamId就是个动态参数,很多时候也称为URL params.
路由匹配排序
匹配会把符合路由配置的所有segment都拿出来,如下所示:
[ "/", "/teams", "/teams/:teamId", "/teams/:teamId/edit", "/teams/new", "/privacy", "/tos", "/contact-us",];
那我们最后到底挑哪个呢?就拿/teams/new来说,匹配结果是:
/teams/new
/teams/:teamId
RR会自动基于各种segment规则来计算最具体的那个来返回,这里就是 /teams/new。
没有path的路由
你可能注意到了之前奇怪的路由配置:
<Route index element={<Home />} />
<Route index element={<LeagueStandings />} />
<Route element={<PageLayout />} />
<Home/> 和 <LeagueStandings/> 就是之前定义的index routes, 而<PageLayout/> 就是layout route。我们将在后面介绍他们是如何渲染的。
路由Route Matches
When a route matches the URL, it's represented by a match object. A match for <Route path=":teamId" element={<Team/>}/> would look something like this:
{
pathname: "/teams/firebirds",
params: {
teamId: "firebirds"
},
route: {
element: <Team />,
path: ":teamId"
}
}
由于我们的路由实际是个树结构,因此我们的匹配结果就是一个数组,还是拿/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>
匹配结果如下:
[
{
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",
},
},
];
渲染
最后我们看看是如何渲染的。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为例,其最终的渲染结果为:
<App>
<Teams>
<Team />
</Teams>
</App>
Outlets
Outlet就类似于vue-router中的route-view,用于做子路由的组件占位符,比如上面的例子中App的定义如下:
function App() {
return (
<div>
<GlobalNav />
<Outlet />
<GlobalFooter />
</div>
);
}
Index Routes
还记得之前的/teams路由配置:
<Route path="teams" element={<Teams />}>
<Route path=":teamId" element={<Team />} />
<Route path="new" element={<NewTeamForm />} />
<Route index element={<LeagueStandings />} />
</Route>
如果路由是/teams/firebirds,那么对应的结构是:
<App>
<Teams>
<Team />
</Teams>
</App>
那如果路由是/teams呢?答案就是LeagueStandings
<App>
<Teams>
<LeagueStandings />
</Teams>
</App>
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>
导航
在RR中有两种方式来导航:
<Link>navigate
Link
这个是主要的的推荐方式,RR会确保一切ok:包括覆盖浏览器默认行为,然后在push一个元素到stack中,location对象变更,渲染更新。
嵌套路由同样适用于Link,比如在<Teams>组件中调用:
<Link to="psg" />
<Link to="new" />
那么link结果是/teams/psg 和 /teams/new。
Navigate hook
示例:
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 })
}}>
navigate也支持相对路径:
navigate("psg");
数据获取hooks
let location = useLocation();
let urlParams = useParams();
let [urlSearchParams] = useSearchParams();
希望以上的内容能够帮助你更好的理解RR的运行机制。