在react-router4中进行代码拆分的方法(基于webpack)


Posted in Javascript onMarch 08, 2018

前言

随着前端项目的不断扩大,一个原本简单的网页应用所引用的js文件可能变得越来越庞大。尤其在近期流行的单页面应用中,越来越依赖一些打包工具(例如webpack),通过这些打包工具将需要处理、相互依赖的模块直接打包成一个单独的bundle文件,在页面第一次载入时,就会将所有的js全部载入。但是,往往有许多的场景,我们并不需要在一次性将单页应用的全部依赖都载下来。例如:我们现在有一个带有权限的"订单后台管理"单页应用,普通管理员只能进入"订单管理"部分,而超级用户则可以进行"系统管理";或者,我们有一个庞大的单页应用,用户在第一次打开页面时,需要等待较长时间加载无关资源。这些时候,我们就可以考虑进行一定的代码拆分(code splitting)。

实现方式

简单的按需加载

代码拆分的核心目的,就是实现资源的按需加载。考虑这么一个场景,在我们的网站中,右下角有一个类似聊天框的组件,当我们点击圆形按钮时,页面展示聊天组件。

btn.addEventListener('click', function(e) {
  // 在这里加载chat组件相关资源 chat.js
});

从这个例子中我们可以看出,通过将加载chat.js的操作绑定在btn点击事件上,可以实现点击聊天按钮后聊天组件的按需加载。而要动态加载js资源的方式也非常简单(方式类似熟悉的jsonp)。通过动态在页面中添加<scrpt>标签,并将src属性指向该资源即可。

btn.addEventListener('click', function(e) {
  // 在这里加载chat组件相关资源 chat.js
  var ele = document.createElement('script');
  ele.setAttribute('src','/static/chat.js');
  document.getElementsByTagName('head')[0].appendChild(ele);
});

代码拆分就是为了要实现按需加载所做的工作。想象一下,我们使用打包工具,将所有的js全部打包到了bundle.js这个文件,这种情况下是没有办法做到上面所述的按需加载的,因此,我们需要讲按需加载的代码在打包的过程中拆分出来,这就是代码拆分。那么,对于这些资源,我们需要手动拆分么?当然不是,还是要借助打包工具。下面就来介绍webpack中的代码拆分。

代码拆分

这里回到应用场景,介绍如何在webpack中进行代码拆分。在webpack有多种方式来实现构建是的代码拆分。

import()

这里的import不同于模块引入时的import,可以理解为一个动态加载的模块的函数(function-like),传入其中的参数就是相应的模块。例如对于原有的模块引入import react from 'react'可以写为import('react')。但是需要注意的是,import()会返回一个Promise对象。因此,可以通过如下方式使用:

btn.addEventListener('click', e => {
  // 在这里加载chat组件相关资源 chat.js
  import('/components/chart').then(mod => {
    someOperate(mod);
  });
});

可以看到,使用方式非常简单,和平时我们使用的Promise并没有区别。当然,也可以再加入一些异常处理:

btn.addEventListener('click', e => {
  import('/components/chart').then(mod => {
    someOperate(mod);
  }).catch(err => {
    console.log('failed');
  });
});

当然,由于import()会返回一个Promise对象,因此要注意一些兼容性问题。解决这个问题也不困难,可以使用一些Promise的polyfill来实现兼容。可以看到,动态import()的方式不论在语意上还是语法使用上都是比较清晰简洁的。

require.ensure()

在webpack 2的官网上写了这么一句话:

require.ensure() is specific to webpack and superseded by import().

所以,在webpack 2里面应该是不建议使用require.ensure()这个方法的。但是目前该方法仍然有效,所以可以简单介绍一下。包括在webpack 1中也是可以使用。下面是require.ensure()的语法:

require.ensure(dependencies: String[], callback: function(require), errorCallback: function(error), chunkName: String)

require.ensure()接受三个参数:

  1. 第一个参数dependencies是一个数组,代表了当前require进来的模块的一些依赖;
  2. 第二个参数callback就是一个回调函数。其中需要注意的是,这个回调函数有一个参数require,通过这个require就可以在回调函数内动态引入其他模块。值得注意的是,虽然这个require是回调函数的参数,理论上可以换其他名称,但是实际上是不能换的,否则webpack就无法静态分析的时候处理它;
  3. 第三个参数errorCallback比较好理解,就是处理error的回调;
  4. 第四个参数chunkName则是指定打包的chunk名称。

因此,require.ensure()具体的用法如下:

btn.addEventListener('click', e => {
  require.ensure([], require => {
    let chat = require('/components/chart');
    someOperate(chat);
  }, error => {
    console.log('failed');
  }, 'mychat');
});

Bundle Loader

除了使用上述两种方法,还可以使用webpack的一些组件。例如使用Bundle Loader:

npm i --save bundle-loader

使用require("bundle-loader!./file.js")来进行相应chunk的加载。该方法会返回一个function,这个function接受一个回调函数作为参数。

let chatChunk = require("bundle-loader?lazy!./components/chat");
chatChunk(function(file) {
  someOperate(file);
});

和其他loader类似,Bundle Loader也需要在webpack的配置文件中进行相应配置。Bundle-Loader的代码也很简短,如果阅读一下可以发现,其实际上也是使用require.ensure()来实现的,通过给Bundle-Loader返回的函数中传入相应的模块处理回调函数即可在require.ensure()的中处理,代码最后也列出了相应的输出格式:

/*
Output format:
  var cbs = [],
    data;
  module.exports = function(cb) {
    if(cbs) cbs.push(cb);
      else cb(data);
  }
  require.ensure([], function(require) {
    data = require("xxx");
    var callbacks = cbs;
    cbs = null;
    for(var i = 0, l = callbacks.length; i < l; i++) {
      callbacks[i](data);
    }
  });
*/

react-router v4 中的代码拆分

最后,回到实际的工作中,基于webpack,在react-router4中实现代码拆分。react-router 4相较于react-router 3有了较大的变动。其中,在代码拆分方面,react-router 4的使用方式也与react-router 3有了较大的差别。

在react-router 3中,可以使用Route组件中getComponent这个API来进行代码拆分。getComponent是异步的,只有在路由匹配时才会调用。但是,在react-router 4中并没有找到这个API,那么如何来进行代码拆分呢?

在react-router 4官网上有一个代码拆分的例子。其中,应用了Bundle Loader来进行按需加载与动态引入

import loadSomething from 'bundle-loader?lazy!./Something'

然而,在项目中使用类似的方式后,出现了这样的警告:

Unexpected '!' in 'bundle-loader?lazy!./component/chat'. Do not use import syntax to configure webpack loaders import/no-webpack-loader-syntax
Search for the keywords to learn more about each error.

在webpack 2中已经不能使用import这样的方式来引入loader了(no-webpack-loader-syntax)

Webpack allows specifying the loaders to use in the import source string using a special syntax like this:

var moduleWithOneLoader = require("my-loader!./my-awesome-module");

This syntax is non-standard, so it couples the code to Webpack. The recommended way to specify Webpack loader configuration is in a Webpack configuration file.

我的应用使用了create-react-app作为脚手架,屏蔽了webpack的一些配置。当然,也可以通过运行npm run eject使其暴露webpack等配置文件。然而,是否可以用其他方法呢?当然。

这里就可以使用之前说到的两种方式来处理:import()或require.ensure()。

和官方实例类似,我们首先需要一个异步加载的包装组件Bundle。Bundle的主要功能就是接收一个组件异步加载的方法,并返回相应的react组件:

export default class Bundle extends Component {
  constructor(props) {
    super(props);
    this.state = {
      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({
        mod: mod.default ? mod.default : mod
      });
    });
  }

  render() {
    return this.state.mod ? this.props.children(this.state.mod) : null;
  }
}

在原有的例子中,通过Bundle Loader来引入模块:

import loadSomething from 'bundle-loader?lazy!./About'

const About = (props) => (
  <Bundle load={loadAbout}>
    {(About) => <About {...props}/>}
  </Bundle>
)

由于不再使用Bundle Loader,我们可以使用import()对该段代码进行改写:

const Chat = (props) => (
  <Bundle load={() => import('./component/chat')}>
    {(Chat) => <Chat {...props}/>}
  </Bundle>
);

需要注意的是,由于import()会返回一个Promise对象,因此Bundle组件中的代码也需要相应进行调整

export default class Bundle extends Component {
  constructor(props) {
    super(props);
    this.state = {
      mod: null
    };
  }

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

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

  load(props) {
    this.setState({
      mod: null
    });
    //注意这里,使用Promise对象; mod.default导出默认
    props.load().then((mod) => {
      this.setState({
        mod: mod.default ? mod.default : mod
      });
    });
  }

  render() {
    return this.state.mod ? this.props.children(this.state.mod) : null;
  }
}

路由部分没有变化

<Route path="/chat" component={Chat}/>

这时候,执行npm run start,可以看到在载入最初的页面时加载的资源如下

在react-router4中进行代码拆分的方法(基于webpack)

而当点击触发到/chat路径时,可以看到

在react-router4中进行代码拆分的方法(基于webpack)

动态加载了2.chunk.js这个js文件,如果打开这个文件查看,就可以发现这个就是我们刚才动态import()进来的模块。

当然,除了使用import()仍然可以使用require.ensure()来进行模块的异步加载。相关示例代码如下:

const Chat = (props) => (
  <Bundle load={(cb) => {
    require.ensure([], require => {
      cb(require('./component/chat'));
    });
  }}>
  {(Chat) => <Chat {...props}/>}
 </Bundle>
);
export default class Bundle extends Component {
  constructor(props) {
    super(props);
    this.state = {
      mod: null
    };
  }

  load = props => {
    this.setState({
      mod: null
    });
    props.load(mod => {
      this.setState({
        mod: mod ? mod : null
      });
    });
  }

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

  render() {
    return this.state.mod ? this.props.children(this.state.mod) : null
  }
}

此外,如果是直接使用webpack config的话,也可以进行如下配置

output: {
  // The build folder.
  path: paths.appBuild,
  // There will be one main bundle, and one file per asynchronous chunk.
  filename: 'static/js/[name].[chunkhash:8].js',
  chunkFilename: 'static/js/[name].[chunkhash:8].chunk.js',
 },

结束

代码拆分在单页应用中非常常见,对于提高单页应用的性能与体验具有一定的帮助。我们通过将第一次访问应用时,并不需要的模块拆分出来,通过scipt标签动态加载的原理,可以实现有效的代码拆分。在实际项目中,使用webpack中的import()、require.ensure()或者一些loader(例如Bundle Loader)来做代码拆分与组件按需加载。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
用js解决数字不能换行问题
Aug 10 Javascript
jQuery数组处理代码详解(含实例演示)
Feb 03 Javascript
firefox下jquery iframe刷新页面提示会导致重复之前动作
Dec 17 Javascript
jquery 倒计时效果实现秒杀思路
Sep 11 Javascript
jQuery中:visible选择器用法实例
Dec 30 Javascript
省市联动效果的简单实现代码(推荐)
Jun 06 Javascript
深入浅析knockout源码分析之订阅
Jul 12 Javascript
javascript中this关键字详解
Dec 12 Javascript
jQuery Plupload上传插件的使用
Apr 19 jQuery
jQuery Ajax 实现分页 kkpager插件实例代码
Aug 10 jQuery
如何搭建一个完整的Vue3.0+ts的项目步骤
Oct 18 Javascript
JavaScript实现网页下拉菜单效果
Nov 20 Javascript
JQuery选中select组件被选中的值方法
Mar 08 #jQuery
vue.js中$set与数组更新方法
Mar 08 #Javascript
vue与vue-i18n结合实现后台数据的多语言切换方法
Mar 08 #Javascript
详解使用vue-cli脚手架初始化Vue项目下的项目结构
Mar 08 #Javascript
改变vue请求过来的数据中的某一项值的方法(详解)
Mar 08 #Javascript
JavaScript满天星导航栏实现方法
Mar 08 #Javascript
vue.js的computed,filter,get,set的用法及区别详解
Mar 08 #Javascript
You might like
第八节 访问方式 [8]
2006/10/09 PHP
php带密码功能并下载远程文件保存本地指定目录 修改加强版
2010/05/16 PHP
PHP文件去掉PHP注释空格的函数分析(PHP代码压缩)
2013/07/02 PHP
PHP中使用SimpleXML检查XML文件结构实例
2015/01/07 PHP
PHP错误机制知识汇总
2016/03/24 PHP
PHP5.6.8连接SQL Server 2008 R2数据库常用技巧分析总结
2019/05/06 PHP
利用PHP内置SERVER开启web服务(本地开发使用)
2021/03/09 PHP
JS实现一键回顶功能示例代码
2013/10/28 Javascript
使用简洁的jQuery方法实现隔行换色功能
2014/01/02 Javascript
如何高效率去掉js数组中的重复项
2016/04/12 Javascript
JavaScript 中有关数组对象的方法(详解)
2016/08/15 Javascript
js实现简单的碰壁反弹效果
2016/08/30 Javascript
Vue.js创建Calendar日历效果
2016/11/03 Javascript
Bootstrap CSS使用方法
2016/12/23 Javascript
详解angular ui-grid之过滤器设置
2017/06/07 Javascript
微信小程序多音频播放进度条问题
2018/08/28 Javascript
vue elementUI 表单校验功能之数组多层嵌套
2019/06/04 Javascript
ES6 Class中实现私有属性的一些方法总结
2019/07/08 Javascript
[01:20:47]DOTA2-DPC中国联赛 正赛 Ehome vs Magma BO3 第一场 1月19日
2021/03/11 DOTA
Python简单获取自身外网IP的方法
2016/09/18 Python
Python实现获取命令行输出结果的方法
2017/06/10 Python
Python画图实现同一结点多个柱状图的示例
2019/07/07 Python
Centos7 下安装最新的python3.8
2019/10/28 Python
python3检查字典传入函数键是否齐全的实例
2020/06/05 Python
python有几个版本
2020/06/17 Python
python 绘制场景热力图的示例
2020/09/23 Python
通过实例解析python subprocess模块原理及用法
2020/10/10 Python
Python调用JavaScript代码的方法
2020/10/27 Python
html5表单及新增的改良元素详解
2016/06/07 HTML / CSS
Farfetch香港官网:汇集全球时尚奢侈品购物平台
2017/11/26 全球购物
车间机修工岗位职责
2014/02/28 职场文书
2014年小学植树节活动方案
2014/03/02 职场文书
人民调解协议书范本
2014/10/11 职场文书
司机个人年终总结
2015/03/03 职场文书
幼师求职自荐信
2015/03/26 职场文书
JavaScript流程控制(分支)
2021/12/06 Javascript