webpack4.0各个击破(4)—— Javascript & splitChunk

webpack作为前端最火的构建工具,是前端自动化工具链最重要的部分,使用门槛较高。本系列是笔者自己的学习记录,比较基础,希望通过问题 + 解决方式的模式,以前端构建中遇到的具体需求为出发点,学习webpack工具中相应的处理办法。(本篇中的参数配置及使用方式均基于webpack4.0版本本篇摘要: 本篇主要介绍基于webpack4.0splitChunks分包技术。

一. Js模块化开发

javascript之所以需要打包合并,是因为模块化开发的存在。开发阶段我们需要将js文件分开写在很多零碎的文件中,方便调试和修改,但如果就这样上线,那首页的http请求数量将直接爆炸。同一个项目,别人2-3个请求就拿到了需要的文件,而你的可能需要20-30个,结果就不用多说了。

但是合并脚本可不是“把所有的碎片文件都拷贝到一个js文件里”这样就能解决的,不仅要解决命名空间冲突的问题,还需要兼容不同的模块化方案,更别提根据模块之间复杂的依赖关系来手动确定模块的加载顺序了,所以利用自动化工具来将开发阶段的js脚本碎片进行合并和优化是非常有必要的。

二. Js文件的一般打包需求

  • 代码编译(TSES6代码的编译)
  • 脚本合并
  • 公共模块识别
  • 代码分割
  • 代码压缩混淆

三. 使用webpack处理js文件

3.1 使用babel转换ES6+语法

babelES6语法的转换工具,对babel不了解的读者可以先阅读《大前端的自动化工厂(3)——Babel》一文进行了解,babelwebpack结合使用的方法也在其中做了介绍,此处仅提供基本配置:

webpack.config.js:

...  
module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: [
          {
             loader: 'babel-loader'
          }
        ]
      }
    ]
  },
 ...

.babelrc:

{
    "presets":[
        ["env",{
            "targets":{
                "browsers":"last 2 versions"
            }
        }
        ]],
    "plugins": [
         "babel-plugin-transform-runtime" 
    ]
}

3.2 脚本合并

使用webpack对脚本进行合并是非常方便的,毕竟模块管理文件合并这两个功能是webpack最初设计的主要用途,直到涉及到分包和懒加载的话题时才会变得复杂。webpack使用起来很方便,是因为实现了对各种不同模块规范的兼容处理,对前端开发者来说,理解这种兼容性实现的方式比学习如何配置webpack更为重要。webpack默认支持的是CommonJs规范,但同时为了扩展其使用场景,webpack在后续的版本迭代中也加入了对ES harmony等其他规范定义模块的兼容处理,具体的处理方式将在下一章《webpack4.0各个击破(5)—— Module篇》详细分析。

3.3 公共模块识别

webpack的输出的文件中可以看到如下的部分:

/******/    function __webpack_require__(moduleId) {
/******/
/******/        // Check if module is in cache
/******/        if(installedModules[moduleId]) {
/******/            return installedModules[moduleId].exports;
/******/        }
/******/        // Create a new module (and put it into the cache)
/******/        var module = installedModules[moduleId] = {
/******/            i: moduleId,
/******/            l: false,
/******/            exports: {}
/******/        };
/******/
/******/        // Execute the module function
/******/        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/        // Flag the module as loaded
/******/        module.l = true;
/******/
/******/        // Return the exports of the module
/******/        return module.exports;
/******/    }

上面的__webpack_require__( )方法就是webpack的模块加载器,很容易看出其中对于已加载的模块是有统一的installedModules对象来管理的,这样就避免了模块重复加载的问题。而公共模块一般也需要从bundle.js文件中提取出来,这涉及到下一节的“代码分割”的内容。

3.4 代码分割

1. 为什么要进行代码分割?

代码分割最基本的任务是分离出第三方依赖库,因为第三方库的内容可能很久都不会变动,所以用来标记变化的摘要哈希contentHash也很久不变,这也就意味着我们可以利用本地缓存来避免没有必要的重复打包,并利用浏览器缓存避免冗余的客户端加载。另外当项目发布新版本时,如果第三方依赖的contentHash没有变化,就可以使用客户端原来的缓存文件(通用的做法一般是给静态资源请求设置一个很大的max-age),提升访问速度。另外一些场景中,代码分割也可以提供对脚本在整个加载周期内的加载时机的控制能力。

2. 代码分割的使用场景

举个很常见的例子,比如你在做一个数据可视化类型的网站,引用到了百度的Echarts作为第三方库来渲染图表,如果你将自己的代码和Echarts打包在一起生成一个main.bundle.js文件,这样的结果就是在一个网速欠佳的环境下打开你的网站时,用户可能需要面对很长时间的白屏,你很快就会想到将Echarts从主文件中剥离出来,让体积较小的主文件先在界面上渲染出一些动画或是提示信息,然后再去加载Echarts,而分离出的Echarts也可以从速度更快的CDN节点获取,如果加载某个体积庞大的库,你也可以选择使用懒加载的方案,将脚本的下载时机延迟到用户真正使用对应的功能之前。这就是一种人工的代码分割。

从上面的例子整个的生命周期来看,我们将原本一次就可以加载完的脚本拆分为了两次,这无疑会加重服务端的性能开销,毕竟建立TCP连接是一种开销很大的操作,但这样做却可以换来对渲染节奏的控制和用户体验的提升异步模块懒加载模块从宏观上来讲实际上都属于代码分割的范畴。code splitting最极端的状况其实就是拆分成打包前的原貌,也就是源码直接上线

3. 代码分割的本质

代码分割的本质,就是在“源码直接上线”“打包为唯一的脚本main.bundle.js”这两种极端方案之间寻找一种更符合实际场景的中间状态,用可接受的服务器性能压力增加来换取更好的用户体验。

4. 配置代码分割

code-splitting技术的配置和使用方法将在下一小节详细描述。

5. 更细致的代码分割

感兴趣的读者可以参考来自google开发者社区的文章《Reduce JavaScript Payloads with Code Splitting》自行研究。

3.5 代码混淆压缩

webpack4中已经内置了UglifyJs插件,当打包模式参数mode设置为production时就会自动开启,当然这不是唯一的选择,babel的插件中也能提供代码压缩的处理,具体的效果和原理笔者尚未深究,感兴趣的读者可以自行研究。

四. 细说splitChunks技术

4.1 参数说明

webpack4废弃了CommonsChunkPlugin插件,使用optimization.splitChunksoptimization.runtimeChunk来代替,原因可以参考《webpack4:连奏中的进化》一文。关于runtimeChunk参数,有的文章说是提取出入口chunk中的runtime部分,形成一个单独的文件,由于这部分不常变化,可以利用缓存。google开发者社区的博文是这样描述的:

The runtimeChunk option is also specified to move webpack's runtime into the vendors chunk to avoid duplication of it in our app code.

splitChunks中默认的代码自动分割要求是下面这样的:

  • node_modules中的模块或其他被重复引用的模块 就是说如果引用的模块来自node_modules,那么只要它被引用,那么满足其他条件时就可以进行自动分割。否则该模块需要被重复引用才继续判断其他条件。(对应的就是下文配置选项中的minChunks为1或2的场景)
  • 分离前模块最小体积下限(默认30k,可修改) 30k是官方给出的默认数值,它是可以修改的,上一节中已经讲过,每一次分包对应的都是服务端的性能开销的增加,所以必须要考虑分包的性价比。
  • 对于异步模块,生成的公共模块文件不能超出5个(可修改) 触发了懒加载模块的下载时,并发请求不能超过5个,对于稍微了解过服务端技术的开发者来说,【高并发】【压力测试】这样的关键词应该不会陌生。
  • 对于入口模块,抽离出的公共模块文件不能超出3个(可修改) 也就是说一个入口文件的最大并行请求默认不得超过3个,原因同上。

4.2 参数配置

splitChunks的在webpack4.0以上版本中的用法是下面这样的:

module.exports = {
  //...
  optimization: {
    splitChunks: {
      chunks: 'async',//默认只作用于异步模块,为`all`时对所有模块生效,`initial`对同步模块有效
      minSize: 30000,//合并前模块文件的体积
      minChunks: 1,//最少被引用次数
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      automaticNameDelimiter: '~',//自动命名连接符
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          minChunks:1,//敲黑板
          priority: -10//优先级更高
        },
        default: {
          test: /[\\/]src[\\/]js[\\/]/
          minChunks: 2,//一般为非第三方公共模块
          priority: -20,
          reuseExistingChunk: true
        }
      },
      runtimeChunk:{
          name:'manifest'
      }
    }
  }

4.3 代码分割实例

注:实例中使用的demo及配置文件已放在附件中。

  • 单页面应用 单页面应用只有一个入口文件,splitChunks的主要作用是将引用的第三方库拆分出来。从下面的分包结果就可以看出,node_modules中的第三方引用被分离了出来,放在了vendors-main.[hash].js中。
  • 多页面应用 多页面应用的情形稍显复杂,以《webpack4:连奏中的进化》一文中的例子进行代码分割处理,源码的依赖关系为: entryA.js: vue vuex component10k entryB.js: vue axios component10k entryC.js: vue vuex axios component10k 经过代码分割后得到的包如下图所示:

splitChunks提供了更精确的分割策略,但是似乎无法直接通过html-webpack-plugin配置参数来动态解决分割后代码的注入问题,因为分包名称是不确定的。这个场景在使用chunks:'async'默认配置时是不存在的,因为异步模块的引用代码是不需要以<script>标签的形式注入html文件的。

chunks配置项设置为allinitial时,就会有问题,例如上面示例中,通过在html-webpack-plugin中配置excludeChunks可以去除pageabout这两个chunk,但是却无法提前排除vendors-about-page这个chunk,因为打包前无法知道是否会生成这样一个chunk。这个场景笔者并没有找到现成的解决方案,对此场景有需求的读者也许可以通过使用html-webpack-plugin事件扩展来处理此类场景,也可以使用折中方案,就是第一次打包后记录下新生成的chunk名称,按需填写至html-webpack-pluginchunks配置项里。

### 4.4 结果分析

通过Bundle Buddy分析工具或webpack-bundle-analyser插件就可以看到分包前后对于公共代码的抽取带来的影响(图片来自参考文献的博文):

五. 参考及附件说明

【1】附加中文件说明:

  • webpack.spa.config.js——单页面应用代码分割配置实例
  • main.js——单页面应用入口文件
  • webpack.multi.config.js——多页面应用代码分割配置实例
  • entryA.js,entryB.js,entryC.js——多页面应用的3个入口

【2】参考文献: 《Reduce JavaScript Payloads with Code Splitting》

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏后端技术探索

再说TCP神奇的40ms

TCP是一个复杂的协议,每个机制在带来优势的同时也会引入其他的问题。 Nagel算法和delay ack机制是减少发送端和接收端包量的两个机制, 可以有效减少网...

14430
来自专栏linux、Python学习

Linux运维工程师必备的80个监控工具全集(上)

随着互联网行业的不断发展,各种监控工具多得不可胜数。这里列出网上最全的监控工具。让你可以拥有超过80种方式来管理你的机器。

22000
来自专栏高性能分布式系统设计

【GO语言】合理配置GOMAXPROCS提升一倍以上的性能

GOMAXPROCS 用默认的,就是CPU的硬件线程数目, 对于大部分File IO密集的应用是不合适的。 至少应该配置到硬件线程数目的5倍以上, 最大1024...

46890
来自专栏陈树义

Java并发编程:进程和线程之由来

Java多线程基础:进程和线程之由来   在前面,已经介绍了Java的基础知识,现在我们来讨论一点稍微难一点的问题:Java并发编程。当然,Java并发编程涉及...

38440
来自专栏开源优测

JMeter监听器10

前言 在jmeter中,通过监听器组件来提供查看、保存、和读取已保存的测试结果功能。 默认情况下,测试结果将被存储为xml格式的文件,文件的后缀: ".jtl"...

33640
来自专栏vue学习

1.前期准备工作

1、首先我先创建一个仓库,大家fork这个仓库(https://github.com/Ewall1106/mall),以此仓库为核心,我会把每天新完成的代码提交...

8710
来自专栏CSDN技术头条

Java 程序员必备的 IntelliJ IDEA 插件

IntelliJ 在业界被公认为最好的 java 开发工具之一,尤其在智能代码助手、代码自动提示、重构、CVS 整合、代码审查、 创新的 GUI 设计等方面的功...

27160
来自专栏IT技术精选文摘

分布式存储理论与实践初探 (二)

数据分布 分布式存储系统需要将数据分布到多个节点,并在多个节点之间实现负载均衡。常见的数据分布的方式有两种:一种是哈希分布,如一致性哈希,典型的系统是Amazo...

24460
来自专栏重庆的技术分享区

【翻译】使用Akka HTTP构建微服务:CDC方法

17730
来自专栏安斌的专栏

再说TCP神奇的40ms

本文结合具体的 tcpdump 包,分析触发 delay ack 的场景,相关的内核参数, 以及规避的方案。

11.5K70

扫码关注云+社区

领取腾讯云代金券