React三原理和路由

代码下载

React 组件通讯原理

setState() 说明

setState() 是异步更新数据的,使用该语法时,后面的 setState() 不要依赖于前面的 setState(),可以多次调用 setState() ,只会触发一次重新渲染:

    this.setState({ count: this.state.count + 1 })
    console.log('count: ', this.state.count);
    this.setState({ count: this.state.count + 1 })
    console.log('count: ', this.state.count);

推荐使用 setState((state, props) => {}) 语法,参数state表示最新的state,参数props表示最新的props:

    this.setState((state, props) => {
      console.log('第一次 count: ', state.count);
      return { count: state.count + 1 }
    })
    this.setState((state, props) => {
      console.log('第二次 count: ', state.count);
      return { count: state.count + 1 }
    })
    console.log('count: ', this.state.count);

第二个参数在状态更新(页面完成重新渲染)后立即执行某个操作,语法 setState(updater[, callback])

    this.setState((state, props) => {
      return { count: state.count + 1 }
    }, () => {
      console.log('count: ', this.state.count);
    })

JSX 语法的转化过程

JSX 仅仅是 createElement() 方法的语法糖(简化语法),JSX 语法被 @babel/preset-react 插件编译为 createElement() 方法。React 元素是一个对象,用来描述你希望在屏幕上看到的内容:

组件更新机制

setState() 的两个作用: 1. 修改 state 2. 更新组件(UI)

过程:父组件重新渲染时,也会重新渲染子组件。但只会渲染当前组件子树(当前组件及其所有子组件)

// 组件更新机制
class UpdateCom extends React.Component {
  state = { color: 'skyblue'}
  render() {
    console.log('根组件 render');
    return (
      <div className='root' style={{ backgroundColor: this.state.color}}>
        <p>根组件</p>
        <button onClick={this.changeColor}>变色</button>
        <div className='rootContainer'>
          <LeftCom></LeftCom>
          <RightCom></RightCom>
        </div>
      </div>
    )
  }
  getColor = () => Math.floor(Math.random() * 256)
  changeColor = () => {
    this.setState({ color: `rgb(${this.getColor()}, ${this.getColor()}, ${this.getColor()})` })
  }
}
class LeftCom extends React.Component {
  state = { count: 0 }
  render() {
    console.log('左父组件 render');
    return (
      <div className='leftParent'>
        <p>左父组件 count: {this.state.count}</p>
        <button onClick={() => this.setState({count: this.state.count + 1})}>+1</button>
        <div className='leftParentContainer'>
          <LeftChildOne></LeftChildOne>
          <LeftChildTwo></LeftChildTwo>
        </div>
      </div>
    )
  }
}
class RightCom extends React.Component {
  state = { count: 0 }
  render() {
    console.log('右父组件 render');
    return (
      <div className='rightParent'>
        <p>右父组件 count: {this.state.count}</p>
        <button onClick={() => { this.setState({count: this.state.count + 1}) }}>+1</button>
        <div className='rightParentContainer'>
          <RightChildOne></RightChildOne>
          <RightChildTwo></RightChildTwo>
        </div>
      </div>
    )
  }
}
const LeftChildOne = () => {
  console.log('左子组件1 render');
   return (<div className='leftChildOne'>左子组件1</div>)
  }
const LeftChildTwo = () => { 
  console.log('左子组件2 render');
  return (<div className='leftChildTwo'>左子组件2</div>)
}
const RightChildOne = () => { 
  console.log('右子组件1 render');
  return (<div className='rightChildOne'>右子组件1</div>)
}
const RightChildTwo = () => {
  console.log('右子组件2 render');
  return (<div className='rightChildTwo'>右子组件2</div>)
}
ReactDOM.createRoot(document.getElementById('updateCom')).render(<UpdateCom></UpdateCom>)

css:

  width: 800px;
}
.rootContainer, .leftParentContainer, .rightParentContainer {
  display: flex;
}
.leftParent {
  flex: 1;
  background-color: green;
}
.leftParentContainer > div {
  flex: 1;
  background-color: blue;
  margin: 10px;
}
.rightParent {
  flex: 1;
  background-color: cyan;
}
.rightParentContainer > div {
  flex: 1;
  background-color: pink;
  margin: 10px;
}

组件性能优化

1、 减轻 state,state 只存储跟组件渲染相关的数据(比如:count / 列表数据 / loading 等)

注意:不用做渲染的数据不要放在 state 中,比如定时器 id等,对于这种需要在多个方法中用到的数据,应该放在 this 中。

2、避免不必要的重新渲染,父组件更新会引起子组件也被更新,这种思路很清晰

问题:子组件没有任何变化时也会重新渲染?如何避免不必要的重新渲染呢?

解决方式:使用钩子函数 shouldComponentUpdate(nextProps, nextState),通过返回值决定该组件是否重新渲染,返回 true 表示重新渲染,false 表示不重新渲染。更新阶段的钩子函数,组件重新渲染前执行 (shouldComponentUpdate -> render)。

class NumCom extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    console.log('props: ', this.props, 'nextProps: ', nextProps);
    // 根据 props 数据是否变化,决定渲染
    return this.props.num !== nextProps.num
  }
  render() {
    console.log('NumCom render');
    return (<><p>随机数:{this.props.num}</p></>)
  }
}
class RefreshCom extends React.Component {
  state = { num: 1 }
  shouldComponentUpdate(nextProps, nextState) {
    console.log('state: ', this.state, 'nextState: ', nextState);
    // 根据 state 数据是否变化,决定渲染
    return this.state.num !== nextState.num
  }
  render() {
    console.log('RefreshCom render');
    return (
      <>
        <h4>避免不必要的重新渲染</h4>
        <NumCom num={this.state.num}></NumCom>
        <button onClick={() => this.setState({ num: Math.ceil(Math.random()*3)})}>生成随机数</button>
      </>
    )
  }
}

3、纯组件,PureComponent 与 React.Component 功能相似,PureComponent 内部自动实现了 shouldComponentUpdate 钩子,不需要手动比较

原理:纯组件内部通过分别 对比 前后两次 props 和 state 的值,来决定是否重新渲染组件。纯组件内部的对比是 shallow compare(浅层对比),对于值类型来说比较两个值是否相同(直接赋值即可,没有坑);对于引用类型来说只比较对象的引用(地址)是否相同。注意 state 或 props 中属性值为引用类型时,应该创建新数据,不要直接修改原数据!

class PureNum extends React.PureComponent {
  render() {
    console.log('PureNum render');
    return (<><p>随机数:{this.props.num}</p></>)
  }
}
class PureObjNum extends React.PureComponent {
  render() {
    console.log('PureObjNum render');
    return (<><p>随机数:{this.props.numObj.num}</p></>)
  }
}
class PureCom extends React.PureComponent {
  state = {
    num: 1,
    numObj: { num: 1 }
  }
  render() {
    console.log('PureCom render');
    return (
      <>
        <h4>纯组件</h4>
        <PureNum num={this.state.num}></PureNum>
        <button onClick={this.handle}>生成随机数</button>
        <PureObjNum numObj={this.state.numObj}></PureObjNum>
        <button onClick={this.handleOne}>生成随机数对象(修改原对象)</button> <br></br>
        <button onClick={this.handleTwo}>生成随机数对象(创建新对象)</button>
      </>
    )
  }
  handle = () => {
    const number = Math.ceil(Math.random()*3)
    console.log('num: ', this.state.num, ', new num: ', number);
    this.setState({ num: number })
  }
  handleOne = () => {
    // 修改原对象,错误,因为不会刷新
    const number = Math.ceil(Math.random()*3)
    console.log('num: ', this.state.numObj.num, ', new num: ', number);
    const obj = this.state.numObj
    obj.num = number
    this.setState({ numObj: obj})
  }
  handleTwo = () => {
    // 创建新对象,正确,但是也得判断,否则每次都会刷新
    const number = Math.ceil(Math.random()*3)
    console.log('num: ', this.state.numObj.num, ', new num: ', number);
    if (number !== this.state.numObj.num) {
      const obj = {...this.state.numObj, num: number}
      this.setState({ numObj: obj})
    }
  }
}

虚拟DOM和Diff算法

React 更新视图的思想是:只要 state 变化就重新渲染视图,特点就是思路非常清晰。

问题:组件中只有一个 DOM 元素需要更新时,也得把整个组件的内容重新渲染到页面中?答案是否定的。理想状态是部分更新,只更新变化的地方。那么 React 是如何做到部分更新的?虚拟 DOM 配合 Diff 算法。

虚拟 DOM 本质上就是一个 JS 对象,用来描述你希望在屏幕上看到的内容(UI):

执行过程

  1. 初次渲染时,React 会根据初始state(Model),创建一个虚拟 DOM 对象(树)。
  2. 根据虚拟 DOM 生成真正的 DOM,渲染到页面中。
  3. 当数据变化后(setState()),重新根据新的数据,创建新的虚拟DOM对象(树)。
  4. 与上一次得到的虚拟 DOM 对象,使用 Diff 算法 对比(找不同),得到需要更新的内容。
  5. 最终,React 只将变化的内容更新(patch)到 DOM 中,重新渲染到页面。

虚拟DOM最大的特点是 脱离了浏览器的束缚,也就是意味着只要是能支持js的地方都可以用到react,所以为什么说react是可以进行跨平台的开发。

render 方法调用并不意味着浏览器中的重新渲染,render 方法调用仅仅说明创建了新的虚拟 DOM 对象(树)要进行diff 算法。

React 路由

现代的前端应用大多都是 SPA(单页应用程序),也就是只有一个 HTML 页面的应用程序。因为它的用户体
验更好、对服务器的压力更小,所以更受欢迎。为了有效的使用单个页面来管理原来多页面的功能,前端路由
应运而生。

  • 前端路由的功能是让用户从一个视图(页面)导航到另一个视图(页面)
  • 前端路由是一套映射规则,在React中,是 URL路径 与 组件 的对应关系
  • 使用React路由简单来说,就是配置 路径和组件(配对)

使用步骤

1、安装,在项目根目录执行 npm i react-router-domyarn add react-router-dom

2、导入路由的5个核心组件:Router、Routes、Route、Link 或 NavLink

import { BrowserRouter as Router, Routes, Route, Link, NavLink } from 'react-router-dom'

3、使用 Router 组件包裹整个应用(重要)

4、路由的本质就是映射关系,用 Routes(就是router5里的Switch组件) 组件去盛放这层路由映射关系

5、使用 Link 或 NavLink 组件作为导航菜单(路由入口),这两个组件主要包含两个属性:to属性和replace属性。

  • to属性: 用来设置跳转到哪个路径,相当于是push操作;
  • replace属性:和to类似,也会跳转到目标路径,但其执行的是replace操作,设置了replace属性会把路由栈里当前路由替换掉

6、使用 Route 组件配置路由规则和要展示的组件(路由出口),注意 Route 组件必须包裹在 Routes 组件内

  • path属性:用于设置匹配到的路径
  • element属性:设置匹配到路径后要渲染的组件;(在Router5里是使用component属性)
// 使用步骤
const Home = () => (<p>首页</p>)
const First = () => (<p>页面一</p>)
const Second = () => (<p>页面二</p>)
const NotFound = () => (<p>没有发现该内容</p>)

const RouterStep = () => {
  return (
    <>
      <Router>
        <p>Link</p>
        <Link to='/home'>首页</Link>
        <Link to='/first'>页面一</Link> 
        <Link to='/second' replace={true}>页面二</Link>
        <Link to='/xxx'>NotFound</Link>
        <p>NavLink</p>
        <NavLink to='/first' style={({isActive}) => { return isActive ? { color: 'blue' } : {} }}>页面一</NavLink>
        <NavLink to='/second' className={({isActive}) => { return isActive ? 'selected smoll' : ''}}>页面二</NavLink>
        <Routes>
          <Route path='/home' element={<Home></Home>}></Route>
          <Route path='/first' element={<First></First>}></Route>
          <Route path='/second' element={<Second></Second>}></Route>
          <Route path='*' element={<NotFound></NotFound>}></Route>
        </Routes>
      </Router>
    </>
  )
}

说明:
1、Router 组件包裹整个应用,一个 React 应用只需要使用一次

2、两种常用 Router:HashRouter 和 BrowserRouter

  • HashRouter:使用 URL 的哈希值实现(localhost:3000/#/first)
  • (推荐)BrowserRouter:使用 H5 的 history API 实现(localhost:3000/first)

3、Link 或 NavLink 组件:用于指定导航链接(a标签)。最终Link会编译成a标签,而to属性会被编译成 a标签的href属性。

4、Route 组件指定路由展示组件相关信息,Route组件写在哪,渲染出来的组件就展示在哪

  • path属性:路由规则
  • element属性:展示的 React 元素

5、NavLink就是Link组件的样式增强版,它与Link的用法基本相同,只不过就是多了几个属性进行设置样式,并且当前选中的NavLink组件上会多出一个active类名

  • style 属性:接收一个函数,函数接收一个对象,包含isActive属性,表示当前是否被选中;
  • className 属性:接收一个函数,函数接收一个对象,包含isActive属性,表示当前是否被选中

6、通配符,在router6中,支持如下的几种通配符:

  • /xxx 确定的路径名,如 : /home 表示home页面组件能匹配上路径只能是 /home ;
  • /xxx/:xxx 动态路径名,:xxx会动态变化的 。如:/home/:id 表示home页面能匹配上 /home/11、/home/22、/home/abc、/home/xxx 等路径;
  • /xxx/:xxx/xxx动态路径名后跟确定路径,如: /home/:id/abc 表示home页面能匹配上 /home/11/abc、/home/22/abc 、/home/aa/abc 等路径;
  • /xxx/* 确定路径名,后面可以跟多个子路径,如:/home/* 表示home页面能匹配上 /home、/home/12、/home/ab、/home/cd/123 等路径;
  • /xxx/:xxx/* 动态路径名后不限制子路径,如:/home/:id/* 表示home页面匹配 /home/11/abc/bcd、 /home/22/qwe 等路径;

默认路由

现在的路由都是点击导航菜单后展示的,如何在进入页面的时候就展示呢?

默认路由表示进入页面时就会匹配的路由,默认路由path为 /

{/* 默认路由 */}
<Route path='/' element={<home></home>}></Route>

路由重定向

使用Navigate 组件来实现路由的重定向,只要这个组件出现,就会执行重定向跳转,并跳到其对应的to路径中,导入组件:

import { Navigate } from 'react-router-dom';

使用Navigate也很简单,只要将想要被重定向的Route匹配关系中将element属性里的原组件替换成Navigate组件即可;至于想要被重定向到哪个路径,只需要将这个路径写入到Navigate组件中的to属性即可:

<Route path='/' element={<Navigate to='/home' replace></Navigate>}></Route>

路由的执行过程

  • 点击 Link 或 NavLink 组件(a标签),修改了浏览器地址栏中的 url 。
  • React 路由监听到地址栏 url 的变化。
  • React 路由内部遍历所有 Route 组件,使用路由规则( path )与 pathname 进行匹配。
  • 当路由规则(path)能够匹配地址栏中的 pathname 时,就展示该 Route 组件的内容。

匹配模式

  • 精确匹配:只有当 path 和 pathname 完全匹配时才会展示该路由
  • 模糊匹配:只要 pathname 以 path 开头就会匹配成功

在router6中,不必再特别去设置exact属性去进行精确匹配组件了,因为router6中已经帮内置进去了。

如果想模糊匹配某一部分,在路径后加 /*,增加一个匹配规则,将默认路由设置为模糊匹配:

        <Routes>
          <Route path='/*' element={<Home></Home>}></Route>
        </Routes>

路由懒加载

在路由中通常会定义很多不同的页面。如果不应用懒加载的话,很多页面都会打包到同一个js文件中,文件将会异常的大。造成进入首页时,需要加载的内容过多,时间过长,在浏览器中可能会出现短暂的空白页,从而降低用户体验,而运用路由懒加载是将各个模块分开打包,用户查看的时候再加载对应的模块,减少加载用时。

懒加载就是延迟加载,也即在需要的时候才会进行加载所需组件。

在react中借助React.lazy方法来进行懒加载,用法如下 React.lazy(() => import("懒加载组件的路径"))

1、定义一个单文件组件:

import React from "react";

export default class LazyOne extends React.Component {
    render() {
        return (
            <>
                <p>第一个懒加载组件</p>
            </>
        )
    }
}

2、采用lazy懒加载的形式加载组件

  const LazyOne = React.lazy(() => import('./LazyOne'))

3、增加路由匹配规则

<Route path='/lazyOne' element={<LazyOne></LazyOne>}></Route>

4、导航懒加载组件路由

<Link to='/lazyOne'>懒加载一</Link>

5、此时点击路径跳转,控制台会报错,这是因为当我们是用lazy去懒加载时,应该同时引入Suspense去包裹懒加载的组件。也就是说,lazy处理的懒加载组件一定要处于Suspense组件包裹下。

      <React.Suspense>
        <Router>
          <Link to='/lazyOne'>懒加载一</Link>
          <Routes>
            <Route path='/lazyOne' element={<LazyOne></LazyOne>}></Route>
            <Route path='*' element={<NotFound></NotFound>}></Route>
          </Routes>
        </Router>
      </React.Suspense>

引入Suspense之后,再去点击跳转控制台就不会再报错了。

6、当网络不好的时候进行页面跳转,会出现长时间的白屏现象。可以利用Suspense组件的fallback属性来进行改善用户体感,将fallback的值设置为antd的Spin组件:

      <React.Suspense fallback={<Spin></Spin>}>
        <Router>
          <Link to='/lazyOne'>懒加载一</Link>
          <Routes>
            <Route path='/lazyOne' element={<LazyOne></LazyOne>}></Route>
            <Route path='*' element={<NotFound></NotFound>}></Route>
          </Routes>
        </Router>
      </React.Suspense>

fallback是Suspense组件的属性,其作用是当lazy处理的懒加载组件还没有加载出来时要显示的内容。一般地都会往fallback属性里传入一个loading动画,用来缓解加载白屏的问题。

当组件处于加载状态时,fallback组件是替换了整个 Suspense 组件包裹的内容,此时只显示了Spin组件。但实际上在路由切换的时候是不应该全部被 Suspense 的 fallback 替代的,因为路由切换时有的部分是不变的。造成此部分也被替换的原因是,Suspense 组件包裹的范围太广了。按照正常逻辑只要动态加载的那部分被替换,其余不变的部分就仍然展示。对此需要缩小 Suspense 组件的包裹范围,只让它包裹住懒加载的组件即可。这时候可以使用 高阶组件 或 render-props来配合处理懒加载组件:

// 高阶组件
const withLoading = (WrapedComponent) => {
  class LoadingCom extends React.Component {
    render() {
      return (<React.Suspense fallback={<Spin></Spin>}><WrapedComponent {...this.props}></WrapedComponent></React.Suspense>)
    }
  }
  LoadingCom.displayName = `WithLoading${getDisplayName(WrapedComponent)}`
  return LoadingCom
}

// render-props 
function FuncLoading(props) {
  return (
    <React.Suspense fallback={<Spin></Spin>}>
      {props.children}
    </React.Suspense>
  )
}

const RouteLazy = () => {
  const LazyOne = React.lazy(() => import('./LazyOne'))
  const LazyTwo = React.lazy(() => import('./LazyTwo'))
  const LazyThree = React.lazy(() => import('./LazyThree'))
  const LoadingOne = withLoading(LazyTwo)
  return (
    <>
      <React.Suspense fallback={<Spin></Spin>}>
        <Router>
          <div>
            <Link to='/lazyOne'>懒加载一</Link>
            <Link to='/loadingOne'>懒加载二优化</Link>
            <Link to='/loadingTwo'>懒加载三优化</Link>
          </div>
          <Routes>
            <Route path='/lazyOne' element={<LazyOne></LazyOne>}></Route>
            <Route path='/loadingOne' element={<LoadingOne></LoadingOne>}></Route>
            <Route path='/loadingTwo' element={<FuncLoading><LazyThree></LazyThree></FuncLoading>}></Route>
            <Route path='*' element={<NotFound></NotFound>}></Route>
          </Routes>
        </Router>
      </React.Suspense>
    </>
  )
}
ReactDOM.createRoot(document.getElementById('routeLazy')).render(<RouteLazy></RouteLazy>)

其中 LazyTwo、LazyThree 组件与前面的 LazyOne 类似,略。

路由嵌套

在Router5中是将嵌套的子路由直接写在父路由对应的组件内部的。这样有个弊端就是Route太分散了,各个组件里都有Route。Router6.X对此做了一个调整,现在不用分散嵌套子路由,取而代之的是路由嵌套统一在一个 Routes 组件下维护。

想要子路由生效,必须还得在其所属的父路由里引入一个Outlet组件进行路由占位。想要让子路由在哪里展示,就把Outlet放到其内部即可:

import { BrowserRouter as Router, Routes, Route, Link, NavLink, Outlet } from 'react-router-dom'

const ContentOne = () => { return (<p>内容一</p>) }
const ContentTwo = () => { return (<p>内容二</p>) }
const ContentThree = () => { return (<p>内容三</p>) }
const NoContent = () => { return (<p>没有内容</p>) }
// 主体结构组件
const MainBody = () => {
  return (
    <div className='mainBody'>
      {/* 头部区域 */}
      <div className='top'>
        路由嵌套
        <Consumer>
          {(data) => <button onClick={data.logout}>退出</button>}
        </Consumer>
      </div>

      {/* 左边栏区域 */}
      <div className='leftAside'>
        左边栏
        <ul>
          <li><Link to='/'>内容一</Link></li>
          <li><Link to='/two'>内容二</Link></li>
          <li><Link to='/three'>内容三</Link></li>
        </ul>
      </div>
      
      {/* 内容区域 */}
      <div className='content'>
        内容
        <Outlet></Outlet>
      </div>

      {/* 底部区域 */}
      <div className='bottom'>底部区域</div>
    </div>
  )
}
// 登录组件
const Login = () => {
  return (
    <Consumer>
      {(data) => <button onClick={data.login}>登录</button>}
    </Consumer>
  )
}
class RouteNest extends React.Component {
  state = { isLogin: false }
  render() {
    const main = <Provider value={{ logout: () => this.setState({ isLogin: false }) }}><MainBody></MainBody></Provider>
    const login = <Provider value={{ login: () => this.setState({ isLogin: true }) }}><Login></Login></Provider>
    const first = (<>{this.state.isLogin ? main : login}</>)
    return (
      <Router>
        <Routes>
          {/* 父路由(默认) */}
          <Route path='/' element={first}>
            {/* 子路由 */}
            {/* 默认子路由 */}
            {/* <Route path='/' element={<ContentOne></ContentOne>}></Route> */}
            <Route index element={<ContentOne></ContentOne>}></Route>
            <Route path='/two' element={<ContentTwo></ContentTwo>}></Route>
            <Route path='/three' element={<ContentThree></ContentThree>}></Route>
          </Route>
          <Route path='/login' element={<Login></Login>}></Route>
          <Route path='*' element={<NotFound></NotFound>}></Route>
        </Routes>
      </Router>
    )
  }
}
ReactDOM.createRoot(document.getElementById('routeNest')).render(<RouteNest></RouteNest>)

如何设置默认显示的子路由?只需要在想要被设为默认展示的子路由上写一个 index属性即可(注意,被设置index的子路由不能再设置path属性),那么当访问 ‘/’ 就能做到默认展示子路由了,跳转路径也的相应调整为 <Link to='/'>内容一</Link>。也可以直接将默认子路由设置为 <Route path='/' element={<ContentOne></ContentOne>}></Route>

注意:路由嵌套时,子路由的path路径要以父路由的path名开头,否则会报错。

编程式导航

如果希望通过JS代码跳转,需要通过useNavigate获取到navigate对象对象然后进行后续操作。

import { useNavigate } from 'react-router-dom'

使用类组件开发则需要封装高阶函数,也即手动封装withRouter:

function withRouter(WrapedComponent) {
  function ComponentWithRouter(props) {
    const navigate = useNavigate()
    return <WrapedComponent {...props} router={navigate}></WrapedComponent>
  }
  ComponentWithRouter.displayName = `WithRouter${getDisplayName(WrapedComponent)}`

  return ComponentWithRouter
}

可以直接调用 useNavigate 函数去返回一个navigate,然后调用navigate并将跳转路径作为参数传进去,例如:navigate(‘url路径’),当的navigate传入数字时(一般是-1),是跳到路由栈当前路由前面几个对应的路由。 navigate里也可以传第二个options参数: {replace,state} 。replace参数代表替换当前路由栈中的路由,state代表路由传参:

function Red() { return (<div style={{ backgroundColor: 'red', width: '100px', height: '100px' }}></div>) }
function Green() { return (<div style={{ backgroundColor: 'green', width: '100px', height: '100px' }}></div>) }
// 类组件
class ClassComponent extends React.Component {
  render() { return (<>
    <p>类组件</p>
    <button onClick={() => this.props.router('/red')}>红</button>
    <button onClick={() => this.props.router('/green', { replace: true, state: { data: '数据' }})}>绿</button>
    <button onClick={() => this.props.router(-1)}>返回</button>
  </>) }
}
// 函数组件
function FuncComponent() {
  const navigate = useNavigate()
  return (<>
    <p>函数组件</p>
    <button onClick={() => navigate('/red')}>红</button>
    <button onClick={() => navigate('/green', { replace: true, state: { data: '数据' }})}>绿</button>
    <button onClick={() => navigate(-1)}>返回</button>
  </>)
}
function FuncRoute(props) {
  const WithRouterCom = withRouter(ClassComponent)
  return (
    <Router>
      <WithRouterCom></WithRouterCom>
      <FuncComponent></FuncComponent>
      <Routes>
        <Route path='/red' element={<Red></Red>}></Route>
        <Route path='/green' element={<Green></Green>}></Route>
        <Route path='*' element={<NotFound></NotFound>}></Route>
      </Routes>
    </Router>
  )
}
ReactDOM.createRoot(document.getElementById('funcRoute')).render(<FuncRoute></FuncRoute>)

路径如果以 / 开头代表 根路由+当前路径拼接成新路由; 不以 / 开头表示在当前路由后再跟上路径拼接成新路由路径。

navigate("/register") // 跳转后路径为 http://localhost:3000/register
navigate("register") // 跳转后路径为 http://localhost:3000/home/register

路由传参

路由传参一般用到三种方式:动态路径传参、search参数传递、state传递参数。

动态路径传参

如果将path在Route匹配时写成 /detail/:id 那么 /detail/abc/detail/123 都可以匹配到该Route对应的组件并进行显示,针对动态路径传参的这种方式,常用 useParams 来进行获取参数。

import { useParams } from 'react-router-dom'

function Detail() { 
  const params = useParams()
  let value = ''
  switch (params.id) {
    case '1':
      value = '一'
      break;
    case '2':
      value = '二'
      break
    default:
      break;
  }
  return (<p>详情{value}</p>) 
}

function ParamsCom() {
  const navigate = useNavigate()
  return (<>
    <p>动态路径传参</p>
    <Link to='/detail/1'>详情一</Link>
    <button onClick={() => navigate('/detail/2')}>去详情二</button>
  </>)
}

function RouteParams() {
  return (<Router>
    <ParamsCom></ParamsCom>
    <Routes>
      <Route path='/detail/:id' element={<Detail></Detail>}></Route>
      <Route path='*' element={<NotFound></NotFound>}></Route>
    </Routes>
  </Router>)
}
ReactDOM.createRoot(document.getElementById('routeParams')).render(<RouteParams></RouteParams>)

说明:动态路径可以配置多个参数,如 <Route path='/detail/:id/:lastId' element={<Detail></Detail>}></Route>

search传参

seatch 传递参数就是把我们要传递的参数拼接到url上进行传递,具体方式是以 ? 开头,键值对的形式传参,每个参数之间用 & 连接,如 http://localhost:3000/list?page=1&size=10

一般使用 useSearchParams 这个函数来获取 search 参数(也可用useLocation,不过得额外再处理下search参数),注意 useSearchParams 返回的是个数组,且数组里是一个 URLSearchParams 当前值和 set 方法。并且取值时常借助 Object.fromEntries 这个方法:

import { useSearchParams } from 'react-router-dom'

function User() {
  const [searchParams] = useSearchParams()
  const params = Object.fromEntries(searchParams)
  return (<p>id: {params.id}, name: {params.name}, age: {params.age}</p>)
}
function ParamsCom() {
  const navigate = useNavigate()
  return (<>
    <p>search传参</p>
    <Link to='/user?id=1&name=zhangsan&age=18'>用户一</Link>
    <button onClick={() => navigate('/user?id=2&name=lisi&age=19')}>去用户二</button>
  </>)
}
function RouteParams() {
  return (<Router>
    <ParamsCom></ParamsCom>
    <Routes>
      <Route path='/user' element={<User></User>}></Route>
      <Route path='*' element={<NotFound></NotFound>}></Route>
    </Routes>
  </Router>)
}
ReactDOM.createRoot(document.getElementById('routeParams')).render(<RouteParams></RouteParams>)

Object.entries() 是将对象转成一个自身可枚举属性的键值对数组,同样也可以把键值对数组转成了对象。Object.fromEntriesObject.entries 反向,Object.fromEntries() 方法把键值对列表转换为一个对象。

state传参

上面两种方式的传参都有一个缺点即传递的参数都在url路径上体现了,并且涉及到复杂类型的参数传递就显得很麻烦了。如果不想参数在url上面,且想传些对象时就可以采取这种state传参的方式。使用useLocation钩子来获取state参数。

import { useLocation } from 'react-router-dom'

function Content() {
  const { state } = useLocation()
  console.log('state: ', state);
  return (<p>{state.content}</p>)
}
function ParamsCom() {
  const navigate = useNavigate()
  return (<>
    <p>state传参</p>
    <Link to='/content' state={{content: '这是内容一的内容'}}>内容一</Link>
    <button onClick={() => navigate('/content', {state: { content: '这是内容二的内容' } })}>去内容二</button>
  </>)
}
function RouteParams() {
  return (<Router>
    <ParamsCom></ParamsCom>
    <Routes>
      <Route path='/content' element={<Content></Content>}></Route>
      <Route path='*' element={<NotFound></NotFound>}></Route>
    </Routes>
  </Router>)
}
ReactDOM.createRoot(document.getElementById('routeParams')).render(<RouteParams></RouteParams>)

useRoutes 配置化路由

前面都是用Route组件的方式来实现路由配置的,但现在都提倡配置化,能不能把routes相关的配置都维护到一串数据里,不用以组件-属性这种方式管理呢?

在 router5 中需引入的 react-router-config 这个三方库来帮维护。但在 Router6 官方给提供了 useRoutes 这个函数。凭此可以将 routes 对象以参数形式传入 useRoutes 中,此函数会根据 routes 里的匹配关系对应渲染成相关的路由组件。

routes其本身是一个数组,数组里维护各个匹配关系的对象。匹配关系这个对象里一般都有path、element、children、index属性:

  • path属性就是组件对应的路径;
  • element属性就是要对应的组件;
  • index属性就是默认要展示的页面组件;
  • children属性是路由嵌套时需要用也是一个数组,children数组里的属性和外层一样,子路由的配置就是在children属性里维护的。

useRoutes的使用只要将路由配置对象 routes 传到 useRoutes 函数即可:

// 军事新闻组件
const Junshi = () => { return (<p>军事新闻</p>) }
// 财经新闻组件
const Caijin = () => { return (<p>财经新闻</p>) }
// 体验新闻组件
const Tiyu = () => { return (<p>体育新闻</p>) }
// 布局组件
const Layout = () => {
  return (
    <div className='mainBody'>
      {/* 头部区域 */}
      <div className='top'>useRoutes 配置化路由</div>

      {/* 左边栏区域 */}
      <div className='leftAside'>
        左边栏
        <ul>
          <li><Link to='/'>军事</Link></li>
          <li><Link to='/caijin'>财经</Link></li>
          <li><Link to='/tiyu'>体育</Link></li>
        </ul>
      </div>

      {/* 内容区域 */}
      <div className='content'>
        内容
        <Outlet></Outlet>
      </div>

      {/* 底部区域 */}
      <div className='bottom'>底部区域</div>
    </div>
  )
}

// 路由配置对象
const routes = [
  {
    path: '/',
    element: <Layout></Layout>,
    children: [
      {
        index: true,
        element: <Junshi></Junshi>
      },
      {
        path: '/caijin',
        element: <Caijin></Caijin>
      },
      {
        path: '/tiyu',
        element: <Tiyu></Tiyu>
      }
    ]
  },
  {
    path: '*',
    element: <NotFound></NotFound>
  }
]
function RouteCom() {
  console.log('RouteCom render');
  
  // useRoutes 配置化路由
  return (<>{useRoutes(routes)}</>)
}
function ConfigRoute() {
  return (<Router><RouteCom></RouteCom></Router>)
}
ReactDOM.createRoot(document.getElementById('configRoute')).render(<ConfigRoute></ConfigRoute>)
useRoutes 的 rerender 问题

可以发现 useRoutes 配置化路由只要切换路由 RouteCom 组件就会重新渲染,而换成如下方式则不会重新渲染:

function RouteCom() {
  console.log('RouteCom render');

  return <Routes>
    <Route path='/' element={<Layout></Layout>}>
      <Route index element={<Junshi></Junshi>}></Route>
      <Route path='/caijin' element={<Caijin></Caijin>}></Route>
      <Route path='/tiyu' element={<Tiyu></Tiyu>}></Route>
    </Route>
    <Route path='*' element={<NotFound></NotFound>}></Route>
  </Routes>
}

由于 useRoutes 是通过 context 实现的,切换路由时 context 共享出来的 value 值发生了变化,从而使得使用到这个 context 的组件触发了 rerender,这也就造成了使用 useRoutes 的 RouteCom 组件在切换路由时重新渲染了!

  • 13
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值