本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com
嘿,各位前端小伙伴们!今天咱们来聊聊一个既神秘又强大的东西——Babel 插件开发。
别被 "AST"、"代码转换" 这些高大上的词汇吓到,其实 Babel 插件开发就像是给代码做 "整容手术",让老旧的代码变得年轻时尚,让复杂的语法变得简单易懂。
什么是 Babel 插件?
简单来说,Babel 插件就是一个代码转换器。它能够:
• 语法转换:把 ES6 + 语法转换成 ES5
• API 填充:为新 API 添加 polyfill
• 代码优化:移除无用代码、压缩代码
• 功能增强:添加新的语言特性
• 代码分析:收集代码统计信息
你可以把 Babel 插件想象成一个 "代码翻译官",它能读懂各种 "方言"(新语法),然后翻译成所有浏览器都能理解的 "普通话"(标准 JavaScript)。
// 输入代码(ES6箭头函数)const add = (a, b) => a + b;// 经过Babel插件转换后var add = function add(a, b) { return a + b;};看到了吗?箭头函数被转换成了普通函数,这就是 Babel 插件的魔法!
为什么需要 Babel 插件?
你可能会问:"为什么不直接写兼容性好的代码呢?"
这个问题问得好!让我们来看看几个现实场景:
1. 浏览器兼容性
// 你想写的现代代码const users = await fetch('/api/users').then(res => res.json());const activeUsers = users.filter(user => user.isActive);// 但IE11需要这样写var users;fetch('/api/users') .then(function(res) { return res.json(); }) .then(function(data) { users = data; var activeUsers = users.filter(function(user) { return user.isActive; }); });天哪!这谁受得了?有了 Babel 插件,你可以愉快地写现代代码,让插件帮你处理兼容性问题。
2. 新特性提前体验
// 使用实验性的装饰器语法@componentclass MyComponent { @observable count = 0; @action increment() { this.count++; }}通过 Babel 插件,你可以提前使用还在提案阶段的 JavaScript 新特性。
3. 代码优化
// 开发时的代码if (process.env.NODE_ENV === 'development') { console.log('Debug info:', data);}// 生产环境自动移除// (代码被完全删除,不会出现在最终bundle中)4. 框架特定转换
// JSX语法const element = <h1>Hello, {name}!</h1>;// 转换后const element = React.createElement('h1', null, 'Hello, ', name, '!');AST 抽象语法树详解
在深入插件开发之前,我们必须先理解 AST(Abstract Syntax Tree,抽象语法树)。
什么是 AST?
AST 就是代码的 "骨架结构",它把代码解析成一个树形数据结构,每个节点代表代码中的一个语法元素。
// 原始代码const name = 'World';// 对应的AST结构(简化版){"type": "VariableDeclaration","kind": "const","declarations": [{ "type": "VariableDeclarator", "id": { "type": "Identifier", "name": "name" }, "init": { "type": "StringLiteral", "value": "World" } }]}AST 节点类型
Babel 使用的 AST 规范主要基于 ESTree[1],常见的节点类型包括:
// 标识符"Identifier": { name: "variableName" }// 字面量"StringLiteral": { value: "hello" }"NumericLiteral": { value: 42 }"BooleanLiteral": { value: true }// 表达式"BinaryExpression": { left: Node, operator: "+", right: Node }"CallExpression": { callee: Node, arguments: [Node] }"MemberExpression": { object: Node, property: Node }// 语句"ExpressionStatement": { expression: Node }"IfStatement": { test: Node, consequent: Node, alternate: Node }"FunctionDeclaration": { id: Node, params: [Node], body: Node }// 声明"VariableDeclaration": { kind: "const|let|var", declarations: [Node] }"ImportDeclaration": { specifiers: [Node], source: Node }在线 AST 查看器
强烈推荐使用 AST Explorer[2] 来查看代码的 AST 结构:
- 选择 "@babel/parser" 作为解析器
- 输入你的代码
- 右侧会显示对应的 AST 结构
这是学习 AST 最直观的方式!
Babel 的工作原理
Babel 的工作流程可以分为三个阶段:
1. 解析(Parse)
// 代码字符串const code = 'const name = "World";';// 解析成ASTconst ast = babel.parse(code);这个阶段会进行:
• 词法分析:把代码分解成 token(词法单元)
• 语法分析:把 token 组装成 AST
2. 转换(Transform)
// 遍历AST并应用插件const transformedAst = babel.transformFromAst(ast, code, { plugins: [myPlugin]});这是插件发挥作用的阶段:
• 遍历 AST:使用 Visitor 模式访问每个节点
• 应用转换:插件修改、添加或删除节点
3. 生成(Generate)
// 把AST转换回代码字符串const result = babel.generate(transformedAst);console.log(result.code); // 转换后的代码这个阶段会:
• 遍历 AST:深度优先遍历所有节点
• 生成代码:根据节点类型生成对应的代码字符串
• 生成 Source Map:保持代码映射关系
完整流程图
源代码 → [Parse] → AST → [Transform] → 新AST → [Generate] → 目标代码 ↑ ↑ ↑ 词法分析 插件处理 代码生成 语法分析 Visitor遍历 Source MapBabel 插件是什么
插件的基本结构
一个 Babel 插件就是一个函数,它返回一个包含visitor对象的配置:
// 最简单的插件结构functionmyPlugin() {return { visitor: { // 访问器方法 } };}// 或者使用箭头函数constmyPlugin = () => ({visitor: { // 访问器方法 }});插件参数
插件函数可以接收两个参数:
function myPlugin(babel, options) { const { types: t } = babel; return { visitor: { // 使用 t 来操作AST节点 // 使用 options 来获取插件配置 } };}• babel:包含 Babel 的各种工具函数
•
types:用于创建和检查 AST 节点的工具库•
template:用于创建 AST 模板的工具•
traverse:用于遍历 AST 的工具• options:插件的配置选项
使用插件
// babel.config.jsmodule.exports = {plugins: [ // 使用npm包 '@babel/plugin-transform-arrow-functions', // 使用本地插件 './my-plugin.js', // 带配置的插件 ['./my-plugin.js', { option1: 'value1', option2: 'value2' }] ]};Visitor 模式深度解析
Visitor 模式是 Babel 插件的核心,它让我们能够优雅地遍历和修改 AST。
基本概念
const plugin = () => ({ visitor: { // 访问所有的Identifier节点 Identifier(path) { console.log('找到标识符:', path.node.name); }, // 访问所有的函数声明 FunctionDeclaration(path) { console.log('找到函数:', path.node.id.name); } }});Path 对象
path不是 AST 节点本身,而是包含节点信息的包装对象:
visitor: {Identifier(path) { // 当前节点 console.log(path.node); // AST节点 // 父节点信息 console.log(path.parent); // 父AST节点 console.log(path.parentPath); // 父Path对象 // 作用域信息 console.log(path.scope); // 作用域对象 // 节点操作方法 path.remove(); // 删除节点 path.replaceWith(newNode); // 替换节点 path.insertBefore(newNode); // 在前面插入 path.insertAfter(newNode); // 在后面插入 }}访问器方法的类型
visitor: {// 进入节点时调用FunctionDeclaration: { enter(path) { console.log('进入函数声明'); }, exit(path) { console.log('离开函数声明'); } },// 简写形式(等同于enter)Identifier(path) { console.log('访问标识符'); },// 访问多种节点类型"FunctionDeclaration|ArrowFunctionExpression"(path) { console.log('访问任意类型的函数'); }}条件访问
visitor: {// 只访问特定条件的节点CallExpression(path) { // 只处理console.log调用 if (path.get('callee').matchesPattern('console.log')) { // 处理逻辑 } },// 使用路径匹配"Program > BlockStatement > ExpressionStatement"(path) { // 只访问程序顶层块语句中的表达式语句 }}手写第一个 Babel 插件
让我们从一个简单的例子开始:把所有的console.log替换成console.warn。
分析需求
// 输入console.log('Hello');console.log('World', 123);// 期望输出console.warn('Hello');console.warn('World', 123);查看 AST 结构
首先,我们需要了解console.log在 AST 中的结构:
// console.log('Hello') 的AST结构{"type": "CallExpression","callee": { "type": "MemberExpression", "object": { "type": "Identifier", "name": "console" }, "property": { "type": "Identifier", "name": "log" } },"arguments": [{ "type": "StringLiteral", "value": "Hello" }]}编写插件
// console-log-to-warn.jsfunctionconsoleLogToWarn() {return { visitor: { CallExpression(path) { const { node } = path; // 检查是否是console.log调用 if ( node.callee.type === 'MemberExpression' && node.callee.object.name === 'console' && node.callee.property.name === 'log' ) { // 修改属性名从'log'改为'warn' node.callee.property.name = 'warn'; } } } };}module.exports = consoleLogToWarn;使用 types 工具简化代码
function consoleLogToWarn({ types: t }) {return { visitor: { CallExpression(path) { // 使用t.isMemberExpression等工具函数 if ( t.isMemberExpression(path.node.callee) && t.isIdentifier(path.node.callee.object, { name: 'console' }) && t.isIdentifier(path.node.callee.property, { name: 'log' }) ) { // 使用t.identifier创建新节点 path.node.callee.property = t.identifier('warn'); } } } };}更优雅的写法
function consoleLogToWarn({ types: t }) { return { visitor: { CallExpression(path) { // 使用matchesPattern方法 if (path.get('callee').matchesPattern('console.log')) { path.node.callee.property.name = 'warn'; } } } };}测试插件
// test.jsconst babel = require('@babel/core');const plugin = require('./console-log-to-warn');const code = `console.log('Hello');console.error('Error'); // 不应该被修改console.log('World', 123);`;const result = babel.transform(code, { plugins: [plugin]});console.log(result.code);// 输出:// console.warn('Hello');// console.error('Error');// console.warn('World', 123);常用 API 和工具函数
types 工具库
@babel/types是操作 AST 节点的核心工具库:
const t = require('@babel/types');// 创建节点const identifier = t.identifier('myVar');const stringLiteral = t.stringLiteral('hello');const binaryExpression = t.binaryExpression('+', left, right);// 检查节点类型t.isIdentifier(node); // 是否是标识符t.isStringLiteral(node); // 是否是字符串字面量t.isFunctionDeclaration(node); // 是否是函数声明// 带条件的检查t.isIdentifier(node, { name: 'myVar' }); // 是否是名为'myVar'的标识符t.isBinaryExpression(node, { operator: '+' }); // 是否是加法表达式// 创建复杂节点const functionDeclaration = t.functionDeclaration( t.identifier('myFunc'), // 函数名 [t.identifier('param1'), t.identifier('param2')], // 参数 t.blockStatement([ // 函数体 t.returnStatement(t.identifier('param1')) ]));Path 对象的常用方法
visitor: {Identifier(path) { // 节点操作 path.remove(); // 删除当前节点 path.replaceWith(newNode); // 替换当前节点 path.replaceWithMultiple([node1, node2]); // 替换为多个节点 // 插入操作 path.insertBefore(newNode); // 在当前节点前插入 path.insertAfter(newNode); // 在当前节点后插入 // 遍历操作 path.traverse({ Identifier(innerPath) { // 遍历当前节点的子节点 } }); // 查找操作 path.findParent(parent => t.isFunctionDeclaration(parent.node)); path.find(ancestor => t.isProgram(ancestor.node)); // 作用域操作 path.scope.hasBinding('myVar'); // 检查变量是否在作用域中 path.scope.getBinding('myVar'); // 获取变量绑定信息 // 获取子路径 path.get('object'); // 获取object属性的路径 path.get('arguments.0'); // 获取第一个参数的路径 }}template 工具
@babel/template可以让我们用模板字符串创建 AST:
const template = require('@babel/template').default;// 创建语句模板const buildRequire = template(` var %%importName%% = require(%%source%%);`);const ast = buildRequire({importName: t.identifier('myModule'),source: t.stringLiteral('./my-module')});// 创建表达式模板const buildBinaryExpression = template.expression(`LEFT + RIGHT`);const ast2 = buildBinaryExpression({LEFT: t.identifier('a'),RIGHT: t.identifier('b')});// 在插件中使用functionmyPlugin({ types: t, template }) {const buildLogger = template(` console.log("Function %%name%% called with args:", arguments); `);return { visitor: { FunctionDeclaration(path) { const logStatement = buildLogger({ name: t.stringLiteral(path.node.id.name) }); path.node.body.body.unshift(logStatement); } } };}作用域(Scope)操作
visitor: {FunctionDeclaration(path) { const scope = path.scope; // 检查变量绑定 if (scope.hasBinding('myVar')) { console.log('myVar在当前作用域中'); } // 获取绑定信息 const binding = scope.getBinding('myVar'); if (binding) { console.log('变量类型:', binding.kind); // 'var', 'let', 'const', 'param' console.log('引用次数:', binding.references); console.log('定义位置:', binding.path.node); } // 生成唯一标识符 const uniqueId = scope.generateUidIdentifier('temp'); console.log(uniqueId.name); // 'temp', '_temp', '_temp2' 等 // 重命名绑定 scope.rename('oldName', 'newName'); }}高级插件开发技巧
1. 状态管理
有时候我们需要在插件执行过程中保存一些状态:
function myPlugin() {return { visitor: { Program: { enter(path, state) { // 初始化状态 state.functionCount = 0; state.imports = newSet(); }, exit(path, state) { // 在程序结束时输出统计信息 console.log(`找到 ${state.functionCount} 个函数`); console.log(`导入模块:`, Array.from(state.imports)); } }, FunctionDeclaration(path, state) { state.functionCount++; }, ImportDeclaration(path, state) { state.imports.add(path.node.source.value); } } };}2. 插件选项处理
function myPlugin({ types: t }, options = {}) {// 设置默认选项const { logLevel = 'info', prefix = 'LOG:', exclude = [] } = options;return { visitor: { CallExpression(path) { if (path.get('callee').matchesPattern('console.log')) { // 检查是否在排除列表中 const filename = this.file.opts.filename; if (exclude.some(pattern => filename.includes(pattern))) { return; } // 根据配置修改日志级别 if (logLevel === 'warn') { path.node.callee.property.name = 'warn'; } // 添加前缀 if (prefix && path.node.arguments.length > 0) { const firstArg = path.node.arguments[0]; if (t.isStringLiteral(firstArg)) { firstArg.value = prefix + ' ' + firstArg.value; } } } } } };}3. 条件转换
function conditionalTransform({ types: t }) {return { visitor: { CallExpression(path) { // 只在生产环境移除console.log if (process.env.NODE_ENV === 'production') { if (path.get('callee').matchesPattern('console.log')) { path.remove(); } } }, IfStatement(path) { // 移除永远不会执行的if语句 const test = path.node.test; if (t.isBooleanLiteral(test) && test.value === false) { path.remove(); } // 简化永远为真的if语句 elseif (t.isBooleanLiteral(test) && test.value === true) { path.replaceWithMultiple(path.node.consequent.body); } } } };}4. 递归处理
function deepTransform({ types: t }) {return { visitor: { ObjectExpression(path) { // 递归处理嵌套对象 functionprocessObject(objPath) { objPath.node.properties.forEach(prop => { if (t.isObjectProperty(prop) && t.isObjectExpression(prop.value)) { // 递归处理嵌套对象 processObject(objPath.get(`properties.${objPath.node.properties.indexOf(prop)}.value`)); } }); } processObject(path); } } };}5. 错误处理
function safeTransform({ types: t }) {return { visitor: { CallExpression(path) { try { // 可能出错的转换逻辑 if (path.get('callee').matchesPattern('someFunction')) { // 转换逻辑 } } catch (error) { // 记录错误但不中断编译 console.warn(`转换失败: ${error.message}`); console.warn(`位置: ${path.node.loc?.start.line}:${path.node.loc?.start.column}`); } } } };}实战案例:代码优化插件
让我们开发一个实用的插件:自动优化代码中的性能问题。
需求分析
我们的插件要解决以下问题:
1. 移除生产环境的 console 语句
2. 优化字符串拼接:
'a' + 'b'→'ab'3. 移除无用的变量声明
4. 简化布尔表达式:
!!true→true
完整插件代码
// babel-plugin-optimize-code.jsfunctionoptimizeCodePlugin({ types: t }, options = {}) {const { removeConsole = process.env.NODE_ENV === 'production', optimizeStrings = true, removeUnusedVars = true, simplifyBooleans = true } = options;return { name: 'optimize-code', visitor: { // 1. 移除console语句 CallExpression(path) { if (!removeConsole) return; if ( t.isMemberExpression(path.node.callee) && t.isIdentifier(path.node.callee.object, { name: 'console' }) ) { // 如果console调用是表达式语句,直接移除 if (t.isExpressionStatement(path.parent)) { path.parentPath.remove(); } else { // 否则替换为undefined path.replaceWith(t.identifier('undefined')); } } }, // 2. 优化字符串拼接 BinaryExpression(path) { if (!optimizeStrings) return; const { node } = path; if ( node.operator === '+' && t.isStringLiteral(node.left) && t.isStringLiteral(node.right) ) { // 合并字符串字面量 path.replaceWith( t.stringLiteral(node.left.value + node.right.value) ); } }, // 3. 移除无用的变量声明 VariableDeclarator(path) { if (!removeUnusedVars) return; const binding = path.scope.getBinding(path.node.id.name); if (binding && binding.references === 0) { // 如果变量没有被引用,移除声明 if (path.parent.declarations.length === 1) { // 如果是唯一的声明,移除整个声明语句 path.parentPath.remove(); } else { // 否则只移除这个声明 path.remove(); } } }, // 4. 简化布尔表达式 UnaryExpression(path) { if (!simplifyBooleans) return; const { node } = path; if (node.operator === '!') { // 简化双重否定: !!x → Boolean(x) if ( t.isUnaryExpression(node.argument) && node.argument.operator === '!' ) { path.replaceWith( t.callExpression( t.identifier('Boolean'), [node.argument.argument] ) ); } // 简化字面量否定: !true → false elseif (t.isBooleanLiteral(node.argument)) { path.replaceWith( t.booleanLiteral(!node.argument.value) ); } } }, // 5. 优化条件表达式 ConditionalExpression(path) { const { test, consequent, alternate } = path.node; // 如果条件是字面量,直接返回对应分支 if (t.isBooleanLiteral(test)) { path.replaceWith(test.value ? consequent : alternate); } // 优化相同结果的条件表达式: condition ? x : x → x elseif (t.isIdentifier(consequent) && t.isIdentifier(alternate)) { if (consequent.name === alternate.name) { path.replaceWith(consequent); } } } } };}module.exports = optimizeCodePlugin;使用示例
// babel.config.jsmodule.exports = { plugins: [ ['./babel-plugin-optimize-code', { removeConsole: true, optimizeStrings: true, removeUnusedVars: true, simplifyBooleans: true }] ]};测试效果
// 输入代码const unusedVar = 'not used';const message = 'Hello' + ' ' + 'World';console.log('Debug info');const result = !!true ? 'yes' : 'no';const same = condition ? value : value;// 输出代码(优化后)const message = 'Hello World';const result = 'yes';const same = value;调试与测试
1. 调试技巧
function debugPlugin({ types: t }) {return { visitor: { CallExpression(path) { // 输出节点信息 console.log('节点类型:', path.node.type); console.log('节点位置:', path.node.loc); console.log('父节点:', path.parent.type); // 输出完整的AST结构 console.log('AST:', JSON.stringify(path.node, null, 2)); // 输出作用域信息 console.log('作用域绑定:', Object.keys(path.scope.bindings)); } } };}2. 单元测试
// test/plugin.test.jsconst babel = require('@babel/core');const plugin = require('../src/my-plugin');functiontransform(code, options = {}) {return babel.transform(code, { plugins: [[plugin, options]] }).code;}describe('My Babel Plugin', () => {test('should transform console.log to console.warn', () => { const input = `console.log('hello');`; const output = transform(input); expect(output).toBe(`console.warn('hello');`); });test('should not transform console.error', () => { const input = `console.error('error');`; const output = transform(input); expect(output).toBe(`console.error('error');`); });test('should handle plugin options', () => { const input = `console.log('hello');`; const output = transform(input, { disable: true }); expect(output).toBe(`console.log('hello');`); });});3. 快照测试
// test/snapshots.test.jsconst babel = require('@babel/core');const plugin = require('../src/my-plugin');const fs = require('fs');const path = require('path');describe('Plugin Snapshots', () => {const fixturesDir = path.join(__dirname, 'fixtures');const fixtures = fs.readdirSync(fixturesDir); fixtures.forEach(fixture => { test(`should transform ${fixture}`, () => { const input = fs.readFileSync( path.join(fixturesDir, fixture, 'input.js'), 'utf8' ); const output = babel.transform(input, { plugins: [plugin] }).code; expect(output).toMatchSnapshot(); }); });});4. 性能测试
// test/performance.test.jsconst babel = require('@babel/core');const plugin = require('../src/my-plugin');test('plugin performance', () => {const largeCode = ` ${'console.log("test");'.repeat(10000)} `;const start = Date.now(); babel.transform(largeCode, { plugins: [plugin] });const end = Date.now();console.log(`转换耗时: ${end - start}ms`);expect(end - start).toBeLessThan(1000); // 应该在1秒内完成});生态系统与最佳实践
1. 常用的 Babel 插件
// babel.config.js - 一个典型的配置module.exports = {presets: [ ['@babel/preset-env', { targets: { browsers: ['> 1%', 'last 2 versions'] }, useBuiltIns: 'usage', corejs: 3 }], '@babel/preset-react', '@babel/preset-typescript' ],plugins: [ // 语法转换 '@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-optional-chaining', '@babel/plugin-proposal-nullish-coalescing-operator', // 开发工具 '@babel/plugin-transform-runtime', 'babel-plugin-import', // 按需导入 // 优化插件 ['babel-plugin-transform-remove-console', { exclude: ['error', 'warn'] }], // 自定义插件 './plugins/my-custom-plugin' ],env: { development: { plugins: [ 'react-hot-loader/babel' ] }, production: { plugins: [ 'babel-plugin-transform-remove-console' ] } }};2. 插件开发最佳实践
命名规范
// 好的命名babel-plugin-transform-arrow-functionsbabel-plugin-syntax-jsxbabel-plugin-proposal-class-properties// 不好的命名my-babel-pluginbabel-stufftransformer插件结构
// 推荐的插件结构my-babel-plugin/├── src/│ ├── index.js // 主插件文件│ ├── utils.js // 工具函数│ └── visitors/ // 访问器模块│ ├── expressions.js│ └── statements.js├── test/│ ├── fixtures/ // 测试用例│ └── index.test.js├── package.json├── README.md└── .babelrc // 插件自身的Babel配置错误处理
function myPlugin({ types: t }) {return { visitor: { CallExpression(path) { try { // 转换逻辑 } catch (error) { // 提供有用的错误信息 throw path.buildCodeFrameError( `转换失败: ${error.message}`, error ); } } } };}性能优化
function optimizedPlugin({ types: t }) {return { visitor: { // 使用具体的访问器而不是通用的 CallExpression(path) { // 早期返回,避免不必要的处理 if (!t.isMemberExpression(path.node.callee)) { return; } // 缓存重复计算的结果 const callee = path.get('callee'); if (callee.matchesPattern('console.log')) { // 处理逻辑 } } } };}总结
通过这篇文章,我们深入了解了 Babel 插件开发的方方面面:
🎯 核心概念
• AST(抽象语法树):代码的结构化表示
• Visitor 模式:遍历和修改 AST 的核心机制
• Path 对象:包含节点信息和操作方法的包装器
• Scope 作用域:管理变量绑定和作用域链
🛠️ 开发技能
• 插件基础结构:理解插件的基本组成
• 常用 API:掌握
@babel/types、@babel/template等工具• 高级技巧:状态管理、条件转换、错误处理
• 性能优化:避免不必要的遍历和计算
📦 实战经验
• 需求分析:从代码转换需求到 AST 操作的思路转换
• 调试技巧:使用 AST Explorer 和调试工具
• 测试策略:单元测试、快照测试、性能测试
• 最佳实践:命名规范、错误处理、插件发布
🚀 进阶方向
如果你想继续深入,可以探索:
1. 复杂转换:学习更复杂的代码转换模式
2. 性能优化:研究大型代码库的转换性能
3. 工具链集成:与 Webpack、Rollup 等工具的集成
4. 语言扩展:为 JavaScript 添加新的语法特性
💡 最后的建议
• 多实践:理论再好也要动手写代码
• 读源码:学习优秀插件的实现方式
• 关注社区:跟上 Babel 和 JavaScript 的发展
• 分享交流:把你的插件分享给社区
记住,Babel 插件开发不仅仅是技术活,更是一种思维方式——如何用程序来理解和改造程序。这种能力在现代前端开发中越来越重要,无论是构建工具、代码分析还是自动化重构,都离不开 AST 操作的基础。
希望这篇文章能帮你打开 Babel 插件开发的大门,让你在前端工程化的道路上走得更远!
如果你觉得这篇文章有帮助,别忘了点赞和分享哦!有问题欢迎在评论区讨论~
引用链接
[1] ESTree: _https://github.com/estree/estree_[2] AST Explorer: _https://astexplorer.net/_