-
Notifications
You must be signed in to change notification settings - Fork 86
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
深入理解 webpack 文件打包机制 #6
Comments
给老哥打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';
/***/ })
]); 文中又说: 一般情况下,一个chunkid对应一个打包出来的js文件,一个js文件加载完毕并执行,则设置 |
@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 上,看兴趣的可以看看:
git clone https://github.com/happylindz/blog.git cd blog/code/webpackBundleAnalysis npm install
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 代码:通过代码分析,可以得出下面的结论:
结尾
好了,讲到这差不多就完了,理解上面的内容对前端模块化会有更多的认知,如果有什么写的不对或者不完整的地方,还望补充说明,希望这篇文章能帮助到你。
The text was updated successfully, but these errors were encountered: