React-Router源碼淺析

一、React-Router的核心原理

React-Router有兩種模式,這兩種模式都是依賴於window對象的方法實現路由跳轉html

  • HashRouter
  • BrowserRouter

1.1 、HashRouter

window.onhashchange事件,點擊詳見MDNreact

### hashChange 實現hack
if(!window.HashChangeEvent)(function(){
	var lastURL=document.URL;
	window.addEventListener("hashchange",function(event){
		Object.defineProperty(event,"oldURL",{enumerable:true,configurable:true,value:lastURL});
		Object.defineProperty(event,"newURL",{enumerable:true,configurable:true,value:document.URL});
		lastURL=document.URL;
	});
}());

# 使用
window.addEventListener('hashChange', function(event) {
    console.log('新URL', event.newURL);
    console.log('老URL', event.oldURL)
})
複製代碼

1.二、 BrowserRouter

經過history API完成連接的跳轉git

1.2.1 pushState方法(摘自MDN)[replaceState用法相同]

pushState() 須要三個參數: 一個狀態對象, 一個標題 (目前被忽略), 和 (可選的) 一個URL. 讓咱們來解釋下這三個參數詳細內容:github

  • 狀態對象 — 狀態對象state是一個JavaScript對象,經過pushState () 建立新的歷史記錄條目。不管何時用戶導航到新的狀態,popstate事件就會被觸發,且該事件的state屬性包含該歷史記錄條目狀態對象的副本。正則表達式

    • 狀態對象能夠是能被序列化的任何東西。緣由在於Firefox將狀態對象保存在用戶的磁盤上,以便在用戶重啓瀏覽器時使用,咱們規定了狀態對象在序列化表示後有640k的大小限制。若是你給 pushState() 方法傳了一個序列化後大於640k的狀態對象,該方法會拋出異常。若是你須要更大的空間,建議使用 sessionStorage 以及 localStorage.
  • 標題 — Firefox 目前忽略這個參數,但將來可能會用到。在此處傳一個空字符串應該能夠安全的防範將來這個方法的更改。或者,你能夠爲跳轉的state傳遞一個短標題。數組

  • URL — 該參數定義了新的歷史URL記錄。注意,調用 pushState() 後瀏覽器並不會當即加載這個URL,但可能會在稍後某些狀況下加載這個URL,好比在用戶從新打開瀏覽器時。新URL沒必要須爲絕對路徑。若是新URL是相對路徑,那麼它將被做爲相對於當前URL處理。新URL必須與當前URL同源,不然 pushState() 會拋出一個異常。該參數是可選的,缺省爲當前URL。瀏覽器

let stateObj = {
    foo: "bar"
}
history.pushState(stateObj,"擺設參數""/index.html")
複製代碼

二、React Router的實現

2.一、React Router的組成

接下來咱們將會逐個實現以上模塊

BrowserRouter.js

技術小結:做爲頂層路由,此處主要定義了路由中所須要的狀態參數。其中路由間的跳轉採用window.history.pushState方法實現,做爲獨立使用的組件,這個例子也靈活使用了React的context傳參的高級使用,值得我在平時負責組件時借鑑使用。安全

import React from 'react';
import Context from './context';
let pushState = window.history.pushState;
window.history.pushState = (state, title, url) => {
    pushState.call(window.history, state, title, url);
    window.onpushstate.call(this, state, url)
}
export default class HashRouter extends React.Component {
    state = {
        location: {
            pathname: window.loation.pathname, 
            state:null
        }
    }
    componentDidMount() {
        window.onpopstate = (event) => {
            if(this.block) {
                let confirm = window.confirm(this.block(this.state.location))
                if(!confirm) return;
            }
            this.setState({
                location: {
                    ...this.state.location,
                    pathname: window.location.pathname,
                    state: event.state
                }
            })
        }
        window.onpushstate = (state, pathname) => {
            this.setState({
                location: {
                    ...this.state.location,
                    pathname,
                    state
                }
            })
        }
    }
    render() {
        let that = this;
        let value = {
            location: that.state.location,
            history: {
                push(to) {
                    if(that.block) {
                        let confirm = window.confirm(that.block(typeof to === 'object'?to:{pathname:to}));
                        if(!confirm) return;
                    }
                    if(typeof to === 'object') {
                        let { pathname, state } = to;
                        window.history.pushState(state, '', pathname)
                    } else {
                        window.history.pushState(null, '', to)
                    }
                },
                block(message) {
                    that.block = message
                }
            }
        }
        return (
            <Context.Provider value={value}>
                {this.props.children}
            </Context.Provider>
        )   
    }
    }
複製代碼

HashRouter.js

與BrowserRouter實現類似bash

import React from 'react';
import Context from './context';
export default class HashRouter extends React.Component {
    state: {
        location: {pathname: window.location.hash.slice(1), state: null } 
    }
    locationState = null;
    componentDidMount() {
        window.location.hash = window.location.hash || '/';
        window.addEventListener('hashchange', () => {
            this.setState({
                location: {
                    ...this.state.location,
                    pathname: window.location.hash.slice(1),
                    state: this.locationState
                }
            })
        })
    }
    
    render() {
        let that = this;
        let value = {
            location: that.state.location,
            history: {
                push(to) {
                    if(that.block) {
                        let confirm = window.confirm(that.block(typeof to === 'object'?to:{pathname:to}));
                        if(!confirm) return;
                    }
                    if(typeof to === 'object'){
                        let {pathname,state} = to;
                        that.locationState = state;
                        window.location.hash = pathname;
                    }else{
                        that.locationState = null;
                        window.location.hash = to;
                    }
                },
                block(message){
                    that.block = message;
                }
            }
    }
    return (
        <Context.Provider value={value}>
            {this.props.children}
        </Context.Provider>
    )
}
複製代碼

context.js

做爲公共消費引用的對象,單獨提取context文件session

import React from 'react';
const context = React.createContext();
export default context;
複製代碼

Route.js

import Route from 'react'
import RouterContext from './context'
import pathToRegexp from 'path-to-regexp'
export default class Route extends React.Component {
    static contextType = RouterContext;
    render() {
        let {
            path="/",
            component: Component,
            exact: false,
            render,
            children
        } = this.props;
        let paramNames = [];
        # https://github.com/pillarjs/path-to-regexp 
        # 使用pathToRegexp正則庫來解析生成路徑參數
        let regxp = pathToRegexp(path, paramNames,{end: exact});
        let result = pathname.match(regxp);
        let props = {
            location: this.context.location,
            history: this.context.history
        }
        # 若是路徑匹配
        if(result) {
            paramNames = paramNames.map(item => item.name);
            let {url, ...values} = result;
            let params = {};
            for(let i = 0; i < paramNames.length; i++) {
                params[paramNames[i]] = values[i]
            }
            props.match = {
                path,
                url,
                isExact: url === pathname,
                params
            }
            # 存在Component,render或者render狀況下,渲染參數
            if(Component) {
                return <Component {...props} />
            } else if(render){
                return render(props);
            } else if(children) {
                return children(props);
            } else {
                return null
            }
        } else {
            if(children) {
                return children(props)
            } else {
                return null
            }
        }
    }
}
複製代碼

Switch

若是咱們直接使用Router對象,就會發現瀏覽器並不能精確匹配顯示所對應的路由組件,所以咱們須要在最外層包裹Switch

import React from 'react';
import pathToRegexp from 'path-to-regexp';
import RouterContext from './context';
export default class Switch extends React.Component {
    static contextType = RouteContext;
    render() {
        # 解耦當前地址欄的路徑
        let {pathname} = this.context.location;
        # 統一子元素對象格式爲數組格式
        let children = Array.isArray(this.props.children)? this.props.children:[this.props.children];
        for(let i = 0; i< children.length; i++) {
            let child = children[i];
            let {
                path = '/',
                exact = false
            } = child.props;
            let paramNames = [];
            # 生成正則表達式
            let regexp = pathToRegexp(path, paramNames, {end:exact});
            let result = pathname.match(regexp);
            if(result) {
                reutrn child;
            }
        }
        return null
    }
}
複製代碼

Link.js

對a元素的封裝

import React from 'react';
import ReactRouter from './context';
export default class Link extends React.Component {
    static contextType = RouterContext
    render() {
        return (
            <a {...this.props} onClick={() => this.context.history.push(this.props.to)}>{this.props.children}</a>
        )
    }
}
複製代碼

Redirect.js

重定向

import React from 'react';
import RouterCOntext from './context';
export default class Redirect extends React.Component {
    static contextType = RouterContext;
    render() {
        this.context.history.push(this.props.to);
        return null;
    }
}
複製代碼

withRouter

當咱們套用多層組件的時候,history參數是空的,這是須要這個高階組件傳參

import React from 'react';
import Route from './Route';
export default function(WrappedComponent) {
    return props => <Route component={WrappedComponent} />
}
複製代碼

Prompt

跳轉校驗,不過基本沒用

import React from 'react';
import RouterContext from './context';
export default class Prompt extends React.Component {
    static contextType = RouterContext;
    componentWillUnmount() {
        this.context.history.block(null)
    }
    render() {
        let history = this.context.history;
        const {when,message} = this.props;
        if(when) {
            history.block(message)
        }else {
            history.block(null)
        }
        return null;
    }
}
複製代碼
相關文章
相關標籤/搜索