🙏向这次肺炎疫情中逝世的同胞表示哀悼。前端
本文首发于政采云前端团队博客: 前端工程师的自我修养-关于 Babel 那些事儿
![]()
随着 Nodejs 的崛起,编译这个昔日在 Java、C++ 等语言中流行的词,在前端也逐渐火了起来,如今一个前端项目在开发过程当中没有编译环节,总感受这个项目是没有灵魂的。提及前端编译就不得不提早端编译界的扛把子 Babel ,大部分前端攻城狮对 Babel 并不陌生,可是在这个 Ctrl+C 和 Ctrl+V 的年代,大多数人对它也只是知道、了解或者听过,少数可能配置过 Babel,但也仅此而已。做为一个有想法和灵魂的前端攻城狮仅仅知道这些是不够的,你须要对 Babel 有一个系统的了解,今天就来聊聊 Babel 那些事儿。node
官方的解释 Babel 是一个 JavaScript 编译器,用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便可以运行在当前版本和旧版本的浏览器或其余环境中。简单来讲 Babel 的工做就是:react
原理很简单,核心就是 AST (抽象语法树)。首先将源码转成抽象语法树,而后对语法树进行处理生成新的语法树,最后将新语法树生成新的 JS 代码,整个编译过程能够分为 3 个阶段 parsing (解析)、transforming (转换)、generating (生成),都是在围绕着 AST 去作文章,话很少说上图:npm
整个过程很清晰,可是,好多东西都是看着简单,可是实现起来贼复杂,好比这里说到的 AST,要是你以为你对 AST 已经信手拈来了,老哥麻烦在下面留下联系方式,我要来找你要简历。言归正传,这里提一下,Babel 只负责编译新标准引入的新语法,好比 Arrow function、Class、ES Modul 等,它不会编译原生对象新引入的方法和 API,好比 Array.includes,Map,Set 等,这些须要经过 Polyfill 来解决,文章后面会提到。json
运行 babel 所需的基本环境segmentfault
npm install i -S @babel/cli
api
@babel/cli 是 Babel 提供的内建命令行工具。提到 @babel/cli 这里就不得不提一下 @babel/node ,这哥俩虽然都是命令行工具,可是使用场景不一样,babel/cli 是安装在项目中,而 @babel/node 是全局安装。数组
@babel/corepromise
npm install i -S @babel/core
浏览器
安装完 @babel/cli 后就在项目目录下执行babel test.js
会报找不到 @babel/core 的错误,由于 @babel/cli 在执行的时候会依赖 @babel/core 提供的生成 AST 相关的方法,因此安装完 @babel/cli 后还须要安装 @babel/core。
安装完这两个插件后,若是在 Mac 环境下执行会出现 command not found: babel
,这是由于 @babel/cli是安装在项目下,而不是全局安装,因此没法直接使用 Babel 命令,须要在 package.json 文件中加上下面这个配置项:
"scripts": { "babel":"babel" }
而后执行 npm run babel ./test.js
,顺利生成代码,此时生成的代码并无被编译,由于 Babel 将原来集成一体的各类编译功能分离出去,独立成插件,要编译文件须要安装对应的插件或者预设,咱们常常看见的什么 @babel/preset-stage-0、@babel/preset-stage-1,@babel/preset-env 等就是干这些活的。那这些插件和预设怎么用呢?下面就要说到 Babel 的配置文件了,这些插件须要在配置文件中交代清楚,否则 Babel 也不知道你要用哪些插件和预设。
安装完基本的包后,就是配置 Babel 配置文件,Babel 的配置文件有四种形式:
babel.config.js
在项目的根目录(package.json
文件所在目录)下建立一个名为 babel.config.js 的文件,并输入以下内容。
module.exports = function (api) { api.cache(true); const presets = [ ... ]; const plugins = [ ... ]; return { presets, plugins }; }
.babelrc
在你的项目中建立名为 .babelrc
的文件
{ "presets": [...], "plugins": [...] }
.babelrc.js
与 .babelrc 的配置相同,你可使用 JavaScript 语法编写。
const presets = [ ... ]; const plugins = [ ... ]; module.exports = { presets, plugins };
package.json
还能够选择将 .babelrc 中的配置信息写到 package.json
文件中
{ ... "babel": { "presets": [ ... ], "plugins": [ ... ], } }
四种配置方式做用都同样,你就合着本身的口味来,那种看着顺眼,你就翻它。
插件是用来定义如何转换你的代码的。在 Babel 的配置项中填写须要使用的插件名称,Babel 在编译的时候就会去加载 node_modules 中对应的 npm 包,而后编译插件对应的语法。
.babelrc
{ "plugins": ["transform-decorators-legacy", "transform-class-properties"] }
插件在预设(Presets) 前运行。
插件的执行顺序是从左往右执行。也就是说在上面的示例中,Babel 在进行 AST 遍历的时候会先调用 transform-decorators-legacy 插件中定义的转换方法,而后再调用 transform-class-properties 中的方法。
参数是由插件名称和参数对象组成的一个数组。
{ "plugins": [ [ "@babel/plugin-proposal-class-properties", { "loose": true } ] ] }
插件名称若是为 @babel/plugin-XX
,可使用短名称@babel/XX
,若是为 babel-plugin-xx
,能够直接使用 xx
。
大部分时间咱们都是在用别人的写的插件,可是有时候咱们老是想秀一下,本身写一个 Babel 插件,那应该怎么操做呢?
插件加载
要致富先修路,要用本身写的插件首先得知道怎么使用自定义的插件。一种方式是将本身写的插件发布到 npm 仓库中去,而后本地安装,而后在 Babel 配置文件中配置插件名称就行了:
npm install @babel/plugin-myPlugin
.babelrc
{ "plugins": ["@babel/plugin-myPlugin"] }
另一种方式就是不发布,直接将写好的插件放在项目中,而后在 babel 配置文件中经过访问相对路径的方式来加载插件:
.babelrc
{ "plugins": ["./plugins/plugin-myPlugin"] }
第一种经过 npm 包的方式通常是插件功能已经完善和稳定后使用,第二种方式通常在开发阶段,本地调试时使用。
编写插件
插件实际上就是在处理 AST 抽象语法树,因此编写插件只须要作到下面三点:
好像少了生成 AST 对象和生成源码的步骤,不急,后面会讲。说一千道一万不如一个例子来的实在,下面实现一个预计算(在编译阶段将表达式计算出来)的插件:
const result = 1 + 2;
转换成:
const result = 3;
在写插件前你须要明确转换先后的 AST 长什么样子,就好像整容同样,你总得选个参考吧。 AST explorer 你值得拥有。
转换前:
转换后:
找到差异,而后就到了用代码来解决问题的时候了
let babel = require('@babel/core'); let t = require('babel-types'); let preCalculator={ visitor: { BinaryExpression(path) { let node = path.node; let left = node.left; let operator = node.operator; let right = node.right; if (!isNaN(left.value) && !isNaN(right.value)) { let result = eval(left.value + operator + right.value); //生成新节点,而后替换原先的节点 path.replaceWith(t.numericLiteral(result)); //递归处理 若是当前节点的父节点配型仍是表达式 if (path.parent && path.parent.type == 'BinaryExpression') { preCalculator.visitor.BinaryExpression.call(null,path.parentPath); } } } } } const result = babel.transform('const sum = 1+2+3',{ plugins:[ preCalculator ] });
上面这段代码,Babel 在编译的时候会深度遍历 AST 对象的每个节点,采用访问者的模式,每一个节点都会去访问插件定义的方法,若是类型和方法中定义的类型匹配上了,就进入该方法修改节点中对应属性。在节点遍历完成后,新的 AST 对象也就生成了。babel-types 提供 AST 树节点类型对象。
上面这样写只是为了咱们开发测试方便,其实最终的完总体是下面这样的:
const types = require('babel-types'); const visitor = { BinaryExpression(path) {//须要处理的节点路径 let node=path.node; let left=node.left; let operator=node.operator; let right=node.right; if (!isNaN(left.value) && !isNaN(right.value)) { let result=eval(left.value+operator+right.value); path.replaceWith(t.numericLiteral(result)); if (path.parent&& path.parent.type == 'BinaryExpression') { preCalculator.visitor.BinaryExpression.call(null,path.parentPath); } } } } module.exports = function(babel){ return { visitor } }
咱们在插件中只须要修改匹配上的 AST 属性,不须要关注源码到 AST 以及新 AST 到源码的过程,这些都是 Babel 去干的事,咱们干好本身的活就行了,其余的交给 babel。这也就解释了我上面的步骤中为嘛没有 AST 的生成和源码的生成,那就不是咱们在插件中干的事儿。
预设就是一堆插件(Plugin)的组合,从而达到某种转译的能力,就好比 react 中使用到的 @babel/preset-react ,它就是下面几种插件的组合。
固然咱们也能够手动的在 plugins 中配置一系列的 plugin 来达到目的,就像这样:
{ "plugins":["@babel/plugin-syntax-jsx","@babel/plugin-transform-react-jsx","@babel/plugin-transform-react-display-name"] }
可是这样一方面显得不那么优雅,另外一方面增长了使用者的使用难度。若是直接使用预设就清新脱俗多了~
{ "presets":["@babel/preset-react"] }
前面提到插件的执行顺序是从左往右,而预设的执行顺序刚好反其道行之,它是从右往左
{ "presets": [ "a", "b", "c" ] }
它的执行顺序是 c、b、a,是否是有点奇怪,这主要是为了确保向后兼容,由于大多数用户将 "es2015" 放在 "stage-0" 以前。
这种场景通常不多,在这个拿来主义的时代,插件咱们都不多写,就更别说自定义预设了。不过前面插件咱们都说了怎么写了,预设咱也不能冷落她呀。
前面提到预设就是已有插件的组合,主要就是为了不使用者配置过多的插件,经过预设把插件收敛起来,其实写起来特别简单,前提是你已经肯定好要用哪些插件了。
import { declare } from "@babel/helper-plugin-utils"; import pluginA from "myPluginA"; import pluginB from "myPluginB" export default declare((api, opts) => { const pragma = opts.pragma; return { plugins: [ [ pluginA, {pragma}//插件传参 ], pluginB ] }; });
其实就是把 Babel 配置中的 plugins 配置放到 presets 中了,实质上仍是在配置 Plugins,只是写 Presets 的人帮咱们配置好了。
@babel/preset-stage-xxx 是 ES 在不一样阶段语法提案的转码规则而产生的预设,随着被批准为 ES 新版本的组成部分而进行相应的改变(例如 ES6/ES2015)。
提案分为如下几个阶段:
preset-es2015 是仅包含 ES6 功能的 Babel 预设。
实际上在 Babel7 出来后上面提到的这些预设 stage-x,preset-es2015 均可以废弃了,由于 @babel/preset-env 出来一统江湖了。
@babel/preset-env
前面两个预设是从 ES 标准的维度来肯定转码规则的,而 @babel/preset-env 是根据浏览器的不一样版本中缺失的功能肯定代码转换规则的,在配置的时候咱们只须要配置须要支持的浏览器版本就行了,@babel/preset-env 会根据目标浏览器生成对应的插件列表而后进行编译:
{ "presets": [ ["env", { "targets": { "browsers": ["last 10 versions", "ie >= 9"] } }], ], ... }
在默认状况下 @babel/preset-env 支持将 JS 目前最新的语法转成 ES5,但须要注意的是,若是你代码中用到了尚未成为 JS 标准的语法,该语法暂时还处于 stage 阶段,这个时候仍是须要安装对应的 stage 预设,否则编译会报错。
{ "presets": [ ["env", { "targets": { "browsers": ["last 10 versions", "ie >= 9"] } }], ], "stage-0" }
虽然能够采用默认配置,但若是不须要照顾全部的浏览器,仍是建议你配置目标浏览器和环境,这样能够保证编译后的代码体积足够小,由于在有的版本浏览器中,新语法自己就能执行,不须要编译。@babel/preset-env 在默认状况下和 preset-stage-x 同样只编译语法,不会对新方法和新的原生对象进行转译,例如:
const arrFun = ()=>{} const arr = [1,2,3] console.log(arr.includes(1))
转换后
"use strict"; var arrFun = function arrFun() {}; var arr = [1, 2, 3]; console.log(arr.includes(1));
箭头函数被转换了,可是 Array.includes 方法,并无被处理,这个时候要是程序跑在低版本的浏览器上,就会出现 includes is not function
的错误。这个时候就须要 polyfill 闪亮登场了。
polyfill
的翻译过来就是垫片,垫片就是垫平不一样浏览器环境的差别,让你们都同样。
@babel/polyfill
@babel/polyfill
模块能够模拟完整的 ES5 环境。
安装:
npm install --save @babel/polyfill
注意 @babel/polyfill 不是在 Babel 配置文件中配置,而是在咱们的代码中引入。
import '@babel/polyfill'; const arrFun = ()=>{} const arr = [1,2,3] console.log(arr.includes(1)) Promise.resolve(true)
编译后:
require("@babel/polyfill"); var arrFun = function arrFun() {}; var arr = [1, 2, 3]; console.log(arr.includes(1)); Promise.resolve(true);
这样在低版本的浏览器中也能正常运行了。
不知道你们有没有发现一个问题,这里是require("@babel/polyfill")
将整个 @babel/polyfill 加载进来了,可是在这里咱们须要处理 Array.includes 和 Promise 就行了,若是这样就会致使咱们最终打出来的包体积变大,显然不是一个最优解。要是能按需加载就行了。其实 Babel 早就为咱们想好了。
useBuiltIns
回过头来再说 @babel/preset-env,他出现的目的就是实现民族大统一,连 stage-x 都干掉了,又怎么会漏掉 Polyfill 这一功能,在 @babel/preset-env 的配置项中提供了 useBuiltIns 这一参数,只要在使用 @babel/preset-env 的时候带上他,Babel 在编译的时候就会自动进行 Polyfill ,再也不须要手动的在代码中引入@babel/polyfill 了,同时还能作到按需加载
{ "presets": [ "@babel/preset-flow", [ "@babel/preset-env", { "targets": { "node": "8.10" }, "corejs": "3", // 声明 corejs 版本 "useBuiltIns": "usage" } ] ] }
注意,这里须要配置一下 corejs 的版本号,不配置编译的时候会报警告。讲都讲到这里了就再顺便提一嘴 useBuiltIns 的机构参数:
import ' @babel/polyfill'
编译后:
"use strict"; require("core-js/modules/es.array.includes"); require("core-js/modules/es.object.to-string"); require("core-js/modules/es.promise"); var arrFun = function arrFun() {}; var arr = [1, 2, 3]; console.log(arr.includes(1)); Promise.resolve(true);
这个时候咱们再借助 Webpack 编译后,产出的代码体积会大大减少。
说完了上面这些你觉得我就说完了吗?
其实 Babel 在编译中会使用一些辅助函数,好比:
class Person { constructor(){} say(word){ console.log(":::",word) } }
编译后:
"use strict"; require("core-js/modules/es.object.define-property"); function _classCallCheck(instance, Constructor) { if (!(instance instanceof 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 Person = /*#__PURE__*/ function () { function Person() { _classCallCheck(this, Person); } _createClass(Person, [{ key: "say", value: function say(word) { console.log(":::", word); } }]); return Person; }();
这些方法会被 inject
到每一个文件中,无法作到复用,这样也会致使打包体积的增长。
没事儿,逢山开路遇水搭桥,是时候让@babel/plugin-transform-runtime
登场了。
@babel/plugin-transform-runtime
@babel/plugin-transform-runtime 可让 Babel 在编译中复用辅助函数,从而减少打包文件体积,不信你看:
npm install --save-dev @babel/plugin-transform-runtime npm install --save @babel/runtime
顺便说一下,这一对 CP 要同时出现,如影随行,因此安装的时候你就一块儿装上吧~
配置 Babel:
{ "presets": [ [ "@babel/preset-env", { "useBuiltIns": "usage", "corejs": 3 } ] ], "plugins": [ "@babel/plugin-transform-runtime" ] }
结果:
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck")); var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass")); var Person = /*#__PURE__*/ function () { function Person() { (0, _classCallCheck2["default"])(this, Person); } (0, _createClass2["default"])(Person, [{ key: "say", value: function say(word) { console.log(":::", word); } }]); return Person; }();
这些用到的辅助函数都从 @babel/runtime 中去加载,这样就能够作到代码复用了。
<img src="https://zcy-cdn.oss-cn-shanghai.aliyuncs.com/f2e-assets/ca10147e-1fca-4431-8197-c942fac395e0.png?x-oss-process=image/quality,Q_75/format,jpg" />
在这个拿来主义的社会,有时候知其然的同时也须要知其因此然。但愿这篇关于 Babel 知识的梳理对你有帮助。
政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 50 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在平常的业务对接以外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推进并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。
若是你想改变一直被事折腾,但愿开始能折腾事;若是你想改变一直被告诫须要多些想法,却无从破局;若是你想改变你有能力去作成那个结果,却不须要你;若是你想改变你想作成的事须要一个团队去支撑,但没你带人的位置;若是你想改变既定的节奏,将会是“5 年工做时间 3 年工做经验”;若是你想改变原本悟性不错,但老是有那一层窗户纸的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但愿参与到随着业务腾飞的过程,亲手推进一个有着深刻的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我以为咱们该聊聊。任什么时候间,等着你写点什么,发给 ZooTeam@cai-inc.com