听说提升技术最快的方法是输入,一直觉得自己文笔烂,技术菜,再加上惰性大,所以都是只看不写,但是一直这样下去能进步就见鬼了,所以做了个决定,给自己定义一个小目标,准备每周写一遍技术文章,管它写的好还是乱,肤浅还是深入,有人看还是没人看,开始写就是进步,坚持写就是一直进步。
这是我第一次在掘金上发布文章,如果有错误欢迎大家指正,在此也鼓励下大家学会输出,知道是一回事,能写出来是另外一回事,能深入浅出的写出来那啥都不是事儿了。
用vue开发项目一年多了,但是一直没因为项目中使用了babel, 这篇文章简单整理下自己所知道的关于babel的知识,文章和代码gihub上也有,欢迎大家可以去github克隆项目。 最后有错误的地方欢迎大家指出来,共同学习和进步,谢谢!
目录
-
- .babelrc
- presets
- env
- 与构建工具集成
-
-
一 babel是什么
Babel is a JavaScript compiler
简单来说就是把 JavaScript 中 es2015+ 的新语法转化为 es5,让低端运行环境 (如浏览器和 node ) 能够认识并执行
二 Babel的工作原理
babel是一个编译器compiler,但是叫转译器transpiler更准确,因为它只是把同种语言的高版本规则翻译成低版本规则,而不像编译器那样,输出的是另一种更低级的语言代码。
Babel的编译过程跟绝大多数其他语言的编译器大致同理,分为三个阶段:
- 解析(parse):使用babylon将代码字符串解析成抽象语法树ast
- 变换(transform):使用babel-traverse, 利用配置好的 plugins/presets遍历ast,更新节点,转变为新的ast
- 再建(generate):使用babel-generator根据变换后的抽象语法树再生成代码字符串
babel编译流程图
既然要讲编译,就必须要了解一下抽象语法树了 是一个在线实时编译工具,可以选择不同的语言和对应的编译器,将源代码解析成抽象语法树, 帮助我们更直观的认识AST的结构,编译器选择babylon6,
在左侧输入下面的的代码function abs(number) { if (number >= 0) { return number; } else {number; }}复制代码
在右侧生成了对应的Ast, 下面的结构图可以更加清楚地看清楚AST树的节点信息
可以看到AST就是由一个个节点构成,每个节点都是源代码语法的一个标签。解析这一步是如何生成抽象语法树的, 给大家甩一个知乎上的一篇文章里面有讲解析器解析的过程, 对编译器有兴趣的小伙伴可以去研究一下github上一个轻量级的编译器实现
此外,以下两点请注意:
- babel本身不具备转码能力,转码都是通过插件完成的,如果不添加插件,那么经过babel后的代码和源代码是一样的
let x = (a) => a*2;复制代码
- babel只是转译新标准引入的语法,比如ES6的箭头函数转译成ES5的函数;而新标准引入的新的原生对象(Symbol),部分原生对象新增的原型方法(数组的include的方法),新增的API等(如Proxy、Set等),这些babel是不会转译的。需要用户自行引入polyfill(后面会介绍)来解决。
let obj = Object.assign({},{ a:1});复制代码
三 使用方法
在项目中使用babel, 首先需要在根目录下新建babel的配置文件.babelrc,使用Babel的第一步,就是配置这个文件。
{ "presets": [], "plugins": []}复制代码
简略情况下,plugins和 presets只要列出字符串格式的名字即可。但如果某个 presets 或者plugins需要一些配置项(或者说参数),就需要把自己先变成数组。第一个元素依然是字符串,表示自己的名字;第二个元素是一个对象,即配置对象
{ "presets": [ // 带了配置项,自己变成数组 [ // 第一个元素是preset的名称 "env", // 第二个元素是对象,列出该preset的配置项 { "module": false } ], // 不带配置项,直接列出名字 "stage-2"]}复制代码
presets和plugins的执行顺序 执行顺序
- plugins 会运行在 Presets 之前。
- plugins 会从前到后顺序执行。
- presets 的顺序则 刚好相反(从后向前)。
- presets 的逆向顺序主要是为了保证向后兼容,因为大多数用户的编写顺序是 ['es2015', 'stage-0']。这样必须先执行 + stage-0 才能确保 babel 不报错。
presets
babel 本身不具有任何转化功能,它把转化的功能都分解到一个个插件里面, 插件是在转换这一阶段起作用的。当我们不配置任何插件时,经过 babel 的代码和输入是相同的。 那么如何配置插件呢,比如 es2015 是一套规范,包含如下二十多个转译插件,大家可以简单了解一下
- check-es2015-constants
- transform-es2015-arrow-functions
- transform-es2015-block-scoped-functions
- transform-es2015-block-scoping
- transform-es2015-classes
- transform-es2015-computed-properties
- transform-es2015-destructuring
- transform-es2015-duplicate-keys
- transform-es2015-for-of
- transform-es2015-function-name
- transform-es2015-literals
- transform-es2015-modules-commonjs
- transform-es2015-object-super
- transform-es2015-parameters
- transform-es2015-shorthand-properties
- transform-es2015-spread
- transform-es2015-sticky-regex
- transform-es2015-template-literals
- transform-es2015-typeof-symbol
- transform-es2015-unicode-regex
- transform-regenerator
如果一个个添加然后安装,就非常麻烦, babel官方就提供了插件合集, 省去了开发者配置的麻烦,这就叫presets, 分为以下几种
- 官方
- env
- react
- flow
- minify
- stage-x
- Stage 0 - 稻草人(strawman): 只是一个想法,经过 TC39 成员提出即可。
- Stage 1 - 提案(proposal): 初步尝试。
- Stage 2 - 初稿(draft): 完成初步规范。
- Stage 3 - 候选(candidate): 完成规范和浏览器初步实现。
- es201x, latest 但因为 env 的出现,使得 es2016 和 es2017 都已经废弃。latest 是 env 的雏形,它是一个每年更新的 preset,目的是包含所有 es201x。但也是因为更加灵活的 env 的出现,已经废弃。
env
env 的核心目的是通过配置得知目标环境的特点,然后只做必要的转换。例如目标浏览器支持 es2015,那么 es2015 这个preset 其实是不需要的,于是代码就可以小一点(一般转化后的代码总是更长),构建时间也可以缩短一些。 如果不写任何配置项,env 等价于 latest,也等价于 es2015 + es2016 + es2017 三个相加(不包含 stage-x 中的插件)。env包含的插件列表维护在
与构建工具集成
babel一般是集成到构建工具里面使用的,这时需要安装构建工具的插件 (webpack 的 babel-loader, rollup 的 rollup-plugin-babel),以目前使用最频繁的打包工具的webpack为例 配置文件.babelrc如下,存放在项目的根目录下
{ "presets": [ ["env", { "modules": false }], "stage-2" ], "plugins": ["transform-runtime"]}复制代码
然后webpack对应的配置
module: { rules: [ { test: /\.js$/, use: ['babel-loader'], exclude: /node_modules/, } ] }复制代码
四 Babel包介绍
可以去github上看看,babel到底包含了多少包, 按照功能分个类
(一) 核心包
- babel-core:babel转译器本身,提供了babel的转译API,如babel.transform等,用于对代码进行转译。像webpack 的babel-loader就是调用这些API来完成转译过程的。
- babylon:js的词法解析器
- babel-traverse:用于对AST的遍历,主要给plugin用
- babel-generator:根据AST生成最终的代码
babel-core的使用
var babel = require('babel-core');// 字符串转码, transform方法的第一个参数是一个字符串,表示需要转换的代码,第二个参数是转换的配置对象babel.transform('code', options);// => { code, map, ast }// 文件转码(异步)babel.transformFile('filename.js', options, function(err, result) { result; // => { code, map, ast }});// 文件转码(同步)babel.transformFileSync('filename.js', options);// => { code, map, ast }// Babel AST转码babel.transformFromAst(ast, code, options);// => { code, map, ast }复制代码
(二) 功能包
- babel-types:用于检验、构建和改变AST树的节点
- babel-template:辅助函数,用于从字符串形式的代码来构建AST树节点
- babel-helpers:一系列预制的babel-template函数,用于提供给一些plugins使用
- babel-code-frames:用于生成错误信息,打印出错误点源代码帧以及指出出错位置
- babel-plugin-xxx:babel转译过程中使用到的插件,其中babel-plugin-transform-xxx是transform步骤使用的
- babel-preset-xxx:transform阶段使用到的一系列的plugin
- babel-polyfill:JS标准新增的原生对象和API的shim,实现上仅仅是core-js和regenerator-runtime两个包的封装
- babel-runtime:功能类似babel-polyfill,一般用于library或plugin中,因为它不会污染全局作用域
babel-polyfill
Babel includes a polyfill that includes a custom regenerator runtime and core-js.
前面说过,Babel默认只转换新的JavaScript句法(syntax),而不转换新的API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的静态方法(比如Object.assign,Array.from, Array.isArray,Array.of), 还有实例方法如Array.prototye.inclueds等,Babel都不会转码。 Babel默认不转码的API清单可以查看babel-plugin-transform-runtime模块的文件。 如果想让这些API和方法运行,必须使用babel-polyfill,为当前环境提供一个垫片。 拿inclueds来说,IE11仍不支持该方法(查看兼容性),在IE11浏览器的控制台里面运行下面的代码会报如下错误
var arr = [1,2,3]arr.includeds(1)// 对象不支持“includes”属性或方法复制代码
我们先来了解一下polyfill, 下面实现了一个includes方法的pollyfill
if(!Array.prototype.includeds) { Object.defineProperty(Array.prototype, 'includeds', { enumarable: false, writable: false, configurable: true, value: function(arg) { if(this == null) { throw new TypeError('"this" is null or not defined'); } var len = this.length; if(!len) { return false } else { return this.indexOf(arg) > -1; } } })}复制代码
这段代码的意思是,如果目标环境中已经存在includeds, 什么都不做,如果没有就在 Array 的原型中定义一个,这便是polyfill 的意义。babel-polyfill 同理 虽说浏览器的特性对新javascript的语法规范支持状况千差万别,但其实可以提炼出两类:
- 浏览器都有,只是不同语法的区别;
- 有的浏览器有,有的浏览器没有。
babel 编译过程处理第一种情况 - 统一语法的形态,通常是高版本语法编译成低版本的,比如 ES6 语法编译成 ES5 或 ES3。而 babel-polyfill 处理第二种情况 - 让目标浏览器支持所有特性,不管它是全局的,还是原型的,或是其它。这样,通过 babel-polyfill,不同浏览器在特性支持上就站到同一起跑线上。
其实babel-polyfill就是一个针对ES2015+环境的shim,实现上来说babel-polyfill包只是简单的把core-js和regenerator runtime包装了下,这两个包才是真正的实现代码所在
- babel-polyfill集成到webpack中的方法
- 先安装包: npm install --save babel-polyfill, 因为polyfill会在源代码之前运行,所以需要安装成dependencies而不是devDependencies
- 在所有代码运行之前增加 require('babel-polyfill'),有以下两种方式
- 在程序入口文件app.js的顶部引用(因为polyfill代码需要在所有其他代码前先被调用): import "babel-polyfill"
- 或者在webpack配置的entry里面第一个引入: module.exports = { entry: ["babel-polyfill", "./app/js"] };
但是,从上面很容易看出, babel-polyfill有两个主要缺点:
- 使用 babel-polyfill 会导致打出来的包非常大,因为 babel-polyfill 是一个整体,把所有方法都加到原型链上。我们并不会用到所有的方法,这就造成了浪费
- babel-polyfill 修改原型链,会污染全局变量,如果我们开发的也是一个类库供其他开发者使用,这种情况就会变得非常不可控。 因此在实际使用中, 更好的选择是babel-runtime
babel-runtime 和 babel-plugin-transform-runtime
babel-runtime is a library that contain's Babel modular runtime helpers and a version of regenerator-runtime.
上面是官方定义,尽快我看得懂每一个单词,但是连起来
我们去看看babel-runtime的包吧,package.json 里没有 main 字段,用法肯定不是 require('babel-runtime')和import那样, 我们时常在项目中看到 .babelrc 中使用 babel-plugin-transform-runtime,而 package.json 中的 dependencies (注意不是 devDependencies) 又包含了 babel-runtime,那这两个是不是成套使用的呢?他们又起什么作用呢?
babel 会转换 js 语法,之前已经提过了。以 Object.assign举例,IE不支持 Object.assign,此时,要想兼容的话,我们有两个方案
- 引入babel-polyfill, 这个当然能解决问题,但是弊端很大,污染全局变量,代码庞大
- 引入该语法插件plugin-transfrom-object-assign来现实特定的转换 进去克隆的项目,执行demo5(
Object.assign({}, {a:1})
)的npm run build1
(babel index.js --out-file compile1.js --plugins=transform-object-assign)生成compile1.js如下代码code1
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source){ if (Object.prototype.hasOwnProperty.call(source, key)){ target[key] = source[key]; } } } return target; };var object = _extends({}, { a: 1 });复制代码
从结果可以看出,这种方式的确解决了兼容性的问题,但是新的问题来了,如果你的项目里有多少个文件用了 Object.assign,_extends 辅助函数会出现多少次,为了避免代码重复,我们必选要把这个方法分离出去, 改成import
引用的形式,babel-plugin-transform-runtime就是来做这些工作的,执行demo5的npm run build2
(babel index.js --out-file compile2.js --plugins=transform-runtime)生成下面文件代码code2
import _extends from "babel-runtime/helpers/extends";_extends({}, { a:1});复制代码
从结果可以看出,定义方法改成了引用,那重复定义就变成了重复引用,就不存在代码重复的问题了。
上面的babel-runtime就是这些方法的集合处,也因此,在使用 babel-plugin-transform-runtime 的时候必须把 babel-runtime 当做依赖。 babel-runtime,它内部集成了
- core-js: 转换一些内置类 (Promise, Symbols等等) 和静态方法 (Array.from 等)。绝大部分转换是这里做的。在代码中使用这些内置类和静态方法时自动引入。
- regenerator: 作为 core-js 的拾遗补漏,主要是 generator/yield 和 async/await 两组的支持。当代码中有使用 generators/async 时自动引入。
- helpers,如上面的 _extends 就是其中之一,其他还有如 jsx, classCallCheck 等等。在代码中有内置的 helpers 使用时(如上面的代码code1)移除定义,并插入引用(于是就变成了code2)。
总结:
- babel-polyfill 与 babel-runtime 的区别,前者改造目标浏览器,让你的浏览器拥有本来不支持的特性;后者改造你的代码,让你的代码能在所有目标浏览器上运行,但不改造浏览器, babel-polyfill比babel-runtime多了对包含高版本 js 中类型的实例方法 (例如 [1,2,3].includes(1))的支持。
- babel-plugin-transform-runtime插件依赖babel-runtime,babel-runtime是真正提供runtime环境的包;也就是说transform-runtime插件是把js代码中使用到的新原生对象和静态方法转换成对runtime实现包的引用。
(三) 工具包
babel-cli
1 . Babel提供babel-cli工具,用于命令行转码, 基本命令如下
// 转码结果输出到标准输出$ npx babel example.js// 转码结果写入一个文件// --out-file 或 -o 参数指定输出文件$ npx babel example.js --out-file compiled.js// 或者$ npx babel example.js -o compiled.js// 整个目录转码// --out-dir 或 -d 参数指定输出目录$ npx babel src --out-dir lib// 或者$ npx babel src -d lib// -s 参数生成source map文件$npx babel src -d lib -s复制代码
babel-cli使用演示
babel-node
babel-cli工具自带一个babel-node命令,提供一个支持ES6的REPL环境。它支持Node的REPL环境的所有功能,而且可以直接运行ES6代码。 它不用单独安装,而是随babel-cli一起安装。然后,执行babel-node就进入REPL环境。 打开cmd终端,输入babel-node
进入PEPL环境,直接输入es6代码运行
babel-register
babel-register模块改写require命令,为它加上一个钩子。此后,每当使用require加载.js、.jsx、.es和.es6后缀名的文件,就会先用Babel进行转码。
babel 7.x
生命不息,升级不止,babel7已经出了,各位小伙伴们可以移步官网去看看,但不是万变不离其宗,核心原理没有变化, 只是语法做了变化,我就用一张图来结束这篇文章吧,要想知道的更多,大家老老实实地去啃官方文档吧。