环境
react:16.13.1
react-router:5.2.0

参考文章
history.listen

场景 1:在查询页面,修改查询表单后,刷新数据.然后跳转页面再返回,需要把之前的表单数据还原
场景 2:在其他的业务页面,点击带有参数的链接,在跳转后需要将带过来的参数设置的到表单中

方案 1(放弃)

使用动态路由,例如:/list/:type/:cat/:page.这种方式不适合,因为参数的数量不确定,而且查询时允许不带参数

方案 2

使用search参数.该方案可以解决方案 1 的问题

实现

  1. 使用类装饰器.
  2. 允许使用默认值
  3. 自动监听search变化
  4. search转化为对象,方便使用
  5. 提供search字符串和{key:value}对象的互转工具

代码

方案中没有使用history.listen,因为无法获取旧的location,从而判断路由中的search是否变化(可能是其他的变化)

src/utils/search-listener.js

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

/**
 * @desc 将Location中的search字符串转换为对象
 * @param {string} search
 */
export function getSearchObject(search = '') {
  const result = {}
  if (search) {
    const searchStr = search.split('?')[1]
    const searchKeyValueArr = searchStr.split('&')
    for (let i = 0; i < searchKeyValueArr.length; i++) {
      const [key, value] = searchKeyValueArr[i].split('=')
      result[key] = decodeURIComponent(value)
    }
  }

  return result
}

/**
 * @desc 将对象转化为search字符串
 * @param {object} obj
 */
export function objectToSearch(obj = {}) {
  let searchStr = ''
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      const value = encodeURIComponent(obj[key])
      searchStr += `${key}=${value}&`
    }
  }

  return searchStr ? '?' + searchStr.slice(0, -1) : ''
}

/**
 * @desc 可监听search变化的装饰器
 *
 * @desc 限制:
 * @desc state.search用于存放参数对象,不能命名冲突
 * @desc onRouteSearchUpdate用于监听更新,不能命名冲突
 *
 * @param {boolean?} listenOnDidMount 是否在组件初始化时就触发'onRouteSearchUpdate'
 */
export default function withSearchListener(listenOnDidMount = true) {
  let initSearch = {}

  return WrappedComponent =>
    withRouter(
      class extends WrappedComponent {
        componentDidMount() {
          // 初始化默认的search
          initSearch = this.state.search || {}

          if (typeof WrappedComponent.prototype.componentDidMount === 'function') {
            WrappedComponent.prototype.componentDidMount.call(this)
          }

          if (listenOnDidMount) {
            this.onRouteSearchUpdate(getSearchObject(this.props.location.search), this.props.location)
          }
        }

        componentDidUpdate(prevProps) {
          if (typeof WrappedComponent.prototype.componentDidUpdate === 'function') {
            WrappedComponent.prototype.componentDidUpdate.call(this)
          }

          if (prevProps.location.search !== this.props.location.search) {
            this.onRouteSearchUpdate(getSearchObject(this.props.location.search), this.props.location)
          }
        }

        /**
         * @desc 当路由中的'search'更新时触发
         * @param {string?} search
         * @param {object?} location
         */
        onRouteSearchUpdate(search = {}, location = {}) {
          // 根据默认的search来合并出新的search
          const nextSearch = { ...initSearch, ...search }

          this.setState({ search: nextSearch }, () => {
            if (typeof WrappedComponent.prototype.onRouteSearchUpdate === 'function') {
              WrappedComponent.prototype.onRouteSearchUpdate.call(this, nextSearch, location)
            }
          })
        }
      }
    )
}

使用

import React, { Component } from 'react'
import withSearchListener from '@/utils/search-listener'

@withSearchListener()
class Page extends Component {
  state = { search: { a: '1', b: '1' } }

  onRouteSearchUpdate(search = {}, location = {}) {
    console.log('search updated', search, location)
  }

  render() {
    return (
      <div>
        <h1>Search route</h1>
        <h2>search :{JSON.stringify(this.state.search)}</h2>
      </div>
    )
  }
}

完整的例子

/**
 * @name SearchRoute
 * @author darcrand
 * @desc
 */

import React, { Component } from 'react'
import { Link } from 'react-router-dom'
import withSearchListener, { objectToSearch } from '@/utils/search-listener'

async function apiGetData(params = {}) {
  console.log('apiGetData', params)
  return Array(10)
    .fill(0)
    .map(_ => ({ id: Math.random(), title: `title - ${~~(Math.random() * 100)}` }))
}

const optionsA = Array(5)
  .fill(0)
  .map((_, i) => ({ value: String(i + 1), label: `A-${i + 1}` }))

const optionsB = Array(5)
  .fill(0)
  .map((_, i) => ({ value: String(i + 1), label: `B-${i + 1}` }))

@withSearchListener()
class SearchRoute extends Component {
  state = { search: { a: '1', b: '1' }, list: [] }

  onParamsChange = (field = {}) => {
    const { search } = this.state
    this.props.history.replace('/search-route' + objectToSearch({ ...search, ...field }))
  }

  onRouteSearchUpdate(search = {}, location = {}) {
    console.log('onRouteSearchUpdate', search, location)
    this.getData()
  }

  getData = async () => {
    const res = await apiGetData(this.state.search)
    this.setState({ list: res })
  }

  render() {
    return (
      <>
        <h1>拥有 路由监听功能的组件</h1>

        <p>通过链接跳转</p>
        <ul>
          <li>
            <Link replace to='/search-route'>
              空参数
            </Link>
          </li>
          <li>
            <Link replace to='/search-route?a=3'>
              参数 a
            </Link>
          </li>
          <li>
            <Link replace to='/search-route?b=2'>
              参数 b
            </Link>
          </li>
          <li>
            <Link replace to='/search-route?a=4&b=3'>
              参数 ab
            </Link>
          </li>
        </ul>

        <p>通过表单参数模拟跳转</p>
        <section>
          {optionsA.map(v => (
            <button key={v.value}>
              <label>
                <input
                  type='radio'
                  name='a'
                  value={v.value}
                  checked={this.state.search.a === v.value}
                  onChange={e => this.onParamsChange({ a: e.target.value })}
                />
                <span>{v.label}</span>
              </label>
            </button>
          ))}
        </section>

        <section>
          {optionsB.map(v => (
            <button key={v.value}>
              <label>
                <input
                  type='radio'
                  name='b'
                  value={v.value}
                  checked={this.state.search.b === v.value}
                  onChange={e => this.onParamsChange({ b: e.target.value })}
                />
                <span>{v.label}</span>
              </label>
            </button>
          ))}
        </section>

        <h2>search :{JSON.stringify(this.state.search)}</h2>

        <ol>
          {this.state.list.map(v => (
            <li key={v.id}>{v.title}</li>
          ))}
        </ol>
      </>
    )
  }
}

export default SearchRoute

darcrand
637 声望20 粉丝