ReactRouter-V4 构建之道与源码分析
多年之后当我回想起初学客户端路由的那个下午,满脑子里充斥着的只是对于单页应用的惊叹与浆糊。彼时我还是将应用代码与路由代码当做两个独立的部分进行处理,就好像同父异母的兄弟尽管不喜欢对方但是不得不在一起。幸而这些年里我能够和其他优秀的开发者进行交流,了解他们对于客户端路由的看法。尽管他们中的大部分与我“英雄所见略同”,但是我还是找到了合适的平衡路由的抽象程度与复杂程度的方法。本文即是我在构建 React Router V4 过程中的考虑以及所谓路由即组件思想的落地实践。首先我们来看下我们在构建路由过程中的测试代码,你可以用它来测试你的自定义路由: 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我们首先来考量下如何构建 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,// 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 } } 现在的 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 }) 其中函数的返回值 const matchPath = (pathname,options) => { const { exact = false,path } = options } 这里我们使用 ES6 的解构赋值,当某个属性未定义时我们使用预定义地默认值,即 const matchPath = (pathname,path } = options if (!path) { return { path: null,url: pathname,isExact: true,} } } 接下来继续考虑具体执行匹配的部分,React Router 使用了 pathToRegex 来检测是否匹配,即可以用简单的正则表达式: const matchPath = (pathname,} } const match = new RegExp(`^${path}`).exec(pathname) } 这里使用的 | 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']` | 这里大家就会看出来,我们会为每个 const matchPath = (pathname,} } 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,} } Link上文我们已经提及通过监听 <Link to='/some-path' replace={false} /> 其中的 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() // route here. } render() { const { to,children} = this.props return ( <a href={to} onClick={this.handleClick}> {children} </a> ) } } 这里实际的跳转操作我们还是执行 History 中的抽象的 const historyPush = (path) => { history.pushState({},null,path) } const historyReplace = (path) => { history.replaceState({},path) } 而后在 class Link extends Component { static propTypes = { to: PropTypes.string.isRequired,} 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> ) } } 组件注册现在我们需要考虑如何保证用户点击了 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()) } 这样的话就保证了无论何时用户点击 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,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 中提供了所谓 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 } } 注意这个组件并没有真实地进行界面渲染,而是仅仅进行了简单的跳转操作。到这里本文也就告一段落了,希望能够帮助你去了解 React Router V4 的设计思想以及 Just Component 的接口理念。我一直说 React 会让你成为更加优秀地开发者,而 React Router 则会是你不小的助力。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |