路由的概念,可以想像一下路由器,當來了一個請求時,路由器做了什么事情?它會把請求的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";