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已經被使用了)