最近做了一个后台管理系统主体框架是基于React进行开发的,因此系统的路由管理,选用了react-router(4.3.1)插件进行路由页面的管理配置。

实现原理剖析

1、hash的方式

    以 hash 形式(也可以使用 History API 来处理)为例,当 url 的 hash 发生变化时,触发 hashchange 注册的回调,回调中去进行不同的操作,进行不同的内容的展示

  1. function Router() {
  2. this.routes = {};
  3. this.currentUrl = '';
  4. }
  5. Router.prototype.route = function(path, callback) {
  6. this.routes[path] = callback || function(){};
  7. };
  8. Router.prototype.refresh = function() {
  9. this.currentUrl = location.hash.slice(1) || '/';
  10. this.routes[this.currentUrl]();
  11. };
  12. Router.prototype.init = function() {
  13. window.addEventListener('load', this.refresh.bind(this), false);
  14. window.addEventListener('hashchange', this.refresh.bind(this), false);
  15. }
  16. window.Router = new Router();
  17. window.Router.init();

    我们也可以自己进行模拟,可以写成这样:

  1. function App() {
  2. // 进入页面时,先初始化当前 url 对应的组件名
  3. let hash = window.location.hash
  4. let initUI = hash === '#login' ? 'login' : 'register'
  5. let [UI, setUI] = useState(initUI);
  6. let onClickLogin = () => {
  7. setUI('Login')
  8. window.location.hash = 'login'
  9. }
  10. let onClickRegister = () => {
  11. setUI('Register')
  12. window.location.hash = 'register'
  13. }
  14. let showUI = () => {
  15. switch(UI) {
  16. case 'Login':
  17. return <Login/>
  18. case 'Register':
  19. return <Register/>
  20. }
  21. }
  22. return (
  23. <div className="App">
  24. <button onClick={onClickLogin}>Login</button>
  25. <button onClick={onClickRegister}>Register</button>
  26. <div>
  27. {showUI()}
  28. </div>
  29. </div>
  30. );
  31. }

    这样其实已经满足我们的要求了,如果我在地址栏里输入 localhost:8080/#login,就会显示 。但是这个 “#” 符号不太好看,如果输入 localhost:8080/login 就完美了。


2、history的方式

    H5 提供了一个好用的 history API,使用 window.history.pushState() 使得我们即可以修改 url 也可以不刷新页面,一举两得。现在只需要修改点击回调里的 window.location.pathname = 'xxx' 就可以了,用 window.history.pushState() 去代替。

  1. function App() {
  2. // 进入页面时,先初始化当前 url 对应的组件名
  3. let pathname = window.location.pathname
  4. let initUI = pathname === '/login' ? 'login' : 'register'
  5. let [UI, setUI] = useState(initUI);
  6. let onClickLogin = () => {
  7. setUI('Login')
  8. window.history.pushState(null, '', '/login')
  9. }
  10. let onClickRegister = () => {
  11. setUI('Register')
  12. window.history.pushState(null, '', '/register')
  13. }
  14. let showUI = () => {
  15. switch(UI) {
  16. case 'Login':
  17. return <Login/>
  18. case 'Register':
  19. return <Register/>
  20. }
  21. }
  22. return (
  23. <div className="App">
  24. <button onClick={onClickLogin}>Login</button>
  25. <button onClick={onClickRegister}>Register</button>
  26. <div>
  27. {showUI()}
  28. </div>
  29. </div>
  30. );
  31. }

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;

  1. componentWillMount() {
  2. const { children, history } = this.props
  3. invariant(
  4. children == null || React.Children.count(children) === 1,
  5. 'A <Router> may have only one child element'
  6. )
  7. // Do this here so we can setState when a <Redirect> changes the
  8. // location in componentWillMount. This happens e.g. when doing
  9. // server rendering using a <StaticRouter>.
  10. //这里执行history.listen()方法;传入一个函数;箭头函数的this指的是父级的作用域中的this值;
  11. this.unlisten = history.listen(() => {
  12. this.setState({
  13. match: this.computeMatch(history.location.pathname)
  14. })
  15. })
  16. }

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 的作用。

  1. Router.prototype.componentWillMount = function componentWillMount() {
  2. // .. 省略其他
  3. var createHistory = this.props.history;
  4. this.history = _useRoutes2[‘default‘](createHistory)({
  5. routes: _RouteUtils.createRoutes(routes || children),
  6. parseQueryString: parseQueryString,
  7. stringifyQuery: stringifyQuery
  8. });
  9. this._unlisten = this.history.listen(function (error, state) {
  10. _this.setState(state, _this.props.onUpdate);
  11. });
  12. };

上面的 _useRoutes2 对 history 操作便是对其做一层包装,所以调用的 this.history 实际为包装以后的对象,该对象含有 _useRoutes2 中的 listen 方法,如下:

  1. function listen(listener) {
  2. return history.listen(function (location) {
  3. // .. 省略其他
  4. match(location, function (error, redirectLocation, nextState) {
  5. listener(null, nextState);
  6. });
  7. });
  8. }

可看到,上面的代码中,主要分为两部分:

  • 使用了 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。使用实例如下:
  1. import Loadable from 'react-loadable';
  2. import Loading from './Loading';
  3. const LoadableComponent = Loadable({
  4. loader: () => import('./Dashboard'),
  5. loading: Loading,
  6. })
  7. export default class LoadableDashboard extends React.Component {
  8. render() {
  9. return <LoadableComponent />;
  10. }
  11. }
  • 2)在router3中的按需加载方式

       route3中实现按需加载只需要按照下面代码的方式实现就可以了。在router4以前,我们是使用getComponent的的方式来实现按需加载,getComponent是异步的,只有在路由匹配时才会调用,router4中,getComponent方法已经被移除,所以这种方法在router4中不能使用。
  1. const about = (location, cb) => {
  2. require.ensure([], require => {
  3. cb(null, require('../Component/about').default)
  4. },'about')
  5. }
  6. //配置route
  7. <Route path="helpCenter" getComponent={about} />
  • 3)异步组件
  • 创建一个异步组件 AsyncComponent
  1. import React from 'react';
  2. export default function (getComponent) {
  3. return class AsyncComponent extends React.Component {
  4. static Component = null;
  5. state = { Component: AsyncComponent.Component };
  6. componentWillMount() {
  7. if (!this.state.Component) {
  8. getComponent().then(({default: Component}) => {
  9. AsyncComponent.Component = Component
  10. this.setState({ Component })
  11. })
  12. }
  13. }
  14. render() {
  15. const { Component } = this.state
  16. if (Component) {
  17. return <Component {...this.props} />
  18. }
  19. return null
  20. }
  21. }
  22. }
  • 使用异步组件:我们将使用asyncComponent动态导入我们想要的组件。
  1. import asyncComponent from './asyncComponent'
  2. const Login = asyncComponent(() => load('login/login'))
  3. const LayoutPage = asyncComponent(() => load('layout/layout'))
  4. const NoticeDeatil = asyncComponent(() => load('noticeDetail/noticeDetail'))
  5. export const appRouterMap = [
  6. {path:"/login",name:"Login",component:Login,auth:false},
  7. {path:"/web",name:"LayoutPage",component:LayoutPage,auth:false},
  8. {path:"/notice/:id",name:"NoticeDeatil",component:NoticeDeatil,auth:false},
  9. ]

使用方法

   这次主要是做一个后台管理系统,因此使用的时候,需要考虑到页面的内容区域以及固定区域的区别。内容区域的内容随url的变化而变化,单固定区域内容保持不变,因此常规的路由配置并不能满足该需求。因此使用的Route嵌套Route的方式实现,外层Route控制固定区域的变化,内层Route控制内容区域的变化。使用实现步骤如下:

1、安装相关依赖

npm install react-router react-router-dom -S


2、配置路由---URL关系映射表

  • 固定区域路由配置
  1. import login from '../pages/login/login'
  2. import home from '../pages/home/home'
  3. // `/`和`:`为关键字,不能作为参数传递
  4. let routers = [{
  5. name: 'login',
  6. path: '/login',
  7. title: '登录',
  8. exact: true,
  9. component: login
  10. }, {
  11. name: 'home', // 名称,必须唯一
  12. path: '/home', // 路径,第一个必须为'/',主名字必须唯一,浏览器导航路径(url)
  13. title: '主页', // 页面title及导航栏显示的名称
  14. exact: false, // 严格匹配
  15. component: home
  16. }
  17. ]
  18. export default routers
  • 内容区域路由配置(此处使用了上面的第一种路由懒加载方法)
  1. import React from 'react'
  2. import Loadable from "react-loadable"
  3. // 注意:参数名不能和路由任何一个path名相同;除path的参数后,path和name必须一样;`/`和`:`为关键字,不能作为参数传递,parent: 'testpage', // 如果是二级路由,需要指定它的父级(必须)
  4. let routers = [
  5. {
  6. name: 'testpage',
  7. path: '/system/user',
  8. title: '用户管理',
  9. exact: false,
  10. component: Loadable({
  11. loader: () => import('../pages/system/user/user'),
  12. loading: () => <div className="page-loading"><span>加载中......</span></div>
  13. })
  14. },
  15. ]
  16. export default routers

3、将路由注入项目

  • 固定区域(关键代码如下)
  1. import Loading from '@components/Loading'
  2. import routers from './routers'
  3. import { HashRouter, Route, Switch, Redirect } from 'react-router-dom'
  4. import page404 from './pages/404/404'
  5. const App = () => {
  6. return (
  7. <div className="app">
  8. <Loading />
  9. <Switch>
  10. {routers.map((r, key) => (
  11. <Route key={key}
  12. {...r} />
  13. ))}
  14. <Redirect from="/"
  15. to={'/login'}
  16. exact={true} />
  17. <Route component={page404} />
  18. </Switch>
  19. </div>
  20. )
  21. }
  22. ReactDOM.render(
  23. <HashRouter>
  24. <ConfigProvider locale={zhCN}>
  25. <App />
  26. </ConfigProvider>
  27. </HashRouter>,
  28. document.getElementById('root')
  29. )
  • 内容区域(home文件关键代码如下)
  1. import { Redirect, Route, Switch } from "react-router-dom"
  2. import routers from '../../views/router'
  3. import Page404 from '../404/404'
  4. ....省略无数代码
  5. <Content className={styles.content}>
  6. <Switch>
  7. {routers && routers.map((r, key) => {
  8. const Component = r.component,
  9. return <Route key={key}
  10. render={props => <Component {...props}
  11. allRouters={routers}
  12. />}
  13. exact={r.exact}
  14. path={match + r.path} />
  15. })}
  16. <Route component={Page404} />
  17. </Switch>
  18. </Content>
  19. ....省略无数代码

react后台管理系统路由方案及react-router原理解析的更多相关文章

  1. 【共享单车】—— React后台管理系统开发手记:Router 4.0路由实战演练

    前言:以下内容基于React全家桶+AntD实战课程的学习实践过程记录.最终成果github地址:https://github.com/66Web/react-antd-manager,欢迎star. ...

  2. 《React后台管理系统实战 :一》:目录结构、引入antd、引入路由、写login页面、使用antd的form登录组件、form前台验证、高阶函数/组件

    实战 上接,笔记:https://blog.csdn.net/u010132177/article/details/104150177 https://gitee.com/pasaulis/react ...

  3. 【共享单车】—— React后台管理系统开发手记:主页面架构设计

    前言:以下内容基于React全家桶+AntD实战课程的学习实践过程记录.最终成果github地址:https://github.com/66Web/react-antd-manager,欢迎star. ...

  4. 《React后台管理系统实战 零》:基础笔记

    day01 1. 项目开发准备 1). 描述项目 2). 技术选型 3). API接口/接口文档/测试接口 2. 启动项目开发 1). 使用react脚手架创建项目 2). 开发环境运行: npm s ...

  5. 【共享单车】—— React后台管理系统开发手记:城市管理和订单管理

    前言:以下内容基于React全家桶+AntD实战课程的学习实践过程记录.最终成果github地址:https://github.com/66Web/react-antd-manager,欢迎star. ...

  6. 《React后台管理系统实战 :二》antd左导航:cmd批量创建子/目录、用antd进行页面布局、分离左导航为单独组件、子路由、动态写左导航、css样式相对陷阱

    一.admin页面布局及路由创建 0)cmd批量创建目录及子目录 //创建各个目录,及charts和子目录bar md home category product role user charts\b ...

  7. 【共享单车】—— React后台管理系统开发手记:UI菜单各个组件使用(Andt UI组件)

    前言:以下内容基于React全家桶+AntD实战课程的学习实践过程记录.最终成果github地址:https://github.com/66Web/react-antd-manager,欢迎star. ...

  8. 《React后台管理系统实战 :三》header组件:页面排版、天气请求接口及页面调用、时间格式化及使用定时器、退出函数

    一.布局及排版 1.布局src/pages/admin/header/index.jsx import React,{Component} from 'react' import './header. ...

  9. 【共享单车】—— React后台管理系统开发手记:Redux集成开发

    前言:以下内容基于React全家桶+AntD实战课程的学习实践过程记录.最终成果github地址:https://github.com/66Web/react-antd-manager,欢迎star. ...

随机推荐

  1. scrapy实现数据持久化、数据库连接、图片文件下载及settings.py配置

    数据持久化的两种方式:(1)基于终端指令的持久化存储:(2)基于管道的持久化存储 基于终端指令的持久化存储 在爬虫文件的parse方法中必须要return可迭代对象类型(通常为列表或字典等)的返回值, ...

  2. flask之路由route

    ''' app.py中的源码def route(self, rule, **options) @app.route()路由参数使用: 1.第一个位置参数为路由分发的请求路径 ①静态参数路由:/inde ...

  3. kubernetes pod的弹性伸缩———基于pod自定义custom metrics(容器的IO带宽)的HPA

    背景 ​ 自Kubernetes 1.11版本起,K8s资源采集指标由Resource Metrics API(Metrics Server 实现)和Custom metrics api(Promet ...

  4. Jmeter基础-HTTP请求

    启动Jmeter 打开jmeter/bin文件/jmeter.bat(Windows执行文件)文件,就可以启动jmeter了 1.创建测试计划 启动后默认有一个TestPlan(测试计划),可修改其名 ...

  5. Mac 软件包管理器Homebrew使用指北

    Homebrew Homebrew由开发者 Max Howell 开发,并基于 BSD 开源,是一个非常方便的软件包包管理器工具. Homebrew 官网 Homebrew 的几个核心概念 在正式介绍 ...

  6. 我的Android知识结构图——20200507停止更新,后续通过标签或分类继续完善结构图

    *持续更新中.调整中(带链接的是已经总结发布的,未带链接是待发布的) *个别知识点在多个分类中都是比较重要部分,为了分类完整性 可能多出都列出了 *每一篇都是认真总结并写出来的,若哪里有问题欢迎指正 ...

  7. Android_四大组件之ContentProvider

    一.概述 ContentProvider(内容提供者)管理对结构化数据集的访问,它们封装数据,并提供用于定义数据安全性的机制.其他应用,通过Context的ContentResolver对象 作为客户 ...

  8. eatwhatApp开发实战(六)

    上次,我们为app添加了本地存储的功能,但会发现一但退出app则存储的商家集合就消失,但其实本地已经存储了记录只是没去读取罢了. 接下来我们来实现这个功能. /** * 获取本地数据 */ priva ...

  9. Mysql面试的技术名词

    面试的技术名词 面试一般会遇到一些名词,其实可能自己都知道其中的道理,但是因为没了解过,当时心里就一句WC,然后弱弱答一句:不好意思这个我只是听过,具体还没了解过: 回表 覆盖索引 最左前缀匹配 索引 ...

  10. 13 . Python3之并发编程

    什么是操作系统? 为什么要有操作系统? 现代的计算机系统主要是由一个或者多个处理器,主存,硬盘,键盘,鼠标,显示器,打印机,网络接口及其他输入输出设备组成. 一般而言,现代计算机系统是一个复杂的系统. ...