前端路由實現與 react-router 源碼分析

原文地址:https://github.com/joeyguo/blog/issues/2javascript

在單頁應用上,前端路由並不陌生。不少前端框架也會有獨立開發或推薦配套使用的路由系統。那麼,當咱們在談前端路由的時候,還能夠談些什麼?本文將簡要分析並實現一個的前端路由,並對 react-router 進行分析。html

一個極簡前端路由實現

說一下前端路由實現的簡要原理,以 hash 形式(也可使用 History API 來處理)爲例,當 url 的 hash 發生變化時,觸發 hashchange 註冊的回調,回調中去進行不一樣的操做,進行不一樣的內容的展現。直接看代碼或許更直觀。前端

function Router() {
    this.routes = {};
    this.currentUrl = '';
}
Router.prototype.route = function(path, callback) {
    this.routes[path] = callback || function(){};
};
Router.prototype.refresh = function() {
    this.currentUrl = location.hash.slice(1) || '/';
    this.routes[this.currentUrl]();
};
Router.prototype.init = function() {
    window.addEventListener('load', this.refresh.bind(this), false);
    window.addEventListener('hashchange', this.refresh.bind(this), false);
}
window.Router = new Router();
window.Router.init();

上面路由系統 Router 對象實現,主要提供三個方法java

  • init 監聽瀏覽器 url hash 更新事件react

  • route 存儲路由更新時的回調到回調數組routes中,回調函數將負責對頁面的更新git

  • refresh 執行當前url對應的回調函數,更新頁面github

Router 調用方式以及呈現效果以下:點擊觸發 url 的 hash 改變,並對應地更新內容(這裏爲 body 背景色)api

<ul> 
    <li><a href="#/">turn white</a></li> 
    <li><a href="#/blue">turn blue</a></li> 
    <li><a href="#/green">turn green</a></li> 
</ul>
var content = document.querySelector('body');
// change Page anything
function changeBgColor(color) {
    content.style.backgroundColor = color;
}
Router.route('/', function() {
    changeBgColor('white');
});
Router.route('/blue', function() {
    changeBgColor('blue');
});
Router.route('/green', function() {
    changeBgColor('green');
});

20160513_150041

以上爲一個前端路由的簡單實現,點擊查看完整代碼,雖然簡單,但實際上不少路由系統的根基都立於此,其餘路由系統主要是對自身使用的框架機制的進行配套及優化,如與 react 配套的 react-router。數組

react-router 分析

react-router 與 history 結合形式

react-router 是基於 history 模塊提供的 api 進行開發的,結合的形式本文記爲 包裝方式。因此在開始對其分析以前,先舉一個簡單的例子來講明如何進行對象的包裝。瀏覽器

// 原對象
var historyModule = {
    listener: [],
    listen: function (listener) {
        this.listener.push(listener);
        console.log('historyModule listen..')
    },
    updateLocation: function(){
        this.listener.forEach(function(listener){
            listener('new localtion');
        })
    }
}
// Router 將使用 historyModule 對象,並對其包裝
var Router = {
    source: {},
    init: function(source){
        this.source = source;
    },
    // 對 historyModule的listen進行了一層包裝
    listen: function(listener) {
        return this.source.listen(function(location){
            console.log('Router listen tirgger.');
            listener(location);
        })
    }
}
// 將 historyModule 注入進 Router 中
Router.init(historyModule);
// Router 註冊監聽
Router.listen(function(location){
    console.log(location + '-> Router setState.');
})
// historyModule 觸發回調
historyModule.updateLocation();

返回:
22

可看到 historyModule 中含有機制:historyModule.updateLocation() -> listener( ),Router 經過對其進行包裝開發,針對 historyModule 的機制對 Router 也起到了做用,即historyModule.updateLocation() 將觸發 Router.listen 中的回調函數 。點擊查看完整代碼
這種包裝形式可以充分利用原對象(historyModule )的內部機制,減小開發成本,也更好的分離包裝函數(Router)的邏輯,減小對原對象的影響。

react-router 使用方式

react-router 以 react component 的組件方式提供 API, 包含 Router,Route,Redirect,Link 等等,這樣可以充分利用 react component 提供的生命週期特性,同時也讓定義路由跟寫 react component 達到統一,以下

render((
  <Router history={browserHistory}>
    <Route path="/" component={App}>
      <Route path="about" component={About}/>
      <Route path="users" component={Users}>
        <Route path="/user/:userId" component={User}/>
      </Route>
      <Route path="*" component={NoMatch}/>
    </Route>
  </Router>
), document.body)

就這樣,聲明瞭一份含有 path to component 的各個映射的路由表。

react-router 還提供的 Link 組件(以下),做爲提供更新 url 的途徑,觸發 Link 後最終將經過如上面定義的路由表進行匹配,並拿到對應的 component 及 state 進行 render 渲染頁面。

<Link to={`/user/89757`}>'joey'</Link>

這裏不細講 react-router 的使用,詳情可見:https://github.com/reactjs/react-router

從點擊 Link 到 render 對應 component ,路由中發生了什麼

爲什麼可以觸發 render component ?

主要是由於觸發了 react setState 的方法從而可以觸發 render component。
從頂層組件 Router 出發(下面代碼從 react-router/Router 中摘取),可看到 Router 在 react component 生命週期之組件被掛載前 componentWillMount 中使用 this.history.listen 去註冊了 url 更新的回調函數。回調函數將在 url 更新時觸發,回調中的 setState 起到 render 了新的 component 的做用。

Router.prototype.componentWillMount = function componentWillMount() {
    // .. 省略其餘
    var createHistory = this.props.history;
    
    this.history = _useRoutes2['default'](createHistory)({
      routes: _RouteUtils.createRoutes(routes || children),
      parseQueryString: parseQueryString,
      stringifyQuery: stringifyQuery
    });
    
    this._unlisten = this.history.listen(function (error, state) {
        _this.setState(state, _this.props.onUpdate);
    });
  };

上面的 _useRoutes2 對 history 操做即是對其作一層包裝,因此調用的 this.history 實際爲包裝之後的對象,該對象含有 _useRoutes2 中的 listen 方法,以下

function listen(listener) {
      return history.listen(function (location) {
          // .. 省略其餘
          match(location, function (error, redirectLocation, nextState) {
            listener(null, nextState);
          });
      });
}

可看到,上面代碼中,主要分爲兩部分

  1. 使用了 history 模塊的 listen 註冊了一個含有 setState 的回調函數(這樣就能使用 history 模塊中的機制)

  2. 回調中的 match 方法爲 react-router 所特有,match 函數根據當前 location 以及前面寫的 Route 路由表匹配出對應的路由子集獲得新的路由狀態值 state,具體實現可見 react-router/matchRoutes ,再根據 state 獲得對應的 component ,最終執行了 match 中的回調 listener(null, nextState) ,即執行了 Router 中的監聽回調(setState),從而更新了展現。

以上,爲起始註冊的監聽,及回調的做用。

如何觸發監聽的回調函數的執行?

這裏還得從如何更新 url 提及。通常來講,url 更新主要有兩種方式:簡單的 hash 更新或使用 history api 進行地址更新。在 react-router 中,其提供了 Link 組件,該組件能在 render 中使用,最終會表現爲 a 標籤,並將 Link 中的各個參數組合放它的 href 屬性中。能夠從 react-router/ Link 中看到,對該組件的點擊事件進行了阻止了瀏覽器的默認跳轉行爲,而改用 history 模塊的 pushState 方法去觸發 url 更新。

Link.prototype.render = function render() {
    // .. 省略其餘
    props.onClick = function (e) {
      return _this.handleClick(e);
    };
    if (history) {
     // .. 省略其餘
      props.href = history.createHref(to, query);
    }
    return _react2['default'].createElement('a', props);
};
  
Link.prototype.handleClick = function handleClick(event) {
    // .. 省略其餘
    event.preventDefault();
    this.context.history.pushState(this.props.state, this.props.to, this.props.query);
};

對 history 模塊的 pushState 方法對 url 的更新形式,一樣分爲兩種,分別在 history/createBrowserHistory 及 history/createHashHistory 各自的 finishTransition 中,如 history/createBrowserHistory 中使用的是 window.history.replaceState(historyState, null, path); 而 history/createHashHistory 則使用 window.location.hash = url,調用哪一個是根據咱們一開始建立 history 的方式。

更新 url 的顯示是一部分,另外一部分是根據 url 去更新展現,也就是觸發前面的監聽。這是在前面 finishTransition 更新 url 以後實現的,調用的是 history/createHistory 中的 updateLocation 方法,changeListeners 中爲 history/createHistory 中的 listen 中所添加的,以下

function updateLocation(newLocation) {
   // 示意代碼
    location = newLocation;
    changeListeners.forEach(function (listener) {
      listener(location);
    });
}
function listen(listener) {
     // 示意代碼
    changeListeners.push(listener);
}

總結

能夠將以上 react-router 的整個包裝閉環總結爲

  1. 回調函數:含有可以更新 react UI 的 react setState 方法。

  2. 註冊回調:在 Router componentWillMount 中使用 history.listen 註冊的回調函數,最終放在 history 模塊的 回調函數數組 changeListeners 中。

  3. 觸發回調:Link 點擊觸發 history 中回調函數數組 changeListeners 的執行,從而觸發原來 listen 中的 setState 方法,更新了頁面

至於前進與後退的實現,是經過監聽 popstate 以及 hashchange 的事件,當前進或後退 url 更新時,觸發這兩個事件的回調函數,回調的執行方式 Link 大體相同,最終一樣更新了 UI ,這裏就再也不說明。

react-router 主要是利用底層 history 模塊的機制,經過結合 react 的架構機制作一層包裝,實際自身的內容並很少,但其包裝的思想筆者認爲很值得學習,有興趣的建議閱讀下源碼,相信會有其餘收穫。

相關文章
相關標籤/搜索