Webpack原理剖析

Posted 全栈人生

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Webpack原理剖析相关的知识,希望对你有一定的参考价值。

webpack用于前端代码的编译、打包、压缩。正如名称中的单词pack,打包是它的核心,包括资源的依赖及加载管理,编译和压缩则是依赖插件完成。剖析webpack打包原理的文章不在少数,这里笔者集各家之言,先抛出一份思维导图:

一千个人心中有一千个哈姆雷特。图中梳理的内容,难免顺应不了所有读者的思路。这里笔者借几个问题,进一步谈谈对webpack的理解,希望能和前端的朋友们有一些思维碰撞。疏漏之处欢迎评论斧正。


1.打包的本质是什么?


笔者的理解是:模块重组+文件合并。

模块化理念在各行各业受用甚广,软件领域的应用仅仅是冰山一角,它是webpack打包的前提。从开发者视角,模块类型包含两大类:

(1) 个人或团队编写的业务模块;

(2) 业务模块中依赖的公共模块(自研库+第三方库);


模块重组,也就是改变模块的聚合形态。中学化学有一则经典的置换方程式,用于生产氯化镁:Mg+2HCl=MgCl2+H2 。

Webpack原理剖析

在webpack打包流程中,可以将Mg类比为"业务模块",HCl为"公共模块",其中Cl为Mg的"依赖项"。而MgCl2这个产物,正是开发人员希望产出的JS bundle。


但是,webpack打包的不同之处在于,它不满足"物质守恒定律"(不产出H2这样的副产品)。模块重组的意义也恰恰在此——按需引用,避免浪费。


前端模块以文件的形式存在。实际场景下,不论置换方程式左侧的文件数有多少,右侧仅生成的少量的bundle。简而言之,除了按需引用,打包过程中还进行了文件的合并。


回想一下若干年前的web页面:

Webpack原理剖析

浏览器端的脚本臃肿及并发难题,终于在webpack时代得以根治。


2.打包后的文件(JS bundle)是如何运行的?


除了前面提到的业务模块和公共模块,JS bundle中实际还包含了webpack生成的,用于串联、管控各模块的代码,业界称之为manifest。初用webpack的同学容易忽略其存在,它是驱动bundle运行的核心,所以也叫webpackBootstrap(可以联想开机时操作系统的引导程序)。若无特殊配置,manifest代码默认包含在bundle头部。


bundle中模块间的依赖合并,依笔者个人理解,分为预合并、运行时合并两种。预合并的使用场景简单粗暴,就是将各模块打包成单一的文件输出给浏览器,这里不重点论述。


运行时合并,包含同步和异步两种情况。意味着浏览器需要接收多个bundle,在运行过程中完成bundle的合并操作。那么问题来了,为什么本可以在编译时完成的工作,要耗费浏览器资源,延后执行呢?一分为二来看,对于没有路由逻辑的单页应用,拆分的意义确实不大。但对于更为常见的多页应用,如果每个页面都相互独立,各自实现模块预合并,性能上就会打折(想象一下办公室人手配一个打印机,而非公用)。运行时合并中公共模块的提取,不仅仅是逻辑上的提取,也是物理位置的提取。


在manifest源码中,厘清module以及installedModule之间的关系,是理解原理的第一步。module的本质,是函数字面量,也就是模块函数。它的执行结果,才是实际可用的模块对象,对应installedModule。webpack_require()这个函数的作用,正是完成这个转换(可以想象成汽车钥匙,用于启动发动机)。


对于另一个重要的对象chunk,最初笔者有一个误解,认为chunk就是异步的modules,其实不然。chunk的本质就是JS bundle,一个chunk包含一个或多个module。chunk之间存在异步依赖时,才会真正使用到installedChunks这个数组。换言之,installedChunks的实际意义,是存放异步chunk的promise信息,而非chunk本身。


关于源码的细节及案例解读,推荐参考这两篇文章:

github搜索:深入理解 webpack 文件打包机制

csdn博客搜索:webpack打包原理和manifest文件分析


3.结合打包原理,如何实现缓存最优化?

前端缓存优化,溯本求源,不外乎是对强缓存及协商缓存的合理配置:

只不过webpack的介入,带来了一些新问题。笔者曾经遇到一个奇怪的现象:执行webpack后查看页面,控制台会偶现报错导致页面白屏,强制刷新才恢复正常。


多页应用场景下,提取公共bundle前,相关页面涉及的所有模块,会排列成一个扁平结构的、去重的数组,每个模块拥有独一无二的下标。接着,webpack会根据minChunks参数的配置(2~infinite),对数组中的公共模块进行不同粒度的提取。分离后各bundle中的模块数组,会对当前bundle未包含的模块下标位留空,从而确保运行时合并过程中,公共bundle和任一业务bundle,都能够像拼图一样无缝结合。如果读着蒙圈,可以借助下图帮助理解:

也就是说,如果两块"拼板"的形状不契合(业务bundle和公共bundle版本不一致),就会"拼接失败",带来上述报错问题。造成bundle变化的不可控因素(非模块自身改变),笔者理解有3种:

(1) webpack 1.x版本,每次打包时对modules数组的排序不稳定;

(2) chunk数量变化,导致公共模块的提取范围发生改变;

(3) minChunk配置改变,导致公共模块的提取粒度发生改变;


在使用强缓存的前提下,一旦浏览器获取的业务bundle与公共bundle版本不一致,可能导致运行时需要引用的某些模块缺失,造成执行失败。


既要保障系统可用性,又要实现缓存最优,需要做到以下两点:

(1)沿用强缓存+协商缓存组合策略,保证缓存利用率最大化;

(2)内容变化时触发缓存刷新,保证文件版本的一致性和实时性;


据此,不难推导出实际的解决思路:使用webapck 2.x以上版本,将文件hash注入文件名之中。


但此方案会带来另一个问题:公共bundle无法实现持久化缓存。因为manifest出于潜在的异步加载需求,在jsonp逻辑中包含了各个bundle的文件名。也就是说,manifest对业务bundle的文件名存在依赖,导致公共bundle的hash码会随业务bundle的改变而改变,从而造成公共bundle无意义的缓存刷新。


对此,社区常见的解决方案是,通过CommonsChunkPlugin,对公共bundle进一步提取,分离出manifest,作为独立文件单独引用,从而实现公共bundle内容的稳定。


为了一个小小的manifest,额外占用一份并发资源,对于强迫症患者必然是难以接受的。笔者在项目中的替代方案是:借助npm构造指令,将hash作为对应<script>的src参数,而非写入文件名中。这样一来,同样保障了manifest内容的稳定,亦无需额外从公共bundle分离。

以上是关于Webpack原理剖析的主要内容,如果未能解决你的问题,请参考以下文章

webpack打包原理

webpack打包原理

webpack-插件

webpack 4.x 初级学习记录

webpack 原理与实战

webpack原理篇(五十一):webpack启动过程分析