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

whynotgonow發表於2018-04-08

1 要實現的功能

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

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

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

2 實現的邏輯

先不說程式碼是怎樣寫的,先上圖,讓大家看一下這個HashRouter到底是個什麼東東:

HashRouter實現邏輯圖1
好吧,肯定有人會說這些圈圈又是什麼東東呀,客官不要著急,待我慢慢解釋:

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

介紹了一下簡單的邏輯,接下來我們就看一下具體是怎樣實現的吧,如下圖:

HashRouter實現邏輯圖2

  • 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驅動頁面的重新渲染。

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,驅動重新渲染。

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的狀態,驅動重新渲染。

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可以手動體驗一下哦。

參考文獻:

相關文章