内容简介:我还记得我最初开始学习前端路由时候的感觉。那时我还年轻不懂事,刚刚开始摸索在过去的几年里,我有幸能够将路由的思想传授给其他开发人员。不幸的是,事实证明,我们大多数人的大脑似乎与我的大脑有着相似的思考方式。我认为这有几个原因。首先,路由通常非常复杂。对于这些库的作者来说,这使得在路由中找到正确的抽象变得更加复杂。其次,由于这种复杂性,路由库的使用者往往盲目地信任抽象,而不真正了解底层的情况,在本教程中,我们将深入解决这两个问题。首先,通过重新创建我们自己的这里是我们的应用程序代码,当我们实现了我们的路由,我们
我还记得我最初开始学习前端路由时候的感觉。那时我还年轻不懂事,刚刚开始摸索 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
(完)
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
An Introduction to the Analysis of Algorithms
Robert Sedgewick、Philippe Flajolet / Addison-Wesley Professional / 1995-12-10 / CAD 67.99
This book is a thorough overview of the primary techniques and models used in the mathematical analysis of algorithms. The first half of the book draws upon classical mathematical material from discre......一起来看看 《An Introduction to the Analysis of Algorithms》 这本书的介绍吧!