前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Babel快速指南

Babel快速指南

作者头像
ayqy贾杰
发布2019-06-12 14:56:54
1K0
发布2019-06-12 14:56:54
举报
文章被收录于专栏:黯羽轻扬黯羽轻扬

一.作用

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个步骤:

代码语言:javascript
复制
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)来进行语义转换的:

代码语言:javascript
复制
       parsing      transforming               generation
String -------> AST ------------> modified AST ----------> String

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

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

parsing

输入JS源码,输出AST

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

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

代码语言:javascript
复制
var a = 'A variable.';

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

代码语言:javascript
复制
{
 "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

代码语言:javascript
复制
{
 "type": "VariableDeclaration",
 "declarations": [
   {
     "type": "VariableDeclarator",
     "id": {
       "type": "Identifier",
       "name": "input"
     },
     "init": {
       "type": "Literal",
       "value": "A variable.",
       "raw": "'A variable.'"
     }
   }
 ],
 "kind": "var"
}

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

代码语言:javascript
复制
[{
 "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映射回代码字符串,例如:

代码语言:javascript
复制
var input;
input = 'A variable.';

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

三.用法

相关npm包

4个核心包:

  • @babel/core:以编程方式来使用Babel(不以CLI方式)
  • @babel/parser:解析输入源码,创建AST
  • @babel/traverse:遍历操作AST
  • @babel/generator:把AST转回JS代码

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-js和regenerator 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:会作为运行时依赖打包进去,不污染全局作用域,更适合类库

简单示例

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

代码语言:javascript
复制
// 输入
const numberFive = 5;
// 要求输出
const NUMBER_FIVE = 5;

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

代码语言:javascript
复制
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);

输出:

代码语言:javascript
复制
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插件的一般格式为:

代码语言:javascript
复制
export default function(babel) {
 return {
   // 必需,配合traverse使用的visitor对象
   visitor: {},   // 可选,继承其它插件,比如识别JSX、async function等语法
   inherits: OtherPlugin,
   // 可选,插件执行前,初始化状态,如cache
   pre(state) {},
   // 可选,插件执行后,收尾清理工作
   post(state) {}
 }
}

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

代码语言:javascript
复制
// 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来完成:

代码语言:javascript
复制
npx babel plugins --no-babelrc --presets=@babel/preset-env --out-dir lib

也可以通过npm scripts来做:

代码语言:javascript
复制
"scripts": {
 "compile-plugins": "babel plugins --no-babelrc --presets=@babel/preset-env --out-dir lib"
}

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

配置

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

代码语言:javascript
复制
{
 "plugins": ['./lib/babel-plugin-transform-const-name.js']
}

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

代码语言:javascript
复制
SyntaxError: Unexpected token export

应用

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

代码语言:javascript
复制
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来跑:

代码语言:javascript
复制
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

输出:

代码语言:javascript
复制
"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,具体实现如下:

代码语言:javascript
复制
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来兜底

输入:

代码语言:javascript
复制
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);
}

输出:

代码语言:javascript
复制
"use strict";var result = 2 > 1 ? undefined : undefined;if (true) {}if (true) {
 var three = 2 + (1);
}

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

代码语言:javascript
复制
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);

输出:

代码语言:javascript
复制
var log;
log(4);
var c = window.console;
c.log(5); // 存在误伤void undefined;

常量编译替换

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

代码语言:javascript
复制
{
 "c1": "#FFFFFF",
 "c2": "#00FFFF",
 "c3": "#FF00FF",
 "c4": "#FFFF00"
}

插件内容如下:

代码语言:javascript
复制
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));
         }
       }
     }
   }
 }
}

输入:

代码语言:javascript
复制
function render() {
 return <div style={{color: _GET_CONFIG('c3')}}></div>
}

输出:

代码语言:javascript
复制
"use strict";function render() {
 return <div style={{
   color: "#FF00FF"
 }}></div>;
}

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

代码语言:javascript
复制
let x = 'c3';
_GET_CONFIG(x);
let get = _GET_CONFIG;
get('c4');

输出:

代码语言:javascript
复制
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能够根据代码输出流程图,读源码可以参考,也可以用来分析祖传逻辑

参考资料

  • AST for JavaScript developers
  • jamiebuilds/babel-handbook
  • 使用 babel 插件来打造真正的“私有”属性
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-10-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端向后 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一.作用
  • 二.原理
    • parsing
      • transforming
        • generation
        • 三.用法
          • 相关npm包
            • babylon与@babel/parser
            • @babel/polyfill与@babel-runtime
          • 简单示例
          • 四.插件
            • 定义
              • 编译
                • 配置
                  • 应用
                  • 五.应用场景
                    • 删除调试代码
                      • 常量编译替换
                        • 其它场景
                          • 参考资料
                          相关产品与服务
                          命令行工具
                          腾讯云命令行工具 TCCLI 是管理腾讯云资源的统一工具。使用腾讯云命令行工具,您可以快速调用腾讯云 API 来管理您的腾讯云资源。此外,您还可以基于腾讯云的命令行工具来做自动化和脚本处理,以更多样的方式进行组合和重用。
                          领券
                          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档