我还记得我最初开始学习前端路由时候的感受。那时我还年轻不懂事,刚刚开始摸索SPA
。从一开始我就把程序代码和路由代码分开对待,我感受这是两个不一样的东西,它们就像同父异母的亲兄弟,彼此不喜欢可是不得不在一块儿生活。前端
在过去的几年里,我有幸可以将路由的思想传授给其余开发人员。不幸的是,事实证实,咱们大多数人的大脑彷佛与个人大脑有着类似的思考方式。我认为这有几个缘由。首先,路由一般很是复杂。对于这些库的做者来讲,这使得在路由中找到正确的抽象变得更加复杂。其次,因为这种复杂性,路由库的使用者每每盲目地信任抽象,而不真正了解底层的状况,在本教程中,咱们将深刻解决这两个问题。首先,经过从新建立咱们本身的React Router v4
的简化版本,咱们会对前者有所了解,也就是说,RRv4
是不是一个合理的抽象。react
这里是咱们的应用程序代码,当咱们实现了咱们的路由,咱们能够用这些代码来作测试。完整的demo
能够参考这里git
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的基础知识,所以若是上面的代码还不是很熟悉,请看官方文档。github
首先要注意的是,咱们已经将路由器提供给咱们的两个组件(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属性。它是这样的,bash
<Route path='/settings' render={({ match }) => {
return <Settings authed={isAuthed} match={match} /> }} /> 复制代码
render容许您方便地内联一个函数,该函数返回一些UI,而不是建立一个单独的组件。咱们也会将它添加到propTypes中,react-router
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
(完)