Open cpselvis opened 5 years ago
在专栏课程里,有位同学提到过一个很有意思的问题:“我没装 babel,js 入口里写了个箭头函数,运行 webpack 构建命令后,也成功编译了。这是为什么?”。今天就带领大家一起去探讨下这个话题。
在使用 webpack 的时候,很常见的一个构建优化手段就是缩小构建目标。比如在构建阶段只构建 src 里面的模块代码,对于 node_modules 里面所引入的三方包不进行构建操作。
如果使用的是 webpack 3.x 版本,编写的构建脚本类似这样的,我们通过设置loader 里面的 exclude 字段避免由于解析 node_modules 里面的模块造成的构建耗时:
const path = require('path'); const webpack = require('webpack'); module.exports = { entry: './src/index.js', output: { path: path.join(__dirname, 'dist'), filename: 'bundle.js' }, module: { rules: [ { test: /\.js$/, loader: 'happypack/loader', exclude: path.join(__dirname, 'node_modules') } ] } plugins: [ new webpack.optimize.UglifyJsPlugin() ] };
我们经常会遇到一个问题,假设引入的 npm 包质量不够高,比如 node_modules 里面有 ES6 的语法,那么 webpack 在 uglify 阶段会报错!下面给出两种常见的出错场景:
假设 node_modules 里面存在 ES6 的模板字符串语法,那么在生产环境打包的代码压缩阶段,UglifyJs 会抛出错误。
同样的,你使用 ES6 的箭头函数也是无法正常的压缩代码的。
细心的你一定会发现如果使用的是 webpack 4,这个场景描述的问题将不再出现。webpack 4默认支持 ES6 代码的压缩,这个是什么原因呢?
如果你有对 webpack 4 的依赖包进行过相关分析,比如直接查阅 package.json 文件或者通过 http://npm.broofa.com/ 网站上进行 webpack 依赖图分析。不难发现 webpack 4 里面使用了 terser-webpack-plugin 插件替代了之前一直使用的 uglifyjs-webpack-plugin 作为它的内置插件。
以 4.39.3 这个版本为例,可以看到它的 package.json 文件的依赖包括了terser-webpack-plugin。
我们进一步分析发现 webpack 的 4.26.0 这个版本有一次提交,它的提交内容是对 webpack 内置插件进行了一次切换。
经过这么一次分析,我们可以知道 webpack 4 之所以具备默认压缩 ES6 代码的能力,离不开 terser-webpack-plugin 所起的作用!
在探究 terser-webpack-plugin 插件的原理前,我们先系统的回顾一下代码压缩插件的历史:
备注:压缩插件历史的来源 https://github.com/webpack/webpack/commit/311a7285d36b38bada46102967c431e93ff48a89
到这里,我们可以得出一个基本的结论:terser-webpack-plugin 基于 terser 因此它具备 ES6 的压缩能力,uglifyjs-webpack-plugin v2.x 版本基于 uglify-js,无法支持 ES6 的压缩。
代码压缩原理其实挺简单的,也是 AST 的一个经典的应用案例。它的压缩过程通常是:
JS 源代码 -> AST -> 美化、压缩 -> 新的 AST -> 压缩后的代码
了解了代码压缩的基本流程后,接下来我们看看源码包含了哪些内容,由于 terser 是从 uglify-es Fork 出来进行修改的,因此它的代码结构和 uglify-js 基本一致,只不过 terser 使用了 ES6 模块的静态分析功能。我们以 terser 的源码为例分析下:
然后,我们来一探 terser 和 uglify-js 的差异。对比了之后,发现一个很大的差异是 AST 的支持上面不同。
分析AST的差异发现,下面是两个文件 diff 对比只在 terser 中才有,而这些刚好对应 ES6 的语法。
AST_Arrow, AST_Await, AST_BigInt, AST_Class, AST_ClassExpression, AST_ConciseMethod, AST_Const, AST_DefaultAssign, AST_Destructuring, AST_Expansion, AST_Export, AST_ForOf, AST_Import, AST_Let, AST_NameMapping, AST_NewTarget, AST_PrefixedTemplateString, AST_Super, AST_SymbolMethod, AST_TemplateSegment, AST_TemplateString, AST_Yield
至此,我们发现 webpack4 默认支持 ES6 压缩的关键是:terser 里面实现了 ES6 语法的 AST解析。
@cpselvis 👍 Good Job!
在使用 webpack 的时候,很常见的一个构建优化手段就是缩小构建目标。比如在构建阶段只构建 src 里面的模块代码,对于 node_modules 里面所引入的三方包不进行构建操作。
发现问题
如果使用的是 webpack 3.x 版本,编写的构建脚本类似这样的,我们通过设置loader 里面的 exclude 字段避免由于解析 node_modules 里面的模块造成的构建耗时:
我们经常会遇到一个问题,假设引入的 npm 包质量不够高,比如 node_modules 里面有 ES6 的语法,那么 webpack 在 uglify 阶段会报错!下面给出两种常见的出错场景:
ES6 的模板字符串
假设 node_modules 里面存在 ES6 的模板字符串语法,那么在生产环境打包的代码压缩阶段,UglifyJs 会抛出错误。
ES6 的箭头函数
同样的,你使用 ES6 的箭头函数也是无法正常的压缩代码的。
细心的你一定会发现如果使用的是 webpack 4,这个场景描述的问题将不再出现。webpack 4默认支持 ES6 代码的压缩,这个是什么原因呢?
初步分析
如果你有对 webpack 4 的依赖包进行过相关分析,比如直接查阅 package.json 文件或者通过 http://npm.broofa.com/ 网站上进行 webpack 依赖图分析。不难发现 webpack 4 里面使用了 terser-webpack-plugin 插件替代了之前一直使用的 uglifyjs-webpack-plugin 作为它的内置插件。
以 4.39.3 这个版本为例,可以看到它的 package.json 文件的依赖包括了terser-webpack-plugin。
我们进一步分析发现 webpack 的 4.26.0 这个版本有一次提交,它的提交内容是对 webpack 内置插件进行了一次切换。
经过这么一次分析,我们可以知道 webpack 4 之所以具备默认压缩 ES6 代码的能力,离不开 terser-webpack-plugin 所起的作用!
进一步分析
在探究 terser-webpack-plugin 插件的原理前,我们先系统的回顾一下代码压缩插件的历史:
备注:压缩插件历史的来源 https://github.com/webpack/webpack/commit/311a7285d36b38bada46102967c431e93ff48a89
到这里,我们可以得出一个基本的结论:terser-webpack-plugin 基于 terser 因此它具备 ES6 的压缩能力,uglifyjs-webpack-plugin v2.x 版本基于 uglify-js,无法支持 ES6 的压缩。
原理探究
代码压缩原理其实挺简单的,也是 AST 的一个经典的应用案例。它的压缩过程通常是:
了解了代码压缩的基本流程后,接下来我们看看源码包含了哪些内容,由于 terser 是从 uglify-es Fork 出来进行修改的,因此它的代码结构和 uglify-js 基本一致,只不过 terser 使用了 ES6 模块的静态分析功能。我们以 terser 的源码为例分析下:
然后,我们来一探 terser 和 uglify-js 的差异。对比了之后,发现一个很大的差异是 AST 的支持上面不同。
分析AST的差异发现,下面是两个文件 diff 对比只在 terser 中才有,而这些刚好对应 ES6 的语法。
至此,我们发现 webpack4 默认支持 ES6 压缩的关键是:terser 里面实现了 ES6 语法的 AST解析。