本文会介绍比较基本的编译知识和babel-loader运作原理

babel-loader 是什么?

作为老一派的打包工具, babel-loader 想必大家已经非常熟悉了.它长这样子

// webpack.config.js
module.exports = {
// ...其他配置
module: {
rules: [
{
test: /\.js$/, // 匹配所有 .js 文件
exclude: /node_modules/, // 排除 node_modules 目录
use: 'babel-loader' // 使用 babel-loader 处理
}
]
}
};

babel-loader 如何工作

babel-loader 其实是会去调用 Babel 的核心库 @babel/core 来处理接收到的代码。Babel 首先使用解析器(如 @babel/parser)将 JavaScript 代码解析成抽象语法树(AST)。

AST 这里不做深入探讨, 简单的说AST 就是把代码中的每个语法元素(像变量声明、函数定义、表达式等)抽象成一种树状的数据结构,方便后续对代码进行分析和转换。

例如,对于代码 const message = 'Hello, World!';会被解析成包含 VariableDeclaration、VariableDeclarator、Identifier 和 NumericLiteral 等节点的 AST。

// 源代码
const message = 'Hello, World!';
// 对应的部分 AST 结构
{
"type": "VariableDeclaration",
"kind": "const",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "message"
},
"init": {
"type": "StringLiteral",
"value": "Hello, World!"
}
}
]
}

AST 在线查看, 可以通过这个网址在线查看源码与AST的关系

Babel 通过插件(plugin)和预设(preset)来对 AST 节点进行增删改查, 这个预设可以理解成babel预制的插件, 本质上同插件无异; 比如: 使用 @babel/preset-env 预设可以将 ES6+ 代码转换为向后兼容的 JavaScript 代码,以适配不同的浏览器和环境。

这个对AST节点的修改过程也非常类似与对dom树节点修改的过程, 插件也是 Babel 转换的核心,所有真实的修改过程都发生在这里.

经过插件转换后,Babel 使用代码生成器(@babel/generator)将转换后的 AST 重新生成 JavaScript 代码。这个新生成的代码就是经过转换后的最终代码

基本流程如下:

  1. -> @babel/core

    接收处理文件
  2. -> @beble/parser

    将源文件转换成AST
  3. -> 插件翻译/修改AST

    通过插件对AST进行增删改查
  4. -> @babel/generator

    将修改后的AST在翻译为源码

可以看到 babel-loader 的工作流程其实就是帮助我们调用了babel核心库,

其实不依赖 webpack, babel也可以完成翻译任务,只不过我们要手动调整文件输入输出过程, 而webpack+babel-loader帮我们省略了这一套繁琐的过程.

更具体的可以参考 babel使用指南

什么是babel-loader插件? babel-loader插件可以干什么?

综上所述,理解了babel-loader的工作流程,这两个问题也就很好回答了,

babel-loader插件是一个针对AST节点开放入口,你可以非常便捷的在这里对AST节点进行增删改查, 它可以让我们聚焦文件编译时的核心工作,跳过其他许多繁琐的步骤.

用好babel-loader插件可以让我们更为轻松的去完成众多文件编译任务.

如何制作一个babel-loader插件?

babel插件的api很多, 刚开始接触也会觉得比较抽象,但是babel插件最核心的api其实只有三个

vistor 访问器

它定义了如何访问和修改 AST 节点, 访问器有点类似于webpack中的module.rules配置, 比如在webpack中配置了 { test: /.ts/} 那么编译器只会检查.ts文件, vistor访问器也一样, 你配置了 VariableDeclaration 访问器,所有的变量声明语句会进入这里, 配置了FunctionDeclaration 访问器,所有函数声明会进入这里

例如: function vistor(){}

 visitor : {
FunctionDeclaration(path) {
// 这里的name就是 vistor
const functionName = path.node.id.name;
console.log('访问到函数声明:', functionName);
},
};

以下是比较常见的vistor:

Identifier

作用:表示变量名、函数名、属性名等标识符。可以用于重命名变量、检查特定标识符等操作。

const visitor = {
Identifier(path) {
if (path.node.name === 'oldVariable') {
path.node.name = 'newVariable';
}
}
};

Literal

作用:表示各种字面量,如字符串、数字、布尔值、null 等。可以用于修改字面量的值。

const visitor = {
Literal(path) {
if (typeof path.node.value === 'string') {
path.node.value = path.node.value.toUpperCase();
}
}
};

VariableDeclaration

作用:表示变量声明语句,如 var、let 和 const 声明。可以用于修改变量声明的类型、添加或删除变量声明。

示例:

const visitor = {
VariableDeclaration(path) {
if (path.node.kind === 'var') {
path.node.kind = 'let';
}
}
};

FunctionDeclaration

作用:表示函数声明语句。可以用于修改函数名、参数、函数体等。

示例:

const visitor = {
FunctionDeclaration(path) {
const newFunctionName = path.scope.generateUidIdentifier('newFunction');
path.node.id = newFunctionName;
}
};

BinaryExpression

作用:表示二元表达式,如 a + b、a * b 等。可以用于修改操作符或操作数。

示例:

const visitor = {
BinaryExpression(path) {
if (path.node.operator === '+') {
path.node.operator = '-';
}
}
};

CallExpression

作用:表示函数调用表达式,如 func()、obj.method() 等。可以用于修改调用的函数名、参数等。

示例:

const visitor = {
CallExpression(path) {
if (path.node.callee.name === 'oldFunction') {
path.node.callee.name = 'newFunction';
}
}
};

IfStatement

作用:表示 if 语句。可以用于修改条件表达式、if 块或 else 块的内容。

示例:

const visitor = {
IfStatement(path) {
const newTest = t.booleanLiteral(true);
path.node.test = newTest;
}
};

ReturnStatement

作用:表示 return 语句。可以用于修改返回值。

示例:

const visitor = {
ReturnStatement(path) {
const newReturnValue = t.numericLiteral(0);
path.node.argument = newReturnValue;
}
};

path

path是一个很重要的api了, 它用来检查节点路径信息和获取AST node信息等..., 这个可以理解相当于web开发中的window对象

1. 访问节点信息

  • 获取节点本身:通过 path.node 可以直接访问当前遍历到的 AST 节点,进而获取节点的各种属性。
module.exports = function(babel) {
const { types: t } = babel;
return {
visitor: {
Identifier(path) {
const node = path.node;
console.log('Identifier 节点名称:', node.name);
}
}
};
};
  • 获取父节点:使用 path.parent 可以获取当前节点的父节点,这在需要根据父节点信息来处理当前节点时非常有用。
module.exports = function(babel) {
const { types: t } = babel;
return {
visitor: {
Identifier(path) {
const parentNode = path.parent;
if (t.isVariableDeclarator(parentNode)) {
console.log('当前 Identifier 是变量声明的一部分');
}
}
}
};
};

2. 节点操作

  • 替换节点:path.replaceWith(newNode) 方法用于用一个新的节点替换当前节点。
module.exports = function(babel) {
const { types: t } = babel;
return {
visitor: {
Identifier(path) {
if (path.node.name === 'oldName') {
const newNode = t.identifier('newName');
path.replaceWith(newNode);
}
}
}
};
};
  • 删除节点:path.remove() 方法可用于从 AST 中移除当前节点。
module.exports = function(babel) {
const { types: t } = babel;
return {
visitor: {
// 假设要移除所有 console.log 调用
CallExpression(path) {
const callee = path.node.callee;
if (t.isMemberExpression(callee) &&
t.isIdentifier(callee.object, { name: 'console' }) &&
t.isIdentifier(callee.property, { name: 'log' })
) {
path.remove();
}
}
}
};
};

3. 遍历控制

  • 继续遍历子节点:path.traverse(visitor) 方法允许在当前节点的子树中继续遍历,使用自定义的访问器对象。
module.exports = function(babel) {
const { types: t } = babel;
return {
visitor: {
FunctionDeclaration(path) {
const customVisitor = {
Identifier(subPath) {
console.log('Function 内部的 Identifier:', subPath.node.name);
}
};
path.traverse(customVisitor);
}
}
};
};
  • 跳过子节点遍历:path.skip() 方法可以跳过当前节点的子节点遍历,直接进入下一个兄弟节点。
module.exports = function(babel) {
const { types: t } = babel;
return {
visitor: {
ObjectExpression(path) {
// 跳过 ObjectExpression 节点的子节点遍历
path.skip();
}
}
};
};

4. 作用域管理

  • 获取作用域:path.scope 可以获取当前节点所在的作用域,作用域对象提供了许多方法用于管理变量和标识符。
module.exports = function(babel) {
const { types: t } = babel;
return {
visitor: {
Identifier(path) {
const scope = path.scope;
const binding = scope.getBinding(path.node.name);
if (binding) {
console.log('变量绑定信息:', binding);
}
}
}
};
};
  • 创建新的标识符:path.scope.generateUidIdentifier(name) 方法用于生成一个唯一的标识符,避免命名冲突。
module.exports = function(babel) {
const { types: t } = babel;
return {
visitor: {
FunctionDeclaration(path) {
const newId = path.scope.generateUidIdentifier('newFunction');
path.node.id = newId;
}
}
};
};

types

它主要用于创建、验证和操作抽象语法树(AST)节点, 也是使用非常频繁的api,相当于web开发中的document对象

创建 AST 节点

  • 创建标识符节点:使用 t.identifier(name) 可以创建一个标识符节点,用于表示变量名、函数名等。
const babel = require('@babel/core');
const t = babel.types; // 创建一个名为 'message' 的标识符节点
const identifier = t.identifier('message');
  • 创建函数声明节点:t.functionDeclaration(id, params, body) 可用于创建函数声明节点,其中 id 是函数名,params 是参数数组,body 是函数体。
const id = t.identifier('add');
const param1 = t.identifier('a');
const param2 = t.identifier('b');
const params = [param1, param2];
const body = t.blockStatement([
t.returnStatement(
t.binaryExpression('+', param1, param2)
)
]);
const functionDeclaration = t.functionDeclaration(id, params, body);

验证 AST 节点类型

  • 判断节点是否为标识符:t.isIdentifier(node) 用于判断一个节点是否为标识符节点。
const babel = require('@babel/core');
const t = babel.types; const node = t.identifier('test');
if (t.isIdentifier(node)) {
console.log('这是一个标识符节点');
}
  • 判断节点是否为函数声明:t.isFunctionDeclaration(node) 可判断一个节点是否为函数声明节点。
const id = t.identifier('multiply');
const param1 = t.identifier('x');
const param2 = t.identifier('y');
const params = [param1, param2];
const body = t.blockStatement([
t.returnStatement(
t.binaryExpression('*', param1, param2)
)
]);
const functionNode = t.functionDeclaration(id, params, body);
if (t.isFunctionDeclaration(functionNode)) {
console.log('这是一个函数声明节点');
}

操作 AST 节点

  • 修改标识符节点的名称:可以直接修改标识符节点的 name 属性。
const babel = require('@babel/core');
const t = babel.types; const identifier = t.identifier('oldName');
identifier.name = 'newName';
  • 修改函数声明节点的参数:可以修改函数声明节点的 params 属性。
const id = t.identifier('subtract');
const param1 = t.identifier('m');
const param2 = t.identifier('n');
const params = [param1, param2];
const body = t.blockStatement([
t.returnStatement(
t.binaryExpression('-', param1, param2)
)
]);
const functionNode = t.functionDeclaration(id, params, body); // 添加一个新的参数
const newParam = t.identifier('c');
functionNode.params.push(newParam);

辅助生成复杂代码结构

  • 生成条件语句:可以使用 t.ifStatement(test, consequent, alternate) 生成 if 语句。
const test = t.binaryExpression('>', t.identifier('a'), t.identifier('b'));
const consequent = t.blockStatement([
t.expressionStatement(
t.callExpression(
t.identifier('console.log'),
[t.stringLiteral('a 大于 b')]
)
)
]);
const alternate = t.blockStatement([
t.expressionStatement(
t.callExpression(
t.identifier('console.log'),
[t.stringLiteral('a 小于等于 b')]
)
)
]);
const ifStatement = t.ifStatement(test, consequent, alternate);

如果需要更多的api查阅或者访问器的特性, 点击详细的官方文档

下面是一个最简单的插件演示:

// myBabelPlugin.js
module.exports = function (babel) {
// 从 babel 对象中解构出 types 模块,用于操作 AST 节点
const { types: t } = babel; return {
// visitor 对象定义了如何访问和修改 AST 节点
visitor: {
// 这里以 Identifier 节点为例,当遍历到 Identifier AST节点时会执行此函数
Identifier(path) {
// path 表示当前节点的路径,包含了节点的上下文信息
if (path.node.name === 'oldIdentifier') {
// 如果节点的名称是 'oldIdentifier',则将其修改为 'newIdentifier'
path.node.name = 'newIdentifier';
}
}
}
};
};

在webpack中引入

const path = require('path');
// 引入我们编写的插件
const myBabelPlugin = require('../my-babel-plugin/myBabelPlugin'); module.exports = {
// 入口文件
entry: './src/index.js',
// 输出配置
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
module: {
rules: [
{
// 匹配 .js 文件
test: /\.js$/,
// 排除 node_modules 目录
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
// 使用我们编写的插件
plugins: [myBabelPlugin]
}
}
}
]
}
};

babel-loader 如何工作? 什么是babel-loader插件? babel-loader插件可以干什么? 如何制作一个babel-loader插件?的更多相关文章

  1. 怎样写一个webpack loader

    div{display:table-cell;vertical-align:middle}#crayon-theme-info .content *{float:left}#crayon-theme- ...

  2. 手把手教你撸一个 Webpack Loader

    文:小 boy(沪江网校Web前端工程师) 本文原创,转载请注明作者及出处 经常逛 webpack 官网的同学应该会很眼熟上面的图.正如它宣传的一样,webpack 能把左侧各种类型的文件(webpa ...

  3. 快速写一个babel插件

    es6/7/8的出现,给我们带来了很多方便,但是浏览器并不怎么支持,目前chrome应该是支持率最高的,所以为了兼容我们只能把代码转成es5,这应该算是我们最初使用babel的一个缘由,随着业务的开发 ...

  4. 如何用纯 CSS 创作一个菱形 loader 动画

    效果预览 在线演示 按下右侧的"点击预览"按钮可以在当前页面预览,点击链接可以全屏预览. https://codepen.io/comehope/pen/eKzjqK 可交互视频教 ...

  5. 前端每日实战:45# 视频演示如何用纯 CSS 创作一个菱形 loader 动画

    效果预览 按下右侧的"点击预览"按钮可以在当前页面预览,点击链接可以全屏预览. https://codepen.io/comehope/pen/eKzjqK 可交互视频教程 此视频 ...

  6. 63.1拓展之纯 CSS 创作一个摇摇晃晃的 loader

    效果地址:https://scrimba.com/c/cqKv4VCR HTML code: <div class="loader"> <span>Load ...

  7. 63.(原65)纯 CSS 创作一个摇摇晃晃的 loader

    原文地址:https://segmentfault.com/a/1190000015424389 修改后地址:https://scrimba.com/c/cqKv4VCR HTML code: < ...

  8. 45.纯 CSS 创作一个菱形 loader 动画

    原文地址:https://segmentfault.com/a/1190000015208027#articleHeader3 感想: 网格布局-> display: grid; HTML co ...

  9. 如何用纯 CSS 创作一个摇摇晃晃的 loader

    效果预览 在线演示 按下右侧的"点击预览"按钮可以在当前页面预览,点击链接可以全屏预览.https://codepen.io/comehope/pen/oyJvpe 可交互视频 此 ...

  10. 案例实战之如何写一个webpack loader

    通过以下几个实例掌握webpack loader的写法 1.写一个多语言替换的loader 在index.js在页面上插入了一个{{title}}文本,我们需要在打包的时候将其替换成对应的多语言 fu ...

随机推荐

  1. .NET开发规范v1.0

    一. 编程规范 (一) 命名风格 命名要找更有表现力的词,更专业的词,比如获取数据不用get而使用fetch 别害怕长名称,长而具有描述性的名称比短而令人费解的名称好 为作用域大的名字采用更长的名字, ...

  2. IOS中的Context Menu

    IOS中的Context Menu 通过长按组件或者3D touch方式,周边全部虚化,弹出一个可操作的菜单,并且菜单之间也可以嵌套 IOS13之后已经弃用UIViewControllerPrevie ...

  3. 使用 OpenXML 创建第一个 Word 文档

    using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Wordprocessing; public class Op ...

  4. solon 集成 activemq-client (sdk)

    原始状态的 activemq-client sdk 集成非常方便,也更适合定制.就是有些同学,可能对原始接口会比较陌生,会希望有个具体的示例. <dependency> <group ...

  5. OpenLens 6.3.0 无法查案日志和进入 Pod Shell 解决方法

    原因 OpenLens 6.3.0开始移除了Pod的查看日志和进入Pod Shell按钮,无法查看日志和进入Pod操作. 解决办法 OpenLens 6.3.0开始这两个功能以插件形式提供,需下载op ...

  6. 出现io.netty.util.IllegalReferenceCountException: refCnt: 0, decrement: 1的原因及解决办法

    错误信息 在使用Netty构建Server服务器的时候,之前用得好好的,最近整理了一下就出现了以下错误: WARN (AbstractChannelHandlerContext.java:294)- ...

  7. C#/.NET/.NET Core技术前沿周刊 | 第 18 期(2024年12.16-12.22)

    前言 C#/.NET/.NET Core技术前沿周刊,你的每周技术指南针!记录.追踪C#/.NET/.NET Core领域.生态的每周最新.最实用.最有价值的技术文章.社区动态.优质项目和学习资源等. ...

  8. Qt数据库应用13-通用数据库分页

    一.前言 数据库分页展示,在所有的涉及到数据库记录的项目中都是需要的,除了简单的设备信息表.用户信息表这种很少几条几十条数据量的表除外,其余的日志记录表等都需要分页展示数据,少量的数据可以滚动条下拉查 ...

  9. Qt数据库应用4-数据打印到纸张

    一.前言 数据能够打印到pdf文件,当然可以打印到纸张,而且使用qprinter默认就是打印到纸张的,上一篇文章写得功能是打印到pdf,其实还要单独特殊设置打印到文件,并指定格式为pdf.不指定输出文 ...

  10. IM开发干货分享:IM客户端不同版本兼容运行的技术思路和实践总结

    本文由巩鹏军分享,原题"IM兼容性基建",本文有修订. 1.引言 一个成熟的IM成品,在运营过程中随着时间的推移,会发布不同的版本,但为了用户体验并不能强制要求用户必须升级到最新版 ...