React-Router源码浅析

741 阅读4分钟

1、React-Router的核心原理

React-Router有两种模式,这两种模式都是依赖于window对象的方法实现路由跳转

  • HashRouter
  • BrowserRouter

1.1 、HashRouter

window.onhashchange事件,点击详见MDN

### 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.2、 BrowserRouter

通过history API完成链接的跳转

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

pushState() 需要三个参数: 一个状态对象, 一个标题 (目前被忽略), 和 (可选的) 一个URL. 让我们来解释下这三个参数详细内容:

  • 状态对象 — 状态对象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")

2、React Router的实现

2.1、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实现相似

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文件

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;
    }
}