zoukankan      html  css  js  c++  java
  • 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>
    ....省略无数代码
    
  • 相关阅读:
    CODING x 百果园 _ 水果零售龙头迈出 DevOps 体系建设第一步
    Nocalhost 亮相 CD Foundation 国内首届 Meetup,Keith Chan 将出席致辞
    做云原生时代标准化工具,实现高效云上研发工作流
    打造数字化软件工厂 —— 一站式 DevOps 平台全景解读
    WePack —— 助力企业渐进式 DevOps 转型
    CODING Compass —— 打造行云流水般的软件工厂
    Nocalhost —— 让云原生开发回归原始而又简单
    CODING 代码资产安全系列之 —— 构建全链路安全能力,守护代码资产安全
    Nocalhost:云原生开发新体验
    使用 Nocalhost 开发 Kubernetes 中的 APISIX Ingress Controller
  • 原文地址:https://www.cnblogs.com/monkeySoft/p/13071997.html
Copyright © 2011-2022 走看看