react-router 已经经历了好几个版本的大更新。 在这里咱们打算参照v4.0的设计思想 创一个轮子: tiny-routerjavascript
现代浏览器提供了 提供了对history栈中内容的操做的api。 重要的有 pushState, replaceState。html
var stateObj = { foo: "bar" }; history.pushState(stateObj, "page 2", "bar.html");
这将使浏览器地址栏显示为 http://xxxx/bar.html,但并不会致使浏览器加载 bar.html ,甚至不会检查bar.html 是否存在。java
var stateObj = { foo: "bar" }; history.replaceState(stateObj, "page 2", "bar.html");
这也将使浏览器地址显示 http://xxxx/bar.html, 也不会加载。 浏览器也不会检查bar.html 是否存在。
pushState和replaceState的区别在于 回退的时候。 push是在history栈中加了一个记录, 而repalce是替换一个记录。
这两个方法都接收3个参数, 分别是:react
每当url改变的时候,视图view对应改变,就是一个基本的路由了。 经过history api能够很方便的修改url。 如今咱们须要作的是监听每次url的改变,从而达到修改view的目的, 咱们须要对history对象进行一些封装, 从而达到监听的目的。 代码以下:git
const his = window.history class History { constructor() { this.listeners = [] } push = path => { his.pushState({}, "", path); this.notifyAll() } listen = listener => { this.listeners.push(listener) return () => { this.listeners = this.listeners.filter(ele => ele !== listener) } } notifyAll = () => { this.listeners.forEach(lis => { lis() }) } } export default new History()
声明一个History类, 能够注册listener, 当push一个地址的时候, History类底层调用history.push 方法去修改路由, 而后通知注册的listener。 这样在路由改变的时候,咱们的处理函数就能够第一时间的进行处理了。github
react-router v4.0 里面最重要的组件莫过于Route。 这个组件接收一个path属性, 一旦url和path匹配, 就展现这个Route指定的组件。页面上一次能够展现出多个Route组件,只要Route的path属性和url匹配。
咱们的Route 是同样的。 为了实现这个效果, 咱们须要在每次url变化的时候检测新的url和path是否匹配,一旦匹配,就展现对应的组件。这就须要每一个Route组件在初始化的时候在History上注册一个监听器, 从而让Route能够及时的响应url变化
代码以下:正则表达式
class Route extends Component { constructor(props) { super(props) this.state = { match: matchPath(...) } this.unlisten = history.listen(this.urlChange) } componentWillUnmount() { this.unlisten() } urlChange = () => { const pathname = location.pathname this.setState({ match: matchPath(...) }) } render() { const { match } = this.state if(!match) return // 具体的渲染... } }
Route的path 能够是字符串或者正则表达式。 除此以外Route还有 exact(精确匹配), strict(结尾/), sensitive(大小写)这3个属性。 他们共同决定了一个匹配url的正则表达式PathReg(这里的匹配规则和react-router是彻底同样的)。 npm
咱们假定path, exact, strict, sensitive属性是不可改变的(实际上来讲, 也的确没有修改它们的必要)。 这样话, 咱们就能够在Route组件初始化的时候生成这个PathReg。redux
const compilePath = (pattern = '/', options) => { const { exact = false, strict = false, sensitive = false } = options const keys = [] const re = pathToRegexp(pattern, keys, { end: exact, strict, sensitive }) return { re, keys } } class Route extends Component { static propTypes = { path: PropTypes.string, component: PropTypes.func, render: PropTypes.func, exact: PropTypes.bool, strict: PropTypes.bool, } constructor(props) { super(props) this.pathReAndKeys = compilePath(props.path, { exact: props.exact, strict: props.strict, sensitive: props.sensitive }) this.state = { match: matchPath(...) } this.unlisten = history.listen(this.urlChange) } }
咱们在组件的constructor里面, 预先生成了pathReAndKeys。
而后在每次matchPath的时候, 直接使用这个正则。 mathPath的代码以下:segmentfault
const matchPath = (pathname, props, pathReAndKeys) => { const { path = '/', exact = false } = props const { re, keys } = pathReAndKeys const match = re.exec(pathname) if (!match) return null const [ url, ...values ] = match const isExact = pathname === url if (exact && !isExact) return null return { path, // the path pattern used to match url: path === '/' && url === '' ? '/' : url, // the matched portion of the URL isExact, // whether or not we matched exactly params: keys.reduce((memo, key, index) => { memo[key.name] = values[index] return memo }, {}) } }
当路由不匹配的时候, 直接返回null。 不然返回一个匹配信息的对象。 例如:
url | path | match.params |
---|---|---|
/user | /user | {} |
/user/12 | /user/:id | {id: '12'} |
/user/12/update | /user/:id/:op | {id: 12, op: 'update'} |
这里处理路径用了path-to-regexp 这个库。
匹配路径只是过程, 渲染出对应的view才是最终的目标!Route组件提供了 3个属性: component, render, children。 具体用法以下:
// one class A extends Component { render() { return <div>A</div> } } // Route默认会把匹配的信息注入到组件A的props <Route path='/a' component={A} /> // two <Route path='/a' render={({match}) => (<div>A</div>)} // three Route默认会把匹配的信息注入到组件A的props <Route> <A> </Route>
完整的Route 代码以下:
import React, { Component } from 'react' import PropTypes from 'prop-types' import { compilePath, matchPath } from './util' import history from './history' class Route extends Component { static propTypes = { path: PropTypes.string, component: PropTypes.func, render: PropTypes.func, exact: PropTypes.bool, strict: PropTypes.bool, } constructor(props) { super(props) this.pathReAndKeys = compilePath(props.path, { exact: props.exact, strict: props.strict, sensitive: props.sensitive }) this.state = { match: matchPath(location.pathname, props, this.pathReAndKeys) } this.unlisten = history.listen(this.urlChange) } componentWillReceiveProps(nextProps) { const {path, exact, strict} = this.props if (nextProps.path !== path || nextProps.exact !== exact || nextProps.strict !== strict) { console.warn("you should not change path, exact, strict props") } } componentWillUnmount() { this.unlisten() } urlChange = () => { const pathname = location.pathname this.setState({ match: matchPath(pathname, this.props, this.pathReAndKeys) }) } render() { const { match } = this.state if(!match) return const { children, component, render } = this.props if (component) { const Comp = component return <Comp match={match}/> } if (render) { return render({ match }) } return React.cloneElement(React.Children.only(children), { match }) } } export default Route
与react-router相同。 咱们一样提供一个Link组件, 来实现 “声明式”的路由跳转。
Link本质上来讲就是一个原生的a标签, 而后在点击的时候history.push到to属性所指定的地址去。
import React, { Component } from 'react' import history from './history' export default class Link extends Component { handleClick = e => { const { onClick, to } = this.props if (onClick){ onClick(e) } e.preventDefault() history.push(to) } render() { return ( <a {...this.props} onClick={this.handleClick}/> ) } }
这里有一个网站,它模拟了一般业务场景下的路由跳转。 路由由tiny-router管理的。
首页 导航在页头点击切换
我的信息页 在左侧导航切换
npm install tinyy-router
(注意是 tinyy 两个y,由于tiny-router已经被使用了)