Babel认识与简单插件开发

公司内部分享会的一个内容,其中借鉴了@zxg_神说要有光 大佬的小册《Babel 插件通关秘籍》中的一些内容,感兴趣的可以去看看小册,强烈推荐!

一、Babel 是啥?

Babel (发音[ˈbæbəl])最开始叫 6to5,顾名思义是 es6 转 es5,但是后来随着 es 标准的演进,有了 es7、es8 等, 6to5 的名字已经不合适了,所以改名为了 babel。

Babel 是一个广泛使用的 JavaScript 编译器,它可以将最新版本的 JavaScript 代码转换为向后兼容的代码,以便在不支持最新特性的旧浏览器和环境中运行。Babel 是一个开源项目,可以通过 npm 安装并在 Node.js 或浏览器中使用。

编译是啥?

编译是一种将高级语言转换成低级语言的技术,它需要进行词法分析、语法分析、语义分析等过程,最终生成抽象语法树(AST),并通过转译器或者编译器将AST转换成目标代码的字符串,解释执行或者生成机器码。

编译的难点在于如何保证语义的等价转换以及进行各种编译优化。此外,分词过程中需要使用有限状态机(DFA)来处理最小的单词格式,而组装过程中需要使用LL或LR算法,根据一两个单词或者组装结果来决定应该往下看几个单词,以此组装出正确的AST。

二、Babel的作用与使用场景

Babel的主要作用是将最新的ECMAScript标准的代码转换成浏览器可以识别的旧版本JavaScript。使用场景包括:

  • 跨浏览器兼容性:让开发者能够使用最新的语法特性,而无需担心浏览器兼容性问题;

    • react、vue等前端框架中将最新的ECMAScript标准的代码(ES6/ES7等)转换为浏览器可识别的ES5代码
  • 语法扩展与代码转换:让开发者可以使用实验性的JavaScript特性,甚至可以创建自定义语法,开发一些转译工具

    • 函数插桩(函数中自动插入一些代码,例如埋点代码)、自动国际化
    • 开发类似Taro这种可转译多端的前端框架
  • 优化代码:通过插件,Babel可以帮助优化代码,提高性能。

    • 进行代码压缩、代码分离、代码优化(消除无用代码、重复代码,去除console打印)

Babel官方文档地址: Babel 是什么? · Babel 中文文档 | Babel中文网

三、Babel的核心原理

主要原理是将输入的 JavaScript 代码解析成一个 AST(抽象语法树)对象,然后对 AST 进行变换和操作,最后再将 AST 转换回 JavaScript 代码输出。

具体来说,Babel 的转换流程可以分为以下几个步骤:

  1. 解析(parse)

    1. 通过 parser 把源码转成抽象语法树(AST)
    2. 解析器将代码解析成一系列的语法节点,每个语法节点代表代码中的一个语法结构(如表达式、语句、函数等)。
  2. 转换(transform)

    1. 遍历 AST,调用各种 transform 插件对 AST 进行增删改
    2. 插件是一组函数,每个函数接收一个 AST 节点并返回一个新的 AST 节点。插件可以用来实现各种转换,例如将箭头函数转换为普通函数、将 ES6 模块转换为 CommonJS 模块等。
  3. 生成(generate)

    1. 把转换后的 AST 打印成目标代码,并生成 sourcemap

    2. 代码生成器会遍历 AST 节点并输出相应的 JavaScript 代码。

在这个过程中,Babel 还会处理一些其他的任务,例如代码压缩、语法检查等。

扩展:AST 是个啥?

JavaScript AST(抽象语法树)是一种表示 JavaScript 代码结构的树状数据结构。它将 JavaScript 代码的每个语法单元(如表达式、语句、函数等)表示为一个节点,并使用树形结构将它们组织起来。

例如,以下是一个简单的 JavaScript 代码片段的 AST 表示:

function add(a, b) {
return a + b;
}
function add(a, b) {
    return a + b;
}
function add(a, b) { return a + b; }

对应的 AST 如下所示:

Program
└── FunctionDeclaration
├── Identifier (name="add")
├── FunctionExpression
│ ├── Identifier (name="a")
│ ├── Identifier (name="b")
│ └── BinaryExpression
│ ├── Identifier (name="a")
│ ├── Identifier (name="b")
│ └── Operator ("+")
└── ReturnStatement
└── BinaryExpression
├── Identifier (name="a")
├── Identifier (name="b")
└── Operator ("+")
Program
    └── FunctionDeclaration
        ├── Identifier (name="add")
        ├── FunctionExpression
        │   ├── Identifier (name="a")
        │   ├── Identifier (name="b")
        │   └── BinaryExpression
        │       ├── Identifier (name="a")
        │       ├── Identifier (name="b")
        │       └── Operator ("+")
        └── ReturnStatement
            └── BinaryExpression
                ├── Identifier (name="a")
                ├── Identifier (name="b")
                └── Operator ("+")
Program └── FunctionDeclaration ├── Identifier (name="add") ├── FunctionExpression │ ├── Identifier (name="a") │ ├── Identifier (name="b") │ └── BinaryExpression │ ├── Identifier (name="a") │ ├── Identifier (name="b") │ └── Operator ("+") └── ReturnStatement └── BinaryExpression ├── Identifier (name="a") ├── Identifier (name="b") └── Operator ("+")

在这个 AST 中,顶层节点是Program,表示整个 JavaScript 程序。

它有一个子节点FunctionDeclaration,表示一个函数声明。

FunctionDeclaration节点本身有三个子节点:函数名Identifier节点、函数参数Identifier节点和函数体FunctionExpression节点。

函数体节点本身又有两个子节点:一个BinaryExpression节点,表示函数体中的计算逻辑,以及一个ReturnStatement节点,表示函数的返回语句。

重点工具: AST 在线可视化站点

AST(抽象语法树)格式有一些统一的规范。目前,JavaScript 社区普遍使用的是 ESTree 规范,该规范定义了 JavaScript AST 的基本结构和节点类型,是 JavaScript AST 格式的事实标准。

四、核心功能与在项目中的用法

以下是 Babel 的一些核心功能和用法演示:

  1. 安装 Babel

要安装 Babel,需要在命令行中运行以下命令:

npm install --save-dev @babel/core @babel/cli
npm install --save-dev @babel/core @babel/cli
npm install --save-dev @babel/core @babel/cli

这将安装 Babel 核心库和 CLI 命令行工具。安装完成后,可以使用 Babel 转换 JavaScript 代码。

  1. 配置 Babel

Babel 需要一个配置文件来确定希望转换哪些代码,以及如何进行转换。可以使用.babelrc文件或在package.json文件中添加 Babel 配置。以下是一个简单的.babelrc文件:

{
"presets": [
"@babel/preset-env"
]
}
{ 
    "presets": [
        "@babel/preset-env"
      ]

 }
{ "presets": [ "@babel/preset-env" ] }

这个配置告诉 Babel 使用@babel/preset-env预设来转换代码。这个预设可以根据目标浏览器和环境自动确定要使用的转换插件。预设可以理解成插件的集合。

  1. 转换代码

一旦设置了 Babel 配置文件,可以使用babel命令行工具将的 JavaScript 代码转换为向后兼容的代码。例如,要转换一个名为index.js的文件,可以运行以下命令:

npx babel index.js --out-file index-compiled.js
 npx babel index.js --out-file index-compiled.js
npx babel index.js --out-file index-compiled.js

这将使用 Babel 将index.js转换为向后兼容的代码,并将结果输出到index-compiled.js文件中。

  1. 使用 Babel 插件

Babel 还提供了许多插件,可以使用它们来执行转换。例如,如果想使用 ES6 的箭头函数语法,可以使用@babel/plugin-transform-arrow-functions插件。要安装此插件,可以运行以下命令:

npm install --save-dev @babel/plugin-transform-arrow-functions
npm install --save-dev @babel/plugin-transform-arrow-functions
npm install --save-dev @babel/plugin-transform-arrow-functions

然后,可以将插件添加到的 Babel 配置文件中:

{
"plugins": [
"@babel/plugin-transform-arrow-functions"
]
}
 {
  "plugins": [
        "@babel/plugin-transform-arrow-functions"
      ]

}
{ "plugins": [ "@babel/plugin-transform-arrow-functions" ] }

这将告诉 Babel 在转换代码时使用箭头函数插件。

总的来说,Babel 是一个非常有用的工具,可以帮助在现代 JavaScript 特性和向后兼容性之间找到平衡。它提供了许多插件和预设,使可以轻松地自定义的代码转换。

五、Babel 开发插件

  1. 安装 Babel 开发环境

在开始开发 Babel 插件之前,需要安装 Babel 的开发环境。可以通过运行以下命令来安装 Babel 的开发环境:

npm install --save-dev @babel/core @babel/cli @babel/parser @babel/traverse @babel/types
 npm install --save-dev @babel/core @babel/cli @babel/parser @babel/traverse @babel/types
npm install --save-dev @babel/core @babel/cli @babel/parser @babel/traverse @babel/types

这将安装 Babel 的核心库和一些辅助库,以便可以处理 JavaScript AST(抽象语法树)并生成转换代码。

  1. 创建一个新的 Babel 插件

创建一个新的 Babel 插件很简单。可以创建一个新的 JavaScript 文件,并导出一个函数,该函数将被 Babel 调用并传递 AST 节点和一些选项。例如:

module.exports = function consolePlugin() {
return {
visitor: {
// 插件的转换逻辑
}
};
};
module.exports = function consolePlugin() {
      return {
        visitor: {
          // 插件的转换逻辑
        }
      };
    };
module.exports = function consolePlugin() { return { visitor: { // 插件的转换逻辑 } }; };

在这个例子中,我们导出一个函数,该函数返回一个对象,该对象具有一个名为visitor的属性。该属性是一个对象,包含了一组访问器函数,这些函数将在处理 AST 时被调用。

  1. 实现插件的转换逻辑

在的插件中,需要实现访问器函数来处理 AST 节点并进行转换。访问器函数将接收以下参数:

  • path:表示 AST 节点的路径,可以用来访问节点的属性和子节点。
  • state:表示插件的状态,可以用来存储和共享数据。

例如,以下是一个访问器函数,它将将所有console.jr()语句替换console.log('自定义前缀/代码位置信息')

const types = require("@babel/types")
const consolePlugin = ({ prefix = "jr:", showFilename = false } = {}) => {
const t = types
return {
visitor: {
CallExpression(path,state) {
const { callee, arguments: args } = path.node
// 判断调用的是否是 console.jr 方法
if (
callee.type === "MemberExpression" &&
callee.object.name === "console" &&
callee.property.name === "jr"
) {
// 构造新的 console.log 方法调用
const newCallee = t.memberExpression(t.identifier("console"), t.identifier("log"))
// 构造位置信息
const loc = path.node.loc
const line = loc.start.line
const column = loc.start.column
const filename = loc.filename ? loc.filename + ":" : ""
// 添加自定义标识和位置信息
const message = `${prefix} ${showFilename ? filename + line + ":" + column : ""}`
const newArgs = [t.stringLiteral(message), ...args]
// 替换原始的调用
path.replaceWith(t.callExpression(newCallee, newArgs))
// 记录已经转换过的位置
if (state.opts && state.opts.transpiledPositions) {
state.opts.transpiledPositions.push({
filename,
line,
column,
})
}
}
},
},
}
}
module.exports = consolePlugin
const types = require("@babel/types")
const consolePlugin = ({ prefix = "jr:", showFilename = false } = {}) => {
  const t = types
  return {
    visitor: {
      CallExpression(path,state) {
        const { callee, arguments: args } = path.node

        // 判断调用的是否是 console.jr 方法
        if (
          callee.type === "MemberExpression" &&
          callee.object.name === "console" &&
          callee.property.name === "jr"
        ) {
          // 构造新的 console.log 方法调用
          const newCallee = t.memberExpression(t.identifier("console"), t.identifier("log"))

          // 构造位置信息
          const loc = path.node.loc
          const line = loc.start.line
          const column = loc.start.column
          const filename = loc.filename ? loc.filename + ":" : ""

          // 添加自定义标识和位置信息
          const message = `${prefix} ${showFilename ? filename + line + ":" + column : ""}`
          const newArgs = [t.stringLiteral(message), ...args]

          // 替换原始的调用
          path.replaceWith(t.callExpression(newCallee, newArgs))

          // 记录已经转换过的位置
          if (state.opts && state.opts.transpiledPositions) {
            state.opts.transpiledPositions.push({
              filename,
              line,
              column,
            })
          }
        }
      },
    },
  }
}
module.exports = consolePlugin
const types = require("@babel/types") const consolePlugin = ({ prefix = "jr:", showFilename = false } = {}) => { const t = types return { visitor: { CallExpression(path,state) { const { callee, arguments: args } = path.node // 判断调用的是否是 console.jr 方法 if ( callee.type === "MemberExpression" && callee.object.name === "console" && callee.property.name === "jr" ) { // 构造新的 console.log 方法调用 const newCallee = t.memberExpression(t.identifier("console"), t.identifier("log")) // 构造位置信息 const loc = path.node.loc const line = loc.start.line const column = loc.start.column const filename = loc.filename ? loc.filename + ":" : "" // 添加自定义标识和位置信息 const message = `${prefix} ${showFilename ? filename + line + ":" + column : ""}` const newArgs = [t.stringLiteral(message), ...args] // 替换原始的调用 path.replaceWith(t.callExpression(newCallee, newArgs)) // 记录已经转换过的位置 if (state.opts && state.opts.transpiledPositions) { state.opts.transpiledPositions.push({ filename, line, column, }) } } }, }, } } module.exports = consolePlugin

在这个例子中,我们实现了一个名为CallExpression的访问器函数,遍历 JavaScript 代码中的所有函数调用,当遇到 console.jr 方法时,就会将其转换为 console.log 方法,并在输出信息中添加自定义标识和位置信息。自定义标识由 prefix 参数指定,位置信息由 showFilename 参数控制是否显示文件名。

  1. 注册插件并进行测试

一旦实现了插件的转换逻辑,需要将其注册到 Babel 中。可以使用 Babel 的plugin方法来注册插件。例如:

// 将源代码转换成新的代码
const { code } = transformSync(sourceCode, {
plugins: [consolePlugin],
parserOpts: {
sourceType: "unambiguous",
},
});
// 将源代码转换成新的代码
const { code } = transformSync(sourceCode, {
  plugins: [consolePlugin],
  parserOpts: {
    sourceType: "unambiguous",
  },
});
// 将源代码转换成新的代码 const { code } = transformSync(sourceCode, { plugins: [consolePlugin], parserOpts: { sourceType: "unambiguous", }, });

在这个例子中,我们将consolePlugin插件注册到 Babel 中。然后,可以使用 Babel 来转换的 JavaScript 代码并测试的插件是否按预期工作。

总的来说,开发 Babel 插件是一个有趣且有用的任务,可以帮助扩展 Babel 并根据的需要自定义代码转换。

扩展

Vue 模版编译中的 AST 与 Babel 中的 AST 的区别

Vue 的 AST(抽象语法树)和 Babel 的 AST 在某些方面相似,但也有一些不同之处。

在 Vue 中,AST 是由 Vue 的模板编译器生成的,用于表示模板中的各种节点和语法。Vue 的 AST 包含了模板中的元素、属性、指令、事件等信息,以及它们的嵌套关系和父子关系。Vue 的 AST 还包含了一些特定的节点类型,例如 v-if、v-for、v-bind 等,这些节点类型是 Vue 特有的。

而在 Babel 中,AST 是由 Babel 编译器生成的,用于表示 JavaScript 代码中的各种节点和语法。Babel 的 AST 包含了 JavaScript 代码中的变量、函数、表达式、语句等信息,以及它们的嵌套关系和父子关系。Babel 的 AST 还包含了一些特定的节点类型,例如箭头函数、类声明、模板字符串等,这些节点类型是 ES6 和 ES7 的新语法。

此外,Babel 的 AST 还支持插件扩展和自定义,开发者可以编写自己的插件来扩展 Babel 的 AST 功能。Vue 的 AST 则没有这样的扩展机制。

总的来说,Vue 的 AST 和 Babel 的 AST 在某些方面相似,它们都是用于表示代码的抽象语法树。但在细节上有很大的不同,因为它们面向的是不同的语言和应用场景。

jsx 解析的 js 对象树和 vue 模版编译的 AST 的区别

JSX 解析的 JavaScript 对象树和 Vue 模板编译的 AST 在某些方面相似,但也有一些不同之处。

在 React 中,JSX 代码会被解析成一个 JavaScript 对象树,这个对象树通常称为虚拟 DOM(Virtual DOM)。虚拟 DOM 对象包含了 UI 元素的类型、属性、子元素等信息,以及它们的嵌套关系和父子关系。虚拟 DOM 对象通常由 React.createElement 函数创建。

而在 Vue 中,模板会被解析成 AST(抽象语法树),这个 AST 包含了模板中的元素、属性、指令、事件等信息,以及它们的嵌套关系和父子关系。Vue 的 AST 还包含了一些特定的节点类型,例如 v-if、v-for、v-bind 等,这些节点类型是 Vue 特有的。

从表面上看,虚拟 DOM 对象和 AST 都是用于表示 UI 元素的 JavaScript 对象树,它们都包含了元素的属性、子元素等信息,以及它们的嵌套关系和父子关系。但在细节上有很大的不同,因为它们面向的是不同的框架和应用场景。

虚拟 DOM 对象通常是 React 应用的核心,它是 React 通过比较新旧虚拟 DOM 对象来实现高效渲染的关键。而 Vue 的 AST 则是 Vue 编译器的核心,它是 Vue 将模板转换成渲染函数的关键。

总的来说,JSX 解析的 JavaScript 对象树和 Vue 模板编译的 AST 在某些方面相似,但在细节上还是有很大的不同。它们是不同框架和应用场景下的抽象语法树表示方式,主要用于实现框架的核心功能,例如高效渲染、组件化等。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYmvNdWi' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片