React Router 6
路由的概念,可以想像一下路由器,当来了一个请求时,路由器做了什么事情?它会把请求的IP地址和路由表进行匹配,匹配成功后,进行转发,直到目标主机。可以看到路由有三部分组成,一个是请求,一个是路由表,一个是匹配转发。对应到前端路由也是一个道理,只不过前端路由是拦截请求,显示不同的页面内容。首先要发起请求,说明你要到哪里去,React Router中定义了<Link>。其次要定义一个路由表,列出匹配规则和匹配成功后要显示什么,就是React Router中一条条的<Route>。最后就是来了请求时进行动态匹配和转发,React提供了<BrowserRouter>和<Routes>。<BrowserRouter> 把<Routes>包起来,<Routes>把<Route>包起来,来了请求,它就能匹配路由表。
<Link> 有一个to属性,就是标明去哪里
<Link to="/home">Home</Link>
<Route> 有两个属性,一个是path,就是列出匹配规则,一个是element,就是匹配成功后要显示什么内容。如果请求‘/home’,就显示<Home>组件内容,就可以如下定义
<Route path=’/home’, element={<Home/>} /> // 要写好Home 组件。
要注意的是elemet 接受的真是React Element。这里只是定义一条路由,路由表里的一条记录。当要匹配其它请求时,还要再写路由, 比如About
<Route path=’/about’, element={<About/>} />
要匹配多少请求,就要写多少条路由。这样一条条的路由就定义好了,放到<BrowserRouter>和<Routes>下面,只要来了请求,就能进行动态匹配。<BrowserRouter>提供页面的URL(Provides the cleanest URLs)。<Routes>进行动态匹配。
使用create-react-app创建项目router-tutorial,然后cd router-tutorial 并npm install react-router-dom。 在index.js中引入BrowserRouter 和<Routes>, BrowserRouter把Route包起来。整个index.js如下
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { Home, About, Contact, Products, Events } from "./pages";
ReactDOM.render(
<BrowserRouter>
<Routes>
<Route path='/' element={<Home />} />
<Route path='/about' element={<About />} />
<Route path='/contact' element={<Contact />} />
<Route path='/products' element={<Products />} />
<Route path='/events' element={<Events />} />
</Routes>
</BrowserRouter>,
document.getElementById('root')
);
为了让路由起作用,还要创建Home, About 等组件。在src目录下,新建一个pages.js文件,内容如下:
import React from 'react'
// 首页内容
export const Home = () => (
<section className="home">
<h1>企业网站</h1>
<p>首页内容</p>
</section>
)
// 企业事件内容
export const Events = () => (
<section className="events">
<h1>企业大事件</h1>
</section>
)
// 公司产品
export const Products = () => (
<section className="products">
<h1>公司产品:手机、电脑</h1>
</section>
)
// 联系我们
export const Contact = () => (
<section className="contact">
<h1>联系我们</h1>
<p>公司电话:0755 - 12345678</p>
</section>
)
// 关于我们
export const About = () => (
<section className="about">
<h1>公司理念</h1>
<p>公司以人为本</p>
</section>
)
npm start,localhost:3000
怎么访问其它页面的内容呢?使用<Link />, <Link>表示要到哪里去,只要设置它的to属性和Route中的path属性一一对应就可以了,如<Link to=’/about’>关于我们</Link>,就表示要到/about下面。Home组件中增加四个<Link>
import { Link } from "react-router-dom";
export const Home = () => (
<section className="home">
<h1>企业网站</h1>
<p>首页内容</p>
<nav>
{/* 添加了四个导航组件Link */}
<Link to='/about'>关于我们</Link>
<Link to='/events'>企业事件</Link>
<Link to='/products'>公司产品</Link>
<Link to='/contact'>联系我们</Link>
</nav>
</section>
)
这时,点击不同的link 就去到不同页面,同时它还会改变地址栏,这时如要在地址栏中随便输入一个路径,页面一片空白,因为没一个路由和它匹配。最好写一个匹配不成功的路由,来处理一下这种情况。那路由的path怎么写?用“*”。要显示的组件可以随便写一下,在pages.js 下面再写一个组件,
export const NotFound404 = () =>(
<div className="whoops-404">
<h1>没有页面可以匹配</h1>
</div>
)
路由就是
<Route path='*' element={<NotFound404/>}></Route>
和普通路由一样,把它加到<Routes>组件下面。*表示,其它路由都不匹配的时候,才匹配它。
<BrowserRouter> <Routes> <Route path='/' element={<Home/>} /> <Route path='/about' element={<About />} /> <Route path='/contact' element={<Contact />} /> <Route path='/products' element={<Products/>} /> <Route path='/events' element={<Events/>} /> <Route path='*' element={<NotFound404/>}></Route> </Routes> </BrowserRouter>
此时,路由有一个问题,那就是点击<About>之后,回不去了,只能点击浏览器的回退按钮,要是页面始终展示导航条就好了。由于导航条在Home组件,也就是说Home组件始终要显示。About等组件的内容都是在点击之后才会显示,也就是先显示Home组件,再显示 About组件,只有先导航到home路由,才有机会导航到about的路由, home路由是about路由的父路由,在React Router 6中,Route可以嵌套,只要一个<Route>包含其它Route,它就是父路由,那么路由就可以这么写
<BrowserRouter> <Routes> <Route path='/' element={<Home />} > <Route path='about' element={<About />} /> <Route path='contact' element={<Contact />} /> <Route path='products' element={<Products />} /> <Route path='events' element={<Events />} /> </Route> <Route path='*' element={<NotFound404 />}></Route> </Routes> </BrowserRouter>
子路由前面的/可以去掉,React会自动组合(父路由/+子路由"about")。那子路由匹配成功后,要展示内容放到什么地方? 由于是子路由,肯定要先匹配父路由,只有父路由匹配成功了,才能匹配子路由。也就是只有父路由对应的组件展示出来了,才有机会展示子路由对应的内容,子路由的内容应该放到父路由的对应的组件里面,也就是About等组件要放到Home组件里面 。那具体怎么写呢?React Router 提供了<Outlet>组件,只要子路由匹配成功,<Outlet />组件可以动态成渲染子路由定义的组件内容。<Outlet />放到Home组件中,具体放到哪里,就看业务需要,比如与导航条并列
import { Link, Outlet } from "react-router-dom";
// 首页内容
export const Home = () => (
<section className="home">
<h1>企业网站</h1>
<p>首页内容</p>
<nav>
{/* 添加了四个导航组件Link */}
<Link to='/about'>关于我们</Link>
<Link to='/events'>企业事件</Link>
<Link to='/products'>公司产品</Link>
<Link to='/contact'>联系我们</Link>
</nav>
<Outlet />
</section>
)
点击每一个<Link>, 和它匹配成功的子路由所定义组件,都会正确地渲染,并且是渲染在<Outlet> 位置,正确的组件替换掉了<Outlet>。
给整个组件添加点样式,pages.css 内容如下,并在index.js中引用
html, body, #root { height: 100%; } h1 { font-size: 3em; color: slategray; } /* home 组件 */ .home { height: 100%; display: flex; flex-direction: column; align-items: center; } .home > nav { display: flex; justify-content: space-around; padding: 1em; width: calc(100% - 2em); border-top: dashed 0.5em ghostwhite; border-bottom: dashed 0.5em ghostwhite; background-color: slategray; } .home > nav a { font-size: 2em; color: ghostwhite; flex-basis: 200px; } /* 其它组件 */ section.events, section.products, section.contact { flex-grow: 1; margin: 1em; display: flex; justify-content: center; align-items: center; } /* 404页面 */ .whoops-404 { position: fixed; top: 0; left: 0; z-index: 99; display: flex; width: 100%; height: 100%; margin: 0; justify-content: center; align-items: center; background-color: darkred; color: ghostwhite; font-size: 1.5em; }
整个页面如下展示
这时也会发现一个问题,刚进入页面时,页面只展示了导航条, 没有显示实际的内容,只有点击某个导航后,才显示内容,这显然不合适,即使不点击,也要看到核心内容,比如公司产品。针对此种情况,React Router 提供了索引路由,
<Route path='/' element={<Home />} >
{/* 索引路由 */}
<Route index element={<Products/>} />
<Route path='about' element={<About />} />
<Route path='contact' element={<Contact />} />
<Route path='products' element={<Products />} />
<Route path='events' element={<Events />} />
</Route>
索引路由和父路由共享路径。当localhost:3000时,path='/'匹配成功,渲染<Home>组件,渲染过程中有<Outlet>组件,就要去匹配子路由,由于索引路由的路径也是父路由的路径,此时URL就是父路由的路径,匹配成功,显示Products组件,也可以把索引路由看作是默认的子路由,当没有其它子路由匹配的时候,就渲染它,总要显示一个子路由吗?稍微修饰一下公司产品模块,列出几个产品,比如手机,电脑等,
export const Products = () => (
<section className="products">
<Link to='/details/telphone'>手机</Link>
<Link to='/details/computer'>电脑</Link>
</section>
)
当点击某个产品,进入产品详情页面,在pages.js中加一个详情组件
// 产品详情组件
export const Details = () => {
return <p>详情内容</p>
}
问题是当跳转到产品详情页面时,它怎么知道是哪个产品呢?匹配的路由怎么写?路由的格式就是路径后面加上冒号 ,再加参数,比如details/:type,组件中可以使用useParams获取。为了展示,把路由写到根路由index.js下
<Route path='products' element={<Products />} /> <Route path='details/:type' element={<Details />} /> <Route path='events' element={<Events />} />
同时Details组组件改为
import { useParams } from "react-router-dom";
export const Details = () => {
let params = useParams();
return <p>产品: {params.type}</p>
}
还有 一个小问题,导航条能不能提示位于哪个导航上?React Router 提供了NavLink 组件, 它和Link功能是一样的,都是标识请求,只是使用的场景不一样。navLink 提供了一个isActive 属性,可以设置高亮样式。
<NavLink to='/about' className={({ isActive }) => isActive ? "selectedStyle" : ""}>关于我们</NavLink>
当选中之后,isActive是true。
除了这种声明式的路由,也可以使用编程式路由,核心就是useNavigate. 在Detail里面,添加button,跳转到Product。
import { useParams, useNavigate } from "react-router-dom";
export const Details = () => {
let params = useParams();
let navigate = useNavigate();
return (
<p>
产品: {params.type}
<br />
<button onClick={() => {
navigate("/products");
}}>后退</button>
</p>
);
}
React Router 主要概念
React Router三个主要作用:监听(订阅)和操作浏览器的历史记录,匹配URL到你配置的路由,从匹配的路由中渲染嵌套的UI(完整的UI)
location是基于浏览器内置的window.location对象,React Router 自己拥有的一个特殊的对象。它表示用户在哪里,几乎就是URL的对象表示,但包含的信息比URL多。Location State则是存在Location对象上的一个值,但它并没有在URL中显示。它存在浏览器的内存中,并不可见,样子像hash或搜索参数。当用户在网页中导航时,浏览器持续追踪每一个location,形成浏览器的历史记录。
history也是一个对象,它使React Router 可以订阅URL的变化,并提供API来操作浏览器的历史记录。
segment:URL和路径模式中两个/之间的部分,比如'/user/123', user和123都是segment。路径模式很像URL,但它包含特殊字符,比如 "/users/:userId"和 "/docs/*" ,一个包含:,一个包含*。路径模式下“:userId” 又称为动态segment,因为它能匹配任意值。URL params就是匹配动态segment成功的值。/users/123 匹配/users/:userId, 123就是url params。
Match也是一个对象,它包含了URL和路由匹配成功的信息,比发path和URL Params。Matches是和当前URL匹配成功的一组路由。
客户端路由时,开发者可以通过API,如history.pushState(),来操作浏览器的历史记录,而不会发送服务器请求,但它仅仅是改变了URL,并没有改变UI。我们需要做的是改变URL的同时,改变UI。问题是浏览器并没有提供一个方法来监听URL,从而订阅变化。为此,React Router创建了histroy 对象,来监听URL的变化。<Router>,就是<BrowserRouter>,它会创建history对象,并订阅浏览器历史记录的变化,当URL变化时,它会更新history对象的state,从而引起APP的重新渲染,正确的UI显示出来。history的state是一个location对象,如下所示
{ pathname: "/bbq/pig-pickins", search: "?campaign=instagram", hash: "#menu", state: null, key: "aefz24ie" }
pathname,路由匹配的部分,路由进行匹配的时候,只匹配pathname。state,来自于history.pushState()。pushState()第一个参数就是state, 但它不会改变URL。React Router 进行抽象,把state放到了location对象中。改变location 的state有两种方式
<Link to="/pins/123" state={{ fromDashboard: true }} />; let navigate = useNavigate(); navigate("/users/123", { state: partialUser });
在跳转到的page,可以使用useLocation获取到
let location = useLocation();
location.state;
只要改变URL,就会改变location 对象,想要获取URL的信息,就使用location对象。当URL改变后,React Router就用location来匹配你配置的路由,然后把匹配成功的拿出来,进行渲染。配置的路由就是<Routes>和<Route>组件。
<Routes> <Route path="/" element={<App />}> <Route index element={<Home />} /> <Route path="teams" element={<Teams />}> <Route path=":teamId" element={<Team />} /> <Route path="new" element={<EditTeam />} /> </Route> </Route> <Route element={<PageLayout />}> <Route path="/privacy" element={<Privacy />} /> </Route> </Routes>
<Routes>会递归遍历它的children,也就是<Route>,把属性拿出来,形成一个对象。
let routes = [ { element: <App />, path: "/", children: [ { index: true, element: <Home /> }, { path: "teams", element: <Teams />, children: [ { path: ":teamId", element: <Team /> }, { path: "new", element: <NewTeamForm /> } ] } ] }, { element: <PageLayout />, children: [ { element: <Privacy />, path: "/privacy" } ] } ];
形成一个总的路由配置表
[ "/", "/teams", "/teams/:teamId", "/teams/new", "/privacy" ];
如果现在有一个URL是/teams/new,哪个路由会匹配它,有两个
/teams/new
/teams/:teamId
这时React Router 必须做决定,因为只能有一条路由匹配,React Router会对你路由进行排名,依据就是路由中的segment, 静态,动态,数量等等,找出最精准匹配的那一条路由。在这里,就是 /teams/new 这条路由。当一条路由成功匹配到URL后,它会以match对象的方式进行展示。匹配到<Route path=":teamId" element={<Team/>}/>这条路由,将会生成下面这个对象
{ pathname: "/teams/firebirds", params: { teamId: "firebirds" }, route: { element: <Team />, path: ":teamId" } }
由于路由是树形结构,一个URL可能匹配到树的整个分支。/teams/firebirds
<Routes> <Route path="/" element={<App />}> <Route path="teams" element={<Teams />}> <Route path=":teamId" element={<Team />} /> </Route> </Route> </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" } } ];
有了匹配的路由,渲染React Element树就简单了
<App> <Teams> <Team /> </Teams> </App>
看一下"/privacy",它匹配的路由是
<Route path="/" element={<App />}> <Route element={<PageLayout />}> <Route path="/privacy" element={<Privacy />} /> </Route> </Route>
要渲染的React Element树
<App> <PageLayout> <Privacy /> </PageLayout> </App>
PageLayout路由有点奇怪,没有path属性,只有Element属性,称为布局路由,仅仅用来做布局的。
V5 和V6的不同
1,React Router v6 使用了大量的React Hooks,因此升级V6前,要先升级React到16.8以上。
2,使用<Routes> 代替<Switch>,<Routes>使用的是最佳匹配路由算法,并且路由能嵌套
3,组件内部有<Link>和<Route>时,<Link>的to属性和Route>的path属性,不用再手动构建,而是直接写
<Route path={`${match.path}/:id`}> -> <Route path=":id" element={<UserProfile />} />
此时<Route path>和<Link to> 是相对路由和Link,它们自动构建在父路由path和URL上。对应的,当路由有后代路由,且这些路由是定义在其它组件中,路由的path要用*,表示深度匹配。
function App() { return ( <BrowserRouter> <Routes> <Route path="/" element={<Home />} /> <Route path="users/*" element={<Users />} /> </Routes> </BrowserRouter> ); } function Users() { return ( <div> <nav> <Link to="me">My Profile</Link> </nav> <Routes> <Route path=":id" element={<UserProfile />} /> <Route path="me" element={<OwnUserProfile />} /> </Routes> </div> ); }
4,<Route>使用element,而不是compoent, render属性,element的取值也是React Element,好处就是可以像普通的React Element传递属性
// animate 是自定义属性 <Route path=":userId" element={<Profile animate={true} />} /> // 组件使用 function Profile({ animate }) { let params = useParams(); let location = useLocation(); }
5, <Route> 的path属性不再接受正则表达式,/users/:id? 无效了。
6,react-router-config包里的功能都集成到V6中,使有useRoute,而不是react-router-config
function App() { let element = useRoutes([ // These are the same as the props you provide to <Route> { path: "/", element: <Home /> }, { path: "invoices", element: <Invoices />, // Nested routes use a children property, which is also // the same as <Route> children: [ { path: ":id", element: <Invoice /> }, { path: "sent", element: <SentInvoices /> } ] }, // Not found routes work as you'd expect { path: "*", element: <NotFound /> } ]); // The returned element will render the entire element // hierarchy with all the appropriate context it needs return element; }
当进行服务端渲染时,要用matchRoutes
7,useNavigate 代替了useHistory , Navigate 组件代替了Redirect 组件
import { Navigate } from "react-router-dom"; function App() { return <Navigate to="/home" replace state={state} />; }
8, <Link>没有了component属性,只能渲染成标签。
9,<NavLink/> 去掉了activeClassName 和 activeStyle, 要使用isActive
10,StaticRouter 称动了react-router-dom/server.
import { StaticRouter } from "react-router-dom/server";