react-router v5.x 源碼分析和理解 SPA router 的原理

這篇文章主要講的是分析 react-router 源碼,版本是 v5.x,以及 SPA 路由實現的原理。

文章首發地址

單頁面應用都用到了路由 router,目前來看實現路由有兩種方法 hash 路由和 H5 History API 實現。

而 react-router 路由,則是用到了 history 庫,該庫其實是對 hash 路由、history 路由、memory 路由(客戶端)進行了封裝。

下面先看看 hash 和 history 是怎樣實現路由的。

hash router

hash 是 location 的屬性,在 URL 中是 #後面的部分。如果沒有#,則返回空字符串。

hash 路由主要實現原理是:hash 改變時,頁面不發生跳轉,即 window 對象可以對 hash 改變進行監聽(hashchange 事件),只要 url 中 hash 發生改變,就會觸發回調。

利用這個特性,下面模擬實現一個 hash 路由。

實現步驟:

  • 初始化一個類
  • 記錄路由、history 歷史值
  • 添加 hashchange 事件,hash 發生改變時,觸發回調
  • 初始添加所有路由
  • 模擬前進和回退功能

代碼如下:

hash.html

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>hash router</title>
</head>
<body>
  <ul>
    <li><a href="#/">turn yellow</a></li>
    <li><a href="#/blue">turn blue</a></li>
    <li><a href="#/green">turn green</a></li>
  </ul>
  <button id='back'>back</button>
  <button id='forward'>forward</button>
  <script src="./hash.js"></script>
</body>
</html>

hash.js

class Routers {
  constructor() {
    this.routes = {}
    this.currentUrl = ''
    this.history = []; // 記錄 hash 歷史值
    this.currentIndex = this.history.length - 1;  // 默認指向 history 中最後一個
    // 默認前進、後退
    this.isBack = false;
    this.isForward = false;

    this.onHashChange = this.onHashChange.bind(this)
    this.backOff = this.backOff.bind(this)
    this.forward = this.forward.bind(this)

    window.addEventListener('load', this.onHashChange, false);
    window.addEventListener('hashchange', this.onHashChange, false);  // hash 變化監聽事件,support >= IE8
  }

  route(path, callback) {
    this.routes[path] = callback || function () { }
  }

  onHashChange() {
    // 既不是前進和後退,點擊 a 標籤時觸發
    if (!this.isBack && !this.isForward) {
      this.currentUrl = location.hash.slice(1) || '/'
      this.history.push(this.currentUrl)
      this.currentIndex++
    }

    this.routes[this.currentUrl]()

    this.isBack = false
    this.isForward = false
  }
  // 後退功能
  backOff() {
    this.isBack = true
    this.currentIndex = this.currentIndex <= 0 ? 0 : this.currentIndex - 1

    this.currentUrl = this.history[this.currentIndex]
    location.hash = `#${this.currentUrl}`
  }
  // 前進功能
  forward() {
    this.isForward = true
    this.currentIndex = this.currentIndex >= this.history.length - 1 ? this.history.length - 1 : this.currentIndex + 1

    this.currentUrl = this.history[this.currentIndex]
    location.hash = `#${this.currentUrl}`
  }
}
// 初始添加所有路由
window.Router = new Routers();
Router.route('/', function () {
  changeBgColor('yellow');
});
Router.route('/blue', function () {
  changeBgColor('blue');
});
Router.route('/green', function () {
  changeBgColor('green');
});

const content = document.querySelector('body');
const buttonBack = document.querySelector('#back');
const buttonForward = document.querySelector('#forward')

function changeBgColor(color) {
  content.style.backgroundColor = color;
}
// 模擬前進和回退
buttonBack.addEventListener('click', Router.backOff, false)
buttonForward.addEventListener('click', Router.forward, false)

hash 路由兼容性好,缺點就是實現一套路由比較複雜些。

在線預覽

history router

history 是 HTML5 新增的 API,允許操作瀏覽器的曾經在標籤頁或者框架裏訪問的會話歷史記錄。

history 包含的屬性和方法:

  • history.state 讀取歷史堆棧中最上面的值
  • history.length 瀏覽歷史中元素的數量
  • window.history.replaceState(obj, title, url) 取代當前瀏覽歷史中當前記錄
  • window.history.pushState(obj, title, url) 往瀏覽歷史中添加歷史記錄,不跳轉頁面
  • window.history.popstate(callback) 對歷史記錄發生變化進行監聽,state 發生改變時,觸發回調事件
  • window.history.back() 後退,等價於瀏覽器返回按鈕
  • window.history.forward() 前進,等價於瀏覽器前進按鈕
  • window.history.go(num) 前進或後退幾個頁面,num 爲負數時,後退幾個頁面

實現步驟:

  • 初始化一個類
  • 記錄路由
  • 添加 popstate 事件,state 發生改變時,觸發回調
  • 初始添加所有路由

代碼如下:

history.html

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>h5 router</title>
</head>
<body>
  <ul>
      <li><a href="/">turn yellow</a></li>
      <li><a href="/blue">turn blue</a></li>
      <li><a href="/green">turn green</a></li>
  </ul>
  <script src='./history.js'></script>
</body>
</html>

history.js

class Routers {
  constructor() {
    this.routes = {};
    this._bindPopState();
  }

  route(path, callback) {
    this.routes[path] = callback || function () { };
  }

  go(path) {
    history.pushState({ path: path }, null, path);
    this.routes[path] && this.routes[path]();
  }

  _bindPopState() {
    window.addEventListener('popstate', e => {  // 監聽 history.state 改變
      const path = e.state && e.state.path;
      this.routes[path] && this.routes[path]();
    });
  }
}

// 初始時添加所有路由
window.Router = new Routers();
Router.route('/', function () {
  changeBgColor('yellow');
});
Router.route('/blue', function () {
  changeBgColor('blue');
});
Router.route('/green', function () {
  changeBgColor('green');
});

const content = document.querySelector('body');
const ul = document.querySelector('ul');

function changeBgColor(color) {
  content.style.backgroundColor = color;
}

ul.addEventListener('click', e => {
  if (e.target.tagName === 'A') {
    debugger
    e.preventDefault();
    Router.go(e.target.getAttribute('href'));
  }
});

兼容性:support >= IE10

在線預覽

react-router

react-router 分爲四個包,分別爲 react-routerreact-router-domreact-router-configreact-router-native,其中 react-router-dom 是瀏覽器相關 API,react-router-native 是 React-Native 相關 API,react-router 是核心也是共同部分 API,react-router-config 是一些配置相關。

react-router 是 React指定路由,內部 API 的實現也是繼承 React 一些屬性和方法,所以 react-router 內 API 也是 React 組件。

react-router 還用到了 history 庫,這個庫主要是對 hash 路由、history 路由、memory 路由的封裝。

下面我們講到的是 react-router v5.x 版本,用到了 context,context 減少了組件之間顯性傳遞 props,具體用法可以看看官方文檔。

創建 context:

var createNamedContext = function createNamedContext(name) {
    var context = createContext();
    context.displayName = name;
    return context;
};

var context = createNamedContext("Router");

Router

定義一個類 Router,它繼承 React.Component 屬性和方法,所以 Router 也是 React 組件。

_inheritsLoose(Router, _React$Component);
// 等價於 
Router.prototype = Object.create(React.Component)  

在 Router 原型對象上添加 React 生命週期 componentDidMountcomponentWillUnmountrender 方法。

一般情況下,Router 都是作爲 Route 等其他子路由的上層路由,使用了 context.Provider,接收一個 value 屬性,傳遞 value 給消費子組件。

var _proto = Router.prototype; 
_proto.componentDidMount = function componentDidMount() {
    // todo
};

_proto.componentWillUnmount = function componentWillUnmount(){
    // todo
};

_proto.render = function render() {
    return React.createElement(context.Provider, props);
};

history 庫中有個方法 history.listen(callback(location)) 對 location 進行監聽,只要 location 發生變化了,就會 setState 更新 location,消費的子組件也可以拿到更新後的 location,從而渲染相應的組件。

核心源碼如下:

var Router =
    function (_React$Component) {
        // Router 從 React.Component 原型上的繼承屬性和方法
        _inheritsLoose(Router, _React$Component);  

        Router.computeRootMatch = function computeRootMatch(pathname) {
            return {
                path: "/",
                url: "/",
                params: {},
                isExact: pathname === "/"
            };
        };

        function Router(props) { // 首先定義一個類 Router,也是 React 組件
            var _this;

            _this = _React$Component.call(this, props) || this; // 繼承自身屬性和方法
            _this.state = {
                location: props.history.location
            };

            _this._isMounted = false;
            _this._pendingLocation = null;

            if (!props.staticContext) {  // 如果不是 staticRouter,則對 location 進行監聽
                _this.unlisten = props.history.listen((location) => { // 監聽 history.location 變化,如果有變化,則更新 locaiton
                    if (_this._isMounted) {
                        _this.setState({
                            location: location
                        });
                    } else {
                        _this._pendingLocation = location;
                    }
                });
            }

            return _this;
        }

        var _proto = Router.prototype;  // 組件需要有生命週期,在原型對象上添加 componentDidMount、componentWillUnmount、render 方法

        _proto.componentDidMount = function componentDidMount() {
            this._isMounted = true;

            if (this._pendingLocation) {
                this.setState({
                    location: this._pendingLocation
                });
            }
        };

        _proto.componentWillUnmount = function componentWillUnmount() {
            if (this.unlisten) this.unlisten();  // 停止監聽 location
        };

        _proto.render = function render() {
            // 使用了 React Context 傳遞 history、location、match、staticContext,使得所有子組件都可以獲取這些屬性和方法
            // const value = {
            //   history: this.props.history,
            //   location: this.state.location,
            //   match: Router.computeRootMatch(this.state.location.pathname),
            //   staticContext: this.props.staticContext
            // }

            // return (
            //   <context.Provider value={value}>
            //     {this.props.children}
            //   </context.Provider>
            // )
            return React.createElement(context.Provider, {
                children: this.props.children || null,
                value: {
                    history: this.props.history,
                    location: this.state.location,
                    match: Router.computeRootMatch(this.state.location.pathname),
                    staticContext: this.props.staticContext // staticContext 是 staticRouter 中的 API,不是公用 API
                }
            });
        };

        return Router;
    }(React.Component);

Route

Route 一般作爲 Router 的子組件,主要是匹配 path 和渲染相應的組件。

Route 使用了 context.Consumer(消費組件),訂閱了 Router 提供的 context,一旦 location 發生改變,context 也會改變,判斷當前 location.pathname 是否與子組件的 path 是否匹配,如果匹配,則渲染對應組件,否則就不渲染。

Route 因爲有些庫傳遞組件方式不同,所以有多種渲染,部分代碼如下:

const {children, render, component} = this.props
let renderEle = null;

// 如果有 children,則渲染 children
if (children &&  !isEmptyChildren(children)) renderEle = children

// 如果組件傳遞的是 render 及 match 是匹配的,則渲染 render
if (render && match) renderEle = render(props)

// 如果組件傳遞 component 及 match 是匹配的,則渲染 component 
if (component && match) renderEle = React.createElement(component, props)

return (
    <context.Provider value={obj}>
        {renderEle}
    </context.Provider>
)

核心源碼如下:

var Route =
    function (_React$Component) {
        _inheritsLoose(Route, _React$Component);

        function Route() {
            return _React$Component.apply(this, arguments) || this;
        }

        var _proto = Route.prototype;

        _proto.render = function render() {
            var _this = this;
            // context.Consumer 每個 Route 組件都可以消費 Router 中 Provider 提供的 context
            return React.createElement(context.Consumer, null, function (context$$1) {
                var location = _this.props.location || context$$1.location;
                var match = _this.props.computedMatch ? _this.props.computedMatch
                    : _this.props.path ? matchPath(location.pathname, _this.props) : context$$1.match;  // 是否匹配當前路徑

                var props = _extends({}, context$$1, { // 處理用 context 傳遞的還是自己傳遞的 location 和 match
                    location: location,
                    match: match
                });

                var _this$props = _this.props,
                    children = _this$props.children,
                    component = _this$props.component,  
                    render = _this$props.render;

                if (Array.isArray(children) && children.length === 0) {
                    children = null;
                }

                // let renderEle = null

                // 如果有 children,則渲染 children
                // if (children &&  !isEmptyChildren(children)) renderEle = children

                // 如果組件傳遞的是 render 及 match 是匹配的,則渲染 render
                // if (render && props.match) renderEle = render(props)

                //  如果組件傳遞 component 及 match 是匹配的,則渲染 component 
                // if (component && props.match) renderEle = React.createElement(component, props)

                // return (
                //     <context.Provider value={props}>
                //        {renderEle}
                //     </context.Provider>
                // )

                return React.createElement(context.Provider, { // Route 內定義一個 Provider,給 children 傳遞 props
                    value: props
                }, children && !isEmptyChildren(children) ? children : props.match ? component ? React.createElement(component, props) : render ? render(props) : null : null);
            });
        };

        return Route;
    }(React.Component);

Redirect

重定向路由:from 是從哪個組件來,to 表示要定向到哪裏。

 <Redirect from="home" to="dashboard" />

根據有沒傳屬性 push,有傳則是往 state 堆棧中新增(history.push),否則就是替代(history.replace)當前 state。

Redirect 使用了 context.Consumer(消費組件),訂閱了 Router 提供的 context,一旦 location 發生改變,context 也會改變,則也會觸發重定向。

源碼如下:

function Redirect(_ref) {
    var computedMatch = _ref.computedMatch,
        to = _ref.to,
        _ref$push = _ref.push,
        push = _ref$push === void 0 ? false : _ref$push;
    return React.createElement(context.Consumer, null, (context$$1) => { // context.Consumer 第三個參數是一個函數
        var history = context$$1.history,
            staticContext = context$$1.staticContext;

        // method 方法:判斷是否是替換(replace)當前的 state,還是往 state 堆棧中添加(push)一個新的 state
        var method = push ? history.push : history.replace;
        // 生成新的 location
        var location = createLocation(computedMatch ? typeof to === "string" ? generatePath(to, computedMatch.params) : _extends({}, to, {
            pathname: generatePath(to.pathname, computedMatch.params)
        }) : to); 

        if (staticContext) { // 當渲染一個靜態的 context 時(staticRouter),立即設置新 location
            method(location);
            return null;
        }
        // Lifecycle 是一個 return null 的空組件,但定義了 componentDidMount、componentDidUpdate、componentWillUnmount 生命週期
        return React.createElement(Lifecycle, {
            onMount: function onMount() {
                method(location);
            },
            onUpdate: function onUpdate(self, prevProps) {
                var prevLocation = createLocation(prevProps.to);
                // 觸發更新時,對比前後 location 是否相等,不相等,則更新 location
                if (!locationsAreEqual(prevLocation, _extends({}, location, {
                    key: prevLocation.key
                }))) {
                    method(location);
                }
            },
            to: to
        });
    });
}

Switch

Switch 組件的子組件必須是 Route 或 Redirect 組件。

Switch 使用了 context.Consumer(消費組件),訂閱了 Router 提供的 context,一旦 location 發生改變,context 也會改變,判斷當前 location.pathname 是否與子組件的 path 是否匹配,如果匹配,則渲染對應的子組件,其他都不渲染。

源碼如下:

var Switch =
    function (_React$Component) {
        _inheritsLoose(Switch, _React$Component);

        function Switch() {
            return _React$Component.apply(this, arguments) || this;
        }

        var _proto = Switch.prototype;

        _proto.render = function render() {
            var _this = this;

            return React.createElement(context.Consumer, null, function (context$$1) {
                var location = _this.props.location || context$$1.location;
                var element, match; 
               
                React.Children.forEach(_this.props.children, function (child) {
                    if (match == null && React.isValidElement(child)) {
                        element = child;
                        var path = child.props.path || child.props.from;
                        match = path ? matchPath(location.pathname, _extends({}, child.props, {
                            path: path
                        })) : context$$1.match;
                    }
                });
                return match ? React.cloneElement(element, {
                    location: location,
                    computedMatch: match  // 加強版的 match
                }) : null;
            });
        };

        return Switch;
    }(React.Component);

Link

Link 組件作用是跳轉到指定某個路由。Link 實際是對 <a> 標籤進行了封裝。

點擊時會觸發以下:

  • 改變 url,但使用了 e.preventDefault(),所以頁面沒有發生跳轉。
  • 根據是否傳遞屬性 replace,有傳就是替代當前 state(history.replace),否則是往 state 堆棧中新增(history.push),從而路由發生了改變。
  • 路由發生了改變,由於 Router 中有對 location 進行監聽,從而通過 context 傳遞給消費子組件,匹配 path 是否相同,渲染相應的組件。

核心源碼如下:

function LinkAnchor(_ref) {
    var innerRef = _ref.innerRef,
        navigate = _ref.navigate,
        _onClick = _ref.onClick,
        rest = _objectWithoutPropertiesLoose(_ref, ["innerRef", "navigate", "onClick"]); // 剩餘屬性

    var target = rest.target;
    return React.createElement("a", _extends({}, rest, {  // a 標籤
        ref: innerRef ,
        onClick: function onClick(event) {
            try {
                if (_onClick) _onClick(event);
            } catch (ex) {
                event.preventDefault(); // 使用 e.preventDefault() 防止跳轉
                throw ex;
            }

            if (!event.defaultPrevented && // onClick prevented default
                event.button === 0 && ( // ignore everything but left clicks
                    !target || target === "_self") && // let browser handle "target=_blank" etc.
                !isModifiedEvent(event) // ignore clicks with modifier keys
            ) {
                event.preventDefault();
                navigate(); // 改變 location
            }
        }
    }));
}

function Link(_ref2) {
    var _ref2$component = _ref2.component,
        component = _ref2$component === void 0 ? LinkAnchor : _ref2$component,
        replace = _ref2.replace,
        to = _ref2.to, // to 跳轉鏈接的路徑
        rest = _objectWithoutPropertiesLoose(_ref2, ["component", "replace", "to"]);

    return React.createElement(__RouterContext.Consumer, null, function (context) {
        var history = context.history;
        // 根據 to 生成新的 location
        var location = normalizeToLocation(resolveToLocation(to, context.location), context.location);
        var href = location ? history.createHref(location) : "";

        return React.createElement(component, _extends({}, rest, {
            href: href,
            navigate: function navigate() {
                var location = resolveToLocation(to, context.location);
                var method = replace ? history.replace : history.push;
                method(location); // 如果有傳 replace,則是替換掉當前 location,否則往 history 堆棧中添加一個 location
            }
        }));
    });
}

BrowserRouter

BrowserRouter 用到了 H5 history API,所以可以使用 pushState、replaceState 等方法。

源碼中主要是用到了 history 庫 createBrowserHistory 方法創建了封裝過 history 對象。

把封裝過的 history 對象傳遞給 Router 的 props。

var BrowserRouter =
    function (_React$Component) {
        _inheritsLoose(BrowserRouter, _React$Component); // BrowserRouter 繼承 React.Component 屬性和方法

        function BrowserRouter() {
            var _this;

            for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
                args[_key] = arguments[_key];
            }

            _this = _React$Component.call.apply(_React$Component, [this].concat(args)) || this; // 繼承自身屬性和方法
            _this.history = createBrowserHistory(_this.props); // 創建 browser history 對象,是支持 HTML 5 的 history API
            return _this;
        }

        var _proto = BrowserRouter.prototype;

        _proto.render = function render() {
            return React.createElement(Router, { // 以 Router 爲 element,history 和 children 作爲 Router 的 props
                history: this.history,
                children: this.props.children
            });
        };

        return BrowserRouter;
    }(React.Component);

HashRouter

HashRouter 與 BrowserRouter 的區別,主要是創建是以 window.location.hash 爲對象,返回一個 history,主要是考慮到兼容性問題。

var HashRouter =
    function (_React$Component) {
        _inheritsLoose(HashRouter, _React$Component);

        function HashRouter() {
            var _this;

            for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
                args[_key] = arguments[_key];
            }

            _this = _React$Component.call.apply(_React$Component, [this].concat(args)) || this;
            _this.history = createHashHistory(_this.props); // 創建 hash history 對象,兼容老瀏覽器 hash,其他和 BrowserRouter 沒有大區別
            return _this;
        }

        var _proto = HashRouter.prototype;

        _proto.render = function render() {
            return React.createElement(Router, {
                history: this.history,
                children: this.props.children
            });
        };

        return HashRouter;
    }(React.Component);

總結

  • react-router 分爲四個包,分別爲 react-routerreact-router-domreact-router-configreact-router-native,其中 react-router-dom 是瀏覽器相關 API,react-router-native 是 React-Native 相關 API,react-router 是核心也是共同部分 API,react-router-config 是一些配置相關。
  • react-router 是 React指定路由,內部 API 的實現也是繼承 React 一些屬性和方法,所以說 react-router 內 API 也是 React 組件。
  • react-router 還用到了 history 庫,這個庫主要是對 hash 路由、history 路由、memory 路由的封裝。
  • Router 都是作爲 Route 等其他子路由的上層路由,使用了 context.Provider,接收一個 value 屬性,傳遞 value 給消費子組件。
  • history 庫中有個方法 history.listen(callback(location)) 對 location 進行監聽,點擊某個 Link 組件,改變了 location,只要 location 發生變化了,通過 context 傳遞改變後的 location,消費的子組件拿到更新後的 location,從而渲染相應的組件。

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章