課程目標(biāo)
- 理解前端路由的作用;
- 掌握 React-Router 各 API 使用細(xì)節(jié);
- 可根據(jù)項(xiàng)目需求,在 React 項(xiàng)目中,組織合理的路由方案。
課程內(nèi)容
路由
- 路由:根據(jù)不同的 url 規(guī)則,給用戶展示不同的視圖(頁(yè)面);
- 當(dāng)應(yīng)用變得復(fù)雜的時(shí)候,就需要分塊的進(jìn)行處理和展示,傳統(tǒng)模式下,我們是把整個(gè)應(yīng)用分成了多個(gè)頁(yè)面,然后通過(guò) url 進(jìn)行連接。但是這種方式也有一些問(wèn)題,每次切換頁(yè)面都需要重新發(fā)送所有請(qǐng)求和渲染整個(gè)頁(yè)面,不止性能上會(huì)有影響,同時(shí)也會(huì)導(dǎo)致整個(gè) JavaScript 重新執(zhí)行,丟失狀態(tài)。
SPA
- Single Page Application:?jiǎn)雾?yè)面應(yīng)用,整個(gè)應(yīng)用只加載一個(gè)頁(yè)面(入口頁(yè)面),后續(xù)在與用戶的交互過(guò)程中,通過(guò) DOM 操作在這個(gè)單頁(yè)上動(dòng)態(tài)生成結(jié)構(gòu)與內(nèi)容。
優(yōu)點(diǎn)
- 有更好的用戶體驗(yàn)(減少請(qǐng)求、渲染和頁(yè)面跳轉(zhuǎn)產(chǎn)生的等待與空白),頁(yè)面切換快;
- 重前端,數(shù)據(jù)和頁(yè)面內(nèi)容由異步請(qǐng)求(AJAX)+ DOM 操作來(lái)完成,前端處理更多的業(yè)務(wù)邏輯。
缺點(diǎn)
- 首次進(jìn)入慢;
- 不利于 SEO。
SPA 的頁(yè)面切換機(jī)制
- 雖然 SPA 的內(nèi)容都是在一個(gè)頁(yè)面通過(guò) JavaScript 動(dòng)態(tài)處理的,但是還是需要根據(jù)需求在不同的情況下區(qū)分內(nèi)容展示,如果僅僅只是依靠 JavaScript 內(nèi)部機(jī)制去判斷,邏輯會(huì)變得過(guò)于復(fù)雜,通過(guò)把 JavaScript 與 URL 進(jìn)行結(jié)合的方式:JavaScript 根據(jù) URL 的變化,來(lái)處理不同的邏輯,交互過(guò)程中只需要改變 URL 即可。這樣把不同的 URL 與 JavaScript 對(duì)應(yīng)的邏輯進(jìn)行關(guān)聯(lián)的方式就是路由,其本質(zhì)上與后端路由的思想是一樣的。
前端路由
- 前端路由只是改變了 URL 或 URL 中的某一部分,但一定不會(huì)直接發(fā)送請(qǐng)求,可以認(rèn)為僅僅只是改變了瀏覽器地址欄上的 URL 而已,JavaScript 通過(guò)各種手段處理這種 URL 的變化,然后通過(guò) DOM 操作來(lái)動(dòng)態(tài)的改變當(dāng)前頁(yè)面的結(jié)構(gòu);
- URL 的變化不會(huì)直接發(fā)送 HTTP 請(qǐng)求;
- 業(yè)務(wù)邏輯由前端 JavaScript 來(lái)完成。
目前前端路由的主要模式
- 基于 URL Hash 的路由;
- 基于 HTML5 History API 的路由。
React Router
- 理解了路由的基本機(jī)制以后,也不需要重復(fù)造輪子,我們可以直接使用 React Router 庫(kù);
- React Router 提供了多種不同環(huán)境下的路由庫(kù):web、native;
- 官網(wǎng):https://reactrouter.com/。
基于 Web 的 React Router
- 基于 web 的 React Router 為:react-router-dom;
- 安裝:npm i -S react-router-dom。
路由模式
BrowserRouter 組件 -- history
- 基于 HTML5 History API 的路由組件。
HashRouter 組件 -- hash
- 基于 URL Hash 的路由組件。
功能組件
- Route 組件:
- 匹配規(guī)則(v6 之前):
- 默認(rèn)模糊匹配,當(dāng)前 URL 以該 path 為開(kāi)始時(shí),則匹配成功;
- exact:精確匹配,URL===path || URL===path/;
- strict:嚴(yán)格匹配,URL===path,要注意 strict 必須與精確匹配一起使用才生效;
- 多路徑匹配:通過(guò)數(shù)組實(shí)現(xiàn);
- 動(dòng)態(tài)路由,見(jiàn)最下面。
- 渲染視圖:
- component;
- render。
import { Route } from 'react-router-dom'; import About from './views/About'; import Hire from './views/Hire'; import Home from './views/Home'; /** * 匹配規(guī)則: * 1、默認(rèn)情況-——模糊匹配,當(dāng)前 URL 以該 path 為開(kāi)始時(shí),就能匹配:/index、/index/、/index/xxx; * 2、exact——精確匹配,URL===path || URL===path/; * 3、strict——嚴(yán)格匹配,URL===path,但是需要與 exact 一起使用才生效; * 4、多路徑匹配——通過(guò)數(shù)組匹配。 */ function App() { // 當(dāng)有值需要傳遞給對(duì)應(yīng)路由組件時(shí),可以通過(guò) render 屬性實(shí)現(xiàn)。 const user = { name: 'yjw', age: 18 } return ( <> <div>React Router Page</div> <Route exact path={['/', '/home', '/index']} component={Home} /> <Route exact strict path='/about' component={About} /> <Route path='/hire' render={() => { return <Hire user={user} /> }} /> </> ); } export default App;// react-router 6 之后的使用方式 import { Routes, Route } from 'react-router-dom'; import About from './views/About'; import Hire from './views/Hire'; import Home from './views/Home'; function App() { return ( <> <Routes> {/* 默認(rèn) 精確匹配,嚴(yán)格匹配 */} <Route path='home' element={<Home />} /> <Route path='/about' element={<About />} /> <Route path='/hire' element={<Hire />} /> </Routes> </> ); } export default App; - 匹配規(guī)則(v6 之前):
- 鏈接組件:
- Link;
- NavLink:
- activeClassName;
- activeStyle;
- isActive: function。
// 可以使用 a 標(biāo)簽實(shí)現(xiàn),但是頁(yè)面每次都會(huì)刷新 export default () => { return <> <a href='/home' >首頁(yè)</a> <span> | </span> <a href='/about' >關(guān)于</a> <span> | </span> <a href='/hire' >加入</a> </> }import { Link, NavLink } from "react-router-dom" /** * 應(yīng)用內(nèi)鏈接:Link 或者 NavLink * 應(yīng)用外鏈接:a 標(biāo)簽 */ /** * NavLink 用于導(dǎo)航的鏈接制作 * - 當(dāng)當(dāng)前的 url 和 NavLink 的 to 屬性匹配后,則會(huì)給當(dāng)前的標(biāo)簽加一個(gè)選中狀態(tài),注:NavLink 默認(rèn)情況下也是模糊匹配; * - activeClassName:當(dāng)前項(xiàng)被選中后的 className,默認(rèn)為 active; * - activeStyle:當(dāng)前項(xiàng)被選中后的 style; * - isActive:function,返回一個(gè) Boolean 值,表示該標(biāo)簽的 class、style 始終是 active 狀態(tài)或者 非active 狀態(tài)。 */ export default () => { return <div> <NavLink to='/' activeClassName='homeActive'>首頁(yè)</NavLink> <span> | </span> <NavLink to='/about' activeStyle={{color: 'red'}}>關(guān)于</NavLink> <span> | </span> <NavLink to='/hire' isActive={()=>{return true}} activeStyle={{color: 'yellow'}}>加入</NavLink> <span> | </span> <a >百度</a> </div> } - Switch 組件:只匹配一個(gè)路徑;
- Redirect 組件:當(dāng)輸入的 url 不合法時(shí),可以重定向到 404:
- form 屬性;
- to 屬性。
import { Route, Switch, Redirect } from 'react-router-dom'; import Nav from './Nav'; import About from './views/About'; import Hire from './views/Hire'; import Home from './views/Home'; import View404 from './views/404.js'; function App() { // 當(dāng)有值需要傳遞給對(duì)應(yīng)路由組件時(shí),可以通過(guò) render 屬性實(shí)現(xiàn)。 const user = { name: 'yjw', age: 18 } return ( <> <div>React Router Page</div> <Nav /> <Switch> <Route exact path={['/', '/home', '/index']} component={Home} /> <Route exact path='/about' component={About} /> <Route path='/about/join' render={() => { return <Hire user={user} /> }} /> <Route path='/404' component={View404} /> {/* 當(dāng)路徑找不到時(shí),顯示404頁(yè)面,并且將 url 顯示為404 */} <Redirect to='/404' /> </Switch> </> ); } export default App;
路由參數(shù)
- 通過(guò) props,可以獲取 history、location、match 和 staticContext。
history
- action:“PUSH” || “POP” || “REPLACE”;
- “PUSH”:應(yīng)用內(nèi)通過(guò)連接跳轉(zhuǎn)到當(dāng)前視圖的,或者通過(guò) push 方法跳轉(zhuǎn)到當(dāng)前視圖;
- “POP”:直接輸入地址跳轉(zhuǎn)到當(dāng)前應(yīng)用,或者從外部鏈接跳轉(zhuǎn)進(jìn)來(lái)的;
- “REPLACE”:通過(guò)重定向跳轉(zhuǎn)或者通過(guò) replace 方法跳轉(zhuǎn);
- go:function,go(n)——跳轉(zhuǎn)歷史記錄n步;
- goBack:function,goBack()——返回歷史記錄上一步;
- goForward:function,goForward()——前進(jìn)歷史記錄下一步;
- length:當(dāng)前源在歷史記錄中記錄的條目數(shù);
- push:function,push(path[,state])——跳轉(zhuǎn)視圖,向歷史記錄中,添加新的一條記錄,從而影響視圖;state 的值其實(shí)就是修改 location 下面的 state 的值,同 replace 方法中的 state;
- replace:function,replace(path[,state])——跳轉(zhuǎn)視圖,替換掉歷史記錄中當(dāng)前這條;
- block:當(dāng)離開(kāi)當(dāng)前組件時(shí),會(huì)彈窗提示;全局函數(shù),如果只需要在當(dāng)前組件進(jìn)行彈窗提示,在當(dāng)前組件即將卸載時(shí),調(diào)用該方法的返回值進(jìn)行移除;
- createHref:function,createHref(location),當(dāng) url 比較復(fù)雜時(shí),比如含有參數(shù)和hash,這時(shí)可以使用該方法,返回一個(gè) url 地址,但是需要再調(diào)用 push、replace 來(lái)進(jìn)行跳轉(zhuǎn);
- listen:監(jiān)聽(tīng) url 跳轉(zhuǎn),如果跳轉(zhuǎn)了會(huì)打印 location和action,同樣是全局函數(shù)。
location:
- hash:hash 值,url 中 # 后面的內(nèi)容;
- pathname:當(dāng)前的 url,不包含參數(shù)和hash;
- search:當(dāng)前的 search 值,? 后面的內(nèi)容;
- state:undefined、push 或 replace 傳遞的信息。
match:匹配信息
- isExact:boolean,和 Route 中配置沒(méi)有關(guān)系,取決于當(dāng)前 path 和 url 是否能精確匹配;
- params:{} 動(dòng)態(tài)路由的參數(shù);
- path:和 pathname 不是一個(gè)概念,這里的 path 是當(dāng)前 route 的 path 值;
- url:當(dāng)前 url 中,被當(dāng)前 path 匹配成功的部分。
路由信息獲取
高階路由 - withRouter
- 非路由組件獲取路由信息
import { NavLink, withRouter } from "react-router-dom"; function SubNav(props) { console.log('SubNav: ', props); return ( <div className='sub-nav'> <NavLink isActive={(...args) => { console.log('arg: ', args) return true; }} to='/list/all' >全部</NavLink> <NavLink to='/list/good'>精華</NavLink> <NavLink to='/list/share'>分享</NavLink> <NavLink to='/list/ask'>問(wèn)答</NavLink> </div> ); } const Nav = withRouter(SubNav); export default Nav;
路由 Hooks,v5 版本之后引入了 Hooks。
- useLocation();
- useHistory();
- useRouteMatch();
- useParams():獲取動(dòng)態(tài)路由的參數(shù)信息。
- 以下為完整練習(xí)代碼:
// App.js import { Route, Switch, Redirect } from 'react-router-dom'; import Nav from './Nav'; import About from './views/About'; import Hire from './views/Hire'; import Home from './views/Home'; import View404 from './views/404.js'; import List from './views/list/List'; import './index.css'; const types = ['good', 'good', 'share', 'ask']; function App() { // 當(dāng)有值需要傳遞給對(duì)應(yīng)路由組件時(shí),可以通過(guò) render 屬性實(shí)現(xiàn)。 const user = { name: 'yjw', age: 18 } return ( <div className="wrap"> <div>React Router Page</div> <Nav /> <Switch> <Route path={['/home', '/index']} component={Home} /> <Route path='/about' component={About} /> <Route path='/join' render={() => { return <Hire user={user} /> }} /> {/* component 自帶路由參數(shù),通過(guò) render 傳遞路由參數(shù)時(shí)需要手動(dòng)將參數(shù)傳遞過(guò)去 - 動(dòng)態(tài)路由: :代表動(dòng)態(tài)路由,: 后面代表的是名字 */} {/* <Route // path={['/list', '/list/:type', '/list/:type/:page']} path='/list/:type?/:page?' render={ (props) => { return <List {...props}/> }} exact /> */} <Route path='/list/:type?/:page?' exact render={({match}) => { console.log('route-> ', match) const {type='good', page='1'} = match.params; if(types.includes(type) && String(parseInt(page))===page && parseInt(page)>0) { return <List /> } else { return <Redirect to="/404" /> } }} /> <Route path='/404' component={View404} /> <Redirect to='/404' /> </Switch> </div> ); } /** * list 的路徑問(wèn)題: * - /list * - /list/分類 * - list/分類/頁(yè)碼 */ export default App;// Nav.js import { Link, NavLink } from "react-router-dom" /** * 應(yīng)用內(nèi)鏈接:Link 或者 NavLink * 應(yīng)用外鏈接:a 標(biāo)簽 */ /** * NavLink 用于導(dǎo)航的鏈接制作 * - 當(dāng)當(dāng)前的 url 和 NavLink 的 to 屬性匹配后,則會(huì)給當(dāng)前的標(biāo)簽加一個(gè)選中狀態(tài),注:NavLink 默認(rèn)情況下也是模糊匹配; * - activeClassName:當(dāng)前項(xiàng)被選中后的 className,默認(rèn)為 active; * - activeStyle:當(dāng)前項(xiàng)被選中后的 style; * - isActive:function,返回一個(gè) Boolean 值,表示該標(biāo)簽的 class、style 始終是 active 狀態(tài)或者 非active 狀態(tài)。 */ export default () => { return <div className='nav'> <NavLink to='/home' >首頁(yè)</NavLink> <span> | </span> <NavLink to='/about' >關(guān)于</NavLink> <span> | </span> <NavLink to='/join' >加入</NavLink> <span> | </span> <NavLink to='/list' >產(chǎn)品列表</NavLink> </div> }// List.js import ListList from "./ListList"; import Pagination from "./Pagination"; import SubNav from "./SubNav"; function List(props) { return ( <> <h3>List</h3> <SubNav /> <ListList /> <Pagination /> </> ); } export default List;// SubNav.js import { NavLink, useParams } from "react-router-dom"; function SubNav(props) { const { type='good' } = useParams(); return ( <div className='sub-nav'> <NavLink to='/list/good'>精華</NavLink> <NavLink to='/list/share'>分享</NavLink> <NavLink to='/list/ask'>問(wèn)答</NavLink> </div> ); } export default SubNav;// Pagination.js import { useParams } from "react-router"; import { Link } from "react-router-dom"; import data from './data'; const limit = 6; function Pagination() { const { type='good', page='1' } = useParams(); const nowData = data[type]; const pageLen = Math.ceil(nowData.length/limit); const renderPage = () => { const inner = [] for (let i = 1; i <= pageLen; i++) { if(pageLen === Number(page)) { inner.push(<span key={i}>{i}</span>) } else { inner.push(<Link to={`/list/${type}/${i}`} key={i}>{i}</Link>) } } return inner; } return ( <div> {renderPage()} </div> ); } export default Pagination;// ListList.js import { useParams } from "react-router"; import data from './data'; const limit = 6; /** * 求頁(yè)數(shù) * 每頁(yè)的第一條:(page-1) * 6 */ function ListList() { const { type='good', page='1' } = useParams(); const nowPage = Number(page); const start = (nowPage - 1) * limit; const end = nowPage * limit; const nowData = data[type]?.filter((item, index)=>(index>=start && index<=end)); return ( <div> <ul> {nowData?.map(d => { return <li key={d.id}>{d.title}</li> })} </ul> </div> ); } export default ListList;