react路由

559 阅读13分钟

路由

1.介绍

  1. Hash模式 :兼容性好,但是带#,上线部署无需考虑服务器配置问题
  2. History模式:兼容性差,但是地址栏无#,但是上线时服务器一定要部署

2.路由使用

1. 安装

npm i -S react-router-dom@5

2. 相关组件:

  • 路由模式组件:包裹整个应用,一个react只需使用一次

    HashRouter:hash BrowserRouter:history

  • 导航组件:Link ==><a></a>

    Link:不会有激活样式 NavLink:如果地址栏中的地址和to属性相匹配,则会有激活样式

  • 路由规则定义组件:指定路由规则和对应匹配成功后要渲染的组件

    Route:

    path属性:路由路径,在地址栏中访问的地址

    component属性:和规则匹配成功后渲染的组件

//App.jsx
// 类组件
import React, { Component } from 'react'
// 1. 定义路由规则
import { Route } from 'react-router-dom'// 2. 引入需要渲染的组件
import Home from './views/Home'
import About from './views/About'class App extends Component {
  render() {
    return (
      <div>
        <h3>App应用</h3>
        <hr />
​
        {/* 
        3. 定义路由规则  (写在jsx语法中)
        path 字符串 路由匹配的路径
        component 路由匹配成功后要渲染的组件 默认用类 
        */}
        <Route path="/home" component={Home} />
        <Route path="/about" component={About} />
      </div>
    )
  }
}
​
export default App
//index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
// BrowserRouter history  注:此方案上线时一定要设置服务器配置
// 对于路由模式组件,一般在使用时,定义一个别名,方便后续的路由模式的切换
import {BrowserRouter as Router} from 'react-router-dom'
// import { HashRouter as Router } from 'react-router-dom'
ReactDOM.render(
    <Router>
        <App/>
    </Router>,
document.getElementById('root'))
​
import React, { Component } from 'react'
import { Route, Link, NavLink } from 'react-router-dom'
import Home from './views/Home'
import About from './views/About'
export default class App extends Component {
  render() {
    return (
      <div>
        <h2>导航区域</h2>
        <hr />
        <ul>
            <li>
                <Link to="/home">首页</Link>
            </li>
            <li>
                <NavLink to="/about">关于我们</NavLink>
            </li>
        </ul>
        <hr />
        <Route path="/home" component={Home}/>
        <Route path="/about" component={About}  />
      </div>
    )
  }
}
​

3.声明式导航

  • 使用Link或者NavLink组件完成声明式导航定义

  • Link和NavLink区别

    Link组件不会根据路由的变化而添加或修改编译后html标签中的属性

    NavLink会根据路由的变化而自动修改编译后html标签中的属性 它有激活样式 当前地址栏中的地址和to属性一样时,则默认会有一个class名称为active,当然你可以修改

    activeClassName 如果使用此属性,则自定义激活样式名称,一般情况下不建议去修改

import React, { Component } from 'react'
import { Route, Link, NavLink } from 'react-router-dom'
import Home from './views/Home'
import News from './views/News'
export default class App extends Component {
  render() {
    return (
      <div>
        <h2>导航区域</h2>
        <hr />
        <ul>
            <li>
                <Link to="/home">首页</Link>
            </li>
            <li>
                <NavLink to="/news">新闻</NavLink>
            </li>
        </ul>
        <hr />
        <Route path="/home" component={Home}/>
        <Route path="/news" component={News}  />
​
      </div>
    )
  }
}

4. 严格模式匹配

  • 定义路由规则默认情况下,react-route-dom它的path匹配是模糊匹配,而且会一直向下匹配下去,直到没有规则停止
  • 严格匹配:exact = {true}
  • <Switch></Switch>:只匹配一次,后面不再匹配,使用时将范围较大的规则写在下面,精准的写在上面
  • 404页面处理,此时一定将 / 弄成严格模式,否则不会到此规则匹配
// 类组件
import React, { Component } from 'react'
// Route 定义路由规则所用
// Link/NavLink 声明式导航  NavLink它有激活样式  统一编译出来的html都为a标签
// Switch 它只会匹配成功一次,后续就不会继续匹配
import { Route, NavLink, Switch } from 'react-router-dom'// 引入需要渲染的组件
import Home from './views/Home'
import About from './views/About'
import NotFound from './views/NotFound'class App extends Component {
  render() {
    return (
      <div>
        <ul>
          <li>
            {/* 它也有一个严格模式 */}
            <NavLink exact={true} to="/">
              home
            </NavLink>
          </li>
          <li>
            <NavLink to="/about">about</NavLink>
          </li>
        </ul>
​
        <hr />
​
        {/* 
          定义路由规则 默认情况下,react-route-dom它的path匹配是模糊匹配,而且会一直向下匹配下去,直到没有规则停止 
          严格匹配 exact={true}
        */}
        {/* <Route exact={true} path="/" component={Home} />
        <Route path="/about" component={About} /> */}
​
        {/* Switch使用后,要把范围较大的规则写在下面,精准的写在上面 */}
        {/* <Switch>
          <Route path="/about" component={About} />
          <Route path="/" component={Home} />
        </Switch> */}
​
        <Switch>
          <Route path="/about" component={About} />
          <Route exact={true} path="/" component={Home} />
          {/* 404页面处理,此时一定要把 / 弄成严格模式,否则不会到此规则匹配 */}
          <Route path="*" component={NotFound} />
        </Switch>
      </div>
    )
  }
}
export default App

5. 编程式导航 (先声明再使用)

  • 编程式导航通过this.props.history对象来完成的,此对象有一个要求
  • 在react路由中this.props要想得到路由中的对象,则默认必须要通过路由规则匹配渲染的组件才能有此对象 - 必须是直接渲染的组件
  • 只有当前组件是路由直接匹配成功后渲染的组件才有此对象
  • 定时器写在生命周期李,挂在之后,componentDidMount,用一个成员属性来保存当前计时器,componentWillUnmount里面清理计时器
//App.jsx
// 类组件
import React, { Component } from 'react'
// Route 定义路由规则所用
// Link/NavLink 声明式导航  NavLink它有激活样式  统一编译出来的html都为a标签
// Switch 它只会匹配成功一次,后续就不会继续匹配
import { Route, NavLink, Switch } from 'react-router-dom'// 引入需要渲染的组件
import Home from './views/Home'
import About from './views/About'
import NotFound from './views/NotFound'class App extends Component {
  render() {
    return (
      <div>
        <ul>
          <li>
            {/* 它也有一个严格模式 */}
            <NavLink exact={true} to="/">
              home
            </NavLink>
          </li>
          <li>
            <NavLink to="/about">about</NavLink>
          </li>
        </ul>
​
        <hr />
        <Switch>
          {/* About组件它是由路由规则匹配成功后直接渲染的,所以它就会有this.props.history对象 */}
          <Route path="/about" component={About} />
          <Route exact={true} path="/" component={Home} />
          <Route path="*" component={NotFound} />
        </Switch>
      </div>
    )
  }
}
export default App
class Home extends Component {
  render() {
    return (
      <div>
        <h3>首页页面</h3>
        <br />
        {/* <Btn history={this.props.history} /> */}
        {/* 把父组件中的props属性全部传给子组件 */}
        <Btn {...this.props} />
      </div>
    )
  }
}
import React, { Component } from 'react'
import { Link } from 'react-router-dom'// 404页面,在3秒后,自动跳转到首页去,此时就需要js来完成
// 通过js来完成的导航切换,编程式导航
// 编程式导航通过this.props.history对象来完成的,此对象有一个要求
// 只有当前组件是路由直接匹配成功后渲染的组件才有此对象class NotFound extends Component {
  // 挂载完成后,弄一个定时器,3秒后到首页
  componentDidMount() {
    // 用一个成员属性来保存当前的计数器
    this.timer = setTimeout(() => {
      this.props.history.push('/')
    }, 3000)
  }
​
  // 销毁进行清理工作
  componentWillUnmount() {
    console.log('走了')
    this.timer && clearTimeout(this.timer)
  }
​
  render() {
    // 通过this.props得到当前组件中的路由对象
    // console.log(this.props)
    // 其中this.props.history方法就是编程式导航所用到的对象
    // this.props.history(string|{pathname:string,search:string,state:object})
    return (
      <div>
        <h3>页面丢失</h3>
        <br />
        <Link to="/">去首页</Link>
        <br />
        <button
          onClick={() => {
            this.props.history.push('/')
          }}
        >
          去首页
        </button>
      </div>
    )
  }
}
export default NotFound

6. 页面路由传参

  • 动态路由传参,必须先定义规则在使用,一般用于详情页

  • React传参方式有三种:

    1. 动态路由传参(param)

      "/detail/:id"形式传递数据

      在落地组件中通过this.match.params得到(对象)

    2. 查询字符串(search)

      通过地址栏中?key = value&key = value传递

      在落地组件中通过this.props.location.search得到(字符串),需要自行来解析对象

    3. 隐式传参(state),通过地址栏是观察不到的

      通过路由对象中的state属性进行数据传递

      在落地组件中通过this.props.location.state得到(对象),在hash模式中此方案可能会刷新丢失数据,,在history模式下没有任何问题

import React, { Component } from 'react'
import Info from './ui/Info'
import { searchToObject } from '../../utils/tools'

// 获取动态路由参数数据 this.props.match.params 对象来完成获取  对象
// 获取search字符串数据 this.props.location.search  字符串,需要自行来解析为对象
// 获取state数据 this.props.location.state 对象 在hash模式中此方案可能会有刷新丢失数据的情况,在history模式下没有任何问题

class Detail extends Component {
  render() {
    console.log('动态路由参数', this.props.match.params)
    console.log('search字符串', this.props.location.search)
    console.log('state数据', this.props.location.state)

    // 可以使用es6提供一个URL对象来完成对于search字符串的解析,从而方便调用
    // const search = new URLSearchParams(this.props.location.search)
    // console.log('得到search字符串中的name字段的值', search.get('name'),search.get('age'))

    // item它是一个数组件,数组的元素1是key,元素2是value
    // for(let item of search.entries()){
    // 解构去写
    // for (let [key, value] of search.entries()) {
    //   console.log(key, value)
    // }

    console.log('search字符串', searchToObject(this.props.location.search))

    return (
      <div>
        <div>动态路由参数,必须先定义规则,才能使用,一般用于详情页面中使用 -- 它的url地址较为美观</div>
        <hr />
        <h3>动态中路由参数数据:{this.props.match.params.id}</h3>
        <Info {...this.props} />
      </div>
    )
  }
}
export default Detail
import React, { Component } from 'react'
import Info from './ui/Info'
import { searchToObject } from '../../utils/tools'
 
export default class Detail extends Component {
  render() {
    return (
      <div>
        <h3>动态中路由参数数据 {this.props.match.params.id} </h3>
        <Info {...this.props} />
      </div>
    )
  }
}

7. 嵌套路由

  • 路由前缀相同可以嵌套起来
  • 子路由上一定写上父路由的地址

注意:如果当前路由有子路由,一定不可以给父路由添加严格匹配

  • 通过this.props.match.path得到当前子路由的父路由路径,用此完成路由拼接

     const prefixUrl = this.props.match.path
    
    <Switch>
                {/* 子路由中一定要写上父路由的地址 */}
                <Route path={`${prefixUrl}/dashboard`} component={Dashboard} />
                <Route path={`${prefixUrl}/user`} component={User} />
              </Switch>
    

8. 三种路由渲染方式

  • component(类渲染或者函数)

    1. component类渲染方式:<Route path="/rd" component = {Render} />

      1.它会自动的把路由对象映射到渲染组件的props属性中

      2.类在路由规则匹配的环境中不能写业务判断,如当前页面只有登录才能访问,它不可写判断

      3.它不会重复去创建渲染组件

    2. 函数渲染方式:一定返回一个jsx对象

      <Route
                path="/rd"
                component={router => {
                  // 在路由规则匹配成功后,还可以进行别的业务判断
                  if ('?username=admin' === router.location.search) {
                    return <Render {...router} />
                  } else {
                    // return <div>你没有权限</div>
                    // 如果你没有权限则跳转到登录页
                    return <Redirect to="/login" />
                  }
                }}
      

      1.函数的方式,则需要你手动的把router对象传入到渲染组件的props属性中

      2.函数的方式,它可以在匹配规则后,进行对应的业务判断,如果满足则渲染

      3.当前路由宿主中有数据变化后,它则会销毁之前的组件再重新创建一个新的组件,性能较差

  • render(函数):它集合了component类和函数组件中的优点 ,需要手动的传递路由对象给渲染的组件的props属性中

    <Route
              path="/rd"
              render={router => {
                // 在路由规则匹配成功后,还可以进行别的业务判断
                if ('?username=admin' === router.location.search) {
                  return <Render {...router} />
                } else {
                  return <div>你没有权限</div>
                }
              }}
    
  • children(函数或者组件)

    1. jsx方式,精确匹配方式,当前path必须和地址栏地址一样时才来渲染

      children的jsx方式,默认情况下它没有返回给所匹配的渲染组件中this.props中注入路由对象,所以一般不用

    2. 回调函数方式:全匹配,它渲染不关心当前的path和地址栏规则是否一致,它都会渲染出来

      如果当前地址栏中的地址和path路径要是一致时,则router对象中的match属性则为对象,如果不匹配则为null

      <Route
                path="/rd"
                children={router => {
                  console.log(router.match)
                  return <Render {...router} />
                }}
      
       <Route
                path="/rd"
                children={router => {
                  console.log(router);
                  if (router.match) {
                    return <Render {...router} />
                  }
                  return <div>没有找到</div>
                }}
              />
      

9.withRouter高阶组件

  • 作用:把不是通过路由直接渲染出来的组件,将react-router的history、location、match三个对象传入props对象上
  • 默认情况下必须是经过路由匹配渲染的组件才存在this.props,才拥有路由参数,才能使用编程式导航的写法,执行this.props.history.push('/uri')跳转到对应路由的页面,然而不是所有组件都直接与路由相连的,当这些组件需要路由参数时,使用withRouter就可以给此组件传入路由参数,此时就可以使用this.props
import React, { Component } from 'react'
import { Route, withRouter } from 'react-router-dom'
import Render from './views/Render'
import Login from './views/Login'
class App extends Component {
  render() {
    console.log('App', this.props)
    return (
      <div>
        <Route path="/login" component={Login} />
        {/* <Route path="/rd" component={Render} /> */}
        {/* 让宿主组件有路由对象,可以向下传下去 */}
        <Route path="/rd" children={<Render {...this.props} />} />
      </div>
    )
  }
}

// 使用高阶组件进行了增强 当前此组件就有了路由对象
export default withRouter(App)

在函数组件中使用hooks函数来完成数据的获取

import React from 'react'
// useLocation:得到location对象  search字符串  state
// useHistory:得到history对象   编程式导航
// useParams: 得到params数据 动态路由参数
import { useLocation, useHistory, useParams } from 'react-router-dom'

const Form = props => {
  // console.log('login', props)
  // console.log(props.location.search)

  const location = useLocation()
  const history = useHistory()
  const params = useParams()

  console.log(location, history, params)

  return (
    <div>
      <button
        onClick={() => {
          history.push('/rd')
        }}
      >
        去rd页面
      </button>
    </div>
  )
}

export default Form

10. 自定义导航组件

因为有时候,在项目中给非a标签添加导航,默认Link或NavLink它编译后都只能是a标签,这样就不能满足项目需要,此时就需要自定义导航组件,来完成指定编译后生成的html标签

  • 定义组件:

    通过props接受数据

    给props设置类型限制 n npm i -S prop-types

    给props进行默认设置

import React from 'react'
// 针对于props进行类型限制
import types from 'prop-types'
import { useHistory, Route } from 'react-router-dom'

import {Container} from './style'

// Mylink它仿 Link组件

// tag进行别名设置,因为在react组件中,首字母必面大写
const Mylink = ({ to, children, tag: Tag }) => {
  const history = useHistory()

  const goto = () => {
    history.push(to)
  }
  return (
    <Container>
      <Route
        path={to}
        children={({ match }) => {
          let activeClass = match ? 'active' : ''

          return (
            <Tag className={activeClass} onClick={goto}>
              {children}
            </Tag>
          )
        }}
      />
    </Container>
  )
}

// 针对于当前的组件对于props类型进行限制
Mylink.propTypes = {
  // 属性名:类型限制
  // to: types.oneOfType([types.string,types.object])
  // to属性必须要传入的属性,类型为字符串
  to: types.string.isRequired,
  tag: types.string
}

// 设置默认值
Mylink.defaultProps = {
  tag: 'a'
}

export default Mylink
// 类组件
import React, { Component } from 'react'
// Route 定义路由规则所用
// Link/NavLink 声明式导航  NavLink它有激活样式  统一编译出来的html都为a标签
// Switch 它只会匹配成功一次,后续就不会继续匹配
// Redirect 重定向
// withRouter 高阶组件,作用:让不是路由匹配的组件拥有路由对象
import { Route, withRouter } from 'react-router-dom'

import Render from './views/Render'
import Login from './views/Login'

// 自定义导航组件
import Mylink from './components/Mylink'

class App extends Component {
  render() {
    return (
      <div>
        <Mylink to='/login' tag="h3">登录</Mylink>
        <Mylink to='/rd'>渲染</Mylink>

        <hr />
        <Route path="/login" component={Login} />
        <Route path="/rd" component={Render} />
      </div>
    )
  }
}

// 使用高阶组件进行了增强 当前此组件就有了路由对象
export default withRouter(App)

11. 使用装饰器调用高阶组件

  • npm i -D customize-cra@1 react-app-rewired@2  @babel/plugin-proposal-decorators@7
    
  • ////config-overrides
    
    // 此文件是运行在nodejs中,所以用commonjs规范来写
    // 此文件的修改一定要重启项目
    // 此文件的作用,就是对于当前项目的增量配置,类似于vue中的vue.config.js文件作用
    const {
      // 如果原项目中有此配置,则覆盖,如果没有则新增
      override,
      // 让项目支持装饰器
      addDecoratorsLegacy,
      // 设置引用的路径别名,让它像使用vue中的@一样的去使用
      addWebpackAlias
    } = require('customize-cra')
    const path = require('path')
    
    module.exports = override(
      // 支持装饰器
      addDecoratorsLegacy(),
      // 添加webpack别名
      addWebpackAlias({
        ['@']: path.resolve('./src')
      })
    )
    
  • "scripts": {
       "start": "react-app-rewired start",
       "build": "react-app-rewired build",
       "test": "react-scripts test",
       "eject": "react-scripts eject"
     },
    
  • import React, { Component } from 'react'
    
    // 装饰器 es7提出来的,用于装饰类所用,它只能装饰类
    // 装饰器它就是一个函数,此函数可以用来对于类来完成装饰
    
    // // target参数就是当前Demo类
    // const fn = target => {
    //   // console.log(target)
    
    //   // 扩展一个成员属性  给类扩展一个成员属性
    //   target.prototype.name = '李四'
    
    //   // 成员方法
    //   target.prototype.run = function () {
    //     console.log('run方法')
    //   }
    
    //   // 静态方法  通过类来调用
    //   target.test = function () {
    //     console.log('test方法')
    //   }
    // }
    
    // // 装饰类
    // @fn
    // class Demo {}
    // const d = new Demo()
    // // console.log(d)
    // // 调用成员方法
    // d.run()
    // // 调用静态方法
    // Demo.test()
    
    // 装饰器,装饰时有写小括号,则需要返回一个函数
    // 第1个函数中的参数,则为装饰器中小括号的传入过来的内容
    // 返回的函数的参数就是当前的装饰类
    // const fn = (age, name) => target => {
    //   // console.log(age, name)
    //   // console.log(target)
    
    //   target.prototype.name = name
    //   target.prototype.age = age
    // }
    
    // @fn(1, '张三')
    // class Demo {}
    
    // const d = new Demo()
    // console.log(d)
    
    // target当前类的实例对象
    // key就是当前的成员属性的名称
    // description 关于此成员属性的描述
    // const readonly = (target, key, description) => {
    //   // console.log(target,key,description)
    //   // Object.defineProperty
    //   description.writable = false
    // }
    
    // class Demo {
    //   // 通过装饰器来让成员属性只读
    //   @readonly
    //   name = '张三'
    // }
    
    // const d = new Demo()
    // d.name = '李四'
    
    const fn = (target, key, description) => {
      // console.log(target,key,description)
      // 对于原有的方法时行增量添加
      // 对象中的原方法进行暂时的保存起来
      let oldFn = description.value
      description.value = function (...arg) {
        // 调用原方法时,要注意this的指向问题 call/apply
        // oldFn.apply(this, arg)
        oldFn.call(this, ...arg)
        console.log('装饰器中的方法')
      }
    }
    
    class Demo {
      name = '赵六'
      @fn
      push(arg) {
        console.log(this)
        console.log('demo -- push', arg, this.name)
      }
    }
    
    const d = new Demo()
    d.push('变异')
    
    class App extends Component {
      render() {
        return <div>App</div>
      }
    }
    
    export default App
    

\