React 学习笔记(五) -- React路由

1,647 阅读17分钟

GitHub仓库--源码地址

一、 SPA

  • 单页Web应用(single page web application,SPA)。整个应用只有一个完整的页面。
  • 点击页面中的链接不会刷新页面,只会做页面的局部更新。
  • 数据都需要通过ajax请求获取,并在前端异步展现

二、什么是路由

1. 一个路由其实就是一个映射关系(k:v)

key为路径,value可能是function 或者是 component

2. 后端路由:

value是function,用来处理客户端提交的请求

注册路由:router.get(path,function(req,res))

工作过程:当node接收一个请求的时候,根据请求路径找到匹配的路由,调用路由中的函数来处理请求,返回响应的数据

3. 前端路由:

浏览器端路由,value是Component,用于展示页面内容

注册路由:< Route path="/test" component={Test}>

工作过程:当浏览器的path变为/test的时候,当前路由组件就会变成Test组件

4. 前端路由的原理

这个主要是依靠于history,也就是浏览器的历史记录。

浏览器上的记录其实就是一个栈,前进一次就是入栈,后退一次就是出栈。

并且历史记录上有一个监听的方法,可以时时刻刻监听记录的变化。从而判断是否改变路径

History

三、 react-router-dom

印记中文--一个前端各种开发类库的中文文档

1. Chrome您的连接不是私密连接解决办法

在当前页面用键盘输入  thisisunsafe  ,不是在地址栏输入,就直接敲键盘就行了,页面即会自动刷新进入网页。

2. react的路由有三类:

  • web【主要适用于前端】
  • native【主要适用于本地】
  • anywhere【任何地方】

3. 在这主要使用web也就是这个标题 react-router-dom

  • react的一个插件库。
  • 专门用来实现一个SPA应用。
  • 基于react的项目基本都会用到此库。

4. 基本的使用:

1.安装react-router库

yarn add react-router-dom

2.在需要用到react-router的组件中按需引入

import {Link,Route} from 'react-router-dom'

3.导航中的a标签改写成Link标签

< Link to="/路径" >xxx< /Link>

4.展示区写Route标签进行路径的匹配

< Route path = '/路径' component={组件名称}>

5.< App />最外侧包裹了一个< BrowserRouter>或者< HashRouter> 1636085020(1).png

App.jsx

import React, { Component } from 'react'
import {Link,Route} from 'react-router-dom'
import Home from './components/Home'
import About from './components/About'

export default class App extends Component {
	render() {
		return (
			<div>
				<div className="row">
					<div className="col-xs-offset-2 col-xs-8">
						<div className="page-header"><h2>React Router Demo</h2></div>
					</div>
				</div>
				<div className="row">
					<div className="col-xs-2 col-xs-offset-2">
						<div className="list-group">

                                                    {/* 原生html中,靠<a>跳转不同的页面 */}
                                                    {/* <a className="list-group-item" href="./about.html">About</a>
                                                    <a className="list-group-item active" href="./home.html">Home</a> */}

                                                    {/* 在React中靠路由链接实现切换组件--编写路由链接 */}
                                                    <Link className="list-group-item" to="/about">About</Link>
                                                    <Link className="list-group-item" to="/home">Home</Link>
						</div>
					</div>
					<div className="col-xs-6">
						<div className="panel">
							<div className="panel-body">
								{/* 注册路由,进行路径与组件匹配 */}
								<Route path="/about" component={About}/>
								<Route path="/home" component={Home}/>
							</div>
						</div>
					</div>
				</div>
			</div>
		)
	}
}

index.jsx

//引入react核心库
import React from 'react'
//引入ReactDOM
import ReactDOM from 'react-dom'
// 引入BrowserRouter,包裹住App
import { BrowserRouter } from 'react-router-dom'
//引入App
import App from './App'
  //<APP/>最外侧包裹路由器
ReactDOM.render(
	<BrowserRouter>
		<App />
	</BrowserRouter>,
	document.getElementById('root')
)

About

import React, { Component } from 'react'

export default class About extends Component {
	render() {
		return (
			<h3>我是About的内容</h3>
		)
	}
}

Home

import React, { Component } from 'react'

export default class Home extends Component {
	render() {
		return (
			<h3>我是Home的内容</h3>
		)
	}
}

那么使用Link代替a标签之后,在页面上会是什么呢,我们发现其实页面上也是最终把link转化为了a标签

5. 路由组件以及一般组件的区别

1.写法不一样

一般组件:< Demo />

路由组件:< Route path="/demo" component ={Demo}/>

2.存放的位置一般不同

1636093360(1).png

一般组件:components

路由组件:pages

3.接收的内容【props】不同

一般组件:写组件标签的时候传递什么,就能收到什么

路由组件:接收到三个固定的属性【history,location,match】

//常用的属性
history:
    go: ƒ go(n)
    goBack: ƒ goBack()
    goForward: ƒ goForward()
    push: ƒ push(path, state)
    replace: ƒ replace(path, state)
location:
    pathname: "/about"
    search: ""
    state: undefined

match:
    params: {}
    path: "/about"
    url: "/about"

6. NavLink

NavLink可以实现路由链接的高亮:

1. NavLink的使用内联样式

<NavLink exact to="/" activeStyle={{ color: "red", fontSize: "30px" }}>
  首页
</NavLink>
<NavLink to="/about" activeStyle={{ color: "red", fontSize: "30px" }}>
  关于
</NavLink>
<NavLink to="/profile" activeStyle={{ color: "red", fontSize: "30px" }}>
  我的
</NavLink>

注意:NavLink中也有exact,但功能和<Route>中不同,只是让样式精确匹配,在link中没有

2. activeClassName指定样式

默认类名

<NavLink exact to="/">
  首页
</NavLink>
/* 是a元素且类名为active */
a.active {
  color: red;
  font-size: 30px;
}

自定义类名

{/*NavLink在点击的时候就会去找activeClassName="zgc"所指定的class的值,
如果不添加activeClassName默认其属性值为active(activeClassName="active")*/}
<NavLink  activeClassName="zgc" className="list-group-item"  to="/about">About</NavLink>
/* 类名为zgc */ 
.zgc { 
    color: red;
    font-size: 30px; 
}

但是可能一个导航又很多标签,如果这样重复的写NavLink也会造成很多的重复性的代码问题。

因此可以自定义一个NavLink,减少书写重复的样式:

import React, { Component } from 'react'
import {NavLink} from 'react-router-dom'

export default class MyNavLink extends Component {
	render() {
	// console.log(this.props);
      //<NavLink  to = {this.props.to} children = {this.props.children}/>
      // 通过{...对象}的形式解析对象,相当于将props对象中接收的属性全部展开
      //标签体内容props是特殊的一个属性,叫做children 
		return (
			<NavLink activeClassName="atguigu" className="list-group-item" {...this.props}/>
		)
	}
}

​ 在使用的时候:直接写每个标签中不一样的部分就行,比如路径和名称

{/*将NavLink进行封装,成为MyNavLink,通过props进行传参数*/}
import MyNavLink from './components/MyNavLink'
<MyNavLink to = "/about" >About</MyNavLink>

7. Switch的使用

  1. 通常情况下,path和component是一一对应的关系。
  2. Switch可以提高路由匹配效率(单一匹配)。
{/* 用Switch包裹只要匹配到了路径就会展示相应组件,不会再向下寻找 */}
{/* 如下代码只展示Home组件的内容,如果不用Switch组件包裹则相同路径组件都会展示*/}
<Switch>
        <Route path="/about" component={About}/>
        <Route path="/home" component={Home}/>
        <Route path="/home" component={Test}/>
</Switch>

8. 样式错误

拿上面的案例来说:

这里面会有一个样式:

image.png

此时,加载该样式的路径为:

image.png

但是在写路由的时候,你想在路由前面加个前缀,会形成多级目录(注意不是二级路由),

<MyNavLink to = "/cyk/about" >About</MyNavLink>

<Route path="/cyk/about"component={About}/>

这个时候就在刷新页面,就会出现问题:

样式因为路径问题加载失败,此时页面返回public下面的Index.html,因为它将前缀也当成了路径的一部分,导致路径错误,找不到相应文件

image.png

解决这个问题,有三个方法:

1.public/index.html中,样式加载使用绝对位置

 //<link rel="stylesheet" href="./css/bootstrap.css"> 相对路径
   <link rel="stylesheet" href="/css/bootstrap.css">

2.public/index.html中,使用 %PUBLIC_URL%代替 .

 <link rel="stylesheet" href="%PUBLIC_URL%/css/bootstrap.css" >

3.使用HashRouter

因为HashRouter会添加#,默认不会处理#后面的路径,所以也是可以解决的

9. 模糊匹配和精准匹配

react默认是开启模糊匹配的。

比如:

<MyNavLink to = "/home/a/b" >Home</MyNavLink>

此时该标签匹配的路由,分为三个部分 home a b;将会根据这个先后顺序匹配路由。

如下就可以匹配到相应的路由:

<Route path="/home"component={Home}/>

但是如果是下面这个就会失败,也就是说他是根据路径一级一级查询的,可以包含前面那一部分,但并不是只包含部分就可以。

<Route path="/a" component={Home}/>

当然也可以使用这个精确的匹配 exact={true}

如以下:这样就精确的匹配/home,则上面的/home/a/b就不行了

<Route exact={true}  path="/home" component={Home}/>
或者
<Route exact path="/home" component={Home}/>
  1. 默认使用的是模糊匹配(简单记:【输入的路径】必须包含要【匹配的路径】,且顺序要一致)
  2. 开启严格匹配:<Route exact={true} path="/about" component={About}/>
  3. 严格匹配不要随便开启,需要再开,有些时候开启会导致无法继续匹配二级路由

10. 初始化路由--Redirect(重定向)

在配置好路由,最开始打开页面的时候,应该是不会匹配到任意一个组件。这个时候页面就显得极其不合适,此时应该默认的匹配到一个组件。

image.png

此时就需要使用Redirect进行默认匹配了,一般写在所有路由注册的最下方,当所有路由都无法匹配时,跳转到Redirect指定的路由

<Switch>
    <Route path="/about" component={About}/>
    <Route path="/home" component={Home}/>
    //刚上来默认跳转到<Home/>组件
    <Redirect from = "/" to="/home"/>
</Switch>

就可以做到如下的效果:

image.png

再比如说判断是否登录:如果已登录显示用户信息,否则重定向的登录界面

export default class User extends PureComponent {

    state = {
      isLogin: true
    }

  render() {
    return this.state.isLogin ? (
      <div>
        <h2>User</h2>
      </div>
    ): <Redirect to="/login"/>
  }
}

11. 嵌套路由

  • 注册子路由时要写上父路由的path值
  • 路由的匹配是按照注册路由的顺序进行的 简单来说就是在一个路由组件中又使用了一个路由,就形成了嵌套路由。

举个例子来说:

输入图片说明 我们在home这个路由组件中又添加两个组件:

1636101886(1).png

APP.jsx:
<Switch>
    <Route path="/about" component={About}/>
    <Route path="/home" component={Home}/>
    <Redirect to="/about"/>
</Switch>
Home.jsx:
<div>
    <ul className="nav nav-tabs">
    <li>
    	<MyNavLink to = "/home/news">News</MyNavLink>
    </li>
    <li>
    	<MyNavLink  to = "/home/message">Message</MyNavLink>
    </li>
    </ul>
    
    <Switch>
        <Route path = "/home/news" component={News} />
        <Route path = "/home/message" component={Message} />
        <Redirect to="/home/message"/>
    </Switch>
</div>

react中路由的注册是有顺序的,因此在匹配的时候也是按照这个顺序进行的,因此会先匹配父组件中的路由

比如上面的 /home/news的路由处理过程:

1.因为父组件home的路由是先注册的,因此在匹配的时候先去找home的路由,也就是根据/home/news先模糊匹配到/home,如果在父组件使用精确匹配,则出现问题

2.在去Home组件里面去匹配相应的路由,从而找到了/home/news进行匹配,因此找到了News组件。

但是如果开启精确匹配,就会在第一步的时候卡住,这个时候就走不下去了。因此不要轻易的使用精确匹配

12. 向路由组件传递参数

输入图片说明

 1.params参数
        路由链接(携带参数):<Link to='/demo/test/tom/18'}>详情</Link>
        注册路由(声明接收):<Route path="/demo/test/:name/:age" component={Test}/>
        接收参数:const {需要接收的参数} = this.props.match.params
        备注:参数在地址栏中体现,刷新之后有效
 2.search参数(麻烦)
        路由链接(携带参数):<Link to='/demo/test?name=tom&age=18'}>详情</Link>
        注册路由(无需声明,正常注册即可):<Route path="/demo/test" component={Test}/>
        接收参数:this.props.location.search
        备注:
        1.获取到的search是urlencoded编码字符串,需要借助querystring解析,过时了
        2.参数在地址栏中体现,刷新之后有效
3.state参数
        路由链接(携带参数):<Link to={{pathname:'/demo/test',state:{name:'tom',age:18}}}>详情</Link>
        注册路由(无需声明,正常注册即可):<Route path="/demo/test" component={Test}/>
        接收参数:this.props.location.state
        备注:
        1. 参数不在地址栏中体现,但刷新也依然有效
        2. state可以直接接收一个对象
        <NavLink
          to={{
            pathname: "/detail3",
            search: "?name=abc",
            state: info,
          }}
          activeClassName="link-active"
        >详情3</NavLink>

1636114423(1).png

1.params参数

Message.jsx

import React, { Component } from 'react'
import {Link,Route} from 'react-router-dom'
import Detail from './Detail'

export default class Message extends Component {
state = {
messageArr:[
        {id:'01',title:'消息1'},
        {id:'02',title:'消息2'},
        {id:'03',title:'消息3'},
]
}
render() {
const {messageArr} = this.state
return (
    <div>
        <ul>
            {
                messageArr.map((msgObj)=>{
                    return (
                        <li key={msgObj.id}>

{/* 向路由组件传递params参数 */}
{/* <Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link> */}

{/* 向路由组件传递search参数 */}
{/* <Link to={`/home/message/detail/?id=${msgObj.id}&title=${msgObj.title}`}>{msgObj.title}</Link> */}

{/* 向路由组件传递state参数 */}
<Link to={{pathname:'/home/message/detail',state:{id:msgObj.id,title:msgObj.title}}}>{msgObj.title}</Link>

                        </li>
                    )
                })
            }
        </ul>
        <hr/>
{/* 声明接收params参数 */}
{/* <Route path="/home/message/detail/:id/:title" component={Detail}/> */}

{/* search参数无需声明接收,正常注册路由即可 */}
{/* <Route path="/home/message/detail" component={Detail}/> */}

{/* state参数无需声明接收,正常注册路由即可 */}
<Route path="/home/message/detail" component={Detail}/>

    </div>
)
}
}

Detail.jsx

import React, { Component } from 'react'

const DetailData = [
	{id:'01',content:'你好,中国'},
	{id:'02',content:'你好,尚硅谷'},
	{id:'03',content:'你好,未来的自己'}
]
export default class Detail extends Component {
	render() {
		console.log(this.props);
		// 接收params参数
		const {id,title} = this.props.match.params
		const findResult = DetailData.find((detailObj)=>{
			return detailObj.id === id
		})
		return (
			<ul>
				<li>ID:{id}</li>
				<li>TITLE:{title}</li>
				<li>CONTENT:{findResult.content}</li>
			</ul>
		)
	}
}

2.search参数(麻烦)

Detail.jsx

import React, { Component } from 'react'
// import {qs} from 'query-tring'  已废弃
import {qs} from 'url-parse' //要引入

const DetailData = [
    {id:'01',content:'你好,中国'},
    {id:'02',content:'你好,尚硅谷'},
    {id:'03',content:'你好,未来的自己'}
]
export default class Detail extends Component {
    render() {
            // console.log(this.props);
            // 接收search参数
            const {search} = this.props.location
            // console.log(search);
            // const {id,title} = qs.parse(search.slice(1)) 已废弃
            const {id,title} = qs.parse(search)

            const findResult = DetailData.find((detailObj)=>{
                    return detailObj.id === id
            })
            return (
                    <ul>
                        <li>ID:{id}</li>
                        <li>TITLE:{title}</li>
                        <li>CONTENT:{findResult.content}</li>
                    </ul>
            )
    }
}

3.state参数

Detail.jsx

import React, { Component } from 'react'

const DetailData = [
	{id:'01',content:'你好,中国'},
	{id:'02',content:'你好,尚硅谷'},
	{id:'03',content:'你好,未来的自己'}
]
export default class Detail extends Component {
	render() {
		console.log(this.props);

		// 接收state参数
		const {id,title} = this.props.location.state || {}

		const findResult = DetailData.find((detailObj)=>{
			return detailObj.id === id
		}) || {}
		return (
			<ul>
				<li>ID:{id}</li>
				<li>TITLE:{title}</li>
				<li>CONTENT:{findResult.content}</li>
			</ul>
		)
	}
}

13. push 与 replace 模式

默认情况下,开启的是 push 模式,也就是说,每次点击跳转,都会向栈中压入一个新的地址,在点击返回时,可以返回到上一个打开的地址,

当我们在读消息的时候,有时候我们可能会不喜欢这种繁琐的跳转,我们可以开启 replace 模式,这种模式与 push 模式不同,它会将当前地址替换成点击的地址,也就是替换了新的栈顶

我们只需要在需要开启的链接上加上replace={true}或者简写为replace

<Link replace to={{ pathname: '/home/message/detail', state: { id: msgObj.id, title: msgObj.title } }}>{msgObj.title}</Link>

14. 编程式路由导航

通过onClick函数调用的形式,将LinkNavLink换成了history上的API,其余部分没啥区别(传参,接收参数之类的)

借助this.prosp.history对象上的API对操作路由跳转、前进、后退
        -this.prosp.history.push()
        -this.prosp.history.replace()
        -this.prosp.history.goBack()
        -this.prosp.history.goForward()
        -this.prosp.history.go()

image.png Message

import React, { Component } from 'react'
import {Link,Route} from 'react-router-dom'
import Detail from './Detail'

export default class Message extends Component {
state = {
    messageArr:[
        {id:'01',title:'消息1'},
        {id:'02',title:'消息2'},
        {id:'03',title:'消息3'},
    ]
}

replaceShow = (id,title)=>{
//replace跳转+携带params参数
//this.props.history.replace(`/home/message/detail/${id}/${title}`)

//replace跳转+携带search参数
// this.props.history.replace(`/home/message/detail?id=${id}&title=${title}`)

//replace跳转+携带state参数
this.props.history.replace(`/home/message/detail`,{id:id,title:title})
}

pushShow = (id,title)=>{
//push跳转+携带params参数
// this.props.history.push(`/home/message/detail/${id}/${title}`)

//push跳转+携带search参数
// this.props.history.push(`/home/message/detail?id=${id}&title=${title}`)

//push跳转+携带state参数
this.props.history.push(`/home/message/detail`,{id:id,title:title})

}

back = ()=>{
this.props.history.goBack()
}

forward = ()=>{
this.props.history.goForward()
}

go = ()=>{
this.props.history.go(-2)
}

    render() {
        const {messageArr} = this.state
        return (
            <div>
            <ul>
            {
            messageArr.map((msgObj)=>{
            return (
            <li key={msgObj.id}>

                {/* 向路由组件传递params参数 */}
                {/* <Link to={`/home/message/detail/${msgObj.id}/${msgObj.title}`}>{msgObj.title}</Link> */}

                {/* 向路由组件传递search参数 */}
                {/* <Link to={`/home/message/detail/?id=${msgObj.id}&title=${msgObj.title}`}>{msgObj.title}</Link> */}

                {/* 向路由组件传递state参数 */}
                <Link to={{pathname:'/home/message/detail',state:{id:msgObj.id,title:msgObj.title}}}>{msgObj.title}</Link>

                &nbsp;<button onClick={()=> this.pushShow(msgObj.id,msgObj.title)}>push查看</button>
                &nbsp;<button onClick={()=> this.replaceShow(msgObj.id,msgObj.title)}>replace查看</button>
            </li>
            )
            })
            }
            </ul>
            <hr/>
            {/* 声明接收params参数 */}
            {/* <Route path="/home/message/detail/:id/:title" component={Detail}/> */}

            {/* search参数无需声明接收,正常注册路由即可 */}
            {/* <Route path="/home/message/detail" component={Detail}/> */}

            {/* state参数无需声明接收,正常注册路由即可 */}
            <Route path="/home/message/detail" component={Detail}/>

            <button onClick={this.back}>回退</button>&nbsp;
            <button onClick={this.forward}>前进</button>&nbsp;
            <button onClick={this.go}>go</button>
            </div>
        )
    }
}

15. withRouter

withRouter是专门用于解决在一般组件中使用路由组建API的问题

当我们需要在页面内部添加回退前进等按钮时,由于这些组件我们一般通过一般组件的方式去编写,因此我们会遇到一个问题,无法获得 history 对象,这正是因为我们采用的是一般组件造成的。只有路由组件才能获取到 history 对象

因此我们需要如何解决这个问题呢

我们可以利用 react-router-dom 对象下的 withRouter 函数来对我们导出的 Header 组件进行包装,这样我们就能获得一个拥有 history 对象的一般组件

我们需要对哪个组件包装就在哪个组件下引入

import React, { Component } from 'react'
import {withRouter} from 'react-router-dom' //引入withRouter

class Header extends Component {

back = ()=>{
        this.props.history.goBack()
}

render() {
    console.log('Header组件收到的props是',this.props);
    return (
        <div className="page-header">
            <h2>React Router Demo</h2>
            <button onClick={this.back}>回退</button>&nbsp;
        </div>
    )
}
}

export default withRouter(Header)

//withRouter可以加工一般组件,让一般组件具备路由组件所特有的API
//withRouter的返回值是带着路由组件所特有的API的新组件

16. BrowserRouter 和 HashRouter 的区别

1.底层原理不一样:
        BrowserRouter使用的是H5的history API,不兼容IE9及以下版本。
        HashRouter使用的是URL的哈希值。
2.path表现形式不一样
        BrowserRouter的路径中没有#,例如:localhost:3000/demo/test
        HashRouter的路径包含#,例如:localhost:3000/#/demo/test
3.刷新后对路由state参数的影响
        (1).BrowserRouter没有任何影响,因为state保存在history对象中。
        (2).HashRouter刷新后会导致路由state参数的丢失!!!
4.备注:HashRouter可以用于解决一些路径错误相关的问题。

17. hash与history实现原理

hash

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>

  <div id="app">
    <a href="#/home">首页</a>
    <a href="#/about">关于</a>

    <div class="router-view"></div>
  </div>
  
  <script>
    // 获取router-view的DOM
    const routerViewEl = document.getElementsByClassName("router-view")[0];

    // 监听URL的改变
    window.addEventListener("hashchange", () => {
      switch (location.hash) {
        case "#/home":
          routerViewEl.innerHTML = "首页";
          break;
        case "#/about":
          routerViewEl.innerHTML = "关于";
          break;
        default:
          routerViewEl.innerHTML = "";
      }
    })
  </script>
</body>
</html>

history

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>

  <div id="app">
    <a href="/home">首页</a>
    <a href="/about">关于</a>

    <div class="router-view"></div>
  </div>

  <script>
    // 1.获取router-view的DOM
    const routerViewEl = document.getElementsByClassName("router-view")[0];

    // 获取所有的a元素, 自己来监听a元素的改变
    const aEls = document.getElementsByTagName("a");
    for (let el of aEls) {
      el.addEventListener("click", e => {
        e.preventDefault();
        const href = el.getAttribute("href");
        history.pushState({}, "", href);
        urlChange();
      })
    }

    // 执行返回操作时, 依然来到urlChange
    window.addEventListener('popstate',urlChange);

    // 监听URL的改变
    function urlChange() {
      switch (location.pathname) {
        case "/home":
          routerViewEl.innerHTML = "首页";
          break;
        case "/about":
          routerViewEl.innerHTML = "关于";
          break;
        default:
          routerViewEl.innerHTML = "";
      }
    }。。
  </script>
</body>
</html>

18. react-router-config--路由统一配置

目前我们所有的路由定义都是直接使用Route组件,并且添加属性来完成的。 但是这样的方式会让路由变得非常混乱,我们希望将所有的路由配置放到一个地方进行集中管理: 这个时候可以使用react-router-config来完成

  • 安装react-router-config :yarn add react-router-config
  • 配置路由映射的关系数组
  • 使用renderRoutes函数完成配置 在src目录下新建router文件夹

router/index.js

import Home from '../pages/home';
import About, { AboutHisotry, AboutCulture, AboutContact, AboutJoin } from '../pages/about';
import Profile from '../pages/profile';
import User from '../pages/user';

const routes = [
  {
    path: "/",
    exact: true,
    component: Home
  },
  {
    path: "/about",
    component: About,
    routes: [
      {
        path: "/about",
        exact: true,
        component: AboutHisotry
      },
      {
        path: "/about/culture",
        component: AboutCulture
      },
      {
        path: "/about/contact",
        component: AboutContact
      },
      {
        path: "/about/join",
        component: AboutJoin
      },
    ]
  },
  {
    path: "/profile",
    component: Profile
  },
  {
    path: "/user",
    component: User
  }
]

export default routes;

在App中使用react-router-config

import React, { PureComponent } from "react";
import { renderRoutes } from "react-router-config";
//导入routes
import routes from "./router";

import {
  NavLink,
  withRouter,
} from "react-router-dom";

import "./App.css";

class App extends PureComponent {
  
  render() {
    return (
      <div>
      
        <NavLink exact to="/" activeClassName="link-active">
          首页
        </NavLink>
        <NavLink to="/about" activeClassName="link-active">
          关于
        </NavLink>
        <NavLink to="/profile" activeClassName="link-active">
          我的
        </NavLink>
        <NavLink to="/user" activeClassName="link-active">
          用户
        </NavLink>
     
      {/* 
        <Switch>
          <Route exact path="/" component={Home} />
          <Route path="/about" component={About} />
          <Route path="/profile" component={Profile} />
          <Route path="/user" component={User} />
        </Switch> 
     */}
        
        {renderRoutes(routes)}
      </div>
    );
  }

}

export default withRouter(App);

在子路由中使用react-router-config

注意:在子路由组件中不能导入全部的routes,只需要当前组件下需要的routes

import React, { PureComponent } from "react";
import { NavLink, Switch, Route } from "react-router-dom";
import { renderRoutes, matchRoutes } from "react-router-config";

export function AboutHisotry(props) {
  return <h2>企业成立于2000年, 拥有悠久的历史文化</h2>;
}

export function AboutCulture(props) {
  return <h2>创新/发展/共赢</h2>;
}

export function AboutContact(props) {
  return <h2>联系电话: 020-68888888</h2>;
}

export function AboutJoin(props) {
  return <h2>投递简历到aaaa@123.com</h2>;
}

export default class About extends PureComponent {
  render() {
    
// 如果组件通过renderRoutes(routes)方式渲染,则会有一个route属性,可以得到当前点击路由的子路由
    console.log(this.props.route);
 
   // matchRoutes返回所有你匹配到的route,如下,返回this.props.route.routes中/about
   // const branch = matchRoutes(this.props.route.routes, "/about");
   //console.log(branch);

    return (
      <div>
        <NavLink exact to="/about" activeClassName="about-active">
          企业历史
        </NavLink>
        <NavLink exact to="/about/culture" activeClassName="about-active">
          企业文化
        </NavLink>
        <NavLink exact to="/about/contact" activeClassName="about-active">
          联系我们
        </NavLink>
        <button onClick={(e) => this.jumpToJoin()}>加入我们</button>

        {/* <Switch>
          <Route exact path="/about" component={AboutHisotry}/>
          <Route path="/about/culture" component={AboutCulture}/>
          <Route path="/about/contact" component={AboutContact}/>
          <Route path="/about/join" component={AboutJoin}/>
        </Switch>
        */}

        {renderRoutes(this.props.route.routes)}
      </div>
    );
  }
}

四、 流行的开源 React UI组件库

1. material-ui(国外)

1. 官网: www.material-ui.com/#/

2. github: github.com/callemall/m…

2. ant-design(国内蚂蚁金服)

1. 官网: ant.design/index-cn

2. Github: github.com/ant-design/…

antd的按需引入+自定主题

3.x版本按需引入:
1.安装依赖:yarn add react-app-rewired customize-cra babel-plugin-import 
yarn add less@3.12.2\
yarn add less-loader@7.1.0
2.修改package.json
        ....
            "scripts": {
                    "start": "react-app-rewired start",
                    "build": "react-app-rewired build",
                    "test": "react-app-rewired test",
                    "eject": "react-scripts eject"
            },
        ....
3.根目录下创建config-overrides.js
//配置具体的修改规则
//组件按需加载
const { override, fixBabelImports } = require('customize-cra');

module.exports = override(
    fixBabelImports('import', {
        libraryName: 'antd',
        libraryDirectory: 'es',
        style: 'css',
    }),
);
//组件按需加载并且自定义主题
const { override, fixBabelImports,addLessLoader} = require('customize-cra');
module.exports = override(
        fixBabelImports('import', {
                libraryName: 'antd',
                libraryDirectory: 'es',
                style: true,
        }),
        addLessLoader({
                lessOptions:{
                        javascriptEnabled: true,
                        modifyVars: { '@primary-color': 'green' },
                }
        }),
);
4.备注:不用在组件里亲自引入样式了,即:import 'antd/dist/antd.css'应该删掉,这是全部引入
4.x版本css按需引入:github.com/umijs/babel…

实例:

import React, { Component } from 'react'
import { Button,DatePicker } from 'antd';
// import 'antd/dist/antd.css' 全部引入,若配置按需引入则删除
import {WechatOutlined,WeiboOutlined,SearchOutlined} from '@ant-design/icons'
const { RangePicker } = DatePicker;

export default class App extends Component {
	render() {
		return (
			<div>
				App....
				<button>点我</button>
				<Button type="primary">按钮1</Button>
				<Button >按钮2</Button>
				<Button type="link">按钮3</Button>
				<WechatOutlined />
				<Button type="primary" icon={<SearchOutlined />}>
					Search
				</Button>
				<WechatOutlined />
				<WeiboOutlined />
				<DatePicker/>
				<RangePicker/>
			</div>
		)
	}
}
Craco的使用步骤
  • 第一步:安装craco: yarn add @craco/craco
  • 第二步:修改package.json文件
    1. 原本启动时,我们是通过react-scripts来管理的;
    2. 现在启动时,我们通过craco来管理;
 "scripts": { 
 - "start": "react-scripts start",
 - "build": "react-scripts build",
 - "test": "react-scripts test", 
   "eject": "react-scripts eject",
 + "start": "craco start",
 + "build": "craco build", 
 + "test": "craco test",
 }
  • 第三步:在根目录下创建craco.config.js文件用于修改默认配置
 module.exports = { 
    // 配置文件 
 }
配置主题

按照配置主题的要求,自定义主题需要用到类似less-loader提供的 less 变量覆盖功能: 首先把 src/App.css 文件修改为 src/App.less,然后修改样式引用为 less 文件。

/* src/App.js */
- import './App.css';
+ import './App.less';
/* src/App.less */
- @import '~antd/dist/antd.css';
+ @import '~antd/dist/antd.less';

我们可以引入 craco-less 来帮助加载 less 样式和修改变量;

安装 craco-less: yarn add craco-less

修改craco.config.js中的plugins: 使用modifyVars可以在运行时修改LESS变量;

const CracoLessPlugin = require('craco-less');

module.exports = {
  plugins: [
    {
      plugin: CracoLessPlugin,
      options: {
        lessLoaderOptions: {
          lessOptions: {
            modifyVars: { '@primary-color': '#1DA57A' },
            javascriptEnabled: true,
          },
        },
      },
    }
  ],
}

引入antd的样式时,引入antd.less文件: import 'antd/dist/antd.less';

修改后重启 yarn start,如果看到一个绿色的按钮就说明配置成功了。

配置别名

在项目开发中,某些组件或者文件的层级会较深,

  • 如果我们通过上层目录去引入就会出现这样的情况:../../../../components/button;
  • 如果我们可以配置别名,就可以直接从根目录下面开始查找文件:@/components/button,甚至是:components/button; 配置别名也需要修改webpack的配置,当然我们也可以借助于 craco 来完成
const path = require("path");
//dir为当前文件(craco-根路径)路径,__dirname为下方resolve()传递过来的路径,然后拼接
const resolve = dir => path.resolve(__dirname, dir);

module.exports = {
  plugins: [
    //...
  ],
  webpack: {
    alias: {
      "@": resolve("src"),
      "components": resolve("src/components")
    }
  }
}

在导入时就可以按照下面的方式来使用了:

import HYCommentInput from '@/components/comment-input';
import HYCommentItem from 'components/comment-item';

案例

让moment显示时间为中文--导入国际化文件;

简体:import "moment/locale/zh-cn";

繁体:import "moment/locale/zh-hk";

1640751154(1).png 1640751282(1).png

//App.js

import React, { PureComponent } from "react";

import CommentInput from "./components/CommentInput";
import CommentItem from "./components/CommentItem";

export default class App extends PureComponent {
  state = {
    commentList: [],
  };

  render() {
    return (
      <div style={{ width: "500px", padding: "20px" }}>
        {this.state.commentList.map((item, index) => {
          return (
            <CommentItem
              key={item.id}
              comment={item}
              //removeItem={(e) => this.removeComment(index)}
              removeItem={(e) => this.removeComment(item.id)}
            />
          );
        })}
        <CommentInput submitComment={this.submitComment} />
      </div>
    );
  }

//子调父
  submitComment = (info) => {
    console.log(info);
    this.setState({
      commentList: [...this.state.commentList, info],
    });
  };
//子调父
  removeComment(index) {
    // const newCommentList = [...this.state.commentList];
    // newCommentList.splice(index, 1);
    // this.setState({
    //   commentList: newCommentList,
    // });

    this.setState({
      commentList: this.state.commentList.filter((item) => item.id !== index),
    });
  }
}

//CommentInput.js

import React, { PureComponent } from "react";

import moment from "moment";

import { Input, Button } from "antd";

export default class CommentInput extends PureComponent {
  state = {
    content: "",
  };

  render() {
    return (
      <div>
        <Input.TextArea
          rows={4}
          value={this.state.content}
          onChange={(e) => this.handleChange(e)}
        />
        <Button type="primary" onClick={(e) => this.addComment()}>
          添加评论
        </Button>
      </div>
    );
  }

  handleChange(event) {
    this.setState({
      content: event.target.value,
    });
  }

  addComment() {
    const commentInfo = {
      id: moment().valueOf(),
      avatar:
        "https://upload.jianshu.io/users/upload_avatars/1102036/c3628b478f06.jpeg?imageMogr2/auto-orient/strip|imageView2/1/w/240/h/240",
      nickname: "zgc",
      datetime: moment(),
      content: this.state.content,
      comments: [],
    };

    this.props.submitComment(commentInfo);

    this.setState({
      content: "",
    });
  }
}

//CommentItem.js

import React, { PureComponent } from "react";

import { Comment, Avatar, Tooltip } from "antd";
import { DeleteOutlined } from "@ant-design/icons";

export default class CommentItem extends PureComponent {
  render() {
    const { nickname, avatar, content, datetime } = this.props.comment;

    return (
      <Comment
        author={<a href="/#">{nickname}</a>}
        avatar={<Avatar src={avatar} alt={nickname} />}
        content={<p>{content}</p>}
        datetime={
          <Tooltip title={datetime.format("YYYY-MM-DD HH:MM:ss")}>
            <span>{datetime.fromNow()}</span>
          </Tooltip>
        }
        actions={[
          <span onClick={(e) => this.removeItem()}>
            <DeleteOutlined />
            删除
          </span>,
        ]}
      />
    );
  }

  removeItem() {
    this.props.removeItem();
  }
}