メインコンテンツへ移動

Babel クイックガイド

無料2018-10-07#Tool#babel plugin#babel插件#Babylon vs babel-parser#babel应用场景#babel与AST

Babel は何をしているのか?Babel で何ができるのか?

一.作用

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 にいくつかの修正を加えます。例えば変数名 ainput に変更:

{
  "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、初期値はありません。2 番目の文は式文で、具体的には代入式で、演算子は =、左被演算子は識別子 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 の 1 部を含み、 @babel/plugin-transform-runtime プラグインと連携して使用

  • @babel/register:Node 環境下で 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.xxx を削除(consoleCall.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 はコードに基づいてフローチャートを出力でき、ソースコードを読む際に参考になり、祖伝ロジックを分析するのにも使用可能

参考資料

コメント

コメントはまだありません

コメントを書く