react-router-dom 的 HashRouter 也就這麼回事兒

1 要實現的功能

咱們使用React開發項目的時候,基本上是單頁面應用,也就離不開路由。路由看似神祕,當咱們簡單的模擬一下它的核心功能後,發現也就這麼回事兒。本文就詳細的介紹一下react-router-dom HashRouter的核心實現邏輯。react

本文實現的功能主要包含:git

  • HashRouter
  • Route
  • Link
  • MenuLink
  • Switch
  • Redirect

2 實現的邏輯

先不說代碼是怎樣寫的,先上圖,讓你們看一下這個HashRouter究竟是個什麼東東:
HashRouter實現邏輯圖1
好吧,確定有人會說這些圈圈又是什麼東東呀,客官不要着急,待我慢慢解釋:github

  • HashRouter是一個大的容器,它控制着他本身到底渲染成什麼樣子,那麼它是經過什麼控制的呢,看它的名字就能猜出來,那就是window.location.hash
  • HashRouter開始渲染的時候就會拿它本身身上的pathname屬性跟它肚子裏的Routepath進行匹配,匹配上的話,就會渲染Routecomponent對應的組件。
  • Link是怎樣切換路由的呢,很簡單,就是經過this.props.history.push(path)來改變HashRouter中的pathname屬性,進而驅動Route們 進行從新渲染,再次匹配咱們的路由,最終實現路由的切換。

介紹了一下簡單的邏輯,接下來咱們就看一下具體是怎樣實現的吧,以下圖:
HashRouter實現邏輯圖2react-router

  • HashRouter是一個繼承了React.Component的類,這個類上的state包括location,監聽着hash的變化以驅動Route組件的從新渲染,另外還有一個history屬性,能夠切換頁面的路由。
  • 本文要實現的功能中包括RouteLinkMenuLinkSwitchRedirect,其中Route的是基礎是核心,MenuLink和某些有特定邏輯的渲染都是在Route的基礎上實現的。
  • Route組件上能夠接收三種變量,包括componentrenderchildren,其中renderchildren是都是函數,render是根據特定的邏輯渲染元素,children是用來渲染MenuLink,這兩個函數都接收當前路由的props,函數的返回值是要渲染的元素。
  • Switch實現的邏輯是,返回children中跟hash匹配到的第一個「孩子」。

3 具體的代碼邏輯

(1) HashRouter

HashRouterwindow.loacation.hash跟本身的state掛鉤,經過改變本身的state驅動頁面的從新渲染。dom

import React, {Component} from 'react';
import PropTypes from 'prop-types';

export default class HashRouter extends Component {
    constructor() {
        super();
        this.state = {
            location: {
                pathname: window.location.hash.slice(1) || '/', // 當前頁面的hash值
                state: {}   //保存的狀態
            }
        };
    }
    
    // 定義上下文的變量類型
    static childContextTypes = {
        location: PropTypes.object,
        history: PropTypes.object
    }
    
    // 定義上下文的變量
    getChildContext() {
        return {
            location: this.state.location,
            history: {
                push: (path) => { // 就是更新 window.hash值
                    if (typeof path === 'object') {
                        let {pathname, state} = path;
                        this.setState({
                            location: {
                                ...this.state.location,
                                state // {from: '/profile'}
                            }
                        }, () => {
                            window.location.hash = pathname;
                        })
                    } else {
                        window.location.hash = path;
                    }
                }
            }
        }
    }
    
    render() {
        return this.props.children; // 渲染頁面元素
    }
    
    componentDidMount() {
        window.location.hash = window.location.hash.slice(1) || '/';
        // 監聽window的hash的變化,驅動頁面的從新刷新
        window.addEventListener('hashchange', () => {
            this.setState({
                location: {
                    ...this.state.location,
                    pathname: window.location.hash.slice(1) || '/'
                }
            });
        })
    }
}

(2) Route

Route的渲染核心邏輯就是將本身的path和當前頁面的hash進行匹配,匹配上了就渲染相應的元素,匹配不上就什麼都不渲染。函數

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import pathToRegexp from 'path-to-regexp'

export default class Route extends Component {
    // 定義上下文context的類型
    static contextTypes = {
        location: PropTypes.object,
        history: PropTypes.object
    }
    
    render() {
        // 解構傳入Route的props
        let {path, component: Component, render, children} = this.props;
        
        // 解構上下文的屬性
        let {location, history} = this.context;
        let props = {
            location,
            history
        };
        
        // 將傳入Route的path和當前的hash進行匹配
        let keys = [];
        let regexp = pathToRegexp(path, keys, {end: false});
        keys = keys.map(key => key.name);
        
        let result = location.pathname.match(regexp);
        
        if (result) { // 匹配上了
            let [url, ...values] = result;
            props.match = {
                path,
                url,
                params: keys.reduce((memo, key, index) => { // 獲取匹配到的參數
                    memo[key] = values[index];
                    return memo;
                }, {})
            };
            
            if (Component) { // 普通的Route
                return <Component {...props} />;
            } else if (render) { // 特定邏輯的渲染
                return render(props);
            } else if (children) { // MenuLink的渲染
                return children(props);
            } else {
                return null;
            }
        } else { // 沒有匹配上
            if (children) { // MenuLink的渲染
                return children(props);
            } else {
                return null;
            }
        }
    }
}

(3) Redirect

Redirect就幹了一件事,就是改變HashRouterstate,驅動從新渲染。this

import React, {Component} from 'react';
import PropTypes from 'prop-types';

export default class Redirect extends Component {
    // 定義上下文context的Type
    static contextTypes = {
        history: PropTypes.object
    }
    
    componentDidMount() {
        // 跳轉到目標路由
        this.context.history.push(this.props.to);
    }
    
    render() {
        return null;
    }
}

(4) MenuLink

import React, {Component} from 'react';
import Route from "./Route";
import Link from './Link'

export default ({to, children}) => {
    // 若是匹配到了,就給當前組件一個激活狀態的className
    return <Route path={to} children={props => (
        <li className={props.match ? "active" : ""}>
            <Link to={to}>{children}</Link>
        </li>
    )
    }/>
}

(5) Link

Link就是渲染成一個a標籤,而後給一個點擊事件,點擊的時候更改HashRouter的狀態,驅動從新渲染。url

import React, {Component} from 'react';
import PropTypes from 'prop-types';

export default class Link extends Component {
    static contextTypes = {
        history: PropTypes.object
    }
    
    render() {
        return (
            <a onClick={() => this.context.history.push(this.props.to)}>{this.props.children}</a>
        )
    }
}

(6) Switch

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import pathToRegexp from 'path-to-regexp';

export default class Switch extends Component {
    static contextTypes = {
        location: PropTypes.object
    }
    
    render() {
        let {pathname} = this.context.location;
        
        let children = this.props.children;
        for (let i = 0, l = children.length; i < l; i++) {
            let child = children[i];
            let path = child.props.path;
            
            if (pathToRegexp(path, [], {end: false}).test(pathname)) {
                // 將匹配到的第一個元素返回
                return child;
            }
        }
        return null
    }
}

4 寫在最後

好了,這幾個功能介紹完了,你是否對HashRouter的原理有所瞭解了呢?本文只是貼出部分代碼,若是有須要請看demo能夠手動體驗一下哦。spa

參考文獻:code

相關文章
相關標籤/搜索