内容简介:我还记得我最初开始学习前端路由时候的感觉。那时我还年轻不懂事,刚刚开始摸索在过去的几年里,我有幸能够将路由的思想传授给其他开发人员。不幸的是,事实证明,我们大多数人的大脑似乎与我的大脑有着相似的思考方式。我认为这有几个原因。首先,路由通常非常复杂。对于这些库的作者来说,这使得在路由中找到正确的抽象变得更加复杂。其次,由于这种复杂性,路由库的使用者往往盲目地信任抽象,而不真正了解底层的情况,在本教程中,我们将深入解决这两个问题。首先,通过重新创建我们自己的这里是我们的应用程序代码,当我们实现了我们的路由,我们
我还记得我最初开始学习前端路由时候的感觉。那时我还年轻不懂事,刚刚开始摸索 SPA
。从一开始我就把程序代码和路由代码分开对待,我感觉这是两个不同的东西,它们就像同父异母的亲兄弟,彼此不喜欢但是不得不在一起生活。
在过去的几年里,我有幸能够将路由的思想传授给其他开发人员。不幸的是,事实证明,我们大多数人的大脑似乎与我的大脑有着相似的思考方式。我认为这有几个原因。首先,路由通常非常复杂。对于这些库的作者来说,这使得在路由中找到正确的抽象变得更加复杂。其次,由于这种复杂性,路由库的使用者往往盲目地信任抽象,而不真正了解底层的情况,在本教程中,我们将深入解决这两个问题。首先,通过重新创建我们自己的 React Router v4
的简化版本,我们会对前者有所了解,也就是说, RRv4
是否是一个合理的抽象。
这里是我们的应用程序代码,当我们实现了我们的路由,我们可以用这些代码来做测试。完整的 demo
可以参考这里
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
不熟悉,这里做一个基本的介绍,当URL与您在Routes的path中指定的位置匹配时,Routes渲染相应的UI。Links提供了一种声明性的、可访问的方式来导航应用程序。换句话说,Link组件允许您更新URL, Route组件基于这个新URL更改UI。本教程的重点实际上并不是教授RRV4的基础知识,因此如果上面的代码还不是很熟悉,请看官方文档。
首先要注意的是,我们已经将路由器提供给我们的两个组件(Link和Route)引入到我们的应用程序中。我最喜欢React Router v4的一点是,API只是组件。这意味着,如果您已经熟悉React,那么您对组件以及如何组合组件的直觉将继续适用于您的路由代码。对于我们这里的用例来说,更方便的是,因为我们已经熟悉了如何创建组件,创建我们自己的React Router只需要做我们已经做过的事情。
我们将从创建Route组件开始。在深入研究代码之前,让我们先来检查一下这个API(它所需要的 工具 非常方便)。
在上面的示例中,您会注意到可以包含三个props。exact,path和component。这意味着Route组件的propTypes目前是这样的,
static propTypes = { exact: PropTypes.bool, path: PropTypes.string, component: PropTypes.func, } 复制代码
这里有一些微妙之处。首先,不需要path的原因是,如果没有给Route指定路径,它将自动渲染。其次,组件没有标记为required的原因也在于,如果路径匹配,实际上有几种不同的方法告诉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接收到哪些props了,让我们来再次讨论它实际的功能。当URL与您在Route 的path属性中指定的位置匹配时,Route渲染相应的UI。根据这个定义,我们知道将需要一些功能来检查当前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,我们就渲染组件否则返回null。
让我们退一步来讨论一下路由。在客户端应用程序中,用户只有两种方式更新URL。第一种方法是单击锚标签,第二种方法是单击后退/前进按钮。我们的路由器需要知道当前URL并基于它呈现UI。这也意味着我们的路由需要知道什么时候URL发生了变化,这样它就可以根据这个新的URL来决定显示哪个新的UI。如果我们知道更新URL的唯一方法是通过锚标记或前进/后退按钮,那么我们可以开始计划并对这些更改作出响应。稍后,当我们构建组件时,我们将讨论锚标记,但是现在,我想重点关注后退/前进按钮。React Router使用History .listen方法来监听当前URL的变化,但为了避免引入其他库,我们将使用HTML5的popstate事件。popstate正是我们所需要的,它将在用户单击前进或后退按钮时触发。因为基于当前URL呈现UI的是路由,所以在popstate事件发生时,让路由能够侦听并重新呈现也是有意义的。通过重新渲染,每个路由将重新检查它们是否与新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,它将启动重新渲染。
现在,无论我们渲染多少个,它们都会基于forward/back按钮侦听、重新匹配和重新渲染。
在这之前,我们一直使用matchPath函数。这个函数对于我们的路由非常关键,因为它将决定当前URL是否与我们上面讨论的组件的路径匹配。matchPath的一个细微差别是,我们需要确保我们考虑到的exact属性。如果你不知道确切是怎么做的,这里有一个直接来自文档的解释,
当为true时,仅当路径与location.pathname相等时才匹配。
path | location.pathname | exact | matches? |
---|---|---|---|
/one | /one/two | true | no |
/one | /one/two | false | yes |
现在,让我们深入了解matchPath函数的实现。如果您回头看看Route组件,您将看到matchPath是这样的调用的,
const match = matchPath(location.pathname, { path, exact }) 复制代码
match是对象还是null取决于是否存在匹配。基于这个调用,我们可以构建matchPath的第一部分,
const matchPath = (pathname, options) => { const { exact = false, path } = options } 复制代码
这里我们使用了一些ES6语法。意思是,创建一个叫做exact的变量它等于options.exact,如果没有定义,则设为false。还要创建一个名为path的变量,该变量等于options.path。
前面我提到"path不是必须的原因是,如果没有给定路径,它将自动渲染”。因为它间接地就是我们的matchPath函数,它决定是否渲染UI(通过是否存在匹配),现在让我们添加这个功能。
const matchPath = (pathname, options) => { const { exact = false, path } = options if (!path) { return { path: null, url: pathname, isExact: true, } } } 复制代码
接下来是匹配部分。React Router 使用 pathToRegexp 来匹配路径,为了简单我们这里就用简单正则表达式。
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。
我们来看一个例子,当我们路由到 /topics/components
时匹配到的路径。
如果你不熟悉 .exec
,如果它找到匹配它会返回一个包含匹配文本的数组,否则它返回null。
下面是我们的示例应用程序路由到 /topics/components
时的每一次匹配
path | location.pathname | return value |
---|---|---|
/ | /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'] |
注意,我们为应用中的每个 <Route>
都得到了匹配。这是因为,每个 <Route>
在它的渲染方法中调用 matchPath
。
现在我们知道了 .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, } } 复制代码
前面我提到,如果您是用户,那么只有两种方法可以更新 URL
,通过后退/前进按钮,或者单击锚标签。我们已经处理了通过路由中的 popstate
事件侦听器对后退/前进单击进行重新渲染,现在让我们通过构建 <Link>
组件来处理锚标签。
Link
的 API
是这样的,
<Link to='/some-path' replace={false} /> 复制代码
to
是一个字符串,是要链接到的位置,而 replace
是一个布尔值,当该值为 true
时,单击该链接将替换历史堆栈中的当前条目,而不是添加一个新条目。
将这些 propTypes
添加到链接组件中,我们得到,
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> ) } } 复制代码
现在所缺少的就是改变当前的位置。为了做到这一点, React Router
使用了 History
的 push
和 replace
方法,但是我们将使用 HTML5
的 pushState
和 replaceState
方法来避免添加依赖项。
在这篇文章中,我们将 History
库作为一种避免外部依赖的方法,但它对于真正的 React Router
代码非常重要,因为它规范了在不同浏览器环境中管理会话历史的差异。
pushState
和 replaceState
都接受三个参数。第一个是与新的历史记录条目相关联的对象——我们不需要这个功能,所以我们只传递一个空对象。第二个是 title
,我们也不需要它,所以我们传入 null
。第三个,也是我们将要用到的,是一个相对 URL
。
const historyPush = (path) => { history.pushState({}, null, path) } const historyReplace = (path) => { history.replaceState({}, null, path) } 复制代码
现在在我们的 Link
组件中,我们将调用 historyPush
或 historyReplace
取决于 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> ) } } 复制代码
现在,我们只需要再做一件事,这是至关重要的。如果你用我们当前的路由器代码来运行我们的示例应用程序,你会发现一个相当大的问题。导航时, URL
将更新,但 UI
将保持完全相同。这是因为即使我们使用 historyReplace
或 historyPush
函数更改位置,我们的 <Route>
并不知道该更改,也不知道它们应该重新渲染和匹配。为了解决这个问题,我们需要跟踪哪些 <Route>
已经呈现,并在路由发生变化时调用 forceUpdate
。
React Router
通过使用 setState
、 context
和 history
的组合来解决这个问题。监听包装代码的路由器组件内部。
为了保持路由器的简单性,我们将通过将 <Route>
的实例保存到一个数组中,来跟踪哪些 <Route>
已经呈现,然后每当发生位置更改时,我们可以遍历该数组并对所有实例调用 forceUpdate
。
let instances = [] const register = (comp) => instances.push(comp) const unregister = (comp) => instances.splice(instances.indexOf(comp), 1) 复制代码
注意,我们创建了两个函数。每当挂载 <Route>
时,我们将调用 register
;每当卸载 <Route>
时,我们将调用 unregister
。然后,无论何时调用 historyPush
或 historyReplace
(每当用户单击 <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) } ... } 复制代码
现在,让我们更新historyPush和historyReplace,
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
还带了一个额外的 <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
。
原文地址: Build your own React Router v4
(完)
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。