ソースはバベルを解決します

私たちは、直接プロジェクトを作成し、実行します。

npm install -D @babel/cli

私たちは、最新バージョンの7.8.0を使用します

test1.jsテストを作成します。

/* test.js */
const fn = () => {}
new Promise(() => {})
class Test {}
const c = [1, 2, 3].includes(1)
//测试插件1
var a=10;

バベルプロファイル.babelrcを作成します(任意の構成を記述していないため)。

/* .babelrc */
{
 
}

その後、我々は実行します。

npx babel test1.js -o test1.babel.js --config-file .babelrc

最後に、結果test1.babel.jsを見て:

/* test.js */
const fn = () => {};

new Promise(() => {});

class Test {}

const c = [1, 2, 3].includes(1); //测试插件1

var a = 10;

ああ?なぜ、まったく変化しませんか?質問で、私たちは、ソースコードを見て -

より良いバベルのソースを研究するために、我々はgithubのクローンに直接移動します:

git clone https://github.com/babel/babel.git

その後、我々は実行すると:

npx babel test1.js -o test1.babel.js --config-file 

ときに我々が直接オープンパッケージ/バベル-CLI / binに/ babel.js:

#!/usr/bin/env node

require("../lib/babel");

パッケージ/バベル-CLI / SRC /バベル/ index.js:

#!/usr/bin/env node

import parseArgv from "./options";
import dirCommand from "./dir";
import fileCommand from "./file";

const opts = parseArgv(process.argv);

if (opts) {
  const fn = opts.cliOptions.outDir ? dirCommand : fileCommand;
  fn(opts).catch(err => {
    console.error(err);
    process.exitCode = 1;
  });
} else {
  process.exitCode = 2;
}

パッケージ/バベル-CLI / SRC /バベル/ file.js:

export default async function({
  cliOptions,
  babelOptions,
}: CmdOptions): Promise<void> {
	 if (cliOptions.filenames.length) {
    await files(cliOptions.filenames);
  } else {
    await stdin();
  }
}

次に、ファイルのメソッドを実行します。

 async function files(filenames: Array<string>): Promise<void> {
    if (!cliOptions.skipInitialBuild) {
      await walk(filenames);
    }

そして、歩行メソッドを実行します。

async function walk(filenames: Array<string>): Promise<void> {
    
        try {
          return await util.compile(
          );
        } catch (err) {
       
    );
  }

私たちは、(パッケージ/バベル-CLI / SRC /バベル/ util.js)utilのコンパイル方法の最終的な実装を見ることができます:

export function compile(
  filename: string,
  opts: Object | Function,
): Promise<Object> {
  opts = {
    ...opts,
    caller: CALLER,
  };

  return new Promise((resolve, reject) => {
    babel.transformFile(filename, opts, (err, result) => {
      if (err) reject(err);
      else resolve(result);
    });
  });
}

私たちが渡されたパラメータを取得し、バベル-cliの後に見ることができます。

  1. ソースファイルtest1.js
  2. ファイルtest1.babel.jsを入力します。
  3. バベルプロファイル.babelrc
npx babel test1.js -o test1.babel.js --config-file .babelrc

コンパイルされたコードは、メソッドbabel.transformFileのバベルコアにより取得し、最後にコンパイルされたコード-o最後構成バベル-CLI出力を通過させます。

私たちは、どのようなbabel.transformFile方法に焦点を当てて

パッケージ/バベルコア/ SRC / index.js:

export {
  transformFile
} from "./transform-file";
const transformFileRunner = gensync<[string, ?InputOptions], FileResult | null>(
	//加载配置文件
    const config: ResolvedConfig | null = yield* loadConfig(options);
    if (config === null) return null;
	//加载源文件
    const code = yield* fs.readFile(filename, "utf8");
    //开始编译
    return yield* run(config, code);
  },
);

loadConfig方法についてましょう話、我々は.babelrcない渡された覚えは、プラグインのプロパティでプリセットの中に入るし、ファイルを読み込み、後に、プリセットはプラグインとプリセットの集合で、プラグインはプラグインのセットです、我々はそれを変更しよう当社.babelrc設定ファイル:

/* .babelrc */
{
  "presets": [
    ["@babel/preset-env", {
      "modules": false,
      "useBuiltIns": "usage",
      "targets": "ie >= 8"
    }]
  ],
  "plugins": [
    ["@babel/plugin-transform-runtime", {
      "corejs":false
    }],
    ["./plugins/PluginTest1.js"]
  ]
}

その後、実行します。

npx babel test1.js -o test1.babel.js --config-file .babelrc

結果:

import "core-js/modules/es7.array.includes";
import "core-js/modules/es6.string.includes";
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
import "core-js/modules/es6.promise";

/* test.js */
var fn = function fn() {};

new Promise(function () {});

var Test = function Test() {
  _classCallCheck(this, Test);
};

var c = [1, 2, 3].includes(1); //测试插件1

var aaa = 10;

設定ファイル、我々はその後、ゆっくりと導入のプラグインとprestesについては、我々はバベルコアを見ていき、当社のコンフィギュレーション・ファイルをロードする方法です。
バベルコア/ SRC /設定/ full.js:

export default gensync<[any], ResolvedConfig | null>(function* loadFullConfig(
  inputOpts: mixed,
): Handler<ResolvedConfig | null> {
  const result = yield* loadPrivatePartialConfig(inputOpts);
  if (!result) {
    return null;
  }
  const { options, context } = result;

  const optionDefaults = {};
  const passes = [[]];
  try {
    const { plugins, presets } = options;
}

我々は、プラグインをプラグインで提供されているすべてのプリセット、プリセットの実行方法によって、その後のプリセットプロパティプロファイルでプラグインを取得していることがわかります。

「... /インデックス」からコンテキストとしてインポート*;

const loadDescriptor = makeWeakCache(function*(
  { value, options, dirname, alias }: UnloadedDescriptor,
  cache: CacheConfigurator<SimpleContext>,
): Handler<LoadedDescriptor> {

    try {
     const api = {
      ...context,
      ...makeAPI(cache),
    };
      item = value(api, options, dirname);
    } catch (e) {
    
      throw e;
    }
  }

我々はバベル・コアオブジェクトを通過したことをAPIは、我々はパラメータを渡すオプションという、dirnameが私たちの現在のフォルダのディレクトリバベルテストです。

レッツ・進歩は、プラグインPluginTest1.js(= 10は、VaRのAAA = 10となった変数var)を書くために:

module.exports = function (api, options, dirname) {
    let t = api.types;
    console.log(options)
    console.log(dirname)
    return {
        visitor: {
            VariableDeclarator: {
                enter(path,state) {
                    console.log(path)
                    if(path.node.id.name == 'a'){
                        path.node.id.name="aaa";
                    }
                },
                exit() {
                    console.log("Exited!");
                }
            }
        }
    }
};

私たちは、特定の分析カザフスタン後に再び差し込み、我々はプラグインに当社の提供に対応するパラメータは、API、オプション、dirnameにあることがわかります。

module.exports = function (api, options, dirname) {

まあ、我々は終了し、設定ファイルパッケージ/バベルコア/ SRC /変換-file.js前に、その後下がるし続けます。

const transformFileRunner = gensync<[string, ?InputOptions], FileResult | null>(
	//加载配置文件
    const config: ResolvedConfig | null = yield* loadConfig(options);
    if (config === null) return null;
	//加载源文件
    const code = yield* fs.readFile(filename, "utf8");
    //开始编译
    return yield* run(config, code);
  },
);

あなたは、直接実行方法の設定、実行を取得した後、私たちは、見ることができる
パッケージ/バベルコア/ SRC /変換/ index.jsを:

  config: ResolvedConfig,
  code: string,
  ast: ?(BabelNodeFile | BabelNodeProgram),
): Handler<FileResult> {
  const file = yield* normalizeFile(
    config.passes,
    normalizeOptions(config),
    code,
    ast,
  );

  const opts = file.opts;
  try {
    yield* transformFile(file, config.passes);
  } catch (e) {
    e.message = `${opts.filename ?? "unknown"}: ${e.message}`;
    if (!e.code) {
      e.code = "BABEL_TRANSFORM_ERROR";
    }
    throw e;
  }

  let outputCode, outputMap;
  try {
    if (opts.code !== false) {
      ({ outputCode, outputMap } = generateCode(config.passes, file));
    }
  } catch (e) {
    e.message = `${opts.filename ?? "unknown"}: ${e.message}`;
    if (!e.code) {
      e.code = "BABEL_GENERATE_ERROR";
    }
    throw e;
  }

  return {
    metadata: file.metadata,
    options: opts,
    ast: opts.ast === true ? file.ast : null,
    code: outputCode === undefined ? null : outputCode,
    map: outputMap === undefined ? null : outputMap,
    sourceType: file.ast.program.sourceType,
  };
}

もう少しコード、正方形ではないを行います!私たちのステップバイステップ

まず、normalizeFileメソッドの実装を参照してください。

export function* run(
  config: ResolvedConfig,
  code: string,
  ast: ?(BabelNodeFile | BabelNodeProgram),
): Handler<FileResult> {
  const file = yield* normalizeFile(
    config.passes,
    normalizeOptions(config),
    code,
    ast,
  );

パッケージ/バベルコア/ SRC /変換/正規-file.js:

export default function* normalizeFile(
  pluginPasses: PluginPasses,
  options: Object,
  code: string,
  ast: ?(BabelNodeFile | BabelNodeProgram),
): Handler<File> {
  code = `${code || ""}`;

  if (ast) {
    if (ast.type === "Program") {
      ast = t.file(ast, [], []);
    } else if (ast.type !== "File") {
      throw new Error("AST root must be a Program or File node");
    }
    ast = cloneDeep(ast);
  } else {
    ast = yield* parser(pluginPasses, options, code);
  }

私たちはASTを渡していない場合は、それはAST(抽象構文木)オブジェクトを取得するためにパーサーによって、見ることができます。

ASTだから、それは何ですか?

コンピュータサイエンスでは、AST(抽象構文木、AST)、または単に構文木(構文木)は、ソースコードの構文構造の抽象的表現です。これは、ツリーの形で言語の構文構造をプログラミング性能、ツリーの各ノードは、構造化ソースコードを表明しています。

まあ、ヘビー級デビューパーサーのこのバベル
パーサはパーサバベルです。もともとどんぐりプロジェクトから出フォーク。ドングリは、プラグインベースのアーキテクチャを設計した(将来の標準の機能だけでなく、それらのような)使いやすい、非標準の特性のために、非常に高速であります

我々のコードの変換バベルを解析した後:

/* test.js */
const fn = () => {}
new Promise(() => {})
class Test {}
const c = [1, 2, 3].includes(1)
//测试插件1
var a=10;

それはに変換されます。

{
  "type": "Program",
  "start": 0,
  "end": 120,
  "body": [
    {
      "type": "VariableDeclaration",
      "start": 14,
      "end": 33,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 20,
          "end": 33,
          "id": {
            "type": "Identifier",
            "start": 20,
            "end": 22,
            "name": "fn"
          },
          "init": {
            "type": "ArrowFunctionExpression",
            "start": 25,
            "end": 33,
            "id": null,
            "expression": false,
            "generator": false,
            "async": false,
            "params": [],
            "body": {
              "type": "BlockStatement",
              "start": 31,
              "end": 33,
              "body": []
            }
          }
        }
      ],
      "kind": "const"
    },
    {
      "type": "ExpressionStatement",
      "start": 34,
      "end": 55,
      "expression": {
        "type": "NewExpression",
        "start": 34,
        "end": 55,
        "callee": {
          "type": "Identifier",
          "start": 38,
          "end": 45,
          "name": "Promise"
        },
        "arguments": [
          {
            "type": "ArrowFunctionExpression",
            "start": 46,
            "end": 54,
            "id": null,
            "expression": false,
            "generator": false,
            "async": false,
            "params": [],
            "body": {
              "type": "BlockStatement",
              "start": 52,
              "end": 54,
              "body": []
            }
          }
        ]
      }
    },
    {
      "type": "ClassDeclaration",
      "start": 56,
      "end": 69,
      "id": {
        "type": "Identifier",
        "start": 62,
        "end": 66,
        "name": "Test"
      },
      "superClass": null,
      "body": {
        "type": "ClassBody",
        "start": 67,
        "end": 69,
        "body": []
      }
    },
    {
      "type": "VariableDeclaration",
      "start": 70,
      "end": 101,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 76,
          "end": 101,
          "id": {
            "type": "Identifier",
            "start": 76,
            "end": 77,
            "name": "c"
          },
          "init": {
            "type": "CallExpression",
            "start": 80,
            "end": 101,
            "callee": {
              "type": "MemberExpression",
              "start": 80,
              "end": 98,
              "object": {
                "type": "ArrayExpression",
                "start": 80,
                "end": 89,
                "elements": [
                  {
                    "type": "Literal",
                    "start": 81,
                    "end": 82,
                    "value": 1,
                    "raw": "1"
                  },
                  {
                    "type": "Literal",
                    "start": 84,
                    "end": 85,
                    "value": 2,
                    "raw": "2"
                  },
                  {
                    "type": "Literal",
                    "start": 87,
                    "end": 88,
                    "value": 3,
                    "raw": "3"
                  }
                ]
              },
              "property": {
                "type": "Identifier",
                "start": 90,
                "end": 98,
                "name": "includes"
              },
              "computed": false
            },
            "arguments": [
              {
                "type": "Literal",
                "start": 99,
                "end": 100,
                "value": 1,
                "raw": "1"
              }
            ]
          }
        }
      ],
      "kind": "const"
    },
    {
      "type": "VariableDeclaration",
      "start": 110,
      "end": 119,
      "declarations": [
        {
          "type": "VariableDeclarator",
          "start": 114,
          "end": 118,
          "id": {
            "type": "Identifier",
            "start": 114,
            "end": 115,
            "name": "a"
          },
          "init": {
            "type": "Literal",
            "start": 116,
            "end": 118,
            "value": 10,
            "raw": "10"
          }
        }
      ],
      "kind": "var"
    }
  ],
  "sourceType": "module"
}

小さなパートナーは、直接使用することができますコンバータASTのオンライン版を

ここに画像を挿入説明
簡単な解析方法で見てみましょう:

export default function* parser(
  pluginPasses: PluginPasses,
  { parserOpts, highlightCode = true, filename = "unknown" }: Object,
  code: string,
): Handler<ParseResult> {
  try {
    const results = [];
    for (const plugins of pluginPasses) {
      for (const plugin of plugins) {
        const { parserOverride } = plugin;
        if (parserOverride) {
          const ast = parserOverride(code, parserOpts, parse);

          if (ast !== undefined) results.push(ast);
        }
      }
    }

    if (results.length === 0) {
      return parse(code, parserOpts);
    } else if (results.length === 1) {
      yield* []; // If we want to allow async parsers
      if (typeof results[0].then === "function") {
        throw new Error(
          `You appear to be using an async parser plugin, ` +
            `which your current version of Babel does not support. ` +
            `If you're using a published plugin, you may need to upgrade ` +
            `your @babel/core version.`,
        );
      }
      return results[0];
    }
    throw new Error("More than one plugin attempted to override parsing.");
  } catch (err) {
    if (err.code === "BABEL_PARSER_SOURCETYPE_MODULE_REQUIRED") {
      err.message +=
        "\nConsider renaming the file to '.mjs', or setting sourceType:module " +
        "or sourceType:unambiguous in your Babel config for this file.";
      // err.code will be changed to BABEL_PARSE_ERROR later.
    }

    const { loc, missingPlugin } = err;
    if (loc) {
      const codeFrame = codeFrameColumns(
        code,
        {
          start: {
            line: loc.line,
            column: loc.column + 1,
          },
        },
        {
          highlightCode,
        },
      );
      if (missingPlugin) {
        err.message =
          `${filename}: ` +
          generateMissingPluginMessage(missingPlugin[0], loc, codeFrame);
      } else {
        err.message = `${filename}: ${err.message}\n\n` + codeFrame;
      }
      err.code = "BABEL_PARSE_ERROR";
    }
    throw err;
  }
}

コードまたはの多くは、の解析部分と関連したプラグインを言及してみましょう:

  for (const plugins of pluginPasses) {
      for (const plugin of plugins) {
        const { parserOverride } = plugin;
        if (parserOverride) {
          const ast = parserOverride(code, parserOpts, parse);

          if (ast !== undefined) results.push(ast);
        }
      }

我々はプラグインを持っているときに直接parserOverride行くparserOverrideプラグインの決意のバベル/パーサを覆う方法を提供します。
私はそれがハの問題ではありません理解していません!パース特定の利用当社は、解像度の後ろに置きます。

簡単な読み取り解析した後、我々は、メソッドの実行を継続し
たパッケージを/バベルコア/ SRC /変換/ index.js:

export function* run(
  config: ResolvedConfig,
  code: string,
  ast: ?(BabelNodeFile | BabelNodeProgram),
): Handler<FileResult> {
	//获取ast对象
  const file = yield* normalizeFile();

  const opts = file.opts;
  try {
  	//执行转换操作
    yield* transformFile(file, config.passes);
  } catch (e) {
  }

私たちは、transformFile方法を参照し続けることができ、その後、我々ASTオブジェクトがtransformFileメソッドを渡さ:

function* transformFile(file: File, pluginPasses: PluginPasses): Handler<void> {
  for (const pluginPairs of pluginPasses) {
    const passPairs = [];
    const passes = [];
    const visitors = [];

    for (const plugin of pluginPairs.concat([loadBlockHoistPlugin()])) {
      const pass = new PluginPass(file, plugin.key, plugin.options);

      passPairs.push([plugin, pass]);
      passes.push(pass);
      visitors.push(plugin.visitor);
    }

    for (const [plugin, pass] of passPairs) {
      const fn = plugin.pre;
      if (fn) {
        const result = fn.call(pass, file);

        yield* [];
        if (isThenable(result)) {
          throw new Error(
            `You appear to be using an plugin with an async .pre, ` +
              `which your current version of Babel does not support. ` +
              `If you're using a published plugin, you may need to upgrade ` +
              `your @babel/core version.`,
          );
        }
      }
    }

    // merge all plugin visitors into a single visitor
    const visitor = traverse.visitors.merge(
      visitors,
      passes,
      file.opts.wrapPluginVisitorMethod,
    );
    traverse(file.ast, visitor, file.scope);

    for (const [plugin, pass] of passPairs) {
      const fn = plugin.post;
      if (fn) {
        const result = fn.call(pass, file);

        yield* [];
        if (isThenable(result)) {
          throw new Error(
            `You appear to be using an plugin with an async .post, ` +
              `which your current version of Babel does not support. ` +
              `If you're using a published plugin, you may need to upgrade ` +
              `your @babel/core version.`,
          );
        }
      }
    }
  }
}

または語句「DO我々は一歩一歩を見て!!正方形ではないが、」まず、我々は、次を参照してください。

//遍历所有的插件,获取插件的visitor属性,然后传给visitors
 for (const plugin of pluginPairs.concat([loadBlockHoistPlugin()])) {
      const pass = new PluginPass(file, plugin.key, plugin.options);

      passPairs.push([plugin, pass]);
      passes.push(pass);
      visitors.push(plugin.visitor);
    }

だから、何の訪問者ですか?我々は言及カスタムプラグPluginTest1.jsがあることがわかります前に:

module.exports = function (api, options, dirname) {
    let t = api.types;
    console.log(options)
    console.log(dirname)
    return {
        visitor: {
            VariableDeclarator: {
                enter(path,state) {
                    console.log(path)
                    if(path.node.id.name == 'a'){
                        path.node.id.name="aaa";
                    }
                },
                exit() {
                    console.log("Exited!");
                }
            }
        }
    }
};

事前におよそまず話、実は後のお客様は、トラバースは、抽象構文木ASTオブジェクトへのアクセスを提供することを可能にします

次のステップは、他のヘビートラバースデビューのバベルなるよう
バベルトラバース(横断)モジュールを削除し、ノードを追加し、全体のツリーの状態を維持し、交換する責任があります。

runメソッドに戻るには、
パッケージ/バベルコア/ SRC /変換/ index.js:

export function* run(
  config: ResolvedConfig,
  code: string,
  ast: ?(BabelNodeFile | BabelNodeProgram),
): Handler<FileResult> {
 //获取ast对象(parser)
  const file = yield* normalizeFile();
  try {
  //(遍历)模块维护了整棵树的状态,并且负责替换、移除和添加节点。
    yield* transformFile(file, config.passes);
  } catch (e) {
   
  }
({ outputCode, outputMap } = generateCode(config.passes, file));
 

可以看到执行了generateCode方法,这时babel的最后一个重量级选手babel-generator登场了

Babel Generator模块是 Babel 的代码生成器,它读取AST并将其转换为代码和源码映射(sourcemaps)。

最后run方法返回generator生成的代码:

return {
    metadata: file.metadata,
    options: opts,
    ast: opts.ast === true ? file.ast : null,
    code: outputCode === undefined ? null : outputCode,
    map: outputMap === undefined ? null : outputMap,
    sourceType: file.ast.program.sourceType,
  };

整个babel-cli到babel-core的过程随着我们的demo跟我们的源码就讲完了。

我们重新整理一下整个过程:

  1. babel-cli开始读取我们的参数(源文件test1.js、输出文件test1.babel.js、配置文件.babelrc)
  2. babel-core根据babel-cli的参数开始编译
  3. Babel Parser 把我们传入的源码解析成ast对象
  4. Babel Traverse(遍历)模块维护了整棵树的状态,并且负责替换、移除和添加节点(也就是结合我们传入的插件把es6转换成es5的一个过程)
  5. Babel Generator模块是 Babel 的代码生成器,它读取AST并将其转换为代码和源码映射(sourcemaps)。

好啦,到此我们算是把babel的整个过程简单的跑了一下,为了加深对每个流程的理解,我们不经过babel-core跟babel-cli单独去用一下parser、traverse、generator。

//我们的es6源码
const code = `
    const result=a*b;
    const result1=()=>{};
`;
const {parse}=require("@babel/parser");
const traverse =require("@babel/traverse").default;
const t = require("babel-types");
const generator = require("@babel/generator").default;

//把es6源码通过parser转换成ast对象
const ats=parse(code,{
    sourceType: "module"
});
//把ast对象通过traverse转换成es5代码
traverse(ats,{
    enter(path) {
        if (t.isIdentifier(path.node, { name: "a" })) {
          path.node.name = "aa";
        }
        if (path.isArrowFunctionExpression()){ //es6转换成es5
            path.arrowFunctionToExpression({
                // While other utils may be fine inserting other arrows to make more transforms possible,
                // the arrow transform itself absolutely cannot insert new arrow functions.
                allowInsertArrow: false,
                specCompliant: false,
              });
        }
    }
});
//通过generator转换ast最后输出es5代码
console.log(generator(ats));

我们运行一下代码:

$ node ./babel-test/demo/demo1.js 

结果输出:

{ 
  code: 'const result = aa * b;\n\nconst result1 = function () {};',
  map: null,
  rawMappings: null 
}

可以看到,最终我们实现了把es6的箭头函数转换成es5的过程。

代码中我们可以看到:

//把ast对象通过traverse转换成es5代码
traverse(ats,{
    enter(path) {
        if (t.isIdentifier(path.node, { name: "a" })) {
          path.node.name = "aa";
        }
        if (path.isArrowFunctionExpression()){ //es6转换成es5
            path.arrowFunctionToExpression({
                // While other utils may be fine inserting other arrows to make more transforms possible,
                // the arrow transform itself absolutely cannot insert new arrow functions.
                allowInsertArrow: false,
                specCompliant: false,
              });
        }
    }
});

我们打开一个官方的插件babel-plugin-transform-arrow-functions:

import { declare } from "@babel/helper-plugin-utils";
import type NodePath from "@babel/traverse";

export default declare((api, options) => {
  api.assertVersion(7);

  const { spec } = options;
  return {
    name: "transform-arrow-functions",

    visitor: {
      ArrowFunctionExpression(
        path: NodePath<BabelNodeArrowFunctionExpression>,
      ) {
        // In some conversion cases, it may have already been converted to a function while this callback
        // was queued up.
        if (!path.isArrowFunctionExpression()) return;

        path.arrowFunctionToExpression({
          // While other utils may be fine inserting other arrows to make more transforms possible,
          // the arrow transform itself absolutely cannot insert new arrow functions.
          allowInsertArrow: false,
          specCompliant: !!spec,
        });
      },
    },
  };
});

哈哈!! 是不是很简单呢? 其实babel也就是把很多的一些插件组合起来最终实现代码的转换,好啦~ 接下来我们就围绕babel的一些常用的插件做解析了

未完待续~~

欢迎志同道合的小伙伴一起学习、一起进步

欢迎入群~~~~~~~

参考:

https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md#toc-check-if-a-node-is-a-certain-type

公開された128元の記事 ウォンの賞賛113 ビュー340 000 +

おすすめ

転載: blog.csdn.net/vv_bug/article/details/103823257