随着移动设备的升级、网络速度的提高,用户对于web应用的要求越来越高,web应用要提供的功能越来越。功能的增加导致的最直观的后果就是资源文件越来越大。为了维护越来越庞大的客户端代码,提出了模块化的概念来组织代码。webpack作为一种模块化打包工具,随着react的流行也越来越流行。

webpack在官方文档上解释为什么又做一个模块打包工具的时候,是这样说的:

The most pressing reason for developing another module bundler was Code Splitting and that static assets should fit seamlessly together through modularization.

开发一个新的模块打包工具最重要的原因就是Code Splitting,并且还要保证静态资源也可以无缝集成到模块化中。其中Code Splitting是webpack提供的一个重要功能,通过这个功能可以实现按需加载,减少首次加载时间。

Code Splitting

翻译一下官方文档对于Code Splitting的介绍:

对于大型的web 应用而言,把所有的代码放到一个文件的做法效率很差,特别是在加载了一些只有在特定环境下才会使用到的阻塞的代码的时候。Webpack有个功能会把你的代码分离成Chunk,后者可以按需加载。这个功能就是Code Spliiting

Code Spliting的具体做法就是一个分离点,在分离点中依赖的模块会被打包到一起,可以异步加载。一个分离点会产生一个打包文件。 

例如下面使用CommonJS风格的require.ensure作为分离点的代码:

除了这样的写法,还可以在配置文件中使用CommonChunkPlugin合并文件

问题

现在进入正题,本文不会针对React或者Vue做示例,因为这两个框架有很成熟的按需加载方案。 
下面这个例子用Backbone Router做路由,但是其中提到的按需加载方式可以用到大多数路由系统中。 

假设应用有三个路由:

  • 主页
  • 关于
  • 支付

开始时的代码(index.js):

这里有三个url路径:index, about, pay,对应了三个很简单的handler。这样的代码量的时候,这样写是没问题的。 
但是随着功能的增加,handler里的内容会越来越多,所以要先把handler分离到不同的模块里。

逻辑分离

把about的handler放在新的目录下(about/index.js):

index,pay也按照同样的办法分离出去。 
在index.js中修改一下代码:

这样分离之后对于开发而言减轻了痛苦,模块化的好处显而易见。但是分离出去的文件最终还是需要再引入的,最终生成的打包文件还是会非常大,用户从而不得去花很长时间加载一整个大文件。 

打开浏览器的主页,可以看到请求了一个bundle.js文件,里面包含了这个应用的全部模块。 
也就是说这样只是减少了开发的痛苦,对用户而言不会有改善。

使用Code Splitting进行第一次优化

为了不让用户一次加载整个大文件,稍微好点的做法是让用户分开一次一次加载文件。 
正好Code Splitting可以把在分离点中依赖的模块会被打包到一起,然后异步加载。 
修改一下index.js

 

因为require.ensure会生成一个小的打包文件,这样可以保证用户不一次加载全部文件,而是先加载bundle.js,再加载两个小的js文件。 
打开浏览器可以看到加载了三个js文件 

现在浏览器要加载三个文件,增加了http请求数量。但是对于访问频率比较高的主页而言,因为主页的内容是直接打包的,会首先加载,用户看到主页的速度变快了。对于访问about和pay的用户而言,因为http请求数量变多,理论上会更慢的看到内容。是否分割代码应该根据实际情况来分析,因为这篇文章主要说的是代码分割,所以就先假设分离开之后对用户访问更有利。
然而类似about和pay这两个页面用户不会每次都访问,在打开主页的时候就加载about和pay页面的handler是一种浪费,应该等到用户访问about和pay链接的时候再加载对应的js文件。

第二次优化

想法很简单:初始时只规定主页的路由,而对于about和pay这种访问频率比较低的路由就动态加载。动态加载的方式:在处理未定义路由的handler中,通过匹配当前的路径,增加router,然后重新解析页面。 
首先增加一个新路由:'*AllMissing': 'pathFinder' 
pathFinder函数的思路是:先定义好about和pay页面和路由和入口,然后把路由解析成正则表达式,通过正则表达式可以判断出来当前的路径符合哪条路由,然后增加新路由。 
routes.js

router.js,具体的思路在代码注释中:

然后在index.js引入router.js,路由就可以工作了 

在我们看来,路由现在是动态解析,动态加载文件的。打开浏览器,再看一下网络面板。 

打开主页,只请求了bundle.js,文件内容也是只包含了主页的代码。 

再打开about页面,请求了一个1.bundle.js,看一下1.bundle.js的内容就会发现,里面包含了about和pay两个页面的内容。这是webpack强大的地方,前文提到过,一个分离点会产生一个打包文件,而我们因为只有一个require.ensure,所以webpack通过自己的分析就只产生了一个打包文件,精准的包含了我们需要的内容。不得不说,webpack分析代码的功能有点厉害。

直接使用require.ensure是不能保证完全按需加载了,好在有loader可以帮助解决这个问题:bundle-loader. 
只用改变一点点就可以按需加载了:

bundle-loader是一个用来在运行时异步加载模块的loader,使用了bundle-loader加载的文件可以动态的加载。
例如下面的官方示例:

因为webpack在编译阶段会遍历到所有可能会使用到的文件,而bundle-loader就是在所有文件的外层加了一层wraper:

这样,在require文件的时候只是引入了wraper,而且因为每个文件都会产生一个分离点,导致产生了多个打包文件,而打包文件的载入只有在条件命中的情况下才产生,也就可以按需加载。

经过这样的修改,浏览器就可以在不同的路径下加载不同的依赖文件了

总结

在单页应用中使用这样的方式按需加载文件,对于路由库的要求也很简单: 

  • 建立从路由到正则表达式的映射,如果没有的话,自己写也可以 
  • 能够动态的添加路由 
  • 能够加载指定的路由 

大多数路由库都可以做到上面三点,所以这篇文章提出的是比较普遍的办法。当然,如果你用React或者Vue,他们配套的路由会比这个优化的更全面。

 

注:这篇文章的内容参考了https://medium.com/@somebody32/how-to-split-your-apps-by-routes-with-webpack-36b7a8a6231#.ncyca72ms,但是最后作者提出的方案也比较复杂,所以就自己写了一篇,最后的办法比较简单。

原创文章转载请注明:

转载自AlloyTeam:http://www.alloyteam.com/2016/02/code-split-by-routes/

  1. Mr,Yao 2017 年 6 月 4 日

    var Route = export default Backbone.Router.extend 楼主这个写法应该是有问题吧,我用babel 编译报错啊

  2. 极乐网 2016 年 10 月 9 日

    文章很实用!希望能转到我的网站(http://www.dreawer.com),会注明出处的~

  3. 陈荣桂 2016 年 8 月 8 日

    https://www.zhihu.com/question/49273969 代码可以这样分割

  4. 刘聪 2016 年 5 月 12 日

    我这里提示 require.ensure 回调里面的 require 不能用表达式。webpack 是静态扫描依赖的吧。如果用表达式,那就要等到运行时才能获取到真实的依赖了。

  5. transtone 2016 年 4 月 16 日

    “本文不会针对React或者Vue做示例,因为这两个框架有很成熟的按需加载方案。” 是吗?可否明示?我在网上找的方案也都是基于webpack的这个功能。请问这作者说的很成熟的方案分别是什么?可以分享一下吗?

    • 肖建锋 2016 年 5 月 30 日

      Vue 的在这里:http://router.vuejs.org/en/lazy.html

  6. 美图共赏 2016 年 4 月 14 日

    美图在这里:http://www.fydzv.com/

  7. Arthur 2016 年 4 月 11 日

    react按需加载的方案是什么?

    • react-router的按需加载 2016 年 12 月 21 日

      react-router的按需加载

  8. 一叶斋主人 2016 年 3 月 7 日

    code split 确实很必须。

发表评论