💡 Babel 再理解和最佳实践
!本篇文章过于久远,其中观点和内容可能已经不准确,请见谅!~
想分享的是 Babel 这个工具的流程和最佳实践,平时开发必不可少
Babel is a toolchain that is mainly used to convert ECMAScript 2015+ code into a backwards compatible version of JavaScript in current and older browsers or environments.
太长不看请到:最佳实践
Babel
面世的环境是 ES
标准的推进和浏览器标准缓慢实现的时候,天下苦 ES5
久矣,Babel
带着巨大的掌声,借助着 AST
的完备,实现了 ES6
转 ES5
的华丽操作。让苦逼程序员从兼容性的泥潭脱身,更专注于用高级的语法实现业务,兼容性只需要用 babel
处理下就能上线了,让前端很大的劳动力不再纠结于兼容性的魔咒。但是在深入了解
babel
的相关发展之后可能会非常迷惑。cli
, core
, polyfill
, env
, runtime
, plugins
, stage-0
, 2015
, 2016
, preset
, plugins
都是什么鬼,就想简简单单的转个 es6
不行吗?在现成的已有项目,或者用脚手架工具创建的时候很少会在这里纠结太久,但是在性能优化,尤其是编译出来的页面大小考虑后肯定会要啃这块骨头的。
最简单的流程是:
Babel
面临的需求也是很奇怪,因为每一个标准的蛋生都要一个很长的阶段,建议、草案、候选等,标准也很多,ES5
、ES6
、ES7
等,所以就采用了插件和配置的形式。于是出现了 plugin
的配置项。在
babel.config.json
或者 .babelrc
中可以配置:{"plugins": ["transform-class-properties"]}
上面示例的
transform-class-properties
: Class 语法
。添加这个插件就能将 es6 下的 class 带入到 es5 版本中运行。class People {sayName() {console.log(this.name);}}var p = new People();p.sayName = 'hello';// ----------------- 会被编译为下面的代码function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return !!right[Symbol.hasInstance](left); } else { return left instanceof right; } }function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); } }function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }var People = /*#__PURE__*/function () {"use strict";function People() {_classCallCheck(this, People);}_createClass(People, [{key: "sayName",value: function sayName() {console.log(this.name);}}]);return People;}();var p = new People();p.sayName = 'hello';
-- BABEL 《try it out》
配置文件中可以省略固定的babel-plugin-xxxxxx
中的babel-plugin-
,比如"transform-class-properties"
表示使用babel-plugin-transform-class-properties
这个插件。
babel 针对每一个高级语法特性都对应一个 plugin 来做转换,更加灵活。但是平时开发的时候不可能每次编译不通过再引入对应的插件,也不可能将全部的 babel 都打包到默认里面。所以就出现了 Preset 的处理。
Preset
的概念很好理解,很多辅助工具都有,表明可以打包一个配置来直接拿来使用,避免自己维护整个插件列表。比如官方示例推荐的常用预设包:
@babel/preset-env
@babel/preset-flow
@babel/preset-react
@babel/preset-typescript
比如
@babel/preset-env
是一个智能的预设包,可以直接直接使用最新的 Javascript,而不需要精细的考虑具体目标浏览器需要那个语法转换,还包含可选的 polyfill 处理。 还可以让转换出来的 JavaScript bundles 更精简。(后面会详说这个 preset)npm install --save-dev @babel/core @babel/cli @babel/preset-env
{"presets": ["@babel/env"]}
一行命令加上一个配置文件即可上手开发。
这些预设的插件列表都是包含了对应的语法转换需求,这样就让配置更加精简了,如果在这些打包的预设插件之外还需要,或者独立配置,仍然可以在 plugin 的配置中单独引入和配置。
至此,我们能够成功的转换了语法,能够让高级语法运行在老浏览器里面而不需要担心用户浏览器是不是几年前的。
我们尝试下面这个代码,转换后在 ES5 浏览器运行没问题:
const a = ({ num }) => {return num + 1}// 转成了 ----->var a = function a(_ref) {var num = _ref.num;return num + 1;};
但是下面这个代码好像就不太一样了:
const simpleMerge = (a, b) => {return Object.assign(a, b)}// 转成了 ----->var simpleMerge = function simpleMerge(a, b) {return Object.assign(a, b); // Object.assign 这个接口没有实现};
原因是我们平常意义上的兼容性包含了 语法 和 特性 两部分,语法 这块
Babel
可以转换,也就是箭头函数
、Class 语法
、解构处理
等,本质上是写法不同。但是特性接口,比如 Array.from
、Object.assign
、WeakMap
这些不是语法,Babel 本质上并没有处理。这些新的 API、静态方法、实例方法应该是引入 polyfill 或者 shim 的东西来解决的,用来提供平台特性差异的抹平,为了解决这些特性问题,babel 提供了多个方案来解决这个问题。
preset-env 官方推荐的插件集合,里面除了可以根据目标浏览器来智能选择语法插件,还会提供垫片的功能也就是
useBuiltIns
配置。当
useBuiltIns: entry
时,需要手动安装 core-js 包来给 env 使用,但是只需要全局引入一次即可。npm install --save-dev @babel/core @babel/cli @babel/preset-envyarn add core-js@3 regenerator-runtime
{"presets": [["@babel/env",{"targets": "> 1%, IE 10","useBuiltIns": "entry",}]]}
这种情况下需要手动引入 polyfill,env 会自动根据浏览器目标引入具体的垫片。(会引入你声明引入的全部模块,并不是按需引入)。
// 这两句在应用入口引入,会被 env 自动根据 target 设置替换为不同的垫片import "core-js/stable";import "regenerator-runtime/runtime"// 或者在入口仅引入你用到的,env 会只引入 array 相关的垫片,即使你没有使用 array 的方法。import "core-js/es/array";
这里的不是按需引入是如果你 import 了 array,即使你没有使用数组的方法,仍然会引入 array 的相关垫片。只不过如果你 target 设置的足够高,可能 env 并不会插入这个垫片。这样的好处是你只需要在开头引入一次即可,env 会且只会根据你引入的部分插入垫片。
好处是大型项目一次性能处理全部垫片问题;缺点就是包体积偏大、会污染全局、不能按需加载。
这个方法只适合项目中,而不适合类库之类的应用,毕竟侵入性很大。
这个选项会在每个独立的文件中插入使用到的 polyfill 模块,这个不需要手动入口引入全部包,env 会在编译每一个单独文件的时候都自动引入对应的垫片。
npm install --save-dev @babel/core @babel/cli @babel/preset-envyarn add core-js@3 regenerator-runtime
{"presets": [["@babel/env",{"targets": "> 1%, IE 10","useBuiltIns": "usage",}]]}
这也是官方比较推荐的方式,按需加载,能够比较好的解决包体积和方便性的问题,只是会出现和 entry 相同的缺点,就是仍然会引入 corejs 的模块,污染全局的变量和实例方法,侵入性较强。
这个插件的主要目的是将 babel 的很多辅助函数用
@babel/runtime
来代替,增加重用来减小包体积;另一个目的是非侵入性的解决垫片问题,所以对于类库来说是一个更好的垫片方案。npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/plugin-transform-runtimenpm install --save @babel/runtime-corejs3
{"presets": [["@babel/env",{"targets": "> 1%, IE 10","useBuiltIns": false,}]],"plugins": [["@babel/plugin-transform-runtime",{"corejs": 3, // 3 支持实例方法垫片"useESModules": true}]]}
- 对于自己的项目代码,无所谓侵入原生方法:
- 小型项目,包大小敏感:使用
preset-env
+"useBuiltIns": "usage"
的形式。 - 大型项目,包大小不敏感:使用
preset-env
+"useBuiltIns": " entry"
+import core-js
的形式。 - 需要复用辅助函数:使用 1.1 或者 1.2 +
transform-runtime
不配置corejs
- 小型项目,包大小敏感:使用
- 对于类库,严格侵入性,需要辅助函数: 2.1 使用
transform-runtime
+corejs: 3
配置
[
useBuiltIns
] vs [transform-runtime
+ corejs
] 二者选其一,推荐 transform-runtime
+ corejs
配置。这样就能满足大部分的开发需求了。npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/plugin-transform-runtimenpm install --save @babel/runtime-corejs3
{"presets": [["@babel/env",{"targets": "> 1%, IE 10","useBuiltIns": false,}]],"plugins": [["@babel/plugin-transform-runtime",{"corejs": 3, // 3 支持实例方法垫片"useESModules": true}]]}
感谢您的阅读,本文由 Ubug 版权所有。如若转载,请注明出处:Ubug(https://ubug.io/blog/babel-understanding)