课程目标
- 理解前端路由的作用;
- 掌握 React-Router 各 API 使用细节;
- 可根据项目需求,在 React 项目中,组织合理的路由方案。
课程内容
路由
- 路由:根据不同的 url 规则,给用户展示不同的视图(页面);
- 当应用变得复杂的时候,就需要分块的进行处理和展示,传统模式下,我们是把整个应用分成了多个页面,然后通过 url 进行连接。但是这种方式也有一些问题,每次切换页面都需要重新发送所有请求和渲染整个页面,不止性能上会有影响,同时也会导致整个 JavaScript 重新执行,丢失状态。
SPA
- Single Page Application:单页面应用,整个应用只加载一个页面(入口页面),后续在与用户的交互过程中,通过 DOM 操作在这个单页上动态生成结构与内容。
优点
- 有更好的用户体验(减少请求、渲染和页面跳转产生的等待与空白),页面切换快;
- 重前端,数据和页面内容由异步请求(AJAX)+ DOM 操作来完成,前端处理更多的业务逻辑。
缺点
- 首次进入慢;
- 不利于 SEO。
SPA 的页面切换机制
- 虽然 SPA 的内容都是在一个页面通过 JavaScript 动态处理的,但是还是需要根据需求在不同的情况下区分内容展示,如果仅仅只是依靠 JavaScript 内部机制去判断,逻辑会变得过于复杂,通过把 JavaScript 与 URL 进行结合的方式:JavaScript 根据 URL 的变化,来处理不同的逻辑,交互过程中只需要改变 URL 即可。这样把不同的 URL 与 JavaScript 对应的逻辑进行关联的方式就是路由,其本质上与后端路由的思想是一样的。
前端路由
- 前端路由只是改变了 URL 或 URL 中的某一部分,但一定不会直接发送请求,可以认为仅仅只是改变了浏览器地址栏上的 URL 而已,JavaScript 通过各种手段处理这种 URL 的变化,然后通过 DOM 操作来动态的改变当前页面的结构;
- URL 的变化不会直接发送 HTTP 请求;
- 业务逻辑由前端 JavaScript 来完成。
目前前端路由的主要模式
- 基于 URL Hash 的路由;
- 基于 HTML5 History API 的路由。
React Router
- 理解了路由的基本机制以后,也不需要重复造轮子,我们可以直接使用 React Router 库;
- React Router 提供了多种不同环境下的路由库:web、native;
- 官网: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 组件:
- 匹配规则(v6 之前):
- 默认模糊匹配,当前 URL 以该 path 为开始时,则匹配成功;
- exact:精确匹配,URL===path || URL===path/;
- strict:严格匹配,URL===path,要注意 strict 必须与精确匹配一起使用才生效;
- 多路径匹配:通过数组实现;
- 动态路由,见最下面。
- 渲染视图:
- component;
- render。
import { Route } from 'react-router-dom'; import About from './views/About'; import Hire from './views/Hire'; import Home from './views/Home'; /** * 匹配规则: * 1、默认情况-——模糊匹配,当前 URL 以该 path 为开始时,就能匹配:/index、/index/、/index/xxx; * 2、exact——精确匹配,URL===path || URL===path/; * 3、strict——严格匹配,URL===path,但是需要与 exact 一起使用才生效; * 4、多路径匹配——通过数组匹配。 */ function App() { // 当有值需要传递给对应路由组件时,可以通过 render 属性实现。 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> {/* 默认 精确匹配,严格匹配 */} <Route path='home' element={<Home />} /> <Route path='/about' element={<About />} /> <Route path='/hire' element={<Hire />} /> </Routes> </> ); } export default App;
- 匹配规则(v6 之前):
- 链接组件:
- Link;
- NavLink:
- activeClassName;
- activeStyle;
- isActive: function。
// 可以使用 a 标签实现,但是页面每次都会刷新 export default () => { return <> <a href='/home' >首页</a> <span> | </span> <a href='/about' >关于</a> <span> | </span> <a href='/hire' >加入</a> </> }
import { Link, NavLink } from "react-router-dom" /** * 应用内链接:Link 或者 NavLink * 应用外链接:a 标签 */ /** * NavLink 用于导航的链接制作 * - 当当前的 url 和 NavLink 的 to 属性匹配后,则会给当前的标签加一个选中状态,注:NavLink 默认情况下也是模糊匹配; * - activeClassName:当前项被选中后的 className,默认为 active; * - activeStyle:当前项被选中后的 style; * - isActive:function,返回一个 Boolean 值,表示该标签的 class、style 始终是 active 状态或者 非active 状态。 */ export default () => { return <div> <NavLink to='/' activeClassName='homeActive'>首页</NavLink> <span> | </span> <NavLink to='/about' activeStyle={{color: 'red'}}>关于</NavLink> <span> | </span> <NavLink to='/hire' isActive={()=>{return true}} activeStyle={{color: 'yellow'}}>加入</NavLink> <span> | </span> <a href='https://www.baidu.com' >百度</a> </div> }
- Switch 组件:只匹配一个路径;
- Redirect 组件:当输入的 url 不合法时,可以重定向到 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() { // 当有值需要传递给对应路由组件时,可以通过 render 属性实现。 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} /> {/* 当路径找不到时,显示404页面,并且将 url 显示为404 */} <Redirect to='/404' /> </Switch> </> ); } export default App;
路由参数
- 通过 props,可以获取 history、location、match 和 staticContext。
history
- action:“PUSH” || “POP” || “REPLACE”;
- “PUSH”:应用内通过连接跳转到当前视图的,或者通过 push 方法跳转到当前视图;
- “POP”:直接输入地址跳转到当前应用,或者从外部链接跳转进来的;
- “REPLACE”:通过重定向跳转或者通过 replace 方法跳转;
- go:function,go(n)——跳转历史记录n步;
- goBack:function,goBack()——返回历史记录上一步;
- goForward:function,goForward()——前进历史记录下一步;
- length:当前源在历史记录中记录的条目数;
- push:function,push(path[,state])——跳转视图,向历史记录中,添加新的一条记录,从而影响视图;state 的值其实就是修改 location 下面的 state 的值,同 replace 方法中的 state;
- replace:function,replace(path[,state])——跳转视图,替换掉历史记录中当前这条;
- block:当离开当前组件时,会弹窗提示;全局函数,如果只需要在当前组件进行弹窗提示,在当前组件即将卸载时,调用该方法的返回值进行移除;
- createHref:function,createHref(location),当 url 比较复杂时,比如含有参数和hash,这时可以使用该方法,返回一个 url 地址,但是需要再调用 push、replace 来进行跳转;
- listen:监听 url 跳转,如果跳转了会打印 location和action,同样是全局函数。
location:
- hash:hash 值,url 中 # 后面的内容;
- pathname:当前的 url,不包含参数和hash;
- search:当前的 search 值,? 后面的内容;
- state:undefined、push 或 replace 传递的信息。
match:匹配信息
- isExact:boolean,和 Route 中配置没有关系,取决于当前 path 和 url 是否能精确匹配;
- params:{} 动态路由的参数;
- path:和 pathname 不是一个概念,这里的 path 是当前 route 的 path 值;
- url:当前 url 中,被当前 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'>问答</NavLink> </div> ); } const Nav = withRouter(SubNav); export default Nav;
路由 Hooks,v5 版本之后引入了 Hooks。
- useLocation();
- useHistory();
- useRouteMatch();
- useParams():获取动态路由的参数信息。
- 以下为完整练习代码:
// 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() { // 当有值需要传递给对应路由组件时,可以通过 render 属性实现。 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 自带路由参数,通过 render 传递路由参数时需要手动将参数传递过去 - 动态路由: :代表动态路由,: 后面代表的是名字 */} {/* <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 的路径问题: * - /list * - /list/分类 * - list/分类/页码 */ export default App;
// Nav.js import { Link, NavLink } from "react-router-dom" /** * 应用内链接:Link 或者 NavLink * 应用外链接:a 标签 */ /** * NavLink 用于导航的链接制作 * - 当当前的 url 和 NavLink 的 to 属性匹配后,则会给当前的标签加一个选中状态,注:NavLink 默认情况下也是模糊匹配; * - activeClassName:当前项被选中后的 className,默认为 active; * - activeStyle:当前项被选中后的 style; * - isActive:function,返回一个 Boolean 值,表示该标签的 class、style 始终是 active 状态或者 非active 状态。 */ export default () => { return <div className='nav'> <NavLink to='/home' >首页</NavLink> <span> | </span> <NavLink to='/about' >关于</NavLink> <span> | </span> <NavLink to='/join' >加入</NavLink> <span> | </span> <NavLink to='/list' >产品列表</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'>问答</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; /** * 求页数 * 每页的第一条:(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;