是React生态库之一,是可以在CSR和SSR环境下为了React而设计的路由库。
安装:
npm install react-router-dom@6
yarn add react-router-dom@6
基本用法:
//src/index.js
import * as React from 'react';
import ReactDOM from 'react-dom/client';
import {BrowserRouter} from 'react-router-dom';
import App from './App';
const root = ReactDOM.createRoot(
document.getElementById('root');
);
root.render(
<React.StrictMode>
<BrowserRouter>
<App/>
</BrowserRouter>
</React.StrictMode>
);
//src/App.js
import * as React from 'react';
import {Routes, Route, Link} from 'react-router-dom';
import './App.css';
function App() {
return (
<div className='App'>
<h1>Welcome to React Router!</h1>
<Routes>
<Route path='/' element={<Home/>} />
<Route path='about' element={<About/>} />
</Routes>
</div>
)
}
//Home.jsx
function Home() {
return (
<>
<main>
<h2>Welcome to the homepage!</h2>
<p>you can do this, I believe in you.</p>
</main>
<nav>
<Link to='/about'>About</Link>
</nav>
</>
)
}
//About.jsx
function About() {
return (
<main>
<h2>who are we?</h2>
<p>
that feels like an existential question, don't you think?
</p>
</main>
<nav>
<Link to='/'>Home</Link>
</nav>
)
}
配置路由:
import ReactDOM from 'react-dom/client';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
const root = ReactDOM.createRoot(
document.getElementById('root');
);
root.render(
<BrowserRouter>
<Routes>
<Route path='/' element={<App/>}>
<Route index element={<Home/>} />
<Route path='teams' element={<Team/>}>
<Route path=':teamId' element={<Team/>} />
<Route path='new' element={<NewTeamForm/>} />
<Route index element={<LeagueStandings/>} />
</Route>
</Route>
</Routes>
</BrowserRouter>
)
在先前版本的React Router中,针对多个匹配到的router,需要声明出具体的匹配逻辑,但V6相对更“智能”。
修改url的方式:
//Link
import {Link} from 'react-router-dom';
function Home() {
return (
<div>
<h1>Home</h1>
<nav>
<Link to='/'>Home</Link> | {" "}
<Link to='about'>About</Link>
</nav>
</div>
)
}
//useNavigate:更多用于JS操作后跳转使用
import { useNavigate } from 'react-router-dom';
function Invoices() {
let navigate = useNavigate();
return (
<div>
<NewInvoiceForm onSubmit={async (event) => {
let newInvoice = await createInvoice(event.target);
navigate(`/invoices/${newInvoice.id}`);
}} />
</div>
)
}
使用url的路径参数,常用于匹配path参数后fetch数据:
import { Routes, Route, useParams } from 'react-router-dom';
function App() {
return (
<Routes>
<Route path='invoices/:invoiceId' element={<Invoice/>} />
</Routes>
);
}
function Invoice() {
let params = useParams();
return <h1>Invoice{params.invoiceId}</h1>;
}
//example
function Invoice() {
let {invoiceId} = useParams();
let invoice = useFakeFetch(`/api/invoices/${invoiceId}`);
return invoice ? (
<div>
<h1>{invoice.customerName}</h1>
</div>
) : (
<Loading />
)
}
嵌套路由:
function App() {
return (
<Routes>
<Route path='invoices' element={<Invoices />}>
<Route path=':invoiceId' element={<Invoice/>} />
<Route path='sent' element={<SentInvoices/>}/>
</Route>
</Routes>
)
}
//提供三种路由:
///invoices
///invoices/sent
<App>
<Invoices>
<SentInvoices/>
</Invoices>
</App>
///invoices/:invoiceId
<App>
<Invoices>
<Invoice/>
</Invoices>
</App>
//父router中子router可以用<Outlet>表示
import {Routes, Route, Outlet} from 'react-router-dom';
function App() {
return (
<Routes>
<Route path='invoices' element={<Invoices/>}>
<Route path=":invoiceId" element={<Invoices/>} />
<Route path="sent" element={<SentInvoices/>} />
</Route>
</Routes>
)
}
function Invoices() {
return (
<div>
<h1>Invoices</h1>
<Outlet /> //匹配对应的<Invoice/>或<SentInvoices/>
</div>
)
}
function Invoice() {
let { invoiceId } = useParams();
return <h1>Invoice {invoiceId}</h1>;
}
//在根router中添加Link跳转
import { Routes, Route, Link, Outlet } from 'react-router-dom';
function App() {
return (
<Routes>
<Route path='/' element={<Layout/>}>
<Route path='invoices' element={<Invoices/>} />
<Route path='dashboard' element={<Dashboard/>} />
</Routes>
</Routes>
)
}
function Layout() {
return (
<h1>Welcome to the app!</h1>
<nav>
<Link to='invoices'>Invoices</Link> | {" "}
<Link to='dashboard'>Dashboard</Link>
</nav>
<div className='content'>
<Outlet />
</div>
)
}
function Invoices() {
return <h1>Invoices</h1>;
}
function Dashboard() {
return <h1>Dashboard</h1>;
}
index routes:
function App() {
return (
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Activity />} />
<Route path="invoices" element={<Invoices />} />
<Route path="activity" element={<Activity />} />
</Route>
</Routes>
);
}
function Layout() {
return (
<div>
<GlobalNav />
<main>
<Outlet />
</main>
</div>
);
}
// 如果是 "/"
<App>
<Layout>
<Activity />
</Layout>
</App>
relative links:link to 指向的是相同级别的路由。
import { Routes, Route, Link, Outlet } from 'react-router-dom';
function Home() {
return <h1>Home</h1>;
}
function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<nav>
<Link to='invoice'>Invoice</Link> //dashboard/invoices
<Link to='team'>Team</Link> //dashboard/team
</nav>
<hr/>
<Outlet/>
</div>
)
}
function Invoices() {
return <h1>Invoices</h1>;
}
function Team() {
return <h1>Team</h1>;
}
function App() {
return (
<Routes>
<Route path='/' element={<Home/>} />
<Route path='dashboard' element={<Dashboard/>}>
<Route path='invoices' element={<Invoices/>} />
<Route path='team' element={<Team/>} />
</Route>
</Routes>
)
}
兜底routes,多个routes集成在一个组件。
function App() {
return (
<div>
<Sidebar>
<Routes>
<Route path='/' element={<MainNav/>} />
<Route path='dashboard' element={<DashboardNav/>} />
</Routes>
</Sidebar>
<MainContent>
<Routes>
<Route path='/' element={<Home/>}>
<Route path='about' element={<About/>} />
<Route path='support' element={<Support/>} />
</Route>
<Route path='dashboard' element={<Dashboard/>}>
<Route path='invoices' element={<Invoices/>} />
<Route path='team' element={<Team/>} />
</Route>
<Route path='*' element={<NotFound/>} />
</Routes>
</MainContent>
</div>
)
}
升级到V6的一些问题:
import { useLocation, useNavigate, useParams } from 'react-router-dom';
function withRouter(Component) {
function ComponentWithRouterProp(props) {
let location = useLocation();
let navigate = useNavigate();
let params = useParams();
return (
<Component {...props} router={{ location, navigate, params }} />
);
}
return ComponentWithRouterProp;
}
//V6以下
<Route path=":userId" component={Profile} />
<Route path=":userId" render={routeProps => (
<Profile routeProps={routeProps} animate={true} />
)}>
<Route path=":userId" children={({ match }) => ( match ?
(
<Profile match={match} animate={true} />
)
) : (
<NotFound />
)}>
//V6
<Route path=":userId" element={<Profile/>} />
<Route path=":userId" element={<Profile animate={true}/>} />
function Profile({ animate }) {
//使用hooks,在元素定义内处理逻辑
let params = useParams();
let location = useLocation();
}
//V6以下
<Switch>
<Route path="/users" component={Users} />
</Switch>;
function Users() {
return (
<div>
<h1>User</h1>
<Switch>
<Route path="/users/account" component={Account} />
</Switch>
</div>
)
}
//V6
<Routes>
<Route path='/users/*' element={<User/>} />
</Routes>;
function Users() {
return (
<div>
<h1>User</h1>
<Routes>
<Route path="account" element={<Account/>} />
</Routes>
</div>
)
}
//V5
function App() {
return (
<Switch>
<Route path={/(en|es|fr)/} component={Lang} />
</Switch>
);
}
function Lang({ params }) {
let lang = params[0];
let translations = I81n[lang];
//...
}
//V6
function App() {
return (
<Routes>
<Route path="en" element={<Lang lang="en"/>} />
<Route path="es" element={<Lang lang="es"/>} />
<Route path="fr" element={<Lang lang="fr"/>} />
</Routes>
);
}
function Lang({ lang }) {
let translations = I81n[lang];
//...
}
//V5
function App() {
return (
<Switch>
<Route path={/users\/(\d+)/} component={User} />
</Switch>
)
}
function User({ params }) {
let id = params[0];
//...
}
//V6
function App() {
return (
<Routes>
<Route path="/users/:id" element={<ValidateUser/>} />
<Route path="/users/*" element={<NotFound/>} />
</Routes>
);
}
function ValidateUser() {
let params = useParams();
let userId = params.id.match(/\d+/);
if(!userId) {
return <NotFound />
}
return <User id={params.userId} />;
}
function User(props) {
let id = props.id;
//...
}
SPA(单页应用)
特点:只会在首次加载时,向服务器请求资源以加载页面,后续跳转页面是不会再向服务器请求资源,并且不会重新加载页面,会以切换组件重新渲染来达到页面跳转的目的。
页面刷新的场景:
History API(对访问页面堆栈的操作):可以修改浏览器的URL,但是不会重新加载页面。
监听用户点击浏览器前进和后退按钮:
function BrowserRouter(props) {
const RouterContext = createContext();
const HistoryContext = createContext();
const [path, setPath] = useState(() => {
//首次渲染,获取到对应的路由
const { pathname } = window.location;
return pathname || '/';
});
useEffect(() => {
//监听用户点击浏览器的前进、后退按钮跳转到页面
window.addEventListener('popstate', handlePopstate);
return () => {
window.removeEventListener('popstate', handlePopstate);
}
}, []);
const handlePopstate = function(event) {
const { pathname } = window.location;
setPath(pathname);
}
//点击UI跳转页面
const push = function(path) {
setPath(path);
window.history.pushState({path}, null, path);
}
const goBack = function() {
window.history.go(-1);
}
return (
<RouterContext.Provider value={path}>
<HistoryContext.Provider value={{ push, goBack }}>
{props.children}
</HistoryContext.Provider>
</RouterContext.Provider>
)
}
export default BrowserRouter;
//Route
export function Route(props) {
const {component: Component, path: componentPath} = props;
return (
<RouterContext.Consumer>
{(path) => {
return componentPath === path ? <Component/> : null;
}}
</RouterContext.Consumer>
)
}
//为什么不使用useContext:
//因为每当路由变化时,我们都需要重新渲染一个对应的组件,需要监听path的变化
import { useEffect, useState } from "react";
import RouterContext from './routerContext';
import HistoryContext from './historyContext';
//自定义HashRouter
function HashRouter(props) {
const [path, setPath] = useState(() => {
const {hash} = window.location;
if (hash) {
return hash.slice(1);
}
return '/#/';
});
useEffect(() => {
//监听用户点击浏览器的前进,后退按钮跳转到页面
window.addEventListener('hashchange', handlePopstate);
return () => {
window.removeEventListener('hashchange', handlePopstate);
}
}, []);
const handlePopstate = function(event) {
const {hash} = window.location;
setPath(hash.slice(1));
}
//点击UI跳转页面
const push = function(path) {
window.location.hash = path;
}
const goBack = function() {
window.history.go(-1);
}
return (
<RouterContext.Provider value={path}>
<HistoryContext.Provider value={{ push, goBack }}>
{props.children}
</HistoryContext.Provider>
</RouterContext.Provider>
)
}
export default HashRouter;
//Route
export function Route(props) {
const {component: Component, path: componentPath} = props;
return (
<RouterContext.Consumer>
{(path) => {
return componentPath === path ? <Component/> : null;
}}
</RouterContext.Consumer>
)
}
React Router核心功能:
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。
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 } 或 <Route path element> 的路由元素。path是 pattern。当路径模式与当前 URL 匹配时展示。
Route Element: 也就是 <Route>,<Routes> 读取该元素的 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: 专门用于在特定布局内对子路由进行分组。
React Router 的前提是:它必须能够订阅浏览器history stack中的更改。
浏览器在用户浏览时维护自己的历史堆栈。这就是后退和前进按钮的工作方式。在传统网站(没有 JavaScript 的 HTML 文档)中,每次用户单击链接、提交表单或单击后退和前进按钮时,浏览器都会向服务器发出请求。
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.listener(({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变化时更新其状态,并重新渲染
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:
// 通过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;
}
cache.set(cacheKey, data);
setData(data);
});
return () => controller.abort();
}
}, [state, cacheKey]);
useEffect(() => {
setState('loading');
}, [URL]);
return data;
}
在初始渲染时,当历史堆栈发⽣变化时,React Router 会将位置与您的路由配置进行匹配,以提供⼀组要渲染的匹配项。对应的routes,可以使用useRoutes(routesGoHere)获取。
因为routes是树状结构,因此,一个单一的URL可以匹配所有的树中的“分支”。
<Routes>将把位置与你的路由配置相匹配,得到一组匹配的内容,然后呈现一个React元素树。
outlets:
很像slot,<Outlet> 应该在父路由元素中使用以呈现子路由元素,以此让嵌套的子路由展示,当匹配到子路由的路径后,会展示,或不展示。
index routes:
index routes 会将父route的outlet渲染出来,一般会在持久化导航的父路由节点上展示默认的子路由信息。
layout routes:
实际上,layout routes (布局路由)本身不参与匹配,但其子route参与。
可以使用useNavigate方法。但要注意不要随意使用navigate,会增加程序的复杂性。
let navigate = useNavigate();
useEffect(() => {
setTimeout(() => {
navigate('/layout');
}, 3000);
}, []);
//no
<li onClick={() => navigate('/somewhere')} />
//yes
<Link to="somewhere" />
let location = useLocation();
let urlParams = useParams();
let [urlSearchParams] = useSearchParams();
react-router:与运行环境无关,几乎所有与运行平台无关的方法、组件和 hooks 都是在这里定义的。
route:
routes:
useRoutes:
Navigate:
内部还是调用的useNavigate,而useNavigate内部则是对用户传入的路径做处理,获取到最终的路径值,再传递给NavigationContext提供navigator对象。
BrowserRouter 和 HashRouter的区别,是区分链接还是hash,从history库中取到。