由浅入深地教你开发自己的 React Router v4
转载请注明出处,保留原文链接以及作者信息 我还记得我第一次学习开发客户端应用路由时的感觉,那时候我还是一个涉足在“单页面应用”的未出世的小伙子,那会儿,要是说它没把我的脑子弄的跟屎似的,那我是在撒谎。一开始的时候,我的感觉是我的应用程序代码和路由代码是两个独立且不同的体系,就像是两个同父异母的兄弟,互相不喜欢但是又不得不在一起。 经过了一些年的努力,我终于有幸能够教其他开发者关于路由的一些问题了。我发现,好像很多人对于这个问题的思考方式都和我当时很类似。我觉得有几个原因。首先,路由问题确实很复杂,对于那些路由库的开发者而言,找到一个合适的路由抽象概念来解释这个问题就更加复杂。第二,正是由于路由的复杂性,这些路由库的使用者倾向于只使用库就好了,而不去弄懂到底背后是什么原理。 本文中,我们会深入地来阐述这两个问题。我们会通过创建一个简单版本的 React Router v4 来解决第二个问题,而通过这个过程来阐释第一个问题。也就是说通过我们自己构建 RRv4 来解释 RRv4 是否是一个合适的路由抽象。 下面是将要用来测试我们所构建的 React Router 的代码。最终的代码实例你可以在这里得到。 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,就先了解几个基本问题。 本文并不会手把手的教你 RRV4 的基础,所以如果上面的代码你看起来很费劲的话,可以先来这里看一下官方文档。把玩一下里面的例子,当你觉得顺手了的时候,欢迎回来继续阅读。 如上段所说,路由给我们提供了两个组件可以用于你的 app: 现在就来一起创建我们的 static propTypes = { exact: PropTypes.bool,path: PropTypes.string,component: PropTypes.func,} 这里有些小细节。首先, <Route path='/settings' render={({ match }) => { return <Settings authed={isAuthed} match={match} /> }} />
static propTypes = { exact: PropTypes.bool,render: PropTypes.func,} 现在我们知道了 一起来看一下这个匹配函数应该怎么写,暂且把它叫做 class Route extends Component { static propTypes = { exact: PropTypes.bool,} render () { const { path,exact,component,render,} = this.props const match = matchPath( location.pathname,// 全局 DOM 变量 { path,exact } ) if (!match) { // 什么都不做,因为没有匹配上 path 属性 return null } if (component) { // 如果当前地址匹配上了 path 属性 // 以 component 创建新元素并且通过 match 传递 return React.createElement(component,{ match }) } if (render) { // 如果匹配上了且 component 没有定义 // 则调用 render 并以 match 作为参数 return render({ match }) } return null } } 上面的代码即实现了:如果匹配上了 我们再来谈一下路由的问题。在客户端应用这边,一般来讲只有两种方式更新 URL。一种是用户点击 a 标签,一种是点击后退/前进按钮。基本上我们的路由只要关心 URL 的变化并且返回相应的 UI 即可。假设我们知道更新 URL 的方式只有上面两种,那么就可以针对这两种情况做特殊处理了。稍后在构建 class Route extends Component { static propTypes: { path: PropTypes.string,exact: PropTypes.bool,} componentWillMount() { addEventListener("popstate",this.handlePop) } componentWillUnmount() { removeEventListener("popstate",this.handlePop) } handlePop = () => { this.forceUpdate() } render() { const { path,} = 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 } } 这里要注意的是我们只是加了一个 这样就实现了所有的 到现在,我们一直还没有实现的是
接下来就来具体实现 const match = matchPath(location.pathname,exact }) 这里的 match 要么是对象,要么是 null,这得取决于是否匹配上 path。根据这个声明,我们来写 const matchPatch = (pathname,options) => { const { exact = false,path } = options } 这里使用 ES6 语法。上面的意思是,创建一个叫做 exact 的变量,使其等于 options.exact,并且如果非 null 的话则设置其为 false。同样创建一个叫做 path 的变量,使其等于 options.path。 接下来就添加判断是否匹配。React Router 使用 pathToRegex 来实现,只需要写简单的正则匹配就可以了。 const matchPatch = (pathname,path } = options if (!path) { return { path: null,url: pathname,isExact: true,} } const match = new RegExp(`^${path}`).exec(pathname) } 如果匹配上了,那么返回一个包含有所有匹配串的数组,否则返回 null。 下面是我们示例 app 的路由 '/topics/components' 的一些匹配项。
现在我们要做的是添加判断是否有匹配的代码: const matchPatch = (pathname,} } 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,} } 提示一下之前有讲过的,对于用户来讲,有两种方式更新 URL:通过后退/前进按钮和通过点击 a 标签。对于后退/前进点击来说,使用
<Link to='/some-path' replace={false} /> 这里 添加这些 propTypes 到 class Link extends Component { static propTypes = { to: PropTypes.string.isRequired,replace: PropTypes.bool,} } 我们知道在 class Link extends Component { static propTypes = { to: PropTypes.string.isRequired,} handleClick = (event) => { const { replace,to } = this.props event.preventDefault() // 这里是路由 } render() { const { to,children} = this.props return ( <a href={to} onClick={this.handleClick}> {children} </a> ) } } ok,代码写到现在,就差更改当前 URL 了。在 React Router 是使用 History 工程里面的
const historyPush = (path) => { history.pushState({},null,path) } const historyReplace = (path) => { history.replaceState({},path) } 在 class Link extends Component { static propTypes = { to: PropTypes.string.isRequired,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 却没有刷新,这是为什么呢?这是因为,尽管你通过
为了使路由简单,我们通过把所有路由对象放到一个数组里的方式来实现 let instances = [] const register = (comp) => instances.push(comp) const unregister = (comp) => instances.splice(instances.indexOf(comp),1) 注意这里创建了两个函数。当 首先更新 class Route extends Component { static propTypes: { path: PropTypes.string,this.handlePop) register(this) } componentWillUnmount() { unregister(this) removeEventListener("popstate",this.handlePop) } ... } 再更新 const historyPush = (path) => { history.pushState({},path) instances.forEach(instance => instance.forceUpdate()) } const historyReplace = (path) => { history.replaceState({},path) instances.forEach(instance => instance.forceUpdate()) } 这时只要 这就完成了所有的路由代码了,并且实例 app 用这些代码可以完美运行! 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({},path) instances.forEach(instance => instance.forceUpdate()) } const matchPath = (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,} } class Route extends Component { static propTypes: { path: PropTypes.string,{ match }) if (render) return render({ match }) return null } } class Link extends Component { static propTypes = { to: PropTypes.string.isRequired,children} = this.props return ( <a href={to} onClick={this.handleClick}> {children} </a> ) } } 另外:React Router API 还自然派生出了 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 会使你成为一个好的 JavaScript 程序员,而 React Router 会使你成为一个好的 React 程序员。因为一切皆为组件,你懂 React,你就懂 React Router。 我最近正在写一本《React.js 小书》,对 React.js 感兴趣的童鞋,欢迎指点。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |