本文会介绍比较基本的编译知识和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] } } } ] } };
复制代码
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |