jackieli123723 / jackieli123723.github.io

✅lilidong 个人博客
9 stars 0 forks source link

构建您自己的React Router v4 #24

Open jackieli123723 opened 6 years ago

jackieli123723 commented 6 years ago

构建您自己的React Router v4

当我第一次开始学习客户端应用程序中的路由时,我还记得感觉。当时我只是一个小孩,仍然让我的脚湿润了这整个“单页申请”的事情,我会说谎,如果我说它没有在我的大脑上一个粪便。从一开始就好像我的大脑将我的应用程序代码和路由器代码视为两个独特而独特的想法。他们就像步行兄弟,他们彼此不喜欢,但是被迫一起生活在一起。

在过去几年中,我可能在这一点上反对你的批准,幸运的是能够将这种想法传递给其他开发人员。不幸的是,事实证明,我们大多数的大脑似乎与我的类似。我认为这有一些原因。首先,路由一般是相当复杂的。这对于那些图书馆作者来说,找到正确的路由抽象更复杂。第二,由于这种复杂性,路由库的消费者倾向于盲目地信任抽象,而不会真正理解发生了什么。在本教程中,我们将深入解决这两个问题。首先,通过重新创建我们自己的React Router v4的简化版本,然后再说明一下,就是RRv4是否合理的抽象。

这是我们的应用程序代码,我们将使用它来测试我们的〜React路由器实现一旦我们构建它。你可以在这里玩最后的例子

const Home = () => (
  <h2>Home</h2>
)

const About = () => (
  <h2>About</h2>
)

const Topic = ({ topicId }) => (
  <h3>{topicId}</h3>
)

const Topics = ({ match }) => {
  const items = [
    { name: 'Rendering with React', slug: 'rendering' },
    { name: 'Components', slug: 'components' },
    { name: 'Props v. State', slug: 'props-v-state' },
  ]

  return (
    <div>
      <h2>Topics</h2>
      <ul>
        {items.map(({ name, slug }) => (
          <li key={name}>
            <Link to={`${match.url}/${slug}`}>{name}</Link>
          </li>
        ))}
      </ul>
      {items.map(({ name, slug }) => (
        <Route key={name} path={`${match.path}/${slug}`} render={() => (
          <Topic topicId={name} />
        )} />
      ))}
      <Route exact path={match.url} render={() => (
        <h3>Please select a topic.</h3>
      )}/>
    </div>
  )
}

const App = () => (
  <div>
    <ul>
      <li><Link to="/">Home</Link></li>
      <li><Link to="/about">About</Link></li>
      <li><Link to="/topics">Topics</Link></li>
    </ul>

    <hr/>

    <Route exact path="/" component={Home}/>
    <Route path="/about" component={About}/>
    <Route path="/topics" component={Topics} />
  </div>
)

如果您不熟悉React Router v4,这是基本的前提。Route当URL与您在路线的path道具中指定的位置匹配时,会渲染一些UI 。Links提供了一种声明性,可访问的方式来浏览您的应用程序。换句话说,Link组件允许您更新URL,Route组件根据该新URL更改您的UI。本教程的重点并不在于教授RRV4的基础知识,所以如果上面的代码仍然令人困惑,请转到正式的文档,并附上示例,一旦你更舒适,回来。

你应该注意到的第一件事是,我们已经介绍了给我们路由器到我们的应用程序,两个组件,LinkRoute。我最喜欢的React Router v4是API是“Just Components™”。这意味着,如果您已经熟悉React,您对组件的相同直觉以及如何撰写它们将继续适用于您的路由代码。对于我们在这里使用的情况更加方便,因为我们已经熟悉如何创建组件,创建我们自己的React路由器将不仅仅是我们已经熟悉的,创建更多的组件。


我们将从创建我们的Route组件开始。在我们深入了解代码之前,让我们先看看API(它正好是哪个道具需要的)。

在上面的例子中,您会注意到<Route>可以使用三个道具。exactpathcomponent。这意味着propTypes我们的Route组件目前看起来像这样,

static propTypes = {
  exact: PropTypes.bool,
  path: PropTypes.string,
  component: PropTypes.func,
}

这里有几个细节。首先,原因path不是必需的,因为如果Route没有给出路径,它将自动呈现。第二,原因component没有标记为必需,因为实际上有几种不同的方式告诉React Router要路由匹配的UI。一个不在我们上面的例子中的方法就是render道具。看起来像这样,

<Route path='/settings' render={({ match }) => {
  return <Settings authed={isAuthed} match={match} />
}} />

render允许您方便地内联返回一些UI的功能,而不是创建单独的组件。所以我们还将把它添加到我们的propTypes中,

static propTypes = {
  exact: PropTypes.bool,
  path: PropTypes.string,
  component: PropTypes.func,
  render: PropTypes.func,
}

现在我们知道道具Route收到了什么,让我们再来谈一下实际的做法。路线“会在URL与您在路线的path道具中指定的位置匹配时呈现一些UI 。基于这个定义,我们知道这<Route>将需要一些功能来检查当前的URL是否匹配组件的path支持。如果是这样,我们将渲染一些UI。如果没有,我们将返回null来执行任何操作。

让我们看看代码中看起来像什么,相信我们会建立匹配函数matchPath,稍后我们将会调用这个函数。

class Route extends Component {
  static propTypes = {
    exact: PropTypes.bool,
    path: PropTypes.string,
    component: PropTypes.func,
    render: PropTypes.func,
  }

  render () {
    const {
      path,
      exact,
      component,
      render,
    } = this.props

    const match = matchPath(
      location.pathname, // global DOM variable
      { path, exact }
    )

    if (!match) {
      // Do nothing because the current
      // location doesn't match the path prop.

      return null
    }

    if (component) {
      // The component prop takes precedent over the
      // render method. If the current location matches
      // the path prop, create a new element passing in
      // match as the prop.

      return React.createElement(component, { match })
    }

    if (render) {
      // If there's a match but component
      // was undefined, invoke the render
      // prop passing in match as an argument.

      return render({ match })
    }

    return null
  }
}

现在Route看起来很实在 如果当前位置与path传入的prop 相匹配,我们会渲染一些UI,否则,我们不做任何事情。

让我们退一步,谈一下路由。在客户端应用程序中,用户只需两种方式来更新URL。第一种方法是点击锚标签,第二种方式是单击后退/转发按钮。从基础上看,我们的路由器需要了解当前的URL并基于它渲染UI。这也意味着我们的路由器需要意识到URL何时改变,以便它可以根据该新URL找出要显示的新UI。如果我们知道更新URL的唯一方法是通过锚标签或前进/后退按钮,我们可以计划并对这些更改做出反应。稍后,当我们构建<Link>组件时,我们将进入锚标签,但现在我想集中在后退/前进按钮。反应路由器使用历史记录.listen方法来监听当前URL的更改,但为避免引入另一个库,我们将使用HTML5的popstate事件。popstate,这将在用户点击前进或后退按钮时触发,正是我们需要的。因为正是Route基于当前URL渲染UI,所以Route当发生popstate事件时,还可以让侦听和重新呈现的能力也是有意义的。通过重新渲染,每个都Route将重新检查它们是否与新的URL匹配。如果他们这样做,他们会渲染UI,否则,他们什么都不做。让我们看看现在这个样子,

class Route extends Component {
  static propTypes: {
    path: PropTypes.string,
    exact: PropTypes.bool,
    component: PropTypes.func,
    render: PropTypes.func,
  }

  componentWillMount() {
    addEventListener("popstate", this.handlePop)
  }

  componentWillUnmount() {
    removeEventListener("popstate", this.handlePop)
  }

  handlePop = () => {
    this.forceUpdate()
  }

  render() {
    const {
      path,
      exact,
      component,
      render,
    } = this.props

    const match = matchPath(location.pathname, { path, exact })

    if (!match)
      return null

    if (component)
      return React.createElement(component, { match })

    if (render)
      return render({ match })

    return null
  }
}

您应该注意到,我们完成的所有操作都是popstate在组件挂载时添加一个监听器,当popstate事件被触发时,我们调用forceUpdate它将启动重新渲染。

现在,无论<Route>我们渲染多少,每个人都会根据前进/后退按钮进行聆听,重新匹配和重新渲染。

有一件事我们一直在“挥手”,直到这一点是我们的matchPath功能。这个功能对我们的路由器至关重要,因为它是一个功能,它将决定当前URL是否与<Route>上面讨论的组件路径匹配。一个细微差别matchPath是,我们需要确保我们考虑到<Route>小号exact道具。如果你不熟悉什么exact,这里直接来自文档

如果true,如果路径匹配的将只匹配location.pathname 完全相同

路径 location.pathname 精确 火柴?
/one /one/two true 没有
/one /one/two false

现在,我们来看看我们的matchPath功能的实现。如果你回顾我们的Route组件,你会看到这样的签名matchPath

const match = matchPath(location.pathname, { path, exact })

match根据是否有匹配,对象或空值在哪里。基于这个签名,我们可以构建这样的第一部分matchPath

const matchPath = (pathname, options) => {
  const { exact = false, path } = options
}

这里我们使用了一些ES6魔法。我们在说“创建一个名为exact的变量,等同于options.exact,除非未定义,然后将其设置为false。还要创建一个名为path的变量,这个变量等于options.path“。

之前我提到“原因path不是必需的,因为如果Route没有给出路径,它将自动呈现”。那么因为它间接地是我们的matchPath功能,决定是否有某物被渲染(通过是否有一个匹配),让我们现在添加这个功能。

const matchPath = (pathname, options) => {
  const { exact = false, path } = options

  if (!path) {
    return {
      path: null,
      url: pathname,
      isExact: true,
    }
  }
}

现在是匹配的部分。React Router 为此使用pathToRegex,我们将简化事情,只需使用一个简单的Regex。

const matchPath = (pathname, options) => {
  const { exact = false, path } = options

  if (!path) {
    return {
      path: null,
      url: pathname,
      isExact: true,
    }
  }

  const match = new RegExp(`^${path}`).exec(pathname)

}

如果你不熟悉.exec,它将返回一个包含匹配文本的数组,如果它找到一个匹配,否则返回null。

match当我们的示例应用程序路由到`/ topics /组件时,这里是每一个

路径 location.pathname 返回值
/ /topics/components ['/']
/about /topics/components null
/topics /topics/components ['/topics']
/topics/rendering /topics/components null
/topics/components /topics/components ['/topics/components']
/topics/props-v-state /topics/components null
/topics /topics/components ['/topics']

请注意,我们正在match<Route>我们的应用程序中的每一个获得一个。这是因为,每个<Route>调用matchPath它的render方法。

现在我们知道match.exec是什么回报,我们现在需要做的就是弄清楚是否有一场比赛。

const matchPath = (pathname, options) => {
  const { exact = false, path } = options

  if (!path) {
    return {
      path: null,
      url: pathname,
      isExact: true,
    }
  }

  const match = new RegExp(`^${path}`).exec(pathname)

  if (!match) {
    // There wasn't a match.
    return null
  }

  const url = match[0]
  const isExact = pathname === url

  if (exact && !isExact) {
    // There was a match, but it wasn't
    // an exact match as specified by
    // the exact prop.

    return null
  }

  return {
    path,
    url,
    isExact,
  }
}

之前,我提到如果您是用户,通过后退/前进按钮或点击achor标签,真正只需两种方法来更新网址。我们已经通过popstate我们的事件侦听器重新渲染了后退/转发点击Route,现在我们通过构建我们的Link组件来处理锚标签。

API Link看起来像这样,

<Link to='/some-path' replace={false} />

to字符串在哪里,是要链接到的位置,replace是一个布尔值,如果为true,则单击链接将替换历史堆栈中的当前条目,而不是添加新的条目。

将这些propTypes添加到我们的Link组件中,我们得到这个,

class Link extends Component {
  static propTypes = {
    to: PropTypes.string.isRequired,
    replace: PropTypes.bool,
  }
}

现在我们知道我们Link组件中的render方法需要返回一个锚标签,但是我们显然不希望在每次切换路由时都会重新整理页面,所以我们将通过添加一个onClick处理程序来劫持锚标签

class Link extends Component {
  static propTypes = {
    to: PropTypes.string.isRequired,
    replace: PropTypes.bool,
  }

  handleClick = (event) => {
    const { replace, to } = this.props
    event.preventDefault()

    // route here.
  }

  render() {
    const { to, children} = this.props

    return (
      <a href={to} onClick={this.handleClick}>
        {children}
      </a>
    )
  }
}

现在所有缺少的都是改变现在的位置。要做到这一点阵营路由器使用历史pushreplace方法,但我们将使用HTML5的[pushState的](https://developer.mozilla.org/en-US/docs/Web/API/History_API#The_pushState()_method "")和[replaceState](https://developer.mozilla.org/en-US/docs/Web/API/History_API#The_replaceState()_method "")方法,以避免增加的依赖。

我们在这篇文章中挥舞着历史库,作为避免外部依赖关系的一种方式,但它对于真正的React路由器代码至关重要,因为它可以规范在各种浏览器环境中管理会话历史记录的差异。

双方pushStatereplaceState采取三个参数。第一个是与新的历史记录条目相关联的对象 - 我们不需要这个功能,所以我们只需要传递一个空的对象。第二个是标题,我们也不需要,所以我们将传入null。第三个,我们实际使用的是一个相对URL。

const historyPush = (path) => {
  history.pushState({}, null, path)
}

const historyReplace = (path) => {
  history.replaceState({}, null, path)
}

现在在我们的Link组件内部,我们将调用historyPushhistoryReplace依赖于replace支持,

class Link extends Component {
  static propTypes = {
    to: PropTypes.string.isRequired,
    replace: PropTypes.bool,
  }
  handleClick = (event) => {
    const { replace, to } = this.props
    event.preventDefault()

    replace ? historyReplace(to) : historyPush(to)
  }

  render() {
    const { to, children} = this.props

    return (
      <a href={to} onClick={this.handleClick}>
        {children}
      </a>
    )
  }
}

现在还有一个我们需要做的另外一个重要的事情。如果您要使用我们当前的路由器代码与我们的示例应用程序一起玩,您会发现一个相当大的问题。当您浏览时,网址将更新,但UI将保持完全相同。这是因为即使我们使用我们的功能来改变位置,我们historyReplace也不知道这种变化,并且不知道应该重新渲染和重新匹配。为了解决这个问题,我们需要跟踪哪些已经被渲染,并且每当路由改变时调用它们。historyPush``<Route>``<Route>``forceUpdate

React Router通过将包含您的代码的路由器组件中的setState,context和history.listen组合使用来解决此问题。

为了保持路由器的简单性,我们将<Route>通过将其实例推送到数组来跟踪已经呈现的映射,然后每当发生位置更改时,我们可以循环遍历该数组,并在所有实例上调用forceUpdate。

let instances = []

const register = (comp) => instances.push(comp)
const unregister = (comp) => instances.splice(instances.indexOf(comp), 1)

请注意,我们创建了两个功能。register每当<Route>安装unregister时,我们都会打电话,无论何时卸载。然后,每当我们打电话historyPushhistoryReplace(我们每次用户点击一次<Link>),我们可以循环遍历这些实例forceUpdate

我们先来更新我们的<Route>组件,

class Route extends Component {
  static propTypes: {
    path: PropTypes.string,
    exact: PropTypes.bool,
    component: PropTypes.func,
    render: PropTypes.func,
  }

  componentWillMount() {
    addEventListener("popstate", this.handlePop)
    register(this)
  }

  componentWillUnmount() {
    unregister(this)
    removeEventListener("popstate", this.handlePop)
  }

  ...
}

现在,我们来更新historyPushhistoryReplace

const historyPush = (path) => {
  history.pushState({}, null, path)
  instances.forEach(instance => instance.forceUpdate())
}

const historyReplace = (path) => {
  history.replaceState({}, null, path)
  instances.forEach(instance => instance.forceUpdate())
}

🎉现在每当<Link>点击并且位置发生变化时,每个<Route>都会意识到这一点,并重新匹配并重新渲染。

现在,我们的完整的路由器代码看起来像下面的代码,我们上面的示例应用程序完美地与它完成。

import React, { PropTypes, Component } from 'react'

let instances = []

const register = (comp) => instances.push(comp)
const unregister = (comp) => instances.splice(instances.indexOf(comp), 1)

const historyPush = (path) => {
  history.pushState({}, null, path)
  instances.forEach(instance => instance.forceUpdate())
}

const historyReplace = (path) => {
  history.replaceState({}, null, path)
  instances.forEach(instance => instance.forceUpdate())
}

const matchPath = (pathname, options) => {
  const { exact = false, path } = options

  if (!path) {
    return {
      path: null,
      url: pathname,
      isExact: true
    }
  }

  const match = new RegExp(`^${path}`).exec(pathname)

  if (!match)
    return null

  const url = match[0]
  const isExact = pathname === url

  if (exact && !isExact)
    return null

  return {
    path,
    url,
    isExact,
  }
}

class Route extends Component {
  static propTypes: {
    path: PropTypes.string,
    exact: PropTypes.bool,
    component: PropTypes.func,
    render: PropTypes.func,
  }

  componentWillMount() {
    addEventListener("popstate", this.handlePop)
    register(this)
  }

  componentWillUnmount() {
    unregister(this)
    removeEventListener("popstate", this.handlePop)
  }

  handlePop = () => {
    this.forceUpdate()
  }

  render() {
    const {
      path,
      exact,
      component,
      render,
    } = this.props

    const match = matchPath(location.pathname, { path, exact })

    if (!match)
      return null

    if (component)
      return React.createElement(component, { match })

    if (render)
      return render({ match })

    return null
  }
}

class Link extends Component {
  static propTypes = {
    to: PropTypes.string.isRequired,
    replace: PropTypes.bool,
  }
  handleClick = (event) => {
    const { replace, to } = this.props

    event.preventDefault()
    replace ? historyReplace(to) : historyPush(to)
  }

  render() {
    const { to, children} = this.props

    return (
      <a href={to} onClick={this.handleClick}>
        {children}
      </a>
    )
  }
}

奖励:React Router API还附带了一个<Redirect>组件。使用我们以前写的代码,创建这个组件是非常简单的

class Redirect extends Component {
  static defaultProps = {
    push: false
  }

  static propTypes = {
    to: PropTypes.string.isRequired,
    push: PropTypes.bool.isRequired,
  }

  componentDidMount() {
    const { to, push } = this.props

    push ? historyPush(to) : historyReplace(to)
  }

  render() {
    return null
  }
}

注意,这个组件实际上并没有渲染任何UI,而是纯粹作为路由导向器,因此这个名字。

我希望这可以帮助您为React Router创建更好的心理模型,同时帮助您更好地了解React Router的优雅和“Just Components”API。我一直说React会让你成为一个更好的JavaScript开发人员。我现在也相信React Router会让你成为一个更好的React开发者。因为一切只是组件,如果你知道React,你知道React Router。