Babel 介绍与使用

发布于: 6/5/2022 阅读大约需要5分钟

Babel可以说是大家比较属性的工具了, 但是我们经常只是走马观花的了解一下这东西是个啥, 只知道它帮我们转换了代码(至少知道这是个编译器)

官方说明为: Babel 是一个 JavaScript 编译器

既然知道了Babel是一个编译器, 那么就让我们从编译器开始来了解Babel

编译器是什么

编译器可以简单理解为翻译

比如我们开发者开发的代码是ES6, 但是代码的目标运行环境是ES5

👩‍💻 程序员: const a = flag ?? 'a'

💻 ES5环境: WTF? 你说啥??? 我听不懂啊!!! ❌

这时候就需要一个翻译来将我们的ES6+的语言翻译成环境能够听懂的, 这个翻译就称为编译器.

👩‍💻 程序员: 小翻译, 你告诉环境 const a = flag ?? 'a'

🗣 编译器: var _flag; const a = (_flag = flag) !== null && _flag !== void 0 ? _flag : 'a';

💻 ES5环境: 👌 了解!

我们也可以通过jamiebuilds/the-super-tiny-compiler这个项目来了解一下到底什么是编译器.

英文不好的同学可以参考starkwang/the-super-tiny-compiler-cn(代码较老, 较原版稍有不同, 可互相参考)

编译器是如何工作的

我们知道了编译器是什么之后, 那么, 它们是如何’翻译’我们代码的呢?

首先, 一个翻译的基本技能应该就是能听说读写双方的语言.

所以, 编译器一定要能听懂(解析)我们所想要表达的内容, 然后在脑中翻译(转换)成对方能理解的语言, 最后传达(生成)给对方

解析 - tokenizer

解析来说一般会分为两个阶段

  • 词法分析

    将一句话拆分成一个个单词(Token, 标点符号也算), 并标明每个单词的类型

  • 语法分析

    接收词法分析的结果, 分析每个单词(Token)间的关系, 得出语义(也就是AST, 抽象语法树)

// 词法分析器, 代码来自the-super-tiny-compiler.js
function tokenizer(input) {
  var current = 0;
  var tokens = [];
  while (current < input.length) {
    var char = input[current];
    if (char === '(') {
      tokens.push({
        type: 'paren',
        value: '('
      });
      current++;
      continue;
    }
    if (char === ')') {
      tokens.push({
        type: 'paren',
        value: ')'
      });
      current++;
      continue;
    }
    var WHITESPACE = /\s/;
    if (WHITESPACE.test(char)) {
      current++;
      continue;
    }
    var NUMBERS = /[0-9]/;
    if (NUMBERS.test(char)) {
      var value = '';
      while (NUMBERS.test(char)) {
        value += char;
        char = input[++current];
      }
      tokens.push({
        type: 'number',
        value: value
      });
      continue;
    }
    var LETTERS = /[a-z]/i;
    if (LETTERS.test(char)) {
      var value = '';
      while (LETTERS.test(char)) {
        value += char;
        char = input[++current];
      }
      tokens.push({
        type: 'name',
        value: value
      });
      continue;
    }
    throw new TypeError('I dont know what this character is: ' + char);
  }
  return tokens;
}

// 语法分析器: 接收上一步的Token数组, 将它们转换为AST对象
function parser(tokens) {
  var current = 0;
  function walk() {
    var token = tokens[current];
    if (token.type === 'number') {
      current++;
      return {
        type: 'NumberLiteral',
        value: token.value
      };
    }
    if (token.type === 'paren' && token.value === '(') {
      token = tokens[++current];
      var node = {
        type: 'CallExpression',
        name: token.value,
        params: []
      };
      token = tokens[++current];
      while (
        (token.type !== 'paren') ||
        (token.type === 'paren' && token.value !== ')')
      ) {
        node.params.push(walk());
        token = tokens[current];
      }
      current++;
      return node;
    }
    throw new TypeError(token.type);
  }
  var ast = {
    type: 'Program',
    body: []
  };
  while (current < tokens.length) {
    ast.body.push(walk());
  }
  return ast;
}

转换 - parser

知道了原代码的意思和结构后, 就要将每个单词及结构转换成对方能听懂的形式.

废话: 两种语言差异性与转换的工作量基本成正比

function traverser(ast, visitor) {
  function traverseArray(array, parent) {
    array.forEach(child => {
      traverseNode(child, parent);
    });
  }
  function traverseNode(node, parent) {
    let methods = visitor[node.type];
    if (methods && methods.enter) {
      methods.enter(node, parent);
    }
    switch (node.type) {
      case 'Program':
        traverseArray(node.body, node);
        break;
      case 'CallExpression':
        traverseArray(node.params, node);
        break;
      case 'NumberLiteral':
      case 'StringLiteral':
        break;
      default:
        throw new TypeError(node.type);
    }
    if (methods && methods.exit) {
      methods.exit(node, parent);
    }
  }
  traverseNode(ast, null);
}
function transformer(ast) {
  let newAst = {
    type: 'Program',
    body: [],
  };
  ast._context = newAst.body;
  traverser(ast, {
    NumberLiteral: {
      enter(node, parent) {
        parent._context.push({
          type: 'NumberLiteral',
          value: node.value,
        });
      },
    },
    StringLiteral: {
      enter(node, parent) {
        parent._context.push({
          type: 'StringLiteral',
          value: node.value,
        });
      },
    },
    CallExpression: {
      enter(node, parent) {
        let expression = {
          type: 'CallExpression',
          callee: {
            type: 'Identifier',
            name: node.name,
          },
          arguments: [],
        };
        node._context = expression.arguments;
        if (parent.type !== 'CallExpression') {
          expression = {
            type: 'ExpressionStatement',
            expression: expression,
          };
        }
        parent._context.push(expression);
      },
    }
  });
  return newAst;
}

生成 - code generator

根据转换得到的新的AST来生成新的代码

function codeGenerator(node) {
  switch (node.type) {
    case 'Program':
      return node.body.map(codeGenerator)
        .join('\n');
    case 'ExpressionStatement':
      return (
        codeGenerator(node.expression) +
        ';'
      );
    case 'CallExpression':
      return (
        codeGenerator(node.callee) +
        '(' +
        node.arguments.map(codeGenerator)
          .join(', ') +
        ')'
      );
    case 'Identifier':
      return node.name;
    case 'NumberLiteral':
      return node.value;
    case 'StringLiteral':
      return '"' + node.value + '"';
    default:
      throw new TypeError(node.type);
  }
}

整合 - compiler

最后写一个compiler方法将上面方法定义整合, 就完成了一个极简的编译器

function compiler(input) {
  let tokens = tokenizer(input);
  let ast    = parser(tokens);
  let newAst = transformer(ast);
  let output = codeGenerator(newAst);
  return output;
}

Babel可以做什么

  • 语法转换
  • 为目标环境添加缺失特性(通过引用第三方polyfill, 如core-js)
  • 源码转换(codemods)
  • Babel通过语法转换器来支持最新版本的JavaScript语法, 使你的代码可以在并不支持JS新特性的环境中运行.
  • 支持语法拓展, 支持JSX 以及TypeScript等语言
  • 支持插件化, 可以自己开发插件
  • 支持Source map, 可以让我们调试编译后的代码

使用

配置 - options

在项目中配置babel.config.json(后缀名也可是.js, .cjs, .mjs)

详细配置项可参考Options · Babel 中文网 (babeljs.cn)

插件 - plugins

插件用于转译代码, 会在Preset配置之前执行

插件的执行顺序与其定义顺序相同

使用插件:

{
  "plugins": [
    "pluginA",
    ["pluginA"],
    ["pluginA", {}]
  ]
}

如果想自己开发插件请参考: babel-handbook

预设 - presets

预设是指 一组被预先设置好的Babel插件Babel Options

Babel 官方为一些常用环境提供了预设

  • @babel/preset-env: 相当常见的预设, 用于编译ES6+语法
  • @babel/preset-typescript: 由名字就可以看出, 为了编译TypeScript语法
  • @babel/preset-react: 为了编译React语法
  • @babel/preset-flow: 为了Flow语法

除了这些官方提供的预设外, 开源社区也有很多开发者自己开发的优秀的开源预设

使用预设:

module.exports = () => ({
  presets: [
    "presetA", 										// 纯字符串
    ["presetA"],  								// 数组包裹的字符串
    ["presetA", {}]  							// 数组第二个参数为传给预设的参数
  ]
})

如果设置了多个预设, 预设的执行顺序为倒序执行(最后的最先执行), 比如

presets: [a, b, c] 那么执行顺序为 c -> b -> a

相关链接