Open jingzhiMo opened 5 years ago
前阵子去回顾一下tree-shaking的简单原理,然后顺藤摸瓜,逐步把之前不清晰或者不明白的打包基础工具梳理了一遍。
tree-shaking
tree-shaking 就是可以把一些没有用到的代码在打包的过程剔除,进而减少最终的代码体积,例如:
// a.js export const foo = () => {} export const bar = () => {} // b.js import { foo } from './a.js'
因为b.js不包含bar函数,所以最后会被剔除,具体可以看webpack 这篇文章;但是实现tree-shaking的基础是使用ES Module;平常nodeJs所用到的CommonJs的模块定义方式暂时是不能够应用到tree-shaking。ES Module 能实现的主要原因有三个:
b.js
bar
import
export
第一点大概意思是,在模块中,import 与 export 语句不能够嵌套在其他块当中
// ES Module // correctly import { foo } from './a.js' // error if (value) { import { foo } from './a.js' } // CommonJs // correctly var foo = require('./a.js').foo // correctly too if (value) { var foo = require('./a.js').foo }
第二点的大概意思是,导入模块的时候,不能够使用变量进行拼接,只能是字符串常量:
// correctly import foo from './a.js' // error import foo from 'some_module' + SUFFIX
第三点原因,可以结合对模块循环引用的处理不同,来说明一下;
CommonJs对模块的处理是:在遇到require的时候,就进入到对应模块,然后执行依赖模块的代码,引用的模块只会执行一次,后续再依赖相同模块的时候,就不会执行依赖模块的代码;
require
ES Module在遇到import的时候并不会去马上执行依赖模块代码,而至拿到依赖模块的变量引用,当本模块对依赖模块的变量进行计算的时候,才会根据引用去拿数据。
直接文字说明没有代码可能比较晦涩,具体可以参考阮一峰的文章解释循环引用问题。
另外对于ES Module想了解更多的,可以查看exploringjs的文章,讲解得十分清晰,而且带有例子代码。
tree-shaking大概情况就是这样子了,但是实际上,很多依赖的库为了兼容大多数情况,最后都是打包成CommonJs,所以发现很多都没什么用...当然,有部分库会分不同的入口文件,例如main就是CommonJs打包模块的入口,module表示ES Module打包的入口:
main
module
// package.json { name: "package-name", main: "dist/index-cmd.js", module: "dist/index-esm.js" }
这个时候我们需要在webpack设置优先规则:
// webpack.config.js module.exports = { // ... resolve: { // 这是默认配置,可以根据需要进行更改 // 解析路径的时候解析顺序从左往右优先级降低 mainFields: ['module', 'main'] } }
这样子就能够处理那些导出包括ES Module 的库了。
ok,现在再回头看一下webpack的处理过程:babel => tree-shaking => 压缩;我们先从压缩的看起,无意中发现现在webpack默认的压缩工具改了!!!现在默认是:terser与对应的terser plugin,我对压缩工具的处理还停留在uglifyJs...简单看了一下terser的描述,大概意思就是,uglify-es已经停止维护了,uglify-js又不支持对ES6+的处理,所以就forkuglify-es,新建的一个库:
babel => tree-shaking => 压缩
terser
terser plugin
uglifyJs
uglify-es
uglify-js
why-choose-terser
uglify-es is no longer maintained and uglify-js does not support ES6+. terser is a fork of uglify-es that mostly retains API and CLI compatibility with uglify-es and uglify-js@3.
uglify-es is no longer maintained and uglify-js does not support ES6+.
terser is a fork of uglify-es that mostly retains API and CLI compatibility with uglify-es and uglify-js@3.
模块一开始就被babel来处理,但是默认babel会处理为CommonJs,所以配置需要更改。然后也顺便熟悉一下babel的部分插件,此处用@babel/preset-env为例:
@babel/preset-env
// babel.config.js module.exports = { presets: ['@babel/env', { modules: false // 不转换代码中的模块处理方式 }] }
看到这里,也顺便熟悉一下babel的@babel/preset-env和@babel/plugin-transform-runtime;
@babel/plugin-transform-runtime
@babel/preset-env 是一堆插件的组合,通常能够支持最新稳定的 js 语法,而不需要手动去配置;如果对于一些还没有完全确定的js语法,暂时不支持
It is important to note that @babel/preset-env does not support stage-x plugins.
需要注意的是,babel默认是不处理API,只支持语法,例如class语法,箭头函数语法;一些API,例如Promise(ie: ???),Set,String.prototype.includes这些,默认不会转义,需要使用polyfill,这个后面就讲到。
class
Promise
Set
String.prototype.includes
polyfill
@babel/plugin-transform-runtime能够把一部分helper函数,使用模块引入的方式。
A plugin that enables the re-use of Babel's injected helper code to save on codesize.
例如在不使用的情况下(这里用class语法的helper函数作为例子):
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
如果不使用@babel/plugin-transform-runtime插件的时候,这个_createClass函数,在转换的时候,每个包含class文件都会引入这个helper,到最后webpack打包的时候就会有多个这样子的函数,而最后打包的命名规则通常与文件夹与文件名有关系,例如:
_createClass
// 使用前 // index.js function index_createClass () {} // util.js function util_createClass () {}
最后打包出来就很多个这样类似函数,@babel/plugin-transform-runtime就是处理这种情况,能够统一引入,而不是直接把函数内容复制到文件:
// 使用后 var createClass = __webpack.require__(0) // 在文件顶部引入,0 是 webpack 定义模块的id
默认的情况下,@babel/plugin-transform-runtime插件是通过CommonJs方式引用,我们也可以改成ES Module的方式:
// babel.config.js module.exports = { // ... plugins: [ ['@babel/plugin-transform-runtime', { useESModules: true }] ] }
从babel7.4.0开始就放弃了@babel/polyfill的使用,取而代之的是使用core-js来实现polyfill,例子:
@babel/polyfill
core-js
// babel.config.js module.exports = { // ... presets: [ ['@babel/env', { modules: false, corejs: 2, useBuiltIns: 'usage' }] ] }
corejs与useBuiltIns需要配合使用,corejs通常可以指定两个版本2/3;corejs@3版本比corejs@2厉害的地方在于,可以把实例的方法也处理了,例如String.prototype.includes,这个方法属于字符串实例的方法,如果用corejs@2是不能够对这种方法处理的,只能处理一些全局的API,例如Set,Map这些;在@babel/preset-env通过配合corejs + useBuiltIns实现的polyfill,能够根据所需要支持的浏览器(通常是.browserslistrc的内容)与浏览器不支持的API引入对应的polyfill。
corejs
useBuiltIns
2/3
corejs@3
corejs@2
Map
corejs + useBuiltIns
.browserslistrc
例如,支持的浏览器列表中有一项是:safari > 9,而且代码中用到了Set相关,那么就会引入Set的polyfill;如果支持的浏览器列表都是非常新的chrome,那么就不会引入Set的polyfill。还有一个地方需要注意的是,preset中引入的polyfill是会污染全局的API,例如上面所说到的includes方法,会直接在原有的原型链中添加该方法。能否不污染原有的API而引入polyfill?
safari > 9
preset
includes
使用@babel/plugin-transform-runtime插件:
// babel.config.js module.exports = { // ... presets: [ ['@babel/env', { modules: false }] ] plugins: [ ['@babel/plugin-transform-runtime', { corejs: 3, // 使用polyfill useESModules: true }] ] }
处理完之后,原有的API会发生改变,例如:
// 18 只是一个模块的id,会随打包代码不同而改变 var set = __webpack_require__(18) var set_default = __webpack_require.n(set) var set = new Set(['foo']) // 会转义为: var set = new set_default.a(['foo']) // 这里举个例子,可能 a 变量会根据环境不同改变
可以看出,原有的代码,使用新的变量进行代替了,在文件顶部,则会引入对应的模块,这样子只是局部更改,没有污染全局变量;那么有什么不好的?
那就是不能根据.browserslistrc的浏览器进行按需引入,无论浏览器支持与否,都会进行引入对应的模块;假设应用只支持较新版本chrome,当使用@babel/plugin-transform-runtime配合corejs的时候,也会把已支持的API打包到最终的文件...因此有可能使得打包文件变大,所以需要根据情况进行取舍。这个issue的回答也有给到一些关于使用@babel/preset-env与@babel/plugin-transform-runtime的一些建议,可以看一下。
为了减少打包后的体积,首先想到到tree-shaking,但是发现现实的骨干使得情况不能那么简单,还需要配合webpack, terser, babel来处理,每一层都必不可少,了解整个打包流程才使得得到最终的减少打包体积效果...
webpack
babel
写得很清楚
前阵子去回顾一下
tree-shaking
的简单原理,然后顺藤摸瓜,逐步把之前不清晰或者不明白的打包基础工具梳理了一遍。tree-shaking
tree-shaking 就是可以把一些没有用到的代码在打包的过程剔除,进而减少最终的代码体积,例如:
因为
b.js
不包含bar
函数,所以最后会被剔除,具体可以看webpack 这篇文章;但是实现tree-shaking的基础是使用ES Module;平常nodeJs所用到的CommonJs的模块定义方式暂时是不能够应用到tree-shaking。ES Module 能实现的主要原因有三个:import
,export
语句只能在模块顶层的语句出现第一点大概意思是,在模块中,import 与 export 语句不能够嵌套在其他块当中
第二点的大概意思是,导入模块的时候,不能够使用变量进行拼接,只能是字符串常量:
第三点原因,可以结合对模块循环引用的处理不同,来说明一下;
CommonJs对模块的处理是:在遇到
require
的时候,就进入到对应模块,然后执行依赖模块的代码,引用的模块只会执行一次,后续再依赖相同模块的时候,就不会执行依赖模块的代码;ES Module在遇到
import
的时候并不会去马上执行依赖模块代码,而至拿到依赖模块的变量引用,当本模块对依赖模块的变量进行计算的时候,才会根据引用去拿数据。直接文字说明没有代码可能比较晦涩,具体可以参考阮一峰的文章解释循环引用问题。
另外对于ES Module想了解更多的,可以查看exploringjs的文章,讲解得十分清晰,而且带有例子代码。
tree-shaking大概情况就是这样子了,但是实际上,很多依赖的库为了兼容大多数情况,最后都是打包成CommonJs,所以发现很多都没什么用...当然,有部分库会分不同的入口文件,例如
main
就是CommonJs打包模块的入口,module
表示ES Module打包的入口:这个时候我们需要在webpack设置优先规则:
这样子就能够处理那些导出包括ES Module 的库了。
ok,现在再回头看一下webpack的处理过程:
babel => tree-shaking => 压缩
;我们先从压缩的看起,无意中发现现在webpack默认的压缩工具改了!!!现在默认是:terser
与对应的terser plugin
,我对压缩工具的处理还停留在uglifyJs
...简单看了一下terser的描述,大概意思就是,uglify-es
已经停止维护了,uglify-js
又不支持对ES6+的处理,所以就forkuglify-es
,新建的一个库:why-choose-terser
Babel
模块一开始就被babel来处理,但是默认babel会处理为CommonJs,所以配置需要更改。然后也顺便熟悉一下babel的部分插件,此处用
@babel/preset-env
为例:看到这里,也顺便熟悉一下babel的
@babel/preset-env
和@babel/plugin-transform-runtime
;@babel/preset-env
@babel/preset-env
是一堆插件的组合,通常能够支持最新稳定的 js 语法,而不需要手动去配置;如果对于一些还没有完全确定的js语法,暂时不支持需要注意的是,babel默认是不处理API,只支持语法,例如
class
语法,箭头函数语法;一些API,例如Promise
(ie: ???),Set
,String.prototype.includes
这些,默认不会转义,需要使用polyfill
,这个后面就讲到。@babel/plugin-transform-runtime
@babel/plugin-transform-runtime
能够把一部分helper函数,使用模块引入的方式。例如在不使用的情况下(这里用class语法的helper函数作为例子):
如果不使用
@babel/plugin-transform-runtime
插件的时候,这个_createClass
函数,在转换的时候,每个包含class文件都会引入这个helper,到最后webpack打包的时候就会有多个这样子的函数,而最后打包的命名规则通常与文件夹与文件名有关系,例如:最后打包出来就很多个这样类似函数,
@babel/plugin-transform-runtime
就是处理这种情况,能够统一引入,而不是直接把函数内容复制到文件:默认的情况下,
@babel/plugin-transform-runtime
插件是通过CommonJs方式引用,我们也可以改成ES Module的方式:polyfill
从babel7.4.0开始就放弃了
@babel/polyfill
的使用,取而代之的是使用core-js
来实现polyfill,例子:corejs
与useBuiltIns
需要配合使用,corejs
通常可以指定两个版本2/3
;corejs@3
版本比corejs@2
厉害的地方在于,可以把实例的方法也处理了,例如String.prototype.includes
,这个方法属于字符串实例的方法,如果用corejs@2
是不能够对这种方法处理的,只能处理一些全局的API,例如Set
,Map
这些;在@babel/preset-env
通过配合corejs + useBuiltIns
实现的polyfill,能够根据所需要支持的浏览器(通常是.browserslistrc
的内容)与浏览器不支持的API引入对应的polyfill。例如,支持的浏览器列表中有一项是:
safari > 9
,而且代码中用到了Set
相关,那么就会引入Set
的polyfill;如果支持的浏览器列表都是非常新的chrome,那么就不会引入Set
的polyfill。还有一个地方需要注意的是,preset
中引入的polyfill是会污染全局的API,例如上面所说到的includes
方法,会直接在原有的原型链中添加该方法。能否不污染原有的API而引入polyfill?使用
@babel/plugin-transform-runtime
插件:处理完之后,原有的API会发生改变,例如:
可以看出,原有的代码,使用新的变量进行代替了,在文件顶部,则会引入对应的模块,这样子只是局部更改,没有污染全局变量;那么有什么不好的?
那就是不能根据
.browserslistrc
的浏览器进行按需引入,无论浏览器支持与否,都会进行引入对应的模块;假设应用只支持较新版本chrome,当使用@babel/plugin-transform-runtime
配合corejs
的时候,也会把已支持的API打包到最终的文件...因此有可能使得打包文件变大,所以需要根据情况进行取舍。这个issue的回答也有给到一些关于使用@babel/preset-env
与@babel/plugin-transform-runtime
的一些建议,可以看一下。小结
为了减少打包后的体积,首先想到到
tree-shaking
,但是发现现实的骨干使得情况不能那么简单,还需要配合webpack
,terser
,babel
来处理,每一层都必不可少,了解整个打包流程才使得得到最终的减少打包体积效果...参考文章