基于webpack 4.x.x的版本
由于tapable类的重写,所以4.x版本和3.x版本在插件机制上有很打区别 如果你对tapable对象不熟悉,可以假装他是一个事件订阅/发布系统,虽然tapable没那么简单就是了。
webpack中的两个重要对象
- compiler对象
其实就是webpack本身暴露出来给我们使用的一个对象。经常我们在自定义node启动webpack中的方法就可以得到compiler对象,当然一般来说该对象全局唯一的,后续再有compiler对象的创建,就是childComplier 例如:
const
这样我们就可以得到compiler实例了,callback是可选的意思。
- compilation实例
compilation实例是每次编译的时候就会获得的对象,在watch模式下,每次watch都会获得一个新的compilation实例,所以comppiler是每次启动webpack获得的全局唯一对象,而compilation则每次更新都会重新获取一遍。获取compilation方法如下:
// webpack4.x版本
compilation.seal方法
webpack 中处理资源生成chunks和优化压缩主要是在webpack的seal阶段,由于我们讲的是资源的压缩,所以我们主要看seal中关于压缩的代码在哪一块。 seal的代码在compilation.js中的seal方法中,重点我们要关注的方法如下:
/**
然后具体的js代码压缩的方式在uglifyjs-wepack-plugin中,如下:
compilation
uglifyjs-wepack-plugin
前一部分,我们只是简单对uglifyjs-wepack-plugin的源码开了个头,不过为什么我分析webpack的js压缩流就突然要研究uglifyjs-wepack-plugin这个三方包了呢,人生真是处处都是惊喜。 接下来我们看看uglifyjs-wepack-plugin中optimizeFn到底干了什么,uglifyjs-wepack-plugin源码传送门 首先我们看到第一段代码。
const
taskRunner呢是一个多进程的任务执行系统,这个从名字就可以看出来,主要是来自于TaskRunner.js他也是uglifyjs-webpack-plugin的核心,taskRunner有个方法叫run,需要两个参数,第一个是tasks的对象数组,第二个是first-error类型的回调函数,表示任务执行运行完成,当然这里的任务主要是指压缩任务啦.
接下来一大串代码就是为了组织定义tasks这个参数是什么样子的。
const
代码真长,也是真丑,咳咳 但是我们还是要继续看,直接看重点,
const task = {
file,
input,
inputSourceMap,
commentsFile,
extractComments: this.options.extractComments,
uglifyOptions: this.options.uglifyOptions,
minify: this.options.minify,
};
这里有几个重要属性,其中file就是要生成的文件名,input就是文件中的字符串的内容,inputSourceMap就是对应的sourcemap文件内容。
好了,现在我们的tasks已经组装好了,还记得前面的taskRunner我们就可以愉快执行taskRunner的run方法来压缩了。
taskRunner
TaskRunner.js
为了降低大脑负荷,我们考虑,假设taskRunner.js中没有缓存和多进程的情况。 于是整体的taskRunner.run里的代码可以简化成以下这个样子。
run
这边大概的流程就是,我们有一个专门执行js代码压缩的程序任务叫boundWorkers,然后有一个存储结果集的results,然后我们异步并行执行压缩js任务,注:这边并不是多进程js压缩。等所有压缩js的任务执行完了,就执行done函数,done函数的主要作用就是闭包index,可以使得到的结果按照顺序插入results里,这点就很想promise.all了,所以如果自己实现一个promise.all的话就可以考虑这个哟。 等所有任务都执行完了,就调用run的callbcak,也就UglifyJsPlugin的optimizeFn中的taskRunner的回调,而该回调的主要作用就是把获得的results放到compilation的assets上,然后再执行optimizeChunkAssets的callbcak,我们就继续回到了webpack的seal流程中啦。接下来我们继续看看minify.js中到底做了什么压缩操作。
minify.js
来来来,我们先不管别的,把minify的代码主要流程抽取一下,抽取之后就变成这样了。
import
以上代码的核心在这一段
const
这样看来,所有的所有的压缩都是uglify.minify操作的,而uglify又是来自于uglify-js,好了,我们追到现在有点追不动了。不过我们可以试试uglify-js这个三方包,比如这个样子:
const
到现在我们的已经把整个流程梳理的差不多了,我们可以稍微尝试(臆想)着自己写一个压缩程序的demo,只实现部分功能。
让我们尝试写一段压缩程序
Javascript混淆器的原理并不复杂,其核心是对目标代码进行AST Transformation(抽象语法树改写),我们依靠现有的Javascript的AST Parser库,能比较容易的实现自己的Javascript混淆器。以下我们借助 acorn来实现一个if语句片段的改写。 假设我们存在这么一个代码片段:
for
那我们就这样操作一下:
const
当然,我们上面的实现只是一个简单的举例,实际上的混淆器实现会比当前的实现复杂得多,需要考虑非常多的语法上的细节,此处仅抛砖引玉供大家参考学习。
压缩流程总结
- 执行seal事件阶段
- 执行compilation.hooks.optimizeChunkAssets
- 执行uglifyjs-webpack-plugin
- 执行optimizeFn
- 执行runner.runTasks
- 执行runner.runTasks的callback
- 执行optimizeFn的callback
- 执行compilation.hooks.optimizeChunkAssets的callback
如果考虑到多进程和缓存的使用的话,流程图应该长下面这个样子。
参(chao)考(xi)资料
- webpack原理
- webpack群侠传(七):代码压缩和缓存
- 前端核心代码保护技术面面观