Open happylindz opened 6 years ago
给老哥打call!
大神的每一篇博文干货满满呀
@strongcode9527 哈哈哈,也要有人看才行
@happylindz 要持续更新呦,我可是你忠实的粉丝哈哈,最近正想自己学习一下webpack的原理,并且自己撸一个简单的webpack
@happylindz ,看完文章,本地也做了个测试("webpack": "^3.10.0")。但是发现我的vendor.xxxxxx.js不是这样的咧:
// vendor.xxxx.js
webpackJsonp([3,4],{
3: (function(module, exports) {
module.exports = 'util B';
})
});
而是这样的:
webpackJsonp([2],[
/* 0 */
/***/ (function(module, exports) {
module.exports = 'util B';
/***/ })
]);
文中又说:“[3, 4] 代表 chunkId,当加载到 vendor 文件后,installedChunks[3] 和 installedChunks[4] 将会被置为 0,这表明 chunk3,chunk4 已经被加载过了”
,“webpackJsonpCallback 一共有两个参数,chuckIds 一般包含该 chunk 文件依赖的 chunkId 以及自身 chunkId”
一般情况下,一个chunkid对应一个打包出来的js文件,一个js文件加载完毕并执行,则设置installedChunks[chunkId] = 0
,这个很常见。啥时候会出现chunkIds为多个时候?也就是你所说的有依赖chunk的时候。这里就非常迷惑了,一时半会没搞明白,还请作者指点指点!🤓
@xblxc 这个问题我不是很清楚,我猜想它有多个的原因是因为可能存在依赖的 chunkid 是多个,它需要保证之前 chunkid 的回调都执行完。
@xblxc 至于是几个,我觉得没必要太纠结,只要知道它的执行原理即可,如果你要深入研究的话那得去看卡 webpack 源码这个 chunkid 到底是怎么生成的
希望大神经常写一写,受益匪浅。感谢
@baihaiyang 哈哈哈,那要经常来看
marke 学习了
happylindz 楼主 问一个问题 我现在这边的vendor的hash 每次都会改变 runtime已经单独提出到一个文件了。像你文章说的 不用name来标记chunk 而是用数组下标 我在业务中进行任何import操作 都会使vendor更新。查了文章说是因为每个chunk的id是有序的,每次新加import的时候 会改变这个顺序,导致chunkid改变 所以vendor的hash就会每次都跟着变了。 不知道是我用的不对,还是就是要改成以name的方式?
@happylindz 补充一下 我用了name的方式 并且在vendor中引用也用了路径的方式指向module 确实vendor的hash就不会变了 所以想问下到底是什么原因
原文地址
深入理解 webpack 文件打包机制
前言
最近在重拾 webpack 一些知识点,希望对前端模块化有更多的理解,以前对 webpack 打包机制有所好奇,没有理解深入,浅尝则止,最近通过对 webpack 打包后的文件进行查阅,对其如何打包 JS 文件有了更深的理解,希望通过这篇文章,能够帮助读者你理解:
本文所有示例代码全部放在我的 Github 上,看兴趣的可以看看:
webpack 单文件如何打包?
首先现在 webpack 作为当前主流的前端模块化工具,在 webpack 刚开始流行的时候,我们经常通过 webpack 将所有处理文件全部打包成一个 bundle 文件, 先通过一个简单的例子来看:
通过
npm run build:single
可看到打包效果,打包内容大致如下(经过精简):将相对无关的代码剔除掉后,剩下主要的代码:
__webpack_require__
模块加载,先判断 installedModules 是否已加载,加载过了就直接返回 exports 数据,没有加载过该模块就通过modules[moduleId].call(module.exports, module, module.exports, __webpack_require__)
执行模块并且将 module.exports 给返回。很简单是不是,有些点需要注意的是:
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__)
保证了模块加载时 this 的指向 module.exports 并且传入默认参数,很简单,不过多解释。webpack 多文件如何进行代码切割?
webpack 单文件打包的方式应付一些简单场景就足够了,但是我们在开发一些复杂的应用,如果没有对代码进行切割,将第三方库(jQuery)或框架(React) 和业务代码全部打包在一起,就会导致用户访问页面速度很慢,不能有效利用缓存,你的老板可能就要找你谈话了。
那么 webpack 多文件入口如何进行代码切割,让我先写一个简单的例子:
这里我们定义了两个入口 pageA 和 pageB 和三个库 util,我们希望代码切割做到:
那么 webpack 需要怎么配置呢?
单单配置多 entry 是不够的,这样只会生成两个 bundle 文件,将 pageA 和 pageB 所需要的内容全部放入,跟单入口文件并没有区别,要做到代码切割,我们需要借助 webpack 内置的插件 CommonsChunkPlugin。
首先 webpack 执行存在一部分运行时代码,即一部分初始化的工作,就像之前单文件中的
__webpack_require__
,这部分代码需要加载于所有文件之前,相当于初始化工作,少了这部分初始化代码,后面加载过来的代码就无法识别并工作了。这段代码的含义是,在这些入口文件中,找到那些引用两次的模块(如:utilB),帮我抽离成一个叫 vendor 文件,此时那部分初始化工作的代码会被抽离到 vendor 文件中。
这段代码的含义是在 vendor 文件中帮我把初始化代码抽离到 mainifest 文件中,此时 vendor 文件中就只剩下 utilB 这个模块了。你可能会好奇为什么要这么做?
因为这样可以给 vendor 生成稳定的 hash 值,每次修改业务代码(pageA),这段初始化时代码就会发生变化,那么如果将这段初始化代码放在 vendor 文件中的话,每次都会生成新的 vendor.xxxx.js,这样不利于持久化缓存,如果不理解也没关系,下次我会另外写一篇文章来讲述这部分内容。
另外 webpack 默认会抽离异步加载的代码,这个不需要你做额外的配置,pageB 中异步加载的 utilC 文件会直接抽离为 chunk.xxxx.js 文件。
所以这时候我们页面加载文件的顺序就会变成:
执行
npm run build:multiple
即可查看打包内容,首先来看下 manifest 如何做初始化工作(精简版)?与单文件内容一致,定义了一个自执行函数,因为它不包含任何模块,所以传入一个空数组。除了定义了
__webpack_require__
,还另外定义了两个函数用来进行加载模块。首先讲解代码前需要理解两个概念,分别是 module 和 chunk
__webpack_require__
加载的模块,同样的使用数组下标作为 moduleId 且是唯一不重复的。那么为什么要区分 chunk 和 module 呢?
首先使用 installedChunks 来保存每个 chunkId 是否被加载过,如果被加载过,则说明该 chunk 中所包含的模块已经被放到了 modules 中,注意是 modules 而不是 installedModules。我们先来简单看一下 vendor chunk 打包出来的内容。
在执行完 manifest 后就会先执行 vendor 文件,结合上面 webpackJsonp 的定义,我们可以知道 [3, 4] 代表 chunkId,当加载到 vendor 文件后,installedChunks[3] 和 installedChunks[4] 将会被置为 0,这表明 chunk3,chunk4 已经被加载过了。
webpackJsonpCallback
一共有两个参数,chuckIds 一般包含该 chunk 文件依赖的 chunkId 以及自身 chunkId,moreModules 代表该 chunk 文件带来新的模块。简单说说
webpackJsonpCallback
做了哪些事,首先判断 chunkIds 在 installedChunks 里有没有回调函数函数未执行完,有的话则放到 callbacks 里,并且等下统一执行,并将 chunkIds 在 installedChunks 中全部置为 0, 然后将 moreModules 合并到 modules。这里面只有 modules[0] 是不固定的,其它 modules 下标都是唯一的,在打包的时候 webpack 已经为它们统一编号,而 0 则为入口文件即 pageA,pageB 各有一个 module[0]。
然后将 callbacks 执行并清空,保证了该模块加载开始前所以前置依赖内容已经加载完毕,最后判断 moreModules[0], 有值说明该文件为入口文件,则开始执行入口模块 0。
上面解释了一大堆,但是像 pageA 这种同步加载 manifest, vendor 以及 pageA 文件来说,每次加载的时候 callbacks 都是为空的,因为它们在 installedChunks 中的值要嘛为 undefined(未加载), 要嘛为 0(已被加载)。installedChunks[chunkId] 的值永远为 false,所以在这种情况下 callbacks 里根本不会出现函数,如果仅仅是考虑这样的场景,上面的
webpackJsonpCallback
完全可以写成下面这样:但是考虑到异步加载 js 文件的时候(比如 pageB 异步加载 utilC 文件),就没那么简单,我们先来看下 webpack 是如何加载异步脚本的:
大致分为三种情况,(已经加载过,正在加载中以及从未加载过)
我们通过 utilC 生成的 chunk 来进行讲解:
pageB 需要异步加载这个 chunk:
当 pageB 进行某种操作需要加载 utilC 时就会执行
__webpack_require__.e(2, callback)
2,代表需要加载的模块 chunkId(utilC),异步加载 utilC 并将 callback 添加到 installedChunks[2] 中,然后当 utilC 的 chunk 文件加载完毕后,chunkIds 包含 2,发现 installedChunks[2] 是个数组,里面还有之前还未执行的 callback 函数。既然这样,那我就将我自己带来的模块先放到 modules 中,然后再统一执行之前未执行完的 callbacks 函数,这里指的是存放于 installedChunks[2] 中的回调函数 (可能存在多个),这也就是说明这里的先后顺序:
webpack1 和 webpack2 在文件打包上有什么区别?
经过我对打包文件的观察,从 webpack1 到 webpack2 在打包文件上有下面这些主要的改变:
首先,moduleId[0] 不再为入口执行函数做保留,所以说不用傻傻看到 moduleId[0] 就认为是打包文件的入口模块,取而代之的是
window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {}
传入了第三个参数 executeModules,是个数组,如果参数存在则说明它是入口模块,然后就去执行该模块。其次,webpack2 中会默认加载 OccurrenceOrderPlugin 这个插件,即你不用 plugins 中添加这个配置它也会默认执行,那它有什么用途呢?主要是在 webpack1 中 moduleId 的不确定性导致的,在 webpack1 中 moduleId 取决于引入文件的顺序,这就会导致这个 moduleId 可能会时常发生变化, 而 OccurrenceOrderPlugin 插件会按引入次数最多的模块进行排序,引入次数的模块的 moduleId 越小,比如说上面引用的 utilB 模块引用次数为 2(最多),所以它的 moduleId 为 0。
最后说下在异步加载模块时, webpack2 是基于 Promise 的,所以说如果你要兼容低版本浏览器,需要引入
Promise-polyfill
,另外为引入请求添加了错误处理。可以看出,原本基于回调函数的方式已经变成基于 Promise 做异步处理,另外添加了
onScriptComplete
用于做脚本加载失败处理。在 webpack1 的时候,如果由于网络原因当你加载脚本失败后,即使网络恢复了,你再次进行某种操作需要同个 chunk 时候都会无效,主要原因是失败之后没把
installedChunks[chunkId] = undefined;
导致之后不会再对该 chunk 文件发起异步请求。而在 webpack2 中,当脚本请求超时了(2min)或者加载失败,会将 installedChunks[chunkId] 清空,当下次重新请求该 chunk 文件会重新加载,提高了页面的容错性。
这些是我在打包文件中看到主要的区别,难免有所遗漏,如果你有更多的见解,欢迎在评论区留言。
webpack2 如何做到 tree shaking?
什么是 tree shaking,即 webpack 在打包的过程中会将没用的代码进行清除(dead code)。一般 dead code 具有一下的特征:
是不是很神奇,那么需要怎么做才能使 tree shaking 生效呢?
首先,模块引入要基于 ES6 模块机制,不再使用 commonjs 规范,因为 es6 模块的依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,然后清除没用的代码。而 commonjs 的依赖关系是要到运行时候才能确定下来的。
其次,需要开启 UglifyJsPlugin 这个插件对代码进行压缩。
我们先写一个例子来说明:
打包的配置也很简单:
通过
npm run build:es6
对压缩的文件进行分析:引入但是没用的变量,函数都会清除,未执行的代码也会被清除。但是类方法是不会被清除的。因为 webpack 不会区分不了是定义在 classC 的 prototype 还是其它 Array 的 prototype 的,比如 classC 写成下面这样:
webpack 无法保证 prototype 挂载的对象是 classC,这种代码,静态分析是分析不了的,就算能静态分析代码,想要正确完全的分析也比较困难。所以 webpack 干脆不处理类方法,不对类方法进行 tree shaking。
更多的 tree shaking 的副作用可以查阅:Tree shaking class methods
webpack3 如何做到 scope hoisting?
scope hoisting,顾名思义就是将模块的作用域提升,在 webpack 中不能将所有所有的模块直接放在同一个作用域下,有以下几个原因:
在 webpack3 中,这些情况生成的模块不会进行作用域提升,下面我就举个例子来说明:
这个例子比较典型,utilA 被 pageA 和 pageB 所共享,utilB 被 pageB 单独加载,utilC 被 pageB 异步加载。
想要 webpack3 生效,则需要在 plugins 中添加 ModuleConcatenationPlugin。
webpack 配置如下:
运行
npm run build:hoist
进行编译,简单看下生成的 pageB 代码:通过代码分析,可以得出下面的结论:
结尾
好了,讲到这差不多就完了,理解上面的内容对前端模块化会有更多的认知,如果有什么写的不对或者不完整的地方,还望补充说明,希望这篇文章能帮助到你。