jeremygo

jeremygo

我是把下一颗珍珠串在绳子上的人

從零實現一個 React-Router

在實現 React 單頁面應用過程中,必須掌握的一個生態就是 React-Router 路由庫,本文將會從零開始實現React-Router 的關鍵部分。

React-Router v4 是一次顛覆性的更新,完全不兼容以前版本的寫法,相比於之前更容易讓人接受的配置式路由寫法,v4 由一個個路由組件(LinkRouteRedirect...)實現,我覺得這才是真正貼合 React 本身的組件化思想,如果你已經會 React 了,那麼 React-Router 只是多學習幾個組件而已,所以我們實現的關鍵部分主要也是針對 v4 。

起步#

首先我們通過 create-react-app 來快速啟動一個項目,並在 index.js 中敲入官方的 demo,我們先看最終會渲染的 App 組件:

const App = () => (
  <div>
    <ul>
      <li><Link to="/">首頁</Link></li>
      <li><Link to="/about">關於</Link></li>
      <li><Link to="/topics">主題</Link></li>
    </ul>

    <hr/>
    <Route exact path="/" component={Home} />
    <Route path="/about" component={About} />
    <Route path="/topics" component={Topics} />
  </div>
)

其中就包含了 React-Router 最核心的兩個組件: RouteLink

接下來我們就將重點放在這兩個組件的實現上~

Route#

在上述的例子中我們注意到 Route 可以傳入四個 props : exactpathcomponentrender,我們先來實現基本的 Route 組件:

class Route extends Component {
    static propTypes = {
        path: PropTypes.string, // 匹配路徑
        exact: PropTypes.bool,  // 是否精確匹配
        component: PropTypes.func, // 匹配會渲染的組件
        render: PropTypes.func  // 自定義渲染內容
    }

    render () {
        const { path, exact, component, render } = this.props
        // 看路由是否匹配
        const match = matchPath(window.location.pathname, { path, exact })
        // 如果不匹配就返回 null
        if (!match) return null
        // (優先)如果傳了匹配組件
        if (component) return React.createElement(component, { match })
        // 如果自定義了渲染內容
        if (render) return render({ match })

        return null
    }
}

Route 組件的核心就是匹配成功就渲染,不成功就不渲染(返回 null)。

然後我們來看一下 matchPath 匹配函數的實現:

const match = (pathname, options) => {
    const { path, exact = false } = options
    
    // 如果沒有傳 path
    if (!path) return { path: null, url: pathname, isExact: true }
    
    // 正則匹配 url
    const match = new RegExp(`^${path}`).exec(pathname)
    // 不匹配則返回 null
    if (!match) return null
    
    // 判斷是否完全匹配
    const url = match[0]
    const isExact = pathname === url
    if (exact && !isExact) return null
    
    return {
        path,
        url,
        isExact
    }
}

React-Router 為了保證兼容性是引入了 pathToRegex 庫來做正則匹配,這裡我們就簡單用 js 自帶的 RegExp 正則對象來實現 。

到現在我們實現了 Route 組件的匹配渲染邏輯,那麼在實際的路由切換中,如何做到 Route 的重新渲染?

class Route extends Component {
    ...
    componentWillMount () {
        // 監聽瀏覽器 前進/後退 按鈕的點擊
        window.addEventListener('popstate', this.handlePop)
    }
    componentWillUnmount () {
		window.addEventListener('popstate', this.handlePop)
    }
    handlePop = () => {
		this.forceUpdate()
    }
    ...
}

路由會切換有兩種場景,其中一種就是 瀏覽器 前進 / 後退 按鈕的點擊,我們在兩個生命週期函數中監聽這個點擊事件,一旦點擊了就調用自帶的 forceUpdate 方法強制更新 UI 。

React-Router 中使用的是 history.listen 監聽,同樣我們避免引入依賴選擇 HTML5 的 popstate 事件。

另一種路由切換的場景就是 a 標籤的點擊,也就是我們接下來 Link 組件的實現~

Link 組件的核心在於 聲明式 更新 URL,很容易想到它內部最終還是一個 a 標籤 :

class Link extends Component {
    static propTypes = {
        to: PropTypes.string.isRequired,
        replace: PropTypes.bool
    }
	// 阻止默認跳轉,調用自定義更新路由方法
    handleClick = e => {
		const { replace, to } = this.props
        e.preventDefault()
        replace ? historyReplace(to) : historyPush(to)
    }

    render () {
		const { to, children } = this.props
        
        return (
        	<a href={to} onClick={this.handleClick}>
            	{children}
            </a>
        )
    }
}

而自定義的 historyReplacehistoryPush 方法區別在於是插入 history 棧還是替換 history 棧 :

const historyPush = path => window.history.pushState({}, null, path)
const historyReplace = path => window.history.replaceState({}, null, path)

Link 的實現就是這樣,那我們就很容易引出一個問題,點擊 Link 後如何匹配到對應的 Route ? 在 Route 組件的掛載和銷毀前我們有監聽對應的事件,但是對於 Link 的點擊它並不會生效。

因此在 Route 組件加載時我們有必要將它作為一個實例保存下來(不管它有沒有被匹配到)。

componentWillMount () {
	window.addEventListener('popstate', this.handlePop)
    register(this)
}

componentWillUnmount () {
	unregister(this)
    window.addEventListener('popstate', this.handlePop)
}

registerunregister 實現如下:

let instances = []

const register = comp => instances.push(comp)
const unregister = comp => instances.splice(instances.indexOf(comp), 1)

所以在 historyPushhistoryReplace 方法中我們需要遍歷調用各個 Route 實例進而讓它們逐一匹配 :

const historyPush = path => {
    window.history.pushState({}, null, path)
    instances.forEach(instance => instance.forceUpdate())
}

const historyReplace = path => {
    window.history.replaceState({}, null, path)
    instances.forEach(instance => instance.forceUpdate())
}

現在我們將 RouteLink 組件引入 index.js 中啟動可以看到運行正常。

React-Router 在路由組件內使用 setStatecontexthistory.listen 的結合來解決這個問題。

最後#

React Router v4 的原理是很值得學習的,React 可以讓你成為一個更好的 JavaScript 開發者,而 React-Router 可以讓你成為一個更好的 React 開發者。

參考 :

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。