当前位置: 代码迷 >> 综合 >> React(四)——React 路由(react-router-dom)
  详细解决方案

React(四)——React 路由(react-router-dom)

热度:35   发布时间:2023-10-01 00:54:47.0

目录

1.路由

1.1SPA

1.2SPA 的页面切换机制

1.3后端路由与前端路由

1.3.1后端路由

1.3.2前端路由

1.4React.js 中的路由

2.React Router

2.1基于 Web 的 React Router

2.1.1安装

2.1.2概览

3.基础

3.1应用场景(一)——页面切换

3.1.1Router 组件

3.1.2Route 组件

3.1.3Link 组件

3.2应用场景(二)——商品主页和详情页展示

3.2.1状态提升

3.2.2传递 props

3.2.3渲染列表

3.2.4动态路由

3.2.5路由组件

3.3应用场景(三)——价格排序

3.3.1通过 JavaScript 实现排序切换

3.3.2通过路由实现排序切换

3.3.3扩展

3.4应用场景(四)——页面顶部导航高亮

3.4.1NavLink 组件

3.5应用场景(五)——404页面

3.5.1Switch 组件

3.6应用场景(六)——购物车

3.6.1Redirect 组件

3.7应用场景(七)——分页组件

3.7.1withRouter()方法


1.路由

当应用变得复杂的时候,就需要分块的进行处理和展示,传统模式下,我们是把整个应用分成了多个页面,然后通过 URL 进行连接。但是这种方式也有一些问题,每次切换页面都需要重新发送所有请求和渲染整个页面,不止性能上会有影响,同时也会导致整个 JavaScript 重新执行,丢失状态。

传统路由基于后端:浏览器——》发送http请求——》url——》分析URL——》返回不同html代码——》浏览器得到返回数据——》据不同数据类型进行不同处理(根据2进制头部信息)

1.1SPA

Single Page Application : 单页面应用,整个应用只加载一个页面(入口页面),后续在与用户的交互过程中,通过 DOM 操作在这个单页上动态生成结构和内容。

优点:

  • 有更好的用户体验(减少请求和渲染和页面跳转产生的等待与空白),页面切换快

  • 重前端,数据和页面内容由异步请求(AJAX)+ DOM 操作来完成,前端处理更多的业务逻辑

缺点:

  • 首屏处理慢

  • 不利于 SEO

1.2SPA 的页面切换机制

虽然 SPA 的内容都是在一个页面通过 JavaScript 动态处理的,但是还是需要根据需求在不同的情况下分内容展示,如果仅仅只是依靠 JavaScript 内部机制去判断,逻辑会变得过于复杂,通过把 JavaScriptURL 进行结合的方式: JavaScript 根据 URL 的变化,来处理不同的逻辑,交互过程中只需要改变 URL 即可。这样把不同 URLJavaScript 对应的逻辑进行关联的方式就是路由,其本质上与后端路由的思想是一样的。

1.3后端路由与前端路由

后端路由与前端路由在本质上是类似的,都是把不同的 URL 与某个操作进行关联绑定,得到不一样的结果

1.3.1后端路由

通过 HTTP 把请求发送到后端服务器,后端服务器接收到请求以后根据不同的请求 URL 来执行不同的操作,返回处理好的数据(JSON、HTML、JS 代码、CSS、图像……)

  • 需要发送 HTTP 请求

  • 业务逻辑由后端处理,返回处理后的结果给前端(浏览器)

1.3.2前端路由

前端路由只是改变了 URLURL 中的某一部分,但一定不会直接发送请求,可以认为仅仅只是改变了浏览器地址栏上的 URL 而已,JavaScript 通过各种手段处理这种 URL 的变化,然后通过 DOM 操作动态的改变当前页面的结构。

  • URL 的变化不会直接发送 HTTP 请求
  • 业务逻辑由前端 JavaScript 来完成

目前前端路由主要的模式

  • 基于 URL Hash 的路由
  • 基于 HTML5 History API 的路由

1.3.2.1URL Hash

通过修改 URLHash 值来改变 URLHash 的变化是不会发送请求的,同时 JavaScript 通过监听 hashchange 事件来动态处理逻辑和页面渲染。

优点

兼容性好

缺点

URL 不美观,SEO 不友好

1.3.2.2HTML5 History API

封装一个函数,该函数通过 HTML5 History 提供的 API 来动态改变 URL ,这种方式也不会发送请求,然后同时根据要改变的目标 URL 来处理逻辑和页面渲染

URL Hash 模式类似 Vue 中的数据拦截机制

HTML5 History API 模式类似 React.js 中的 setState

1.4React.js 中的路由

React.js 路由的基本思想就是,把不同的 URL 与 指定的某些 React.js 组件进行关联,不同的 URL 渲染显示不同的组件,其它框架(如:vue、angular) 都是一样的思想

2.React Router

理解了路由基本机制以后,也不需要重复造轮子,我们可以直接使用 React Router

https://reacttraining.com/react-router/

React Router 提供了多种不同环境下的路由库

  • web
  • native

2.1基于 WebReact Router

基于 webReact Router 为:react-router-dom

2.1.1安装

npm i -S react-router-dom

2.1.2概览

react-router-dom 的核心是组件,它提供了一系列的组件,如:

  • Router 组件
  • BrowserRouter 组件
  • HashRouter 组件
  • Route 组件
  • Link 组件
  • NavLink 组件
  • Switch 组件
  • Redirect 组件

以及其它一些 API,来完成路由的功能

3.基础

3.1应用场景(一)——页面切换

假设当前应用有两个页面,对应的 URL 与 页面关系

/       :   首页
/about  :   关于我们

3.1.1Router 组件

如果我们希望页面中某个部分的内容需要根据 URL 来动态显示,需要用到 Router 组件 ,该组件是一个容器组件只需要用它包裹 URL 对应的根组件即可。用于确定那个范围的组件收到路由控制。

react-router-dom 为我们提供了几个基于不同模式的 router 子组件

  • BrowserRouter 组件
  • HashRouter 组件
  • MemoryRouter 组件
  • NativeRouter 组件
  • StaticRouter 组件

3.1.1.1BrowserRouter 组件

基于 HTML5 History API 的路由组件,低版本浏览器可能不支持

3.1.1.2HashRouter 组件

基于 URL Hash 的路由组件,路径中包含了#

BrowserRouter组件和HashRouter组件区别:

https://www.cnblogs.com/flamestudio/p/11965991.html

css

ul {margin: 0;padding: 0;
}li {list-style: none;
}.item-list li {padding: 10px;display: flex;justify-content: space-between;height: 30px;line-height: 30px;border-bottom: 1px dotted #333;
}
.item-list li.head {font-weight: bold;
}
.item-list li span {min-width: 200px;
}.pagination {margin: 10px;
}
.pagination a {margin: 0 5px;display: inline-block;width: 30px;height: 30px;background-color: #f4f4f5;line-height: 30px;text-align: center;cursor: pointer;color: #606266;text-decoration: none;
}
.pagination a:hover {color: #409eff;
}
.pagination a.active {background-color: #409eff;color: #fff;cursor: text;
}
.pagination .goto {margin: 0 5px;box-sizing: border-box;width: 80px;height: 30px;border: 1px solid #dcdfe6;outline: none;text-align: center;vertical-align: top;
}

App.js

import React from 'react';import {BrowserRouter as Router} from 'react-router-dom';
// import {HashRouter as Router} from 'react-router-dom';import BaseApp from './BaseApp/Index';function App() {return (<div><h1>React-Router</h1><hr/><Router><BaseApp /></Router></div>);
}export default App;
import {BrowserRouter as Router} from 'react-router-dom'

导入 BrowserRouter 组件,并命名 Router 别名,方便使用

<Router><BaseApp />
</Router>

 只对页面中的 BaseApp 组件使用路由

./BaseApp/Index.js

import React from 'react';
import {Route, Link} from 'react-router-dom';import Home from './Home';
import About from './About';export default class BaseApp extends React.Component {render() {return(<div><h2>路由基础使用</h2><nav><Link to="/">Home</Link><span> | </span><Link to="/about">About</Link></nav><br/><Route exact path='/' component={Home} /><Route path='/about' component={About} /></div>);}}
import {Route} from 'react-router-dom';

3.1.2Route 组件

注意这个组件是没有字母 r 的,通过该组件来设置应用单个路由信息Route 组件所在的区域就是就是URL 与当前 Route 设置的 path 属性匹配的时候,后面 component 将要显示的区域

<Route path='/' component={Home} />

URL 为:'/' 的时候,组件 Home 将显示在这里

exact

exact 属性表示路由使用 精确匹配模式,非 exact 模式下 '/' 匹配所有以 '/' 开头的路由

3.1.3Link 组件

Link 组件用来处理 a 链接 类似的功能(它会在页面中生成一个 a 标签),但设置这里需要注意的,用户点击时react-router-dom 拦截了实际 a 标签的默认动作,然后根据所有使用的路由模式(Hash 或者 HTML5)来进行处理,改变了 URL,但不会发生请求,同时根据 Route 中的设置把对应的组件显示在指定的位置

to 属性

to 属性类似 a 标签中的 href

场景一完整案例代码:

App.js:

import React from 'react';
import './App.css';import BaseRouter from './components/BaseRouter';
// 导入 BrowserRouter 组件,并命名 Router 别名,方便使用
import { BrowserRouter as Router} from 'react-router-dom';function App() {return (<div className="App"><h1>React-Router</h1><hr /><Router><BaseRouter /></Router></div>);
}export default App;

BaseRouter.js:

import React from 'react';import RouterSwitch from './RouterSwitch';
class BaseRouter extends React.Component{render(){return(<div>{/* 场景一:页面切换 */}<RouterSwitch /></div>);}
}export default BaseRouter;

 RouterSwitch.js:**

import React from 'react';
import { Route, Link } from 'react-router-dom';import Home from './Home';
import About from './About';
/*** 场景一:使用路由无刷新切换页面*/
class RouterSwitch extends React.Component {render() {return (<div>{/* <Link>路由组件: 相当于a标签功能;to属性相当于a标签的href属性;拦截了a标签中点击后刷新跳转*/}<nav><Link to="/">首页</Link><span> | </span><Link to="/about">关于我们</Link></nav><br/>{/* Route 组件:用于设置路由信息,path属性匹配路径;component设置要显示的组件;exact表示精确匹配,非 exact 模式下 '/' 匹配所有以 '/' 开头的路由(注意/前不要加点./) */}<Route path="/" exact component={Home} /><Route path="/about" component={About} /></div>);}
}export default RouterSwitch;

 Home.js:

import React from 'react';class Home extends React.Component{render(){return(<div>主页</div>);}
}export default Home;

About.js:

import React from 'react';class About extends React.Component{render(){return(<div>关于我们</div>);}
}export default About;

 效果:

React(四)——React 路由(react-router-dom)React(四)——React 路由(react-router-dom)

3.2应用场景(二)——商品主页和详情页展示

最近生活拮据,想在应用中卖点东西补贴一下家用

需求:商品展示(详情页和主页共享数据)

3.2.1状态提升

前面讲组件中提到过组件状态提升,如果多个组件需要共享一个状态,那么就讲状态提升到使用该状态的共同父级组件上

./BaseApp/Index.js

this.state = {items: [{id: 1,name: 'iPhone XR',price: 542500},{id: 2,name: 'Apple iPad Air 3',price: 377700},{id: 3,name: 'Macbook Pro 15.4',price: 1949900},{id: 4,name: 'Apple iMac',price: 1629900},{id: 5,name: 'Apple Magic Mouse',price: 72900},{id: 6,name: 'Apple Watch Series 4',price: 599900}]
}

3.2.2传递 props

<Route exact path='/' component={Home}

如果 Route 使用的是 component 来指定组件,那么不能使用 props

Route : render

<Route exact path='/' render={() => <Home items={this.state.items} />} />

通过 render 属性来指定渲染函数,render 属性值是一个函数,当路由匹配的时候指定该函数进行渲染

3.2.3渲染列表

Home.js

import React from 'react';import {Link} from 'react-router-dom';export default class Home extends React.Component {render() {let {items} = this.props;return (<div><h2>商品列表</h2><ul className="item-list"><li className="head"><span>名称</span><span>价格</span></li>{items.map(item=>(<li key={item.id}><span><Link to={'/Item/' + item.id}>{item.name}</Link></span><span>¥ {(item.price / 100).toFixed(2)}</span></li>))}</ul></div>);}}

params

在路由中,params 指的是 path 部分可变的部分,在这里我们需要定义一个新的路由用来展示商品的详情,我们希望通过 /item/1/item/2/item/…… 来访问对应 id 的商品,那么后面的数字部分就是可变的 - params,我们也称这样的路由为:动态路由

3.2.4动态路由

为了能给处理上面的动态路由地址的访问,我们需要为 Route 组件配置特殊的 path

<Route path='/item/:id' component={Item} />

3.2.4.1path-to-regexp

path-to-regexp 是一个专门用来处理 URL 的库,它用来了一种类似正则的字符串表示法

: 表示后面的部分为可变,: 后面的单词为匹配后的内容存储的名称,如:/item/1 ,就是 id=1

默认情况下,它为我们提供了几个常用的字符表示:*?+

但是我们可以通过 () 来使用正则

<Route path='/item/:id(\d+)' component={Item} />

上面表示 /item/ 后面只能是数字

3.2.4.2Route : props

当动态路由匹配以后,我们可以通过对应组件的 props 属性来访问当前路由匹配信息

3.2.5路由组件

如果一个组件是通过路由直接访问的,我们称为:路由组件 - 视图组件,其它的类型,根据具体功能可以称为:业务组件UI 组件容器组件,……

如果一个组件是路由组件,那么组件的 props 属性就会自动添加几个与路由有关的几个属性

  • history : 对象,提供了一些用于操作路由的方法,类似原生 JavaScript 中的 history 对象。(可通过this.props.history.push('/')或this.props.go()进行跳转)
  • location : 对象,通过它可以获取当前路由 URL 信息,类似原生 JavaScript 中的 history.location 对象
  • match : 对象,当前路由解析后的 URL 信息

match : params

this.props.match.params.id

注意:非路由组件是没有路由数据的

如果一个组件既要接收手动传入的 props,又想接收路由信息

<Route path='/item/:id(\d+)' render={props => <Item {...props} items={this.state.items} />} />

商品详情

render() {let items = this.props.items;let id = Number(this.props.match.params.id) || 0;let item = items.find(item => item.id === id);return item ? (<div><h2>商品详情 - {item.name}</h2><dt>ID</dt><dd>{item.id}</dd><dt>名称</dt><dd>{item.name}</dd><dt>价格</dt><dd>¥ {(item.price / 100).toFixed(2)}</dd></div>) : <div>不存在该商品!</div>;
}

 场景二完整案例代码:

items.js数据,router.css见上

RouterSwitch.js:因为商品展示是和主页及关于我们一样,所以Route组件设置需在RouterSwitch.js实现

import React from 'react';
import { Route, Link } from 'react-router-dom';import Home from './Home';
import About from './About';
import Item from './Item';// 注意:所有用到的数据都需要进行导出
import items from '../data/items.js';
/*** 场景一:使用路由无刷新切换页面*/
class RouterSwitch extends React.Component {render() {return (<div>{/* <Link>路由组件: 相当于a标签功能;to属性相当于a标签的href属性;拦截了a标签中点击后刷新跳转*/}<nav><Link to="/">首页</Link><span> | </span><Link to="/about">关于我们</Link></nav><br />{/* Route 组件:用于设置路由信息,path属性匹配路径;component设置要显示的组件;exact表示精确匹配,非 exact 模式下 '/' 匹配所有以 '/' 开头的路由(注意/前不要加点./) */}{/* <Route path="/" exact component={Home} /><Route path="/about" component={About} /> */}{/* 场景二:商品展示主页。*/}{/* 如果Route组件需要传递数据,就不能使用compoment属性,而是需要使用render属性 render 属性值是一个函数,当路由匹配的时候指定该函数进行渲染*/}<Route path="/" exact render={el => <Home items={items} />} /><Route path="/about" component={About} />{/* 注意Item的Route组件因为是和主页及关于我们页面一样,所以应该在此组件中切换 */}{/* /item/:id(\d+)表示id后只能是数字,params是path路径下的可变部分,如/item/1 */}{/* 也需要将数据items传递到Item页面,这里是路由组件,所以props属性会自动添加几个与路由有关的几个属性history:对象,location:对象,match:对象通过...props将props属性中所有对象传递到Item组件,且传入items*/}<Route path='/Item/:id(\d+)' render={(props)=><Item {...props} items={items} />} /></div>);}
}export default RouterSwitch;

 Home.js:处理商品展示,和通过Link组件链接到Item组件

import React from 'react';
import '../css/router.css';
import { Link, Route } from 'react-router-dom';class Home extends React.Component {constructor(props) {//商品数据通过props传递过来super(props)}render() {//此处得到的数据已经是数组let { items: { items } } = this.props;return (<div>{/* <div>主页</div> */}{/* 场景二:商品展示主页 */}<h2>商品列表</h2><ul className="item-list"><li className="head"><span>名称</span><span>价格</span></li>{// 点击某行进入详情页items.map(item => (<li key={item.id}><span>{/* 注意Item的Route组件因为是和主页及关于我们页面一样切换不同页面,所以应该在RouterSwitch组件中使用Route组件 */}<Link to={'/Item/'+item.id}>{item.name}</Link></span><span>¥ {(item.price / 100).toFixed(2)} </span></li>))}</ul></div>);}
}export default Home;

Item.js:获取点击的商品详情展示

import React from 'react';class Item extends React.Component{constructor(props){super(props);}render(){//此处可得到所有传过来的history,match,location,items所有数据// let {history,match:{params:{id}},location,items} = this.props;let {match:{params:{id}},items:{items}} = this.props;//通过传递过来的id匹配到数据中的数据,注意从match.params中获得的id是string,必须转为数字类型let itemInfo = items.find(item=>item.id === Number(id));return itemInfo ? (<div><h2>商品详情 - {itemInfo.name}</h2><dt>ID</dt><dd>{itemInfo.id}</dd><dt>名称</dt><dd>{itemInfo.name}</dd><dt>价格</dt><dd>¥ {(itemInfo.price/100).toFixed(2)}</dd></div>) : <div>获取商品详情出错</div>;}
}export default Item;

 效果:

React(四)——React 路由(react-router-dom)

React(四)——React 路由(react-router-dom)

3.3应用场景(三)——价格排序

我想增加一个功能,用户可以按照商品价格的高低进行选择展示。

3.3.1通过 JavaScript 实现排序切换

Home.js

以非受控组件方式控制select框排序。

import React from 'react';
import '../css/router.css';
import { Link, Route } from 'react-router-dom';class Home extends React.Component {constructor(props) {//商品数据通过props传递过来super(props);this.state = {sort : 'desc'};this.sortItems = this.sortItems.bind(this);}//商品排序(注意select需解构直value层,注意select单选的非受控组件也是通过{target:{value}}进行解构)sortItems({target:{value:sort}}){this.setState({sort});}render() {//此处得到的数据已经是数组let { items: { items } } = this.props;//根据sort的值将items进行排序items.sort((a,b)=>{return this.state.sort === 'desc'?a.price - b.price : b.price-a.price;});return (<div>{/* <div>主页</div> */}{/* 场景二:商品展示主页 */}<h2>商品列表</h2>排序:<select value={this.state.sort} onChange={this.sortItems}><option value="desc">从低到高</option><option value="asc">从高到低</option></select><ul className="item-list"><li className="head"><span>名称</span><span>价格</span></li>{// 点击某行进入详情页items.map(item => (<li key={item.id}><span>{/* 注意Item的Route组件因为是和主页及关于我们页面一样切换不同页面,所以应该在RouterSwitch组件中使用Route组件 */}<Link to={'/Item/'+item.id}>{item.name}</Link></span><span>¥ {(item.price / 100).toFixed(2)} </span></li>))}</ul></div>);}
}export default Home;

问题:刷新页面、分享 URL 都会丢失状态。如本来已经从高到低排序好了,分享页面打开后排序又恢复默认从低到高排序

3.3.2通过路由实现排序切换

3.3.2.1queryString

通常我们把 URL ? 后面的内容称为 queryString,在 React.js 中,我们可以通过 this.props.location.search 来获取,它的值是字符串,格式为:?k1=v1&k2=v2,为了方便操作,我们把它转成对象形式

3.3.2.2操作queryString的原生类URLSearchParams

在原生 JavaScript 中内置了一个 URLSearchParams 的类,我们通过它可以很方便的操作 queryString。URLSearchParams 的类会将内部的search变成一个对象。问题:只能解构一层对象的数据。如果要解构多层对象,需要使用第三方库qs库。

let {location: {search}} = this.props;
let qs = new URLSearchParams(search);
let sort = qs.get('sort');

3.3.3扩展

3.3.3.1qs 库

https://www.npmjs.com/package/qs

3.3.3.2安装

npm i -S qs

3.3.3.3使用

通过search获取的对象包含?,可以使用queryString.parse(search, {ignoreQueryPrefix: true});和queryString.parse(search).substring(1);两种方法去掉?。

import queryString from 'qs';
?
let qsTest = queryString.parse(search, {ignoreQueryPrefix: true});
let sort = qsTest.sort;

3.3.3.4通过 JavaScript 编程的方式切换路由

除了使用 <Link> 组件像 a 一样点击跳转路由,我们还可以通过编程的方式(JavaScript) 来切换路由。点击切换排序方式时直接改变URL,且直接跳转。跳转使用props属性下的对象history下的push()方法,push()参数即为跳转后的queryString

let {history} = this.props;
<select defaultValue={sort} onChange={({target:{value}})=>{history.push('/?sort=' + value);}}><option value="desc">从高到低</option><option value="asc">从低到高</option>
</select>

 完整实现排序代码:

  • 因为刷新或分享页面后sort状态改变导致排序混乱,所以需要选择排序后,同时改变URL,且直接跳转,此时使用history对象下的push()方法。因为此时仍然跳转到Home组件即非路由组件,所以需要同时传递props;
  • 通过location对象获取queryString。并使用原生NewSearchParams类或qs类库处理

RouteSwitch_sort.js:

import React from 'react';
import { Route, Link } from 'react-router-dom';import Home from './Home_sort';
import About from '../About';
import Item from '../Item';// 注意:所有用到的数据都需要进行导出
import items from '../../data/items.js';
/*** 场景三:解决刷新页面和分享链接后,排序的sort失效问题*/
class RouterSwitch extends React.Component {render() {return (<div>{/* <Link>路由组件: 相当于a标签功能;to属性相当于a标签的href属性;拦截了a标签中点击后刷新跳转*/}<nav><Link to="/">首页</Link><span> | </span><Link to="/about">关于我们</Link></nav><br />{/* Route 组件:用于设置路由信息,path属性匹配路径;component设置要显示的组件;exact表示精确匹配,非 exact 模式下 '/' 匹配所有以 '/' 开头的路由(注意/前不要加点./) */}{/* <Route path="/" exact component={Home} /><Route path="/about" component={About} /> */}{/* 场景二:商品展示主页。*/}{/* 如果Route组件需要传递数据,就不能使用compoment属性,而是需要使用render属性 render 属性值是一个函数,当路由匹配的时候指定该函数进行渲染*/}{/* <Route path="/" exact render={el => <Home items={items} />} /> */}{/* 场景三:排序且解决sort失效问题*/}{/* 跳转到Home组件时,直接带上sort排序,而Home组件是非路由组件,所以需要同时传入props属性 */}<Route path="/" exact render={(props) => <Home {...props} items={items} />} /><Route path="/about" component={About} />{/* 注意Item的Route组件因为是和主页及关于我们页面一样,所以应该在此组件中切换 */}{/* /item/:id(\d+)表示id后只能是数字,params是path路径下的可变部分,如/item/1 */}{/* 也需要将数据items传递到Item页面,这里是路由组件,所以props属性会自动添加几个与路由有关的几个属性history:对象,location:对象,match:对象通过...props将props属性中所有对象传递到Item组件,且传入items*/}<Route path='/Item/:id(\d+)' render={(props)=><Item {...props} items={items} />} /></div>);}
}export default RouterSwitch;

 Home_sort.js:

import React from 'react';
import '../../css/router.css';
import { Link, Route } from 'react-router-dom';
import QS from 'qs';
/*** 应用场景三:排序并解决sort失效问题* 注意:获取search中sort的地方和排序的地方,是在render方法中,因为无论是首次进入首页还是排序后跳转到首页都需要重新进行获取search和排序*/
class Home extends React.Component {constructor(props) {//商品数据通过props传递过来super(props);}render() {//注意需要此处就获取queryString并进行排序let { items: { items }, history, location:{search} } = this.props;//通过原生JS类URLSearchParams的get()方法获取sort的值,当首次进入时没有search对象,直接设置为asc,在select中也设置defaultValue="asc"// let sort = new URLSearchParams(search).get('sort') || 'asc';//原生URLSearchParams类只使用与一层对象的解构,多层对象解构时就必须使用qs类库// let sort = QS.parse(search.substring(1)).sort;let sort = QS.parse(search,{ignoreQueryPrefix:true}).sort;//排序items.sort((a,b)=>{return sort === 'desc'? a.price-b.price : b.price-a.price;});return (<div>{/* <div>主页</div> */}{/* 场景二:商品展示主页 */}<h2>商品列表</h2>排序:{/* 切换时直接跳转 */}<select defaultValue="asc" onChange={({ target: { value } }) => {//注意此处不能使用returnhistory.push('/?sort=' + value);}}><option value="desc">从低到高</option><option value="asc">从高到低</option></select><ul className="item-list"><li className="head"><span>名称</span><span>价格</span></li>{// 点击某行进入详情页items.map(item => (<li key={item.id}><span>{/* 注意Item的Route组件因为是和主页及关于我们页面一样切换不同页面,所以应该在RouterSwitch组件中使用Route组件 */}<Link to={'/item/' + item.id}>{item.name}</Link></span><span>¥ {(item.price / 100).toFixed(2)} </span></li>))}</ul></div>);}
}export default Home;

3.4应用场景(四)——页面顶部导航高亮

现在,我想给页面顶部的导航加上高亮效果,用来标识当前页面。这个时候,我们就可以使用 react-router-dom 提供的 NavLink 组件来实现。

3.4.1NavLink 组件

NavLinkLink 类似,但是它提供了两个特殊属性用来处理页面导航

Failed prop type: Invalid prop `activeStyle` activeStyle后的style样式写法有错。

React(四)——React 路由(react-router-dom)

3.4.1.1activeStyle

当当前 URLNavLink 中的 to 匹配的时候,激活 activeStyle 中的样式

3.4.1.2activeClassName

activeStyle 类似,但是激活的是 className

3.4.1.3isActive

默认情况下,匹配的是 URLto 的设置,通过 isActive 可以自定义激活逻辑isActive 是一个函数,返回布尔值

index.js

<NavLink to="/" activeStyle={
   {color:'red'}} isActive={(match, location) => {return match || location.pathname.startsWith('/item')
}} exact>Home</NavLink>
<span> | </span>
<NavLink to="/about" activeStyle={
   {color:'red'}} exact>About</NavLink>

3.3.1.4exact

精确匹配

应用场景(四)——高亮导航完整代码实现:

import React from 'react';
import { Route, Link, NavLink } from 'react-router-dom';import Home from '../sort/Home_sort';
import About from '../About';
import Item from '../Item';
import items from '../../data/items.js';
import '../../css/router.css';
/*** 场景四:导航高亮* <NavLink>组件*/
class RouterSwitch extends React.Component {render() {return (<div>{/* <Link>路由组件: 相当于a标签功能;to属性相当于a标签的href属性;拦截了a标签中点击后刷新跳转*/}<nav>{/* Failed prop type: Invalid prop `activeStyle` activeStyle后的style样式写法有错isActive是函数,返回值为布尔值,为TRUE则设置高亮样式,FALSE则不设置*/}<NavLink to="/" exact activeStyle={
   {'color':'red'}} isActive={(match,location)=>{//注意RouterSwitch组件不是路由组件,不能直接获取match,所以需要传入propsreturn match || location.pathname.startsWith("/item");}}>首页</NavLink><span> | </span><NavLink to="/about" exact activeClassName={"homeHighLight"}>关于我们</NavLink></nav><br />{/* Route 组件:用于设置路由信息,path属性匹配路径;component设置要显示的组件;exact表示精确匹配,非 exact 模式下 '/' 匹配所有以 '/' 开头的路由(注意/前不要加点./) */}{/* <Route path="/" exact component={Home} /><Route path="/about" component={About} /> */}{/* 场景二:商品展示主页。*/}{/* 如果Route组件需要传递数据,就不能使用compoment属性,而是需要使用render属性 render 属性值是一个函数,当路由匹配的时候指定该函数进行渲染*/}{/* <Route path="/" exact render={el => <Home items={items} />} /> */}{/* 场景三:排序且解决sort失效问题*/}{/* 跳转到Home组件时,直接带上sort排序,而Home组件是非路由组件,所以需要同时传入props属性 */}<Route path="/" exact render={(props) => <Home {...props} items={items} />} /><Route path="/about" component={About} />{/* 注意Item的Route组件因为是和主页及关于我们页面一样,所以应该在此组件中切换 */}{/* /item/:id(\d+)表示id后只能是数字,params是path路径下的可变部分,如/item/1 */}{/* 也需要将数据items传递到Item页面,这里是路由组件,所以props属性会自动添加几个与路由有关的几个属性history:对象,location:对象,match:对象通过...props将props属性中所有对象传递到Item组件,且传入items*/}<Route path='/item/:id(\d+)' render={(props) => <Item {...props} items={items} />} /></div>);}
}export default RouterSwitch;

3.5应用场景(五)——404页面

当用户访问不存在的路由,我们需要提供一个反馈页面,也就是 404

index.js

<Route exact path='/' render={props => <Home {...props} items={this.state.items} />} />
<Route path='/about' component={About} />
<Route path='/item/:id(\d+)' render={props => <Item {...props} items={this.state.items} />} />
{/*NotFound*/}
<Route component={NotFound} />

NotFound.js

import React from 'react';
?
export default function NotFound() {return (<div>Not Found</div>);
}

默认情况下,React.js 会渲染所有与之匹配的路由组件,上面的案例中,当我们访问任意路由(如:/about),也会匹配 NotFound 路由组件渲染。即路由组件是由穿透性的,所以需要使用Switch组件。

3.5.1Switch 组件

该组件只会渲染首个被匹配的组件,所以把优先级高的组件写在最前面。

<Switch><Route exact path='/' render={props => <Home {...props} items={this.state.items} />} /><Route path='/about' component={About} /><Route path='/item/:id(\d+)' render={props => <Item {...props} items={this.state.items} />} />{/*NotFound*/}<Route component={NotFound} />
</Switch>

3.6应用场景(六)——购物车

现在,我们要给用户增加一个购物车的功能。未登录用户是不能访问购物车的,所以,我们需要在访问购物车的时候加入用户权限验证(鉴权),如果没有登录则跳转到登录。用户信息的存储通过state进行模拟。

index.js

// 添加一个连接
<NavLink to="/cart" activeStyle={
   {color:'red'}} exact>Cart</NavLink>

Cart.js

// 购物车组件
import React from 'react';export default class Cart extends React.Component {render() {return(<div>购物车</div>);}}

我们还要配一个用户系统,比如:注册登录啥的

index.js

// 模拟一个添加登录用户状态的 state
this.state = {users: [{id: 1,username: 'baoge',password: '123'},{id: 2,username: 'MT',password: '123'},{id: 3,username: 'dahai',password: '123'},{id: 4,username: 'zMouse',password: '123'}],userInfo: {id: 0,username: ''},...
}// 添加一个登录链接
{this.state.userInfo.id > 0 ? (<span>{this.state.userInfo.username}</span>) : (<NavLink to='/login' activeStyle={
   {color:'red'}} exact>Login</NavLink>)
}

3.6.1Redirect 组件

未登录用户是不能访问购物车的,所以,我们需要在访问购物车的时候加入用户权限验证(鉴权),如果没有登录则跳转到登录

index.js

<Route path='/cart' render={props => {if (this.state.userInfo.uid > 0) {return <Cart />;} else {// return <Login />;return <Redirect to='/login' />;}
}}  />

to

设置跳转的 URL

登录代码

// index.js
login({username, password}) {return new Promise( (resolve, reject) => {if (!username || !password) {reject('请输入用户名和密码');}let user = this.state.users.find(user => user.username === username && user.password === password);if ( !user ) {reject('用户不存在或者密码错误');}this.setState({userInfo: {id: user.id,username: user.username}});resolve('登录成功');} );
}
...
<Route path='/login' component={props => {return <Login {...props} onLogin={this.login.bind(this)} />;
}} />
// login.js
import React from 'react';export default class Login extends React.Component {constructor(...props) {super(...props);this.login = this.login.bind(this);this.usernameRef = React.createRef();this.passwordRef = React.createRef();}login() {let {onLogin, history: {push}} = this.props;if (typeof onLogin === 'function') {onLogin({username: this.usernameRef.current.value,password: this.passwordRef.current.value}).then(msg=>{alert(msg);push('/');}).catch(e=>alert(e));} }render() {return(<div><p>用户名:<input type="text" ref={this.usernameRef} /></p><p>密码:<input type="password" ref={this.passwordRef} /></p><p><button onClick={this.login}>登录</button></p></div>);}}

 场景六——购物车——用户登录鉴权认证完整代码实现:

分析:

  • 点击购物车时,通过路由判断是否登录,登录则显示购物车页面,未登录显示登录页面;
  • 将登录信息通过state状态保存,并通过其判断是否登录;
  • 由于登录页面进行登录后,需要将登录信息保存到state中,并且在路由时需要使用,所以登录页面的login方法需要调用路由页面登录方法去真正实现登录逻辑;
  • 以上操作会影响到home页面的props对象,所以需要通过{props}将对象传递到下一层

RouterSwitch_cart.js:

import React from 'react';
import { Route, Link, NavLink, Switch, Redirect } from 'react-router-dom';import Home from '../sort/Home_sort';
import About from '../About';
import Item from '../Item';
import items from '../../data/items.js';
import '../../css/router.css';// 场景五:404错误页面
// import NotFound from './NotFound';// 场景六:购物车——用户登录鉴权认证
import Cart from '../cart/Cart';
import Login from '../cart/Login';
import usersData from '../../data/user';/*** 场景六:购物车——用户登录鉴权认证*/
class RouterSwitch extends React.Component {constructor(props){super(props);let {userInfo} = usersData;//需要通过state保存和更新登录用户信息this.state = {userInfo}}login({username, password}) {return new Promise( (resolve, reject) => {if (!username || !password) {reject('请输入用户名和密码');}let {users} = usersData;let user = users.find(user => user.username === username && user.password === password);if ( !user ) {reject('用户不存在或者密码错误');}this.setState({userInfo: {id: user.id,username: user.username}});resolve('登录成功');} );}render() {return (<div>{/* <Link>路由组件: 相当于a标签功能;to属性相当于a标签的href属性;拦截了a标签中点击后刷新跳转*/}<nav><NavLink to="/" exact activeStyle={
   { 'color': 'red' }} isActive={(match, location) => {return match || location.pathname.startsWith("/item");}}>首页</NavLink><span> | </span><NavLink to="/about" exact activeClassName={"homeHighLight"}>关于我们</NavLink><span> | </span>{/* 场景六:购物车——用户鉴权认证*/}<NavLink to="/cart" exact activeClassName={"homeHighLight"}>购物车</NavLink></nav><br />{/* Route 组件:用于设置路由信息,path属性匹配路径;component设置要显示的组件;exact表示精确匹配,非 exact 模式下 '/' 匹配所有以 '/' 开头的路由(注意/前不要加点./) */}{/* <Route path="/" exact component={Home} /><Route path="/about" component={About} /> */}{/* 场景二:商品展示主页。*/}{/* 如果Route组件需要传递数据,就不能使用compoment属性,而是需要使用render属性 render 属性值是一个函数,当路由匹配的时候指定该函数进行渲染*/}{/* <Route path="/" exact render={el => <Home items={items} />} /> */}{/* 场景三:排序且解决sort失效问题*/}{/* 跳转到Home组件时,直接带上sort排序,而Home组件是非路由组件,所以需要同时传入props属性 */}<Switch>{/* 场景六时,注意Home组件本身不是路由组件,需要传递props对象 */}<Route path="/" exact render={(props) => <Home {...props} items={items} />} /><Route path="/about" component={About} />{/* 注意Item的Route组件因为是和主页及关于我们页面一样,所以应该在此组件中切换 */}{/* /item/:id(\d+)表示id后只能是数字,params是path路径下的可变部分,如/item/1 */}{/* 也需要将数据items传递到Item页面,这里是路由组件,所以props属性会自动添加几个与路由有关的几个属性history:对象,location:对象,match:对象通过...props将props属性中所有对象传递到Item组件,且传入items*/}<Route path='/item/:id(\d+)' render={(props) => <Item {...props} items={items} />} />{/* 场景五:404错误页面*/}{/* 如果直接加入Route会每个页面都有404错误页面 */}{/* <Route component={NotFound} /> */}{/* 场景六:购物车——用户鉴权认证判断如果userInfo中id不大于0则没有登录,需要重定向到登录页面*/}<Route path="/login" render={(props)=><Login {...props} onLogin={this.login.bind(this)}/>}/><Route path="/cart" render={()=>{//  注意此处的userInfo需要从state中取,登录后会保存进state中if(this.state.userInfo.id>0){console.log("购物车");return <Cart />}else{console.log("未登录");return <Redirect to='/login'/>}}} /></Switch></div>);}
}export default RouterSwitch;

 Login.js:

import React from 'react';/*** 用户登录:通过ref回调方式,获取并设置非受控组件的值*/
class Login extends React.Component {constructor(props) {super(props);this.login = this.login.bind(this);this.usernameRef = React.createRef();this.passwordRef = React.createRef();}login() {let {onLogin, history: {push}} = this.props;if (typeof onLogin === 'function') {onLogin({username: this.usernameRef.current.value,password: this.passwordRef.current.value}).then(msg=>{alert(msg);push('/');}).catch(e=>alert(e));} }render() {return (<div>用户名:<input ref={this.usernameRef} type="text" name="username" /><br />密码:<input ref={this.passwordRef} type="password" name="password" /><br /><button onClick={this.login}>登录</button></div>);}
}
export default Login;

 Cart.js购物车页面和users.js数据代码见上。

3.7应用场景(七)——分页组件

我突然发现卖东西比做码农要赚钱(主要是能生发),所以加大产品投入量,期待有一天,我的头发要宝哥多,然后推荐给莫莫

随着产品的增多,那么多的数据就不可能一次性全部展示出来,所以我们为商品的展示添加一个分页功能。

  • 分页组件:某个组件或页面需要用到分页功能时就引入分页组件,然后传递分页参数。
  • 且该需要分页的页面组件的path需要只能匹配 / 或/page(页码数字类型),否则不能进行路由。如<Route path="/page(\d)*" render={...return (<Home .../>)} />。
  • 跳转到某页,使用onKeyDown()后调用push()进行跳转,但分页组件不是路由组件,不能直接将props属性及里面的各对象直接向下传递。虽然我们可以通过传参的方式传入,但是如果结构复杂,这样做会特别的繁琐。幸好,我们可以通过 withRouter 方法来注入路由对象
// Pagination.js
import React from 'react';
import PropTypes from 'prop-types';import {withRouter, Link} from 'react-router-dom';class Pagination extends React.Component {static defaultProps = {pages: 1,page: 1}static propTypes = {pages: PropTypes.number,page: PropTypes.number}render() {//pages总共多少页,page当前页码,push跳转方法let {pages, page, history: {push}} = this.props;// console.log(this.props);return (<div className="pagination">{(new Array(pages)).fill('').map((v, i) => {return (<Link key={++i}className={i === page ? 'active' : ''}to={'/'+i}>{i}</Link>);})}前往<input type="text" className="goto" onKeyDown={({target:{value}})=>{if (value !== '') {push('/' + value);}}} />页</div>);}}export default withRouter(Pagination);

3.7.1withRouter()方法

如果一个组件不是路由绑定组件,那么该组件的 props 中是没有路由相关对象的,虽然我们可以通过传参的方式传入,但是如果结构复杂,这样做会特别的繁琐。幸好,我们可以通过 withRouter 方法来注入路由对象。

场景七:分页完整代码实现

RouterSwitch_pagenation.js:注意只有在Route路由时添加page参数,Link时不需要。<Route path="/:page(\d*)" exact render={(props) => <Home {...props} items={items}/>} />

import React from 'react';
import { Route, Link, NavLink, Switch, Redirect } from 'react-router-dom';// import Home from '../sort/Home_sort';
//场景七:
import Home from '../pagenation/Home_pagenation';
import About from '../About';
import Item from '../Item';
import items from '../../data/items.js';
import '../../css/router.css';// 场景五:404错误页面
// import NotFound from './NotFound';// 场景六:购物车——用户登录鉴权认证
// 场景七:分页
import Cart from '../cart/Cart';
import Login from '../cart/Login';
import usersData from '../../data/user';/*** 场景六:购物车——用户登录鉴权认证*/
class RouterSwitch extends React.Component {constructor(props){super(props);let {userInfo} = usersData;//需要通过state保存和更新登录用户信息this.state = {userInfo}}login({username, password}) {return new Promise( (resolve, reject) => {if (!username || !password) {reject('请输入用户名和密码');}let {users} = usersData;let user = users.find(user => user.username === username && user.password === password);if ( !user ) {reject('用户不存在或者密码错误');}this.setState({userInfo: {id: user.id,username: user.username}});resolve('登录成功');} );}render() {return (<div>{/* <Link>路由组件: 相当于a标签功能;to属性相当于a标签的href属性;拦截了a标签中点击后刷新跳转*/}<nav><NavLink to="/" exact activeStyle={
   { 'color': 'red' }} isActive={(match, location) => {return match || location.pathname.startsWith("/item");}}>首页</NavLink><span> | </span><NavLink to="/about" exact activeClassName={"homeHighLight"}>关于我们</NavLink><span> | </span>{/* 场景六:购物车——用户鉴权认证*/}<NavLink to="/cart" exact activeClassName={"homeHighLight"}>购物车</NavLink></nav><br />{/* Route 组件:用于设置路由信息,path属性匹配路径;component设置要显示的组件;exact表示精确匹配,非 exact 模式下 '/' 匹配所有以 '/' 开头的路由(注意/前不要加点./) */}{/* <Route path="/" exact component={Home} /><Route path="/about" component={About} /> */}{/* 场景二:商品展示主页。*/}{/* 如果Route组件需要传递数据,就不能使用compoment属性,而是需要使用render属性 render 属性值是一个函数,当路由匹配的时候指定该函数进行渲染*/}{/* <Route path="/" exact render={el => <Home items={items} />} /> */}{/* 场景三:排序且解决sort失效问题*/}{/* 跳转到Home组件时,直接带上sort排序,而Home组件是非路由组件,所以需要同时传入props属性 */}<Switch>{/* 场景六时,注意Home组件本身不是路由组件,需要传递props对象 场景七时,传入page参数和正则限定*/}<Route path="/:page(\d*)" exact render={(props) => <Home {...props} items={items}/>} /><Route path="/about" component={About} />{/* 注意Item的Route组件因为是和主页及关于我们页面一样,所以应该在此组件中切换 */}{/* /item/:id(\d+)表示id后只能是数字,params是path路径下的可变部分,如/item/1 */}{/* 也需要将数据items传递到Item页面,这里是路由组件,所以props属性会自动添加几个与路由有关的几个属性history:对象,location:对象,match:对象通过...props将props属性中所有对象传递到Item组件,且传入items*/}<Route path='/item/:id(\d+)' render={(props) => <Item {...props} items={items} />} />{/* 场景五:404错误页面*/}{/* 如果直接加入Route会每个页面都有404错误页面 */}{/* <Route component={NotFound} /> */}{/* 场景六:购物车——用户鉴权认证判断如果userInfo中id不大于0则没有登录,需要重定向到登录页面*/}<Route path="/login" render={(props)=><Login {...props} onLogin={this.login.bind(this)}/>}/><Route path="/cart" render={()=>{//  注意此处的userInfo需要从state中取,登录后会保存进state中if(this.state.userInfo.id>0){console.log("购物车");return <Cart />}else{console.log("未登录");return <Redirect to='/login'/>}}} /></Switch></div>);}
}export default RouterSwitch;

Home_pagenation.js:分页使用slice方法实现

import React from 'react';
import '../../css/router.css';
import { Link, Route } from 'react-router-dom';import Pagenation from './Pagenation';class Home extends React.Component {constructor(props) {super(props);}render() {//此处得到的数据已经是数组let { items: { items },match:{params:{page}} } = this.props;console.log(page);//每页条数let num = 3;let pageNum = Number(page);let itemData = items.slice((pageNum-1)*num,page*num);return (<div>{/* <div>主页</div> */}{/* 场景二:商品展示主页 */}<h2>商品列表</h2><ul className="item-list"><li className="head"><span>名称</span><span>价格</span></li>{// 点击某行进入详情页itemData.map(item => (<li key={item.id}><span>{/* 注意Item的Route组件因为是和主页及关于我们页面一样切换不同页面,所以应该在RouterSwitch组件中使用Route组件 */}<Link to={'/item/'+item.id}>{item.name}</Link></span><span>¥ {(item.price / 100).toFixed(2)} </span></li>))}</ul>{/*场景七: 引入分页组件 */}<Pagenation pages={items.length/num} page={Number(page)}/></div>);}
}export default Home;

Pagenation.js:注意withRouter()的使用

import React from 'react';
import { Link, withRouter } from 'react-router-dom';import { number } from 'prop-types';
/*** 场景七:分页组件*/
class Pagenation extends React.Component {constructor(props) {super(props);}//设置总页数和页码的初始值和类型验证static defaultProps = {pages: 1,page: 1};static propTypes = {pages: number.isRequired,page: number.isRequired};render() {let { pages, page, history: { push } } = this.props;return (<div className="pagination">{(new Array(pages)).fill('').map((v, i) => {return (<Linkkey={++i}className={i === page ? 'active' : ''}to={'/' + i}>{i}</Link>);})}前往<input type="text" className="goto" onKeyDown={({ target: { value } }) => {if (value !== '') {push('/' + value);}}} />页</div>);}
}//通过使用withRouter就不需要一层层传递props属性,可直接在此获取,否则history等信息需要从Home组件传过来
export default withRouter(Pagenation);

效果:

React(四)——React 路由(react-router-dom)

  相关解决方案