本文将通过前端模块化发展历史引入webpack出现的原因,解决的问题,以及目前的困境。
至今,前端模块化已经发展了有十余年了,总结起来,它们解决的问题主要有三个
如果对模块化历史感兴趣,推荐阅读前端模块化的十年征程
一些标志性工具诞生时间线:
生态 | 诞生时间(年) |
---|---|
Node.js | 2009 |
NPM | 2010 |
requireJS(AMD) | 2010 |
seaJS(CMD) | 2011 |
broswerify | 2011 |
webpack | 2012 |
rollup | 2018 |
vite | 2020 |
snowpack | 2020 |
在刀耕火种的年代,如果我们需要在项目里使用某个外部模块,我们可能会去官网直接把文件下载下来放到项目中,同时在入口html中通过script标签引用它。
后来NPM出现了,它是一个Node自带的模块管理工具。万千前端开发者们可以通过npm publish
的方式将自己的模块发布到NPM上去。当需要引用外部模块时,通过运行npm install [模块名]
,可以将别人的模块下载到自己项目根目录中一个叫node_modules
的子目录下。
我们可以通过配置化的文件指定引入的依赖版本,也就是package.json
文件。这就解决了外部模块管理的问题。
这个时期,我们使用script标签引入模块,一些模块需要通过一个“立即调用的函数表达式”(IIFE)去组织。
<script>
var module1 = (function(){
var x = 1
return { a: x };
})();
</script>
<script>
var module2 = (function(){
var a = module1.a;
return { b: a };
})();
</script>
随着项目扩大,html文件中会包含大量script标签。这样会出现两个主要的问题:
为了解决以上问题,社区逐渐发展出一些规范来约束模块的加载。第一阶段,占据主流的是AMD && CMD。
首先开始在前端流行的模块化规范是AMD/CMD, 以及实践这两种规范的require.js和Sea.js, AMD和CMD可看作是"在线处理"模块的方案,也就是等到用户浏览web页面下载了对应的require.js和sea.js文件之后,才开始进行模块依赖分析,确定加载顺序和执行顺序。模块组织过程在线上进行。
AMD是Asynchronous Module Definition
,意思就是"异步模块定义"。AMD推崇依赖前置,即通过依赖数组的方式提前声明当前模块的依赖。
CMD即Common Module Definition
,意为“通用模块定义”。CMD推崇依赖就近,在编程需要用到的时候通过调用require方法动态引入
后来,伴随着babel等编译工具和webpack等自动化工具的出现,AMD/CMD逐渐湮没在历史的浪潮当中,然后大家都习惯于用CommonJS和ES6的模块化方式编写代码了。
CommonJS是Node.js使用的模块化方式,而import/export则是ES6提出的模块化规范。它们的语法规则如下。
// ES6
import { a } from './Module';
export const b = 1;
// CommonJS
const a = require(./Module);
module.exports = {
b: 1
}
最开始的时候,浏览器并不能理解这种语法。
但是编译工具babel的出现,使我们在开发环境使用它们变成了可能。
在babel出现之前的AMD/CMD时代,开发和生产的代码并没有明显的区分性。
而babel则将开发和生产这两个流程分开了,让我们在开发时可以更专注于逻辑的实现、代码的便捷性和可阅读性。
至此,ES6编程的时代到来。
受限于早期浏览器并发限制、海量HTTP性能等因素,像之前的AMD/CMD这种“在线编译”方案,会产生一些性能问题。
为了优化模块在线编译导致的耗时,更好的管理依赖的加载和执行顺序,合并部分请求避免并发请求限制导致的阻塞,开发者们创造了一个工具来做这些工作。
webpack就是在这样的背景下应运而生的。
它一开始的定位是通过预先打包的方式,把前端项目里面的多个文件打包成单个文件或少数几个文件,这样的话就可以压缩首次页面访问时的http请求数量,从而提高性能。
当然,代码打包不是一本万利的,它们也面临着一些副作用带来的问题。其中最大的问题就是多个文件合成打包后,打包产物的体积会过大,下载耗时长,如此一来,首屏加载的时间还是会被延长。
webpack于是引入了代码拆分的功能(Code Splitting)来解决这个问题, 从全部打包后退一步:可以打包成多个包。数量还是少于之前无预编译的模块的。
Code Splitting主要有两方面的作用:
一是实现依赖分离,将第三方库和业务代码的分离,比起业务代码,变动较少的第三方库可以更好地利用浏览器缓存机制。
二是实现按需加载,减少首页加载的文件体积,通过路由跳转时再加载对应页面的模块。
随着技术的进步,全方位的自动化构建工具慢慢发展,开发者已经不满足于简单的代码拆分打包,希望有更全方位的自动化代码处理和优化工具,我们希望做到:
等等附加功能。
webpack并没有自己实现所有的功能,它是一个微内核的架构,得益于架构的灵活性,它允许开发者开发自己的loader和plugin来扩展功能。
在此基础上,经过了七八年的发展,配置式的webpack渐渐打败编程式的gulp,成为前端开发者的主流自动化构建工具。
webpack在诞生之初采用集中打包方式进行开发,有两个方面的原因:
一是浏览器的兼容性还不够良好,还没提供对ES6的足够支持,需要把每个JS文件打包成单一bundle中的闭包的方式实现模块化。
二是为了合并请求,减少HTTP/1.1下过多并发请求带来的性能问题。
而技术发展到今天,过去的这些问题已经得到了很大的缓解,因为主流现代浏览器已经能充分支持ES6了,import和export随心使用。而且在HTTP2.0普及后,并发请求的性能问题没有不再突出。
bundleless就是把开发中拖慢速度的打包工作给去掉,从而获得更快的开发速度。代表性工具是vite。
vite大大提高了开发阶段代码的构建速度,解决了webpack被诟病许久的dev环境构建耗时长的问题,但是在生产阶段的打包还是无法超越目前webpack打包的成熟性和通用性,对于非js文件的支持性也不是很好,需要依赖更多的工具处理。
vite的出现警醒了webpack,目前不再是一家独大的市场,不在竞争中前进就会被时代抛弃。webpack目前也在测试版本中引入ESM的打包方式,期待后续它的优化。