• react后台管理系统路由方案及react-router原理解析


        最近做了一个后台管理系统主体框架是基于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>
    ....省略无数代码
    
  • 相关阅读:
    误区30日谈25-30
    误区30日谈21-24
    误区30日谈16-20
    误区30日谈11-15
    误区30日谈6-10
    eclipse如何导入java项目文件
    Spring配置bean文件的底层实现方式
    hibernate中get,load,list,iterate的用法及比较
    MySQL数据库的事务管理
    单元格样式
  • 原文地址:https://www.cnblogs.com/monkeySoft/p/13071997.html
Copyright © 2020-2023  润新知