babel-loader 如何工作? 什么是babel-loader插件? babel-loader插件可以干什么? 如何制作一个babel-loader插件?
本文会介绍比较基本的编译知识和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 代码。这个新生成的代码就是经过转换后的最终代码
基本流程如下:
-> @babel/core
接收处理文件-> @beble/parser
将源文件转换成AST-> 插件翻译/修改AST
通过插件对AST进行增删改查-> @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插件?的更多相关文章
- 怎样写一个webpack loader
div{display:table-cell;vertical-align:middle}#crayon-theme-info .content *{float:left}#crayon-theme- ...
- 手把手教你撸一个 Webpack Loader
文:小 boy(沪江网校Web前端工程师) 本文原创,转载请注明作者及出处 经常逛 webpack 官网的同学应该会很眼熟上面的图.正如它宣传的一样,webpack 能把左侧各种类型的文件(webpa ...
- 快速写一个babel插件
es6/7/8的出现,给我们带来了很多方便,但是浏览器并不怎么支持,目前chrome应该是支持率最高的,所以为了兼容我们只能把代码转成es5,这应该算是我们最初使用babel的一个缘由,随着业务的开发 ...
- 如何用纯 CSS 创作一个菱形 loader 动画
效果预览 在线演示 按下右侧的"点击预览"按钮可以在当前页面预览,点击链接可以全屏预览. https://codepen.io/comehope/pen/eKzjqK 可交互视频教 ...
- 前端每日实战:45# 视频演示如何用纯 CSS 创作一个菱形 loader 动画
效果预览 按下右侧的"点击预览"按钮可以在当前页面预览,点击链接可以全屏预览. https://codepen.io/comehope/pen/eKzjqK 可交互视频教程 此视频 ...
- 63.1拓展之纯 CSS 创作一个摇摇晃晃的 loader
效果地址:https://scrimba.com/c/cqKv4VCR HTML code: <div class="loader"> <span>Load ...
- 63.(原65)纯 CSS 创作一个摇摇晃晃的 loader
原文地址:https://segmentfault.com/a/1190000015424389 修改后地址:https://scrimba.com/c/cqKv4VCR HTML code: < ...
- 45.纯 CSS 创作一个菱形 loader 动画
原文地址:https://segmentfault.com/a/1190000015208027#articleHeader3 感想: 网格布局-> display: grid; HTML co ...
- 如何用纯 CSS 创作一个摇摇晃晃的 loader
效果预览 在线演示 按下右侧的"点击预览"按钮可以在当前页面预览,点击链接可以全屏预览.https://codepen.io/comehope/pen/oyJvpe 可交互视频 此 ...
- 案例实战之如何写一个webpack loader
通过以下几个实例掌握webpack loader的写法 1.写一个多语言替换的loader 在index.js在页面上插入了一个{{title}}文本,我们需要在打包的时候将其替换成对应的多语言 fu ...
随机推荐
- Input报错“Form elements must have labels: Element has no title attribute Element has no placeholde”
喵~ 项目开发难免会遇到些不解的问题,以下总结的是简化版,重在复现问题,解决问题. 写表单时,如果只是单独写了input元素,发现在后台管理会飘红.感觉很奇怪,明明没有写错语法,为什么会飘红呢? 1. ...
- HarmonyOS Next 入门实战 - 创建项目、主题适配
开发一个简单的demo,其中涉及一些鸿蒙应用开发的知识点,其中涉及导航框架,常用组件,列表懒加载,动画,深色模式适配,关系型数据库等内容,在实践中学习和熟悉鸿蒙应用开发. 首先下载并安装 ...
- m4 mac mini本地部署ComfyUI,测试Flux-dev-GGUF的workflow模型10步出图,测试AI绘图性能,基于MPS(fp16),优点是能耗小和静音
m4 mac mini已经发布了一段时间,针对这个产品,更多的是关于性价比的讨论,如果抛开各种补贴不论,价位上和以前发布的mini其实差别不大,真要论性价比,各种windows系统的mini主机的价格 ...
- docker-compose的nginx更换完ssl证书不起作用的完美解决方法
以Harbor为例,ssl证书更新后,docker-compose启动不起作用. 问题出在一句很重要的命令:./prepare 步骤:(Harbor样例) 1. cd /data/ssl 换ssl证 ...
- MybatisPlusException: can not find lambda cache for this entity[]异常解决
文章目录 场景说明 解决方案 场景说明 简单来说,我们系统中许多数据都是树状结构的,所以我定义了一个实体类父类BaseTreePO,并且想封装一个通用的树状对象的Service类,部分代码如下: ...
- Vue3封装一个ElementPlus自定义上传组件2--无弹窗
Vue3封装一个ElementPlus自定义上传组件2--无弹窗 写在前面: 无弹窗的上传组件它来了,依旧是小巧又好用,只不过这回我用的是前端直传的方式,采用http-request进行文件上传,中间 ...
- GNU Make中CPPFLAGS和CXXFLAGS之间的区别
GNU Make 是一个流行的构建工具,用于编译和链接源代码.在 GNU Make 中,CPPFLAGS 和 CXXFLAGS 都是用于指定编译器选项的变量.它们之间的主要区别在于它们分别适用于 C ...
- JavaWeb代码架构中类之间的引用关系
为了加深对Java Web代码架构中类之间的引用关系的理解和记忆,特绘制了这一张图. Java EE应用架构:
- JMeter 线程组全家桶教程
宝子们,今天咱就来唠唠 JMeter 里那些超重要的线程相关的玩意儿,学会了它们,你就能在性能测试的世界里 "横冲直撞" 啦! 一.线程组 -- 性能测试的主力军 想象一下,你开了 ...
- ASP.NET Core - 日志记录系统(二)
本篇接着上一篇 [ASP.NET Core - 日志记录系统(一)] 往下讲,所以目录不是从 1 开始的. 2.4 日志提供程序 2.4.1 内置日志提供程序 ASP.NET Core 包括以下日志记 ...