加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 百科 > 正文

React-Router动态路由设计最佳实践

发布时间:2020-12-15 06:43:20 所属栏目:百科 来源:网络整理
导读:随着单页应用(SPA)概念的日趋火热,React框架在设计和实践中同样也围绕着SPA的概念来打造自己的技术栈体系,其中路由模块便是非常重要的一个组成部分。它承载着应用功能分区,复杂模块组织,数据传递,应用状态维护等诸多功能,如何结合好React框架的技术

随着单页应用(SPA)概念的日趋火热,React框架在设计和实践中同样也围绕着SPA的概念来打造自己的技术栈体系,其中路由模块便是非常重要的一个组成部分。它承载着应用功能分区,复杂模块组织,数据传递,应用状态维护等诸多功能,如何结合好React框架的技术栈特性来进行路由模块设计就显得尤为重要,本文则以探索React动态路由设计最佳实践作为切入点,分享下在实际项目开发中的心得与体会。

为什么需要做动态路由

1.首屏加载效率,随着项目的业务需求持续添加,react中的代码复杂度将面临着持续上升的问题,同时由于react中的jsx和es6语法的文件在实际生产环境中,也会被babel-js重新编译成浏览器所支持的基于ES5的语法模块,各个模块打体积将会变得非常的臃肿不堪,直接影响到页面加载的等待时常。以下图为例,如果不做处理,我们的业务模块通常体积会达到兆级,这对首屏加载速率和用户体验的影响无疑是巨大的。

2.降低模块间的功能影响,react中的jsx无疑是一个很方便的设计,能让开发者像写html一样来书写虚拟dom,但是它同样也贯彻执行着"all in js"的理念,最终构建完成后所有的业务代码都将打包到1-2个bundle文件中,这就等于将所有的功能模块都集中到了一个物理文件中,如果遇到业务处理的复杂性,接口层变更,异常处理出错等诸多代码健壮性问题时,一个子模块出现了错误,就很有可能导致用户界面整体性出错从而无法使用的风险。

3.二八定律,通常在一个应用中,最重要和高频访的功能模块只占其中一小部分,约20%,其余80%尽管是多数,却是次要的。以后台系统为例,普通业务人员通常使用的高频模块只有3-5个,但是业务系统通常会有各式各样的权限设计,不同的权限映射着能访问的路由模块也不尽相同,虽然我们可以在用户的数据访问和路由地址上做拦截限制,但是同样也需要对其能访问的模块资源进行限制,才能做到真正的按需加载,随取随用。

4.工具体系支撑,无论是react-router还是对应搭配的构建工具webpack,其中都有针对动态路由部分的设计与优化,使用好了往往能起到事半功倍的效果。

简化版实现:bundle-loader

bundle-loader是webpack官方出品与维护的一个loader,主要用来处理异步模块的加载,将简单的页面模块转成异步模块,非常方便。

改造前页面

import React from 'react'
import {Route,Router} from 'react-router-dom'
import createHistory from 'history/createHashHistory'
import './app.less'

import ReactChildrenMap from './containers/Commons/ReactChildrenMap'
import Home from './containers/Home/Home'
import Search from './containers/Search/Search'
import BookList from './containers/BookList/BookList'
import BookDetail from './containers/BookDetail/bookDetail.bundle.js'

const history = createHistory()

export default class App extends React.Component {
  render() {
    return (
      <Router history={history}>
        <Route render={({location}) => {
          return (
            <ReactChildrenMap key={location.pathname}>
              <Route location={location} exact path="/" component={Home}/>
              <Route location={location} path="/search" component={Search}/>
              <Route location={location} path="/detail" component={BookDetail}/>
              <Route location={location} path="/bookList/:bookId" component={BookList}/>
            </ReactChildrenMap>
          )
        }}/>
      </Router>
    );
  }
}

在webpack.config.js中增加rules

// npm install bundle-loader -D
// 如果不想通过配置调用,也可以写成: import file from "bundle-loader?lazy&name=my-chunk!./file.js"的内嵌写法

module.exports = {
  module: {
    rules: [
      {
        test: /.bundle.js$/,// 通过文件名后缀自动处理需要转成bundle的文件
        include: /src/,exclude: /node_modules/,use: [{
          loader: 'bundle-loader',options: {
            name: 'app-[name]',lazy: true
          }
        },{
          loader: 'babel-loader',}]
      }
    ]
  }
}

在工程中使用带 xxx.bunlde.js结尾的类型文件时,就会被bundle-loader识别并做编译处理

// bundle-loader处理前
import BookDetail from './containers/BookDetail/bookDetail.bundle.js'

// bundle-loader处理后
module.exports = function(cb) {
  // 自动会被bundle-loader处理成异步加载的写法
  require.ensure([],function(require) {
    cb(require("!!../../../node_modules/babel-loader/lib/index.js!./bookDetail.bundle.js"));
  },"app-bookDetail.bundle");
}
// WEBPACK FOOTER //
// ./containers/BookDetail/bookDetail.bundle.js

创建LazyBundle.js文件,这个文件会用来调用被bundle-loader处理后的组件

// LazyBundle.js
import React,{ Component } from 'react'

export default class LazyBundle extends React.Component {

  state = {
    // short for "module" but that's a keyword in js,so "mod"
    mod: null
  }

  componentWillMount() {
    this.load(this.props)
  }

  componentWillReceiveProps(nextProps) {
    if (nextProps.load !== this.props.load) {
      this.load(nextProps)
    }
  }

  load(props) {
    this.setState({
      mod: null
    })
    
    props.load((mod) => {
      this.setState({
        // handle both es imports and cjs
        mod: mod.default ? mod.default : mod
      })
    })
  }

  render() {
    if (!this.state.mod) {
      return false
    }
    return this.props.children(this.state.mod)
  }
}

对我们需要异步加载的组件函数进行二次封装(注:react-router3和4由于是不兼容升级,所以处理动态路由的方法也略有不同,在此列出了两种版本下的处理方式可供参考)

import LazyBundle from './LazyBundle'
import BookDetail from './containers/BookDetail/bookDetail.bundle.js'

/* use for react-router4
 * component={lazyLoadComponent(BookDetail)}
 */
const lazyLoadComponent = (comp) => (props) => (
  <LazyBundle load={comp}>
    {(Container) => <Container {...props}/>}
  </LazyBundle>
)

/* use for react-router3
 * getComponent={lazyLoadComponentOld(BookDetail)}
 */
function lazyLoadComponentOld(comp) {
  return (location,cb) => {
    comp(module => cb(null,module.default));
  }
}

改造后页面

import React from 'react'
import {Route,Router} from 'react-router-dom'
import createHistory from 'history/createHashHistory'

const history = createHistory()

import './app.less'

import Home from 'containers/Home/Home'
import ReactChildrenMap from './containers/Commons/ReactChildrenMap'
import Search from './containers/Search/Search'
import BookList from './containers/BookList/BookList'
import LazyBundle from './LazyBundle'
import BookDetail from './containers/BookDetail/bookDetail.bundle.js'

/* use for react-router4
 * component={lazyLoadComponent(BookDetail)}
 */
const lazyLoadComponent = (comp) => (props) => (
  <LazyBundle load={comp}>
    {(Container) => <Container {...props}/>}
  </LazyBundle>
)

export default class App extends React.Component {
  render() {
    return (
      <Router history={history}>
        <Route render={({location}) => {
          return (
            <ReactChildrenMap key={location.pathname}>
              <Route location={location} exact path="/" component={Home}/>
              <Route location={location} path="/search" component={Search}/>
              <Route location={location} path="/detail" component={lazyLoadComponent(BookDetail)} />
              <Route location={location} path="/bookList/:bookId" component={BookList}/>
            </ReactChildrenMap>
          )
        }}/>
      </Router>
    );
  }
}

完成构建后我们就可以从浏览器中看到,我们定制后的模块已经被能被支持异步加载了

同时在webpack构建中也能清晰地看到多了一个chunk:

高阶版实现:dynamic-imports

dynamic-imports是webpack在升级到2版本以后,对js的模块处理进行了增强的,其中就有对require.ensure的改进,基于原生的Promise对象进行了重新实现,采用了import()作为资源加载方法,将其看做一个分割点并将其请求的module打包为一个独立的chunk。import()以模块名称作为参数并且返回一个Promise对象,具体介绍可以参考笔者之前写过的翻译文章Webpack2 升级指南和特性摘要,具体使用比对如下:

// require.ensure
module.exports = function (cb) {
  require.ensure([],function(require) {
    var app = require('./file.js');
    cb(app);
  },"custom-chunk-name");
};

// import()
import("./module").then(module => {
    return module.default;
}).catch(err => {
    console.log("Chunk loading failed");
});
// This creates a separate chunk for each possible route
?````

结合import的高级特性,我们就可以省去bundle-loader的处理方式,直接在原生模块上进行动态路由处理,具体设计实现如下:

封装一个高阶组件,用来实现将普通的组件转换成动态组件

import React from 'react'

const AsyncComponent = loadComponent => (
  class AsyncComponent extends React.Component {
    state = {
      Component: null,}

    componentWillMount() {
      if (this.hasLoadedComponent()) {
        return;
      }

      loadComponent()
        .then(module => module.default)
        .then((Component) => {
          this.setState({Component});
        })
        .catch((err) => {
          console.error(`Cannot load component in <AsyncComponent />`);
          throw err;
        });
    }

    hasLoadedComponent() {
      return this.state.Component !== null;
    }

    render() {
      const {Component} = this.state;
      return (Component) ? <Component {...this.props} /> : null;
    }
  }
);

export default AsyncComponent;

对我们需要用到的普通组件进行引入和包装处理

// 组件增强
const Search = AsyncComponent(() => import("./containers/Search/Search"))

// 路由调用
<Route location={location} path="/list" component={BookList} />

利用weback3中的Magic Comments对生成的chunk指定chunkName

const BookList = AsyncComponent(() => 
  import(/* webpackChunkName: "bookList" */ "./containers/BookList/BookList")
)

完成构建后我们就可以从浏览器中看到,我们定制后的模块也和之前一样,被能被支持异步加载了

同时在webpack构建界面中的能看到多了一个chunk,并且chunkName就是我们自定义的名称,对于定位分析一些模块问题时会非常管用。

从中我们也不难发现,相对于bundle-loader,dynamic-imports + AsyncComponent高阶组件的方式更为简单灵活,同时对于现有的代码改动也较小,故作为在实际开发中的首选方案使用,同时我们也推荐一个非常不错的webpack的chunk分析工具webpack-bundle-analyzer,方便查看每个异步路由中的构建的具体模块内容。

One more thing:路由模块的组织

react-router功能强大,上手简单,作为官方唯一指定的路由框架已经成为了react应用开发中必备的部分,但是由于react天生组件化的原因,意味着react-router的配置文件中在实际使用中,会难免出现如下不佳场景:

1、路由配置入口文件持续臃肿,文件越引越多

2、路由配置会随着业务嵌套越来越深,团队协作开发时极易产生冲突

3、非jsx写法,模块清晰简单,但是会导致路由模块和业务模块耦合,不利于集中管理,同时无法明确表达出母子路由的嵌套关系,参见huge-apps

问题来了:如何既保证路由模块的清晰简单,又能集中管理维护,还能支持嵌套定义和动态加载?

借鉴python flask中的blueprint设计思路,重新实现路由模块的划分

经过前面的分析,我们不难发现react-router的路由配置模块会随着业务的深入变得越来越臃肿,其根本原因在于我们将所有的资源和配置信息都写在了一个文件中,这和软件设计中提倡的清晰一单一,低耦合高内聚等指导原则是背道而驰的,为此我们针对路由模块的划分这块进行了重构,改进方式如下:

  1. 将路由模块的整体由一个routes.js文件拆成若干个彼此间互相独立的子路由模块文件,模块的拆分原则可以和业务功能划分一一对应,逐步减少主配置中的内容耦合。
routes
├── asyncComponent.js
├── callManage.js
├── index.js
├── opportunity.js
├── osManage.js
├── salesKit.js
├── salesManage.js
├── system.js
├── uploadOppor.js
└── workBoard.js
  1. 在模块的入口文件index.js中完成对各个子模块的引入,如下所示:
import React from 'react';
import { Route,IndexRedirect } from 'react-router';
import NotFound from '../components/NotFound';
import Layout from '../containers/Main';
import Opportunity from './opportunity';
import OsManage from './osManage';
import SalesKit from './salesKit';
import System from './system';
import CallManage from './callManage';
import SalesManage from './salesManage';
import WorkBoard from './workBoard';
import UploadOppor from './uploadOppor';

const routeList = [
  Opportunity,UploadOppor,OsManage,SalesKit,System,CallManage,SalesManage,WorkBoard
];

export default (
  <Route path='/' component={Layout} >
    {routeList}
    <Route path='*' component={NotFound} />
  </Route>
);
  1. 在子路由模块中完成对应具体业务模块的加载,支持同时混合使用同步和异步组件的管理方式
import React from 'react';
import { Route } from 'react-router';
import UploadOpportunities from '../containers/opportunity/UploadOpportunities'
import UploadVisitOpportunity from '../containers/UploadVisitOpportunity'
import asyncComponent from './asyncComponent'

// upload_frozen_phone
const UploadFrozenPhone = asyncComponent(
  () => import(/* webpackChunkName: "upload_frozen_phone" */'../components/uploadFrozenPhone/UploadFrozenPhone')
);

// upload_phone_state
const UploadPhoneState = asyncComponent(
  () => import(/* webpackChunkName: "upload_phone_state" */'../components/uploadPhoneState/UploadPhoneState')
);

export default (
  <Route key='uploadOpportunities'>
    <Route path='upload_opportunity/:type' component={UploadOpportunities} />
    <Route path='upload_visit_opportunity' component={UploadVisitOpportunity} />
    <Route path='frozen_phone' component={UploadFrozenPhone} />
    <Route path='phone_state' component={UploadPhoneState} />
  </Route>
);

这样重构的好处是即使未来随着业务的深入,对应的开发人员也只需要维护自身负责的子路由模块,再在根路由下进行注册即可使用,并且由于子路由模块都从物理文件上进行了隔离,也能最大程度地减少协作冲突,同时,因为维持了jsx的描述型结构,路由的嵌套关系和集中维护等优点依旧能沿用。

总结

本文从react-router的动态路由实践着手,整合了webpack的bundle-loader,dynamic-imports和高阶组件等实践的明细介绍,附带介绍了改进路由模块的组织方式,以此作为react-router深入实践的经验总结,希望能对各位读者在实际项目开发中有所帮助。

参考文献

Webpack3官方文档
React-Router官方文档
基于Webpack 2的React组件懒加载
React-router 4 按需加载的实现方式及原理
React Router最新指南与异步加载实践

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读