react後臺管理系統路由方案及react-router原理解析

monkeySoft發表於2020-06-09

    最近做了一個後臺管理系統主體框架是基於React進行開發的,因此係統的路由管理,選用了react-router(4.3.1)外掛進行路由頁面的管理配置。

實現原理剖析

1、hash的方式
    以 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();

    我們也可以自己進行模擬,可以寫成這樣:

function App() {
  // 進入頁面時,先初始化當前 url 對應的元件名
  let hash = window.location.hash
  let initUI = hash === '#login' ? 'login' : 'register'

  let [UI, setUI] = useState(initUI);
  let onClickLogin = () => {
    setUI('Login')
    window.location.hash = 'login'
  }
  let onClickRegister = () => {
    setUI('Register') 
    window.location.hash = 'register'
  }
  let showUI = () => {
    switch(UI) {
      case 'Login':
        return <Login/>
      case 'Register':
        return <Register/>
    }
  }
  return (
    <div className="App">
      <button onClick={onClickLogin}>Login</button>
      <button onClick={onClickRegister}>Register</button>
      <div>
          {showUI()}
      </div>
    </div>
  );
}

    這樣其實已經滿足我們的要求了,如果我在位址列裡輸入 localhost:8080/#login,就會顯示 。但是這個 “#” 符號不太好看,如果輸入 localhost:8080/login 就完美了。


2、history的方式
    H5 提供了一個好用的 history API,使用 window.history.pushState() 使得我們即可以修改 url 也可以不重新整理頁面,一舉兩得。現在只需要修改點選回撥裡的 window.location.pathname = 'xxx' 就可以了,用 window.history.pushState() 去代替。

function App() {
  // 進入頁面時,先初始化當前 url 對應的元件名
  let pathname = window.location.pathname
  let initUI = pathname === '/login' ? 'login' : 'register'

  let [UI, setUI] = useState(initUI);
  let onClickLogin = () => {
    setUI('Login')
    window.history.pushState(null, '', '/login')
  }
  let onClickRegister = () => {
    setUI('Register') 
    window.history.pushState(null, '', '/register')
  }
  let showUI = () => {
    switch(UI) {
      case 'Login':
        return <Login/>
      case 'Register':
        return <Register/>
    }
  }
  return (
    <div className="App">
      <button onClick={onClickLogin}>Login</button>
      <button onClick={onClickRegister}>Register</button>
      <div>
          {showUI()}
      </div>
    </div>
  );
}


3、link的實現
    react-router依賴基礎---history,history是一個獨立的第三方js庫,可以用來相容在不同瀏覽器、不同環境下對歷史記錄的管理,擁有統一的API。具體來說裡面的history分為三類:

  • 老瀏覽器的history: 主要通過hash來實現,對應createHashHistory,通過hash來儲存在不同狀態下的history資訊
  • 高版本瀏覽器: 通過html5裡面的history,對應createBrowserHistory,利用HTML5裡面的history
  • node環境下: 主要儲存在memeory裡面,對應createMemoryHistory,在記憶體中進行歷史記錄的儲存

執行URL前進

  • createBrowserHistory: pushState、replaceState
  • createHashHistory: location.hash=*** location.replace()
  • createMemoryHistory: 在記憶體中進行歷史記錄的儲存

執行URL回退

  • createBrowserHistory: popstate
  • createHashHistory: hashchange

React元件為什麼會更新
    其實無論是react-router. react-redux. 能夠使元件更新的根本原因,還是最後出發了setState函式;對於react-router,其實是對history原生物件的封裝,重新封裝了push函式,使得我們在push函式執行的時候,可以觸發在Router元件中元件裝載之前,執行了history.listener函式,該函式的主要作用就是給listeners陣列新增監聽函式,每次執行history.push的時候,都會執行listenrs陣列中新增的listener, 這裡的listener就是傳入的箭頭函式,功能是執行了Router元件的setState函式,Router執行了setState之後,會將當前url位址列對應的url傳遞下去,當Route元件匹配到該位址列的時候,就會渲染該元件,如果匹配不到,Route元件就返回null;

componentWillMount() {
  const { children, history } = this.props

  invariant(
    children == null || React.Children.count(children) === 1,
    'A <Router> may have only one child element'
  )

  // Do this here so we can setState when a <Redirect> changes the
  // location in componentWillMount. This happens e.g. when doing
  // server rendering using a <StaticRouter>.
  //這裡執行history.listen()方法;傳入一個函式;箭頭函式的this指的是父級的作用域中的this值;
  this.unlisten = history.listen(() => {
    this.setState({
      match: this.computeMatch(history.location.pathname)
    })
  })
}

react-router頁面跳轉基本原理
    react-router頁面跳轉的時候,主要是通過框架攔截監聽location的變化,然後根據location中的pathname去同步相對應的UI元件。
    其中在react-router中,URL對應location物件,而UI是有react components來決定的,因此我們要通過router宣告一份含有path to component的詳細對映關係路由表, 觸發 Link 後最終將通過如上面定義的路由表進行匹配,並拿到對應的 component 及 state 進行 render 渲染頁面。
從點選 Link 到 render 對應 component ,路由中發生了什麼
    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);
          });
      });
}

可看到,上面的程式碼中,主要分為兩部分:

  • 使用了 history 模組的 listen 註冊了一個含有 setState 的回撥函式(這樣就能使用 history 模組中的機制)
  • 回撥中的 match 方法為 react-router 所特有,match 函式根據當前 location 以及前面寫的 Route 路由表匹配出對應的路由子集得到新的路由狀態值 state,具體實現可見 react-router/matchRoutes ,再根據 state 得到對應的 component ,最終執行了 match 中的回撥 listener(null, nextState) ,即執行了 Router 中的監聽回撥(setState),從而更新了展示。

4、路由懶載入(元件按需載入)
    當React專案過大的時候,如果初次進入將所有的元件檔案全部載入,那麼將會大大的增加首屏載入的速度,進而影響使用者體驗。因此此時我們需要將路由元件進行按需載入,也就是說,當進入某個URL的時候,再去載入其對應的react component。目前路由的按需載入主要有以下幾種方式:

  • 1)react-loadable
       利用react-loadable這個高階元件,要做到實現按需載入這一點,我們將使用的webpack,react-loadable。使用例項如下:
import Loadable from 'react-loadable';
import Loading from './Loading';
const LoadableComponent = Loadable({
  loader: () => import('./Dashboard'),
  loading: Loading,
})
export default class LoadableDashboard extends React.Component {
  render() {
    return <LoadableComponent />;
  }
}
  • 2)在router3中的按需載入方式
       route3中實現按需載入只需要按照下面程式碼的方式實現就可以了。在router4以前,我們是使用getComponent的的方式來實現按需載入,getComponent是非同步的,只有在路由匹配時才會呼叫,router4中,getComponent方法已經被移除,所以這種方法在router4中不能使用。
    const about = (location, cb) => {
        require.ensure([], require => {
            cb(null, require('../Component/about').default)
        },'about')
    }
    //配置route
    <Route path="helpCenter" getComponent={about} />
  • 3)非同步元件
  • 建立一個非同步元件 AsyncComponent
import React from 'react';

export default function (getComponent) {
  return class AsyncComponent extends React.Component {
    static Component = null;
    state = { Component: AsyncComponent.Component };

    componentWillMount() {
      if (!this.state.Component) {
        getComponent().then(({default: Component}) => {
          AsyncComponent.Component = Component
          this.setState({ Component })
        })
      }
    }
    render() {
      const { Component } = this.state
      if (Component) {
        return <Component {...this.props} />
      }
      return null
    }
  }
}
  • 使用非同步元件:我們將使用asyncComponent動態匯入我們想要的元件。
import asyncComponent from './asyncComponent'
const Login = asyncComponent(() => load('login/login'))
const LayoutPage = asyncComponent(() => load('layout/layout'))
const NoticeDeatil = asyncComponent(() => load('noticeDetail/noticeDetail'))
export const appRouterMap = [
    {path:"/login",name:"Login",component:Login,auth:false},
    {path:"/web",name:"LayoutPage",component:LayoutPage,auth:false},
    {path:"/notice/:id",name:"NoticeDeatil",component:NoticeDeatil,auth:false},
]

使用方法

   這次主要是做一個後臺管理系統,因此使用的時候,需要考慮到頁面的內容區域以及固定區域的區別。內容區域的內容隨url的變化而變化,單固定區域內容保持不變,因此常規的路由配置並不能滿足該需求。因此使用的Route巢狀Route的方式實現,外層Route控制固定區域的變化,內層Route控制內容區域的變化。使用實現步驟如下:
1、安裝相關依賴
npm install react-router react-router-dom -S


2、配置路由---URL關係對映表

  • 固定區域路由配置
import login from '../pages/login/login'
import home from '../pages/home/home'

// `/`和`:`為關鍵字,不能作為引數傳遞
let routers = [{
  name: 'login',
  path: '/login',
  title: '登入',
  exact: true,
  component: login
}, {
  name: 'home', // 名稱,必須唯一
  path: '/home', // 路徑,第一個必須為'/',主名字必須唯一,瀏覽器導航路徑(url)
  title: '主頁', // 頁面title及導航欄顯示的名稱
  exact: false, // 嚴格匹配
  component: home
}
]

export default routers
  • 內容區域路由配置(此處使用了上面的第一種路由懶載入方法)
import React from 'react'
import Loadable from "react-loadable"
// 注意:引數名不能和路由任何一個path名相同;除path的引數後,path和name必須一樣;`/`和`:`為關鍵字,不能作為引數傳遞,parent: 'testpage', // 如果是二級路由,需要指定它的父級(必須)
let routers = [
  {
    name: 'testpage',
    path: '/system/user',
    title: '使用者管理',
    exact: false,
    component: Loadable({
      loader: () => import('../pages/system/user/user'),
      loading: () => <div className="page-loading"><span>載入中......</span></div>
    })
  },
]
export default routers

3、將路由注入專案

  • 固定區域(關鍵程式碼如下)
import Loading from '@components/Loading'
import routers from './routers'
import { HashRouter, Route, Switch, Redirect } from 'react-router-dom'
import page404 from './pages/404/404'
const App = () => {
  return (
    <div className="app">
      <Loading />
      <Switch>
        {routers.map((r, key) => (
          <Route key={key}
            {...r} />
        ))}
        <Redirect from="/"
          to={'/login'}
          exact={true} />
        <Route component={page404} />
      </Switch>
    </div>
  )
}
ReactDOM.render(
  <HashRouter>
    <ConfigProvider locale={zhCN}>
      <App />
    </ConfigProvider>
  </HashRouter>,
  document.getElementById('root')
)
  • 內容區域(home檔案關鍵程式碼如下)
import { Redirect, Route, Switch } from "react-router-dom"
import routers from '../../views/router'
import Page404 from '../404/404'

....省略無數程式碼
<Content className={styles.content}>
  <Switch>
    {routers && routers.map((r, key) => {
      const Component = r.component,
      return <Route key={key}
        render={props => <Component {...props}
          allRouters={routers}
          />}
        exact={r.exact}
        path={match + r.path} />
    })}
    <Route component={Page404} />
  </Switch>
</Content>
....省略無數程式碼

相關文章