内容字号:默认大号超大号

段落设置:段首缩进取消段首缩进

字体设置:切换到微软雅黑切换到宋体

Babel快速指南

2018-10-08 16:20 出处:清屏网 人气: 评论(0

一.作用

Babel is a JavaScript compiler.

结构上属于编译器,由于输入JS源码,输出也是JS源码(所谓source to source),所以也称为 transpiler(转译器)

二.原理

You give Babel some JavaScript code, it modifies the code and generates the new code back out.

具体的,源码转换工作分为3个步骤:

parsing -> transforming -> generation

首先“理解”源码所具有的语义,接着进行语义层面的转换,最后从语义表示形式映射回源码形式

而语义表示形式,在Babel里指的就是AST(抽象语法树):

How it modifies the code? Exactly! It builds AST, traverses it, modifies it based on plugins applied and then generate new code from modified AST.

所以,就代码的表示形式而言,是通过引入中间表示形式(AST)来进行语义转换的:

parsing      transforming               generation
String -------> AST ------------> modified AST ----------> String

整个过程中,parsing和generation是固定不变的,最关键的是transforming步骤,通过babel插件来支持,这是其 扩展性 的关键

P.S.编译原理相关的概念,见再看编译原理

parsing

输入JS源码,输出AST

parsing(解析),对应于编译器的词法分析,及语法分析阶段。输入的源码字符序列经过词法分析,生成具有词法意义的token序列(能够区分出关键字、数值、标点符号等),接着经过语法分析,生成具有语法意义的AST(能够区分出语句块、注释、变量声明、函数参数等)

实际上就是 对代码字符串进行语义识别 的过程,输入一段代码串,如何识别出其语法含义,例如:

var a = 'A variable.';

经过parsing后,生成的AST如下:

{
  "type": "VariableDeclaration",
  "declarations": [
    {
      "type": "VariableDeclarator",
      "id": {
        "type": "Identifier",
        "name": "a"
      },
      "init": {
        "type": "Literal",
        "value": "A variable.",
        "raw": "'A variable.'"
      }
    }
  ],
  "kind": "var"
}

它说:这是个 var 类型的变量声明,变量名叫 a ,初始值是个字面量,值为 "A variable."

没错, AST 能够完整地描述代码所具有的语法含义,有了这份信息,编译器就能像人一样理解代码了,这是进行语义层面转换的基础

P.S.JS代码对应的AST结构可以通过 AST Explorer 工具查看

transforming

输入AST,输出修改过的AST

transforming(转换),对应于编译器的机器无关代码优化阶段(稍微有点牵强,但二者工作内容都是修改AST),对AST做一些修改,比如把变量名 a 改为 input

{
  "type": "VariableDeclaration",
  "declarations": [
    {
      "type": "VariableDeclarator",
      "id": {
        "type": "Identifier",
        "name": "input"
      },
      "init": {
        "type": "Literal",
        "value": "A variable.",
        "raw": "'A variable.'"
      }
    }
  ],
  "kind": "var"
}

修改AST节点属性即可,但如果要把声明与赋值拆开的话,就需要新增AST节点:

[{
  "type": "VariableDeclaration",
  "declarations": [
    {
      "type": "VariableDeclarator",
      "id": {
        "type": "Identifier",
        "name": "input"
      },
      "init": null
    }
  ],
  "kind": "var"
},
{
  "type": "ExpressionStatement",
  "expression": {
    "type": "AssignmentExpression",
    "operator": "=",
    "left": {
      "type": "Identifier",
      "name": "input"
    },
    "right": {
      "type": "Literal",
      "value": "A variable.",
      "raw": "'A variable.'"
    }
  }
}]

它说:第一个语句是个 var 类型的变量声明,变量名叫 input ,没有初始值。第二个语句是个表达式语句,具体的是赋值表达式,操作符是 = ,左操作数是标识符 input ,右操作数是字面量,值为 "A variable."

语义层面的转换具体而言就是对AST进行增、删、改操作 ,修改后的AST可能具有不同的语义,映射回代码字符串也不同

generation

输入AST,输出JS源码

generation(生成),对应于编译器的代码生成阶段,把AST映射回代码字符串,例如:

var input;
input = 'A variable.';

比起parsing,generation的过程相对容易些,拼接字符串而已

三.用法

相关npm包

4个核心包:

8个工具包:

  • @babel/cli :以CLI方式使用Babel,依赖@babel/core

  • @babel/types :AST操作工具库,包括判断、断言、创建3类API( isXXXassertXXXxxx ,例如 t.isArrayExpression(node, opts)t.assertArrayExpression(node, opts)t.arrayExpression(elements)

  • @babel/polyfill :包含一些语言特性补丁(完整的ES2015+环境支持),包括 core-jsregenerator runtime

  • @babel/runtime :包含Babel转换产生的工具方法( _classCallCheck 之类的),以及一份regenerator-runtime,配合 @babel/plugin-transform-runtime 插件使用

  • @babel/register :Node环境下hack require来达到自动编译 require 到的所有文件的目的,配合 @babel/node 运行

  • @babel/template :用来快速创建AST的模板语法,支持占位符

  • @babel/helpers :一系列预定义的@babel/template模板方法, 供Babel插件使用

  • @babel/code-frame :用来输出源码行列相关的错误信息

P.S.关于Babel packages的更多信息,见 babel/packages/README.md

P.S.至于为什么包名都是 @babel/xxx 形式的,一方面出于避免命名冲突考虑,另一方面是为了方便区分官方package与社区package,避免误解,具体见 Renames: Scoped Packages (@babel/x)

babylon与@babel/parser

@babel/parser 是Babel 7推出的,之前叫 Babylon

The Babel parser (previously Babylon) is a JavaScript parser used in Babel.

是Babel的JS解析器,几个特点:

  • 默认开启最新版ES(ES2017)特性支持

  • 保留注释(comment attachment)

  • 支持JSX、Flow、Typescript

  • 支持实验性的语言特性(stage-0及其它阶段的候选特性)

@babel/polyfill与@babel-runtime

这2个东西都是用来提供ES特性补丁的,比如Promise、Set、Map等:

The babel-polyfill and babel-runtime modules are used to serve the same function in two different ways. Both modules ultimately serve to emulate an ES6 environment.

区别在于:

  • @babel/polyfill: 会污染全局作用域 ,适合App和命令行工具

  • @babel/runtime:会作为运行时依赖打包进去,不污染全局作用域,更适合类库

简单示例

把常量名转换成大写,即:

// 输入
const numberFive = 5;
// 要求输出
const NUMBER_FIVE = 5;

清晰起见,分别引用 @babel/parser@babel/traverse@babel/generator (不直接使用 @babel/core 提供的上层API):

const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;

let input = `
const number = 'number';
const numberFive = 5;
const numberSix = 6, numberSeven = numberSix + 1;
const XMLHttpRequest = window.XMLHttpRequest;
let aString = 'string';
var numberEight = numberSeven + 1;
function f() {
  const numberEleven = numberSeven + 4;
  return numberFive + numberEleven + numberEight;
}
`;

// 1.解析
let ast = parser.parse(input);
// 2.转换
function renameConst(name) {
  return name.replace(/([a-z])([A-Z])/, '$1_$2').toUpperCase();
}
function renameConstBindings(path) {
  let ownBindings = path.scope.bindings;
  for (let name in ownBindings) {
    if (ownBindings[name].kind === 'const') {
      path.scope.rename(name, renameConst(name));
    }
  }
}
traverse(ast, {
  Program: {
    exit: renameConstBindings
  },
  Function: {
    exit: renameConstBindings
  }
});
// 3.生成
let output = generate(ast);

// test
console.log(output.code);

输出:

const NUMBER = 'number';
const NUMBER_FIVE = 5;
const NUMBER_SIX = 6,
      NUMBER_SEVEN = NUMBER_SIX + 1;
const XMLHTTP_REQUEST = window.XMLHttpRequest;
let aString = 'string';
var numberEight = NUMBER_SEVEN + 1;

function f() {
  const NUMBER_ELEVEN = NUMBER_SEVEN + 4;
  return NUMBER_FIVE + NUMBER_ELEVEN + numberEight;
}

纯作用域操作(找出常量,再重命名即可),scope相关的更多API见 babel/packages/babel-traverse/src/scope/index.js

四.插件

定义

Babel插件的一般格式为:

export default function(babel) {
  return {
    // 必需,配合traverse使用的visitor对象
    visitor: {},

    // 可选,继承其它插件,比如识别JSX、async function等语法
    inherits: OtherPlugin,
    // 可选,插件执行前,初始化状态,如cache
    pre(state) {},
    // 可选,插件执行后,收尾清理工作
    post(state) {}
  }
}

所以很容易把上面的常量名转换功能包装成Babel插件,把转换部分的 visitor 拿进来即可:

// babel-plugin-transform-const-name.js
export default function(babel) {
  return {
    visitor: {
      Program: {
        exit: renameConstBindings
      },
      Function: {
        exit: renameConstBindings
      }
    }
  }
}

P.S.通过Babel配置选项设置的插件参数,可以通过 state.opts 读取,具体见 Plugin Options

编译

Babel及插件运行的Node环境不支持ES Module( export default ),所以 插件自身需要编译 ,这里通过 @babel/cli 来完成:

npx babel plugins --no-babelrc --presets=@babel/preset-env --out-dir lib

也可以通过npm scripts来做:

"scripts": {
  "compile-plugins": "babel plugins --no-babelrc --presets=@babel/preset-env --out-dir lib"
}

./plugins/ 目录下的插件源码全都转一遍放到 ./lib/ 下,文件名保持不变

配置

一般通过 .babelrc 配置文件(放在项目根目录下)来应用指定插件:

{
  "plugins": ['./lib/babel-plugin-transform-const-name.js']
}

注意,这里用的是编译后的( lib 目录下)插件,否则会报错不支持 export 关键字:

SyntaxError: Unexpected token export

应用

然后通过 @babel/core 让插件跑起来:

const babel = require('@babel/core');
const input = require('fs').readFileSync('./const-rename-input.js', 'utf-8');

let output = babel.transform(input, {
  filename: 'const-rename-input.js'
});
console.log(output.code);

注意 ,要走 .babelrc 配置的话,必须指定 filename ,具体见 babel.transform API is not using .babelrc

.babelrc files are loaded relative to the file being compiled. If this option is omitted, Babel will behave as if babelrc: false has been set.

或者不走 .babelrc 直接通过CLI来跑:

npx babel const-rename-input.js --no-babelrc --presets=@babel/preset-env --plugins=./lib/babel-plugin-transform-const-name.js

P.S.Babel ClI的更多用法,见 Usage

输出:

"use strict";

const NUMBER = 'number';
const NUMBER_FIVE = 5;
const NUMBER_SIX = 6,
      NUMBER_SEVEN = NUMBER_SIX + 1;
const XMLHTTP_REQUEST = window.XMLHttpRequest;
let aString = 'string';
var numberEight = NUMBER_SEVEN + 1;

function f() {
  const NUMBER_ELEVEN = NUMBER_SEVEN + 4;
  return NUMBER_FIVE + NUMBER_ELEVEN + numberEight;
}

五.应用场景

删除调试代码

去掉 console.xxxdebugger ,具体实现如下:

function removeConsoleCall(path, {types: t}) {
  if (path.node.name === 'console') {
    let consoleCall = path.findParent(p => p.isCallExpression());
    if (consoleCall) {
      try {
        consoleCall.remove();
      } catch(ex) {
        consoleCall.replaceWith(t.identifier('undefined'));
      }
    }
  }
}
export default function(babel) {
  return {
    visitor: {
      Identifier: {
        enter(path) {
          removeConsoleCall(path, babel);
        }
      },
      DebuggerStatement: {
        enter(path) {
          path.remove();
        }
      }
    }
  }
}

注意 一个细节 ,默认删掉 console.xxxconsoleCall.remove(); ),但有些情况不能直接删除,比如作为操作数参与运算时,删掉就会引发语法错误,这里利用 path 操作自带的校验,捕获此类错误并以替换成 undefined 来兜底

输入:

console.log(1);
window.console.log(2);
console.error('err');
let result = 2 > 1 ? console.log(3) : window.console.log(4);
if (true) debugger;
if (true) {
  debugger;console.log(2);alert(3);
  let three = 2 + (console.info('info'), 1);
}

输出:

"use strict";

var result = 2 > 1 ? undefined : undefined;

if (true) {}

if (true) {
  var three = 2 + (1);
}

看起来不错,但 对于别名之类难以追踪的东西无能为力 ,例如:

let log = console.log.bind(console);
log(4);
var c = window.console;
c.log(5);
// 存在误伤
void function(c) {
  c.log(6);
  alert(7);
}(window.console);

输出:

var log;
log(4);
var c = window.console;
c.log(5); // 存在误伤

void undefined;

常量编译替换

编译时,把 _GET_CONFIG('c3') 为对应的配置信息,如:

{
  "c1": "#FFFFFF",
  "c2": "#00FFFF",
  "c3": "#FF00FF",
  "c4": "#FFFF00"
}

插件内容如下:

const CONFIG_MAP = {
  "c1": "#FFFFFF",
  "c2": "#00FFFF",
  "c3": "#FF00FF",
  "c4": "#FFFF00"
};

export default function({types: t}) {
  return {
    inherits: require("@babel/plugin-syntax-jsx").default,
    visitor: {
      CallExpression: {
        enter(path) {
          if (path.node.callee.name === '_GET_CONFIG') {
            let args = path.node.arguments.map(v => v.value);
            let configValue = CONFIG_MAP[args[0]] || '';
            path.replaceWith(t.stringLiteral(configValue));
          }
        }
      }
    }
  }
}

输入:

function render() {
  return <div style={{color: _GET_CONFIG('c3')}}></div>
}

输出:

"use strict";

function render() {
  return <div style={{
    color: "#FF00FF"
  }}></div>;
}

同样,只能应对静态替换的场景,不支持别名,也不支持变量:

let x = 'c3';
_GET_CONFIG(x);
let get = _GET_CONFIG;
get('c4');

输出:

var x = 'c3';
"";
var get = _GET_CONFIG;
get('c4');

其它场景

  • 实现强约束:比如 使用 babel 插件来打造真正的“私有”属性 ,用 Symbol 作为私有属性的 key ,把道德规范变成强约束

  • 源码转换:有专门的工具 facebook/jscodeshift ,提供了更方便的API(如 findVariableDeclarators('foo').renameTo('bar') ),尤其适合API升级之类的需要大规模重构的场景,例如 reactjs/react-codemod

  • 格式化:如 Prettier ,进行语义等价的代码风格转换,比如箭头函数参数带不带括号、语句末尾要不要分号之类的

  • 可视化: js2flowchart 能够根据代码输出流程图,读源码可以参考,也可以用来分析祖传逻辑

参考资料


分享给小伙伴们:
本文标签: Babel

相关文章

发表评论愿您的每句评论,都能给大家的生活添色彩,带来共鸣,带来思索,带来快乐。

CopyRight © 2015-2016 QingPingShan.com , All Rights Reserved.

清屏网 版权所有 豫ICP备15026204号