javascript解释器-babel
2019年03月10日

概述

Babel 是一个 JavaScript 编译器

Babel 是一个工具链,主要用于在旧的浏览器或环境中将 ECMAScript 2015+ 代码转换为向后兼容版本的 JavaScript 代码:

  • 转换语法
  • Polyfill 实现目标环境中缺少的功能 (通过 @babel/polyfill)
  • 源代码转换

ES2015 及其他

Babel 通过语法变换器支持最新版本的 JavaScript。
这些 plugins 允许你现在就使用新语法,而无需等待浏览器的支持。查看我们的使用指南以开始使用。
JSX 和 React
Babel 可以转换 JSX 语法!查看 React preset 以开始使用。和babel-sublime 一起使用可以把语法高亮显示提升到一个全新的水平。
你可以通过以下这个命令安装该 preset

npm install --save-dev @babel/preset-react
export default React.createClass({
  getInitialState() {
    return { num: this.getRandomNumber() };
  },
  getRandomNumber() {
    return Math.ceil(Math.random() * 6);
  },
  render() {
    return <div>
      Your dice roll:
      {this.state.num}
    </div>;
  }
});

类型注释(Flow 和 TypeScript)

abel 可以删除类型注释!查看 Flow preset 或 TypeScript preset 以开始使用。请注意 Babel 不会进行类型检查;你仍然可以安装使用 Flow 或者 TypeScript 来进行类型检查。
你可以通过以下这个命令安装 flow preset

npm install --save-dev @babel/preset-flow
// @flow
function square(n: number): number {
  return n * n;
}

也可以通过以下这个命令安装 typescript preset

npm install --save-dev @babel/preset-typescript
function Greeter(greeting: string) {
    this.greeting = greeting;
}

了解更多关于 Flow 和 TypeScript 的信息。

可插拔

Babel 是用插件构建的。你可以使用现有插件编写自己的转换管道或编写自己的插件。通过使用或创建 preset 轻松使用一组插件。了解更多 →
使用 astexplorer.net 动态创建插件或使用 generator-babel-plugin 生成插件模板。

// A plugin is just a function
export default function ({types: t}) {
  return {
    visitor: {
      Identifier(path) {
        let name = path.node.name; // reverse the name: JavaScript -> tpircSavaJ
        path.node.name = name.split('').reverse().join('');
      }
    }
  };
}

可调试

支持 Source map ,因此你可以轻松调试编译过的代码。

规范性

Babel 试图尽可能地遵循 ECMAScript 标准。为了平衡性能,它也可能有特定的一些选项,以便可以更符合规范。

压缩性

Babel 尝试使用尽可能少的代码而不依赖于庞大的运行时环境。
有些情况可能很难达到,因此为了保证可读性、文件大小以及(运行)速度,会针对特定转换牺牲一些合规性,即提供 “loose” 选项。

基本概念

plugin 简介

Babel是代码转换器,比如将ES6转成ES5,或者将JSX转成JS等。借助Babel,开发者可以提前用上新的JS特性,这对生产力的提升大有帮助。
实现Babel代码转换功能的核心,就是Babel插件(plugin)。

原始代码 --> [Babel Plugin] --> 转换后的代码
例子:
源代码如下,这里用到了两个ES6才支持的新特性:箭头函数、for…of。在只支持ES5的浏览器里,这两段代码会报错。
因此,可以借助插件将代码转成ES5。

// index.js
// 箭头函数
[1,2,3].map(n => n + 1);
// 模板字面量
let nick = '程序猿小卡';
let desc = `hello ${nick}`;

安装依赖:

npm install --save-dev @babel/cli 
npm install --save-dev @babel/plugin-transform-es2015-arrow-functions
npm install --save-dev @babel/plugin-babel-plugin-transform-es2015-template-literals

.babelrc

{
    plugins:[
        @babel/plugin-transform-es2015-arrow-functions,
        @babel/plugin-babel-plugin-transform-es2015-template-literals
    ]
}

执行转换:

babel script.js -o build.js
# 不配置.babelrc可以直接使用如下命令
babel --plugin "@babel/plugin-transform-es2015-arrow-functions,@babel/plugin-babel-plugin-transform-es2015-template-literals" script.js -o build.js

结果如下:

[1, 2, 3].map(function (n) {
  return n + 1;
});
let nick = '程序猿小卡';
let desc = 'hello ' + nick;

转换命令中,插件名称可以省去babel-plugin前缀,plugins字段中声明的插件会按照顺序执行。

{
  "plugins": [
    "transform-es2015-arrow-functions",
    "transform-es2015-template-literals"
  ]
}

preset 简介

Babel插件一般尽可能拆成小的力度,开发者可以按需引进。比如对ES6转ES5的功能,Babel官方拆成了20+个插件。
这样的好处显而易见,既提高了性能,也提高了扩展性。比如开发者想要体验ES6的箭头函数特性,那他只需要引入transform-es2015-arrow-functions插件就可以,而不是加载ES6全家桶。
但很多时候,逐个插件引入的效率比较低下。比如在项目开发中,开发者想要将所有ES6的代码转成ES5,插件逐个引入的方式令人抓狂,不单费力,而且容易出错。
这个时候,可以采用Babel Preset。
可以简单的把Babel Preset视为Babel Plugin的集合。比如babel-preset-es2015就包含了所有跟ES6转换有关的插件。
下面通过例子说明。
例子:
还是原来的代码,这次采用babel-preset-es2015进行转换。
首先,安装依赖:

npm install --save-dev @babel/cli 
npm install --save-dev @babel/preset-es2015

.babelrc

{
  "presets": [ "@babel/preset-es2015" ]
}
babel script.js -o build.js
# 如果不配置.babelrc同样可以使用如下命令 
babel --presets babel-preset-es2015 index.js

Plugin与Preset执行顺序

可以同时使用多个Plugin和Preset,此时,它们的执行顺序非常重要。

  1. 先执行完所有Plugin,再执行Preset。
  2. 多个Plugin,按照声明次序顺序执行。
  3. 多个Preset,按照声明次序逆序执行。

Plugin、Preset混用例子

例子:将 index.jsx 编译成 index.js,并且采用 ES5规范。这里包含两个步骤:
将 jsx 语法转成 js 语法。
将 ES6规范 转成 ES5规范。
源代码如下:

// index.jsx
var profile = <div>
  <img src="avatar.png" className="profile" />
  <h3>{[user.firstName, user.lastName].join(' ')}</h3>
</div>;
var foo = () => "foo";

安装依赖:

npm install --save-dev babel-cli 
npm install --save-dev babel-plugin-transform-react-jsx 
npm install --save-dev babel-preset-es2015

配置文件 .babelrc:

{
  "plugins": [ "transform-react-jsx" ],
  "presets": [ "es2015" ]
}

执行转换:
babel script.jsx
转换结果如下:

"use strict";
var profile = React.createElement(
  "div",
  null,
  React.createElement("img", { src: "avatar.png", className: "profile" }),
  React.createElement(
    "h3",
    null,
    [user.firstName, user.lastName].join(' ')
  )
);
var foo = function foo() {
  return "foo";
};

有兴趣的话可以试下下面命令,单独采用bebel-preset-es2015进行代码转换,结果会报语法错误。这从侧面可以印证Plugin的执行顺序在Preset之前。
babel --presets es2015 script.jsx

自定义Babel Preset

前面提到,Preset是Plugin的集合。借助强大的社区,常见的转换功能都已经有人实现的,很多时候,开发者只需要按需引用即可。
在实际开发中,我们需要用到的Plugin/Preset相对比较固定,如果每次都要重复编写,或者拷贝babel配置文件,既繁琐,又容易出错。这个时候,可以考虑自定义Babel Preset。
以前面的 index.jsjsx 编译为例,我们用到了 babel-preset-es2015、babel-plugin-transform-react-jsx,可以创建自定义的 preset,把它们包含进去:()

// mypreset.js
module.exports = {
  presets: [
    require("babel-preset-es2015"),
  ],
  plugins: [    
    require("babel-plugin-transform-react-jsx"),
  ]
};

然后,修改.babelrc。这里因为是本地文件,所有用到了相对路径,如果发布到了npm上,就可以直接用包名。

{
  "presets": [
    "./mypreset.js"
  ]
}

转码过程略,读者自行尝试。

Plugin/Preset配置项

Babel Plugin、Babel Preset都支持配置项,配置项语法都是相同的,如下所示(插件类似)。

{
    "presets": [ 
        presetName01, // 没有配置
        [ presetName02, presetOptions02 ] // 有配置
    ]
}

具体例子如下:

{
  "presets": [
    ["es2015", {
      "loose": true,
      "modules": false
    }]
  ]
}

不同Plugin/Preset的配置项作用可能不同,具体请查阅它们的官方文档。

基本配置

以转译es6语法async/await为es5支持的方法为例:

  1. 安装必要依赖
npm install --save-dev @babel/core @babel/plugin-transform-async-to-generator
  1. 配置脚本
import babel from '@babel/core';
import fs from 'fs';
import path from 'fs';
var options = {
    plugins:[
        "@babel/plugin-transform-async-to-generator"
    ]
}
var sourceDir = './src/';
var targetDir = './build/';
var files = fs.readdirSync(sourceDir);
for(var file of files){
    var p = path.join(sourceDir,file);
    var stat = fs.statSync(p);
    if(stat.isFile()){
        var result = babel.transformFileSync(p,options);
        fs.writeFileSync(path.join(targetDir,file),result.code);
    }
}

babel从6开始,采用@(scope)作为模块的限定名,方便用户区分官方包,也更方便安装

总结

整体来说,babel是一个十分优秀的项目,它把开发人员从繁杂的平台/版本兼容性上解脱了出来,但是babel的配置项比较复杂,个别功能需要了解后,才能够使用.
比如@babel/preset-env,通过polyfill实现es6 API的差异补充,但是在转译客户端(浏览器)代码时,会出现require(...)的代码(浏览器不支持),目前未搞清楚什么情况.