從0開始實現 react-router

從0開始實現 react-router

react-router 已經經歷了好幾個版本的大更新。 在這裏咱們打算參照v4.0的設計思想 創一個輪子: tiny-routerjavascript

history api

現代瀏覽器提供了 提供了對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

  1. 狀態對象 — 狀態對象state是一個JavaScript對象。
  2. 標題
  3. URL — 該參數定義了新的歷史URL記錄。新URL必須與當前URL同源。

監聽url改變

每當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

Route

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

        // 具體的渲染...
    }
}

matchPath

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

Link

與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管理的。

首頁 導航在頁頭點擊切換
header

我的信息頁 在左側導航切換
user

live demo

Github & npm

  1. github 地址:https://github.com/ykforerlan...。 以爲不錯,點個star
  2. 安裝 npm install tinyy-router (注意是 tinyy 兩個y,由於tiny-router已經被使用了)

相關文章

  1. 從0實現一個tiny react(一)
  2. 從0實現一個tinyredux
  3. 從0實現一個tiny react-redux
  4. 爲何咱們須要reselect
相關文章
相關標籤/搜索