# babel7-docs **Repository Path**: yhding/babel7-docs ## Basic Information - **Project Name**: babel7-docs - **Description**: 理解常用的babel相关工具及配置 https://www.babeljs.cn/docs/usage - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2022-12-25 - **Last Updated**: 2022-12-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 为什么打包出来的内容那么大? [你构建的代码为什么这么大](https://zhuanlan.zhihu.com/p/593065108) 代码体积大意味着影响资源加载速度,间接影响浏览器各类指标。例如:内存消耗,内存的增加触发频繁的V8引擎频繁的GC,影响页面交互。 ## babel —— `js编译器`的工具链 ### 工具链有哪些? #### 编译 与 预置配置 提供 `@babel/core` 进行代码编译,并通过`@babel/cli`提供行命令行工具。并预置一套配置包 `@babel/preset-env`。 ```json { "presets": [ [ "@babel/preset-env", { "targets": "> 0.25%, not dead", "useBuiltIns": "usage", "corejs": "3.22" } ] ] } ``` - targets 这里表示:仅包括浏览器市场份额超过0.25%的用户,忽略没有安全更新的浏览器,如 IE10 和 BlackBerry - useBuiltIns 使用的降级模式,后续有提到 - corejs 使用 core-js@3 的原因,core-js@2 分支中已经不会再添加新特性,新特性都会添加到 core-js@3。 预置插件所包含的支持所有最新(不包括 `stage` 阶段)的`js特性`,`stage`阶段的特性需要额外安装对应的插件。编译时会根据开发者指定的预设环境的`targets`选项或`.browserlistrc`来指定目标环境。 #### 插件 并提供很多插件工具供选择,一类是`语法插件`负责`babel`解析特定语法(不是转换)例如:`@babel/plugin-proposal-optional-chaining`可以解析语法可选链: ```js obj?.a.b // => // obj.a && obj.a.b ``` 另一类是`转换插件`负责(非必要的步骤)`解析`特定语法,(但是一定会做的)进行特定于法`ast语法树`的`转换`,以支持特定语法。 ##### `@babel/plugin-transform-runtime`插件 看一下不使用 `@babel/plugin-transform-runtime` 插件的情况: ```js // { // "presets": [ // [ // "@babel/preset-env", // { // "targets": "> 0.25%, not dead", // "useBuiltIns": "usage", // "corejs": "3.22" // } // ] // ] // } "use strict"; require("core-js/modules/es.object.to-string.js"); require("core-js/modules/es.promise.js"); require("core-js/modules/es.promise.finally.js"); Promise.resolve().finally(); ``` 采用的方式是污染全局的`Promise`,当遇到第三方库通过这种污染的方式时环境的`Promise`会是一个迷。 ###### 那么该插件解决了什么?: - 重复注入辅助函数的问题。 - 代码沙盒环境 —— 通过给`corejs`起别名的方式解决污染全局`api`的问题,因为很难控制使用第三方库的环境。 使用了`@babel/plugin-transform-runtime`的编译结果,通过`regenerator: true`将所有的辅助函数都起了别名提供沙盒环境,使用了`corejs: 3`选项解决重复辅助函数的问题。 ```js // 别名 "use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); var _marked = /*#__PURE__*/_regenerator.default.mark(foo); function foo() { return _regenerator.default.wrap(function foo$(_context) { while (1) switch (_context.prev = _context.next) { case 0: case "end": return _context.stop(); } }, _marked); } ``` 需要注意,代码不是直接注入进来,而是通过`@babel/runtime`包引入的。 > 使用 `alias` 解决了污染全局环境。 一般需要配合`@babel/rumtime`或`@babel/runtime-corejs3`一起使用,配置如下: ```json { "presets": [ [ "@babel/preset-env", { "targets": "> 0.25%, not dead", "useBuiltIns": "usage", "corejs": "3.22" } ] ], "plugins": [ [ "@babel/plugin-transform-runtime", { "absoluteRuntime": false, "corejs": 3, "helpers": true, "regenerator": true, "version": "^7.20.7" // version 是@babel/runtime 或者 @babel/runtime-corejs2/3 的版本 } ] ] } ``` 大概的依赖关系 | corejs option头 | Install command | | --------------- | ----------------------------------------- | | false | npm install --save @babel/runtime | | 2 | npm install --save @babel/runtime-corejs2 | | 3 | npm install --save @babel/runtime-corejs3 | **注意**需要的更新的认知:`@babel/runtime` 等价`@babel/runtime-corejs2`等价`@babel/runtime-corejs3`起到的作用都是一样的。 ##### `@babel/runtime`依赖 解决的是重复引入模块的问题,所有辅助函数都会从`@babel/runtime`里引入模块。而未引入的`@babel/plugin-transform-runtime`的情况下,会重复编译多个文件中相同的辅助函数到对应的文件。 > 注意:与 `@babel/plugin-transform-runtime`的`corejs: 3`选项二选一 看到引入`@babel/plugin-transform-runtime`编译结果在没有启用`corejs: 3`的选项下默认是从`@babel/runtime`引入的**辅助函数**模块,所以需要同时引入。打包后的代码: ```js "use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _regenerator = _interopRequireDefault(require("@babel/runtime/regenerator")); var _marked = /*#__PURE__*/_regenerator.default.mark(foo); function foo() { return _regenerator.default.wrap(function foo$(_context) { while (1) switch (_context.prev = _context.next) { case 0: case "end": return _context.stop(); } }, _marked); } ``` 配置如下: ```json { "presets": [ [ "@babel/preset-env", { "targets": "> 0.25%, not dead", } ] ], "plugins": ["@babel/plugin-transform-runtime"] } ``` > @babel/plugin-transform-runtime 默认需要@bable/runtime 注意`useBuiltIns`和`corejs`已经注释掉了,可以看下打包后的代码,引入路径已经从`core-js/modules/***` 变成了`@babel/runtime/***`。 已经没有必要再保留这两个配置了。 如果使用了`@babel/plugin-transform-runtime` `corejs选项`那么请参考如下配置: ##### `@babel/runtime-corejs3` 依赖 需要注意`@babel/preset-env`的配置指定了`corejs`的版本是为了从`corejs/**`包里引入代码, ```js require("core-js/modules/es.promise.finally.js"); ``` 现在使用了`@babel/plugin-transform-runtime`和`@babel/runtime`已经将从`corejs`引入代码改成了从`@babel/runtime`引入**辅助函数** ```js var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); ``` 新一代的`@babel/runtime`可替代产品`@babel/runtime-corejs3`, 需要安装`npm i -S @babel/runtime-corejs3`,移除 `@babel/preset-env` 的 `corejs`配置。 **不使用`corejs3`配置的情况下** ```json { "presets": [ [ "@babel/preset-env", { "targets": "> 0.25%, not dead" } ] ], "plugins": [ [ "@babel/plugin-transform-runtime", { "absoluteRuntime": false, "corejs": 3, "helpers": true, "regenerator": true, "version": "^7.20.7" } ] ] } ``` 这里的相关配置说明: - corejs: 3 使用对应版本的runtime helpers `@babel/runtime-corejs3`。 - helpers: 切换内联的babel助手(classCallCheck, extends等)是否替换成为调用moduleName。 - regenerator: 是否将生成器函数转换为使用不会污染全局范围的再生器运行时。 - absoluteRuntime: 允许用户解析依次运行时,然后输出代码中插入运行时的绝对路径。 - version 根据字段动态确定是 `@bable/runtime`的版本 还是`@bable/runtime-corejs3`的版本。 构建后的代码 ```js "use strict"; var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault").default; var _regeneratorRuntime2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/regeneratorRuntime")); var _marked = /*#__PURE__*/(0, _regeneratorRuntime2.default)().mark(foo); function foo() { return (0, _regeneratorRuntime2.default)().wrap(function foo$(_context) { while (1) switch (_context.prev = _context.next) { case 0: case "end": return _context.stop(); } }, _marked); } ``` 可以注意到路径从`@babel/runtime/***` 替换成了 `@babel/runtime-corejs3`。预置的配置`corejs`就可以进行删除掉了。后续详细的代码降级方式。 ## `babel`的代码降级方式 代码降级,使得低版本浏览器能兼容。按功能分为了: - api降级——api垫片(shim)缺少实现 - 语法降级——语法辅助函数(polyfill)抹平多版本实现 > [shim vs polyfill](https://stackoverflow.com/a/6671015/9672709) > `shim` 已有api的的增强代码。例如:stage1的提案很好,需要实现。 > `polyfill` 没有api的代码实现,面向。例如:旧版本浏览器没有的功能,需要实现才能调用api。 ### api降级——corejs `提供api垫片` #### 按需降级——usage 例如:常见的Promise,Map ```js // preset-env配置 "useBuiltIns": "usage" 时 Promise.resolve().finally(); ``` 执行命令 `npx babel --config-file ./useBuiltInsUsage/babel.config.json ./useBuiltInsUsage/index.js --out-file useBuiltInsUsage/index-out.js` ```js // 会被转换为下面这种方式 "use strict"; require("core-js/modules/es.promise.js"); require("core-js/modules/es.promise.finally.js"); Promise.resolve().finally(); ``` 在需要降级的api使用前加载对应的垫片,污染全局变量或者原型链的方式实现api降级。 手动插入很麻烦,babel提供了自动插入的插件 `@babel/preset-env`。注意:`@babel/polyfill`已经不使用了。 ```json { "presets": [ [ "@babel/preset-env", { "targets": { "edge": "17", "firefox": "60", "chrome": "67", "safari": "11.1" }, "useBuiltIns": "usage" } ] ] } ``` babel根据配置中的目标浏览器环境,选择性插入垫片代码,减少垫片体积。 配置`@babel/preset-env`时 `useBuiltIns` 比较重要,有两个值 全量降级`entry` 和 按需降级`useage`。 如果两个变量都没指定,那么需要手动在项目开始时手动全量导入`core-js` ```js import "core-js/stable" // 包含全部的corejs ``` #### 全量降级 entry 会根据浏览器目标环境生成相应的引入方式 执行命令 `npx babel --config-file ./useBuiltInsEntry/babel.config.json ./useBuiltInsEntry/index.js --out-file ./useBuiltInsEntry/index-out.js` ```js import "core-js/stable" // 包含全部的corejs Promise.resolve().finally(); ``` 会被转换成如下: ```js "use strict"; require("core-js/modules/es.symbol.description.js"); require("core-js/modules/es.symbol.async-iterator.js"); require("core-js/modules/es.symbol.match.js"); require("core-js/modules/es.symbol.match-all.js"); require("core-js/modules/es.symbol.replace.js"); require("core-js/modules/es.symbol.search.js"); require("core-js/modules/es.symbol.split.js"); require("core-js/modules/es.error.cause.js"); require("core-js/modules/es.aggregate-error.js"); require("core-js/modules/es.aggregate-error.cause.js"); require("core-js/modules/es.array.at.js"); require("core-js/modules/es.array.flat.js"); require("core-js/modules/es.array.flat-map.js"); require("core-js/modules/es.array.includes.js"); require("core-js/modules/es.array.reduce.js"); require("core-js/modules/es.array.reduce-right.js"); require("core-js/modules/es.array.reverse.js"); require("core-js/modules/es.array.sort.js"); require("core-js/modules/es.array.unscopables.flat.js"); require("core-js/modules/es.array.unscopables.flat-map.js"); require("core-js/modules/es.array-buffer.constructor.js"); require("core-js/modules/es.array-buffer.slice.js"); require("core-js/modules/es.global-this.js"); require("core-js/modules/es.json.stringify.js"); require("core-js/modules/es.math.hypot.js"); require("core-js/modules/es.number.parse-float.js"); require("core-js/modules/es.number.parse-int.js"); require("core-js/modules/es.number.to-exponential.js"); require("core-js/modules/es.number.to-fixed.js"); require("core-js/modules/es.object.assign.js"); require("core-js/modules/es.object.from-entries.js"); require("core-js/modules/es.object.has-own.js"); require("core-js/modules/es.parse-float.js"); require("core-js/modules/es.parse-int.js"); require("core-js/modules/es.promise.js"); require("core-js/modules/es.promise.all-settled.js"); require("core-js/modules/es.promise.any.js"); require("core-js/modules/es.promise.finally.js"); require("core-js/modules/es.reflect.set.js"); require("core-js/modules/es.reflect.to-string-tag.js"); require("core-js/modules/es.regexp.constructor.js"); require("core-js/modules/es.regexp.dot-all.js"); require("core-js/modules/es.regexp.exec.js"); require("core-js/modules/es.regexp.flags.js"); require("core-js/modules/es.regexp.test.js"); require("core-js/modules/es.regexp.to-string.js"); require("core-js/modules/es.string.at-alternative.js"); require("core-js/modules/es.string.ends-with.js"); require("core-js/modules/es.string.includes.js"); require("core-js/modules/es.string.match.js"); require("core-js/modules/es.string.match-all.js"); require("core-js/modules/es.string.replace.js"); require("core-js/modules/es.string.replace-all.js"); require("core-js/modules/es.string.search.js"); require("core-js/modules/es.string.split.js"); require("core-js/modules/es.string.starts-with.js"); require("core-js/modules/es.string.trim.js"); require("core-js/modules/es.string.trim-end.js"); require("core-js/modules/es.string.trim-start.js"); require("core-js/modules/es.typed-array.float32-array.js"); require("core-js/modules/es.typed-array.float64-array.js"); require("core-js/modules/es.typed-array.int8-array.js"); require("core-js/modules/es.typed-array.int16-array.js"); require("core-js/modules/es.typed-array.int32-array.js"); require("core-js/modules/es.typed-array.uint8-array.js"); require("core-js/modules/es.typed-array.uint8-clamped-array.js"); require("core-js/modules/es.typed-array.uint16-array.js"); require("core-js/modules/es.typed-array.uint32-array.js"); require("core-js/modules/es.typed-array.at.js"); require("core-js/modules/es.typed-array.fill.js"); require("core-js/modules/es.typed-array.from.js"); require("core-js/modules/es.typed-array.of.js"); require("core-js/modules/es.typed-array.set.js"); require("core-js/modules/es.typed-array.sort.js"); require("core-js/modules/es.typed-array.to-locale-string.js"); require("core-js/modules/web.dom-collections.iterator.js"); require("core-js/modules/web.dom-exception.constructor.js"); require("core-js/modules/web.dom-exception.stack.js"); require("core-js/modules/web.dom-exception.to-string-tag.js"); require("core-js/modules/web.immediate.js"); require("core-js/modules/web.queue-microtask.js"); require("core-js/modules/web.structured-clone.js"); require("core-js/modules/web.url.js"); require("core-js/modules/web.url.to-json.js"); require("core-js/modules/web.url-search-params.js"); Promise.resolve().finally(); ``` #### 默认 false ```js import "core-js" Promise.resolve().finally(); ``` 执行命令 `npx babel --config-file ./useBuiltInsFalse/babel.config.json ./useBuiltInsFalse/index.js --out-file ./useBuiltInsFalse/index-out.js` ```js "use strict"; require("core-js"); Promise.resolve().finally(); ``` #### 总结 `entry` 全量降级 方式看似时更优解,但过于理想 1. 通常构建时候考虑到编译速度,`node_modules`下的模块不会参与`babel`编译,仅参与 `webpack` 打包,如果依赖包里没有`代码降级`,那么就会导致最终线上环境js运行异常。 > 实际上这种情况在混乱的npm生态中非常普遍,有不少`npm`包直接使用`tsc`打包,除非开发手动介入,否者产物就缺少`api垫片`,遇到这种情况往往只能在线上发现异常后手动添加到babel.include中进行编译 2. 并不是所有代码都会参与编译,例如一些平台动态下发的脚本,这些平台动态下发的代码完全不经过编译,如果使用了未经降级的api也可能出现js运行异常。 `entry` 与 `usage` 都是存在问题的,所以就有了平台化解决方案,[polyfill.io](https://polyfill.io/) > [创建一个`polyfill.io`](https://polyfill.io/v3/url-builder/)的服务可以根据浏览器访问其服务,响应浏览器所需要的`降级api`,即控制包体积,也能确保未经编译的`js`获得`降级api`。 访问[测试链接](https://polyfill.io/v3/polyfill.min.js?version=3.111.0)后,如果当前浏览器不需要`降级api`时返回结果如下 ```html /* Polyfill service v3.111.0 * Disable minification (remove `.min` from URL path) for more info */ /* Polyfill service v3.111.0 * For detailed credits and licence information see https://github.com/financial-times/polyfill-service. * * Features requested: default * */ /* No polyfills needed for current settings and browser */ ``` 如果需要降级处理的浏览器时会返回具体的`api降级垫片`。需要注意的是需要自部署,还得考虑缓存和兜底。 ### 语法降级——@babel/runtime 辅助函数增强 需要和 `@babel/plugin-transform-runtime` 一起使用。如果没有使用该插件,会遇到如下情况: ```js function* foo() {} ``` 每个引入类似foo的文件都会冗余转换成如下: ```js // ... 其他代码实现 // 这里是冗余的代码,可能会多次打包 var _marked = /*#__PURE__*/_regeneratorRuntime().mark(foo); function foo() { return _regeneratorRuntime().wrap(function foo$(_context) { while (1) switch (_context.prev = _context.next) { case 0: case "end": return _context.stop(); } }, _marked); } ``` 如果使用了该插件,转换结果是: ```js "use strict"; var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault").default; var _regeneratorRuntime2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/regeneratorRuntime")); var _marked = /*#__PURE__*/(0, _regeneratorRuntime2.default)().mark(foo); function foo() { return (0, _regeneratorRuntime2.default)().wrap(function foo$(_context) { while (1) switch (_context.prev = _context.next) { case 0: case "end": return _context.stop(); } }, _marked); } ``` 语法辅助函数使用 `@babel/runtime` 进行依赖提取。 ## Tree-Shaking 减少构建产物体积的有效方式之一。为什么要启用`tree-shaking`? - `package.json` 指定 `module` 字段,地址执行esm产物 - `package.json` 声明 `sideEffets: false` 字段,告诉webpack哪些依赖包没有副作用,或者指明哪些副作用模块的地址。 ### esm 具备静态分析能力,这是`tree-shaking`的前置条件,所以我们需要`babel`构建我们的源代码时保留`import`语法,不要编译成`commonjs`。 ```js { "presets": [ [ "@babel/preset-env", { "modules": false // 保留ESM语法 } ] ] } ``` 将以下代码转换为: ```js function* foo() {} ``` ```js // commonjs "use strict"; var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault").default; var _regeneratorRuntime2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/regeneratorRuntime")); var _marked = /*#__PURE__*/(0, _regeneratorRuntime2.default)().mark(foo); function foo() { return (0, _regeneratorRuntime2.default)().wrap(function foo$(_context) { while (1) switch (_context.prev = _context.next) { case 0: case "end": return _context.stop(); } }, _marked); } // esm 模块 import _regeneratorRuntime from "@babel/runtime-corejs3/helpers/regeneratorRuntime"; var _marked = /*#__PURE__*/_regeneratorRuntime().mark(foo); function foo() { return _regeneratorRuntime().wrap(function foo$(_context) { while (1) switch (_context.prev = _context.next) { case 0: case "end": return _context.stop(); } }, _marked); } ``` ### sideEffects 为什么需要`package.json`需要声明`sideEffects`? `css-loader`引入`css`文件是很典型的例子 ```js import "./button.css" ``` 对于 `webpack` 来说引入了该文件,但是没有引用该模块,引入的该文件就是一个副作用,类似的还有 `style-loader` 他会为`html`插入`style`标签。 如果`webpack`认为他没副作用,那么在生成`minify`时会删除这行代码,导致最终样式错乱,告诉webpack该文件时副作用,那么就需要指定 `sideEffets` ```json { "sideEffets": ["*.css"] } ``` ## 重复依赖 依赖重复打包是前端开发常见问题,容易出现在长期无人维护的依赖包中。 - A@1 => B@1 - C@1 == B@2 == A@1 => B@1 可以使用`find-duplicate-dependencies`和`webpack-bundle-analyzer`这些工具辅助我们排查依赖重复打包的问题 ## 最佳实践 ### `@company/app-builder` 构建应用 - 启用 `polyfill.io` 方案 降低包大小 - 启用 `@company/babel-base` 的 `@babel/plugin-transform-runtime` 为应用和依赖包启用语法辅助函数抽离 ### `@company/module-builder` 构建依赖包 - 关闭 `esm` 语法转换,为 `@company/app-builder` 提供 `tree-shaking` 时提供前置条件 ### `@company/babel-base` 封装`babel`配置 - 关闭`core-js`的降级 ### 总结 至于依赖重复的问题,需要开发者接入做版本选择,所以我们可以考虑在部署平台构建时自动上报 Dependency graph 数据,然后由性能分析平台将重复依赖的问题邮件抄送给相关开发者进行优化。 ## 综上分析 从构建工具角度分析,如果减少构建产物体积。仅仅处理应用的构建是不够的,为了最佳效果还是得开发介入内部依赖包的构建。只有具备全场景的构建能力才能最大限度降低代码的构建体积。 ## tips > 这部分没有做验证 ### babel配置 执行顺序 ```json { "presets": ["@babel/preset-env", "@babel/preset-react"], "plugins": [ "@babel/plugin-transform-runtime", "@babel/plugin-proposal-class-properties", "@babel/plugin-syntax-dynamic-import" ] } ``` `presets`倒序执行: `"@babel/preset-env" <== "@babel/preset-react"` `plugins`顺序执行: `"@babel/plugin-transform-runtime" ==> "@babel/plugin-proposal-class-properties" ==> "@babel/plugin-syntax-dynamic-import"` ### 配置自定义 `preset` ```js module.exports = function () { return { presets: [ require("@babel/preset-env") ], plugins: [ [require("@babel/plugin-proposal-class-properties"), { loose: true }], require("@babel/plugin-proposal-object-rest-spread") ] } } ``` 更多参考 [自定义babel preset配置](https://babeljs.io/docs/en/babel-standalone#customization) 官方参考 [babel-preset-react](https://github.com/babel/babel/tree/main/packages/babel-preset-react) ### 配置 `webpack` `babel-loader` [webpack babel-loader](https://webpack.js.org/loaders/babel-loader/) ## 参考资料 - [你构建的代码为什么这么大?](https://zhuanlan.zhihu.com/p/593065108) - [不容错过的 Babel7 知识?](https://juejin.cn/post/6844904008679686152)