babel插件入门

简介

在前端项目中babel无处不在,这章将实现一个将代码console.log(xxx)转换为console.log('行号:',xxx)的babel插件。让我们对babel插件开发有初步的知识体系,从而可以针对自己业务实现对应的构建需求。例如可以做自动埋点|自动国际化 | 代码高亮 | 页面主题工程化等功能解放自己提升效率。

babel

Babel 是一个通用的多用途 JavaScript 编译器。通过 Babel 你可以使用(并创建)下一代的 JavaScript,以及下一代的 JavaScript 工具。Babel 把用最新标准编写的 JavaScript 代码向下编译成可以在今天随处可用的版本。其还支持持语法扩展, 例如JSX | 静态类型检查

AST

抽象语法树(abstract syntax code,AST)是源代码的抽象语法结构的树状表示,树上的每个节点都表示源代码中的一种结构,之所以说是抽象的,抽象表示把js代码进行了结构化的转化,转化为一种数据结构。
来自Abstract syntax tree文章的截图,代码最终生成的结构模板
image.png
下面基于astexplorer.net在线将代码转换为AST。后续我们将对如下的数据进行处理从而达到我们需要的效果
image.png

开始干活

功能分析

  • 先来看看我们要转换的原始代码转换后的代码效果:
// 原始代码
function AKclown () {
   console.log('AKclown')
}

// 转换后的代码
function AKclown () {
   console.log('文件名:index.js,行号:2','AKclown')
}
  • 如下是原始代码转换后的代码AST语法的差异:
    转换前的AST
    image.png
    转换后的AST
    image.png
    根据上面图片对比不难看出,我们只需要CallExpression表达式下新增一个aruguments参数,类型为StringLiteral

  • 获取行号: 通过path.node.loc.start.line就可以获取到行号
    image.png

通过上面分析我们console.log(xxx)转换为console.log('行号:',xxx)的实现有了大致的理解。其涉及到bable的三步骤: 首先将console.log(xxx)解析成AST、其次对原始AST进行改造生成新的AST、生成最终代码
Babel 的三个主要处理步骤分别是: 解析(parse)转换(transform)生成(generate)

解析(parse)

使用@babel/parsersourceCode解析成AST语法树。
解析分为两个步骤:词法分析(Lexical Analysis)和 语法分析(Syntactic Analysis)。.

const parser = require('@babel/parser');
const sourceCode = 'console.log('AKclown')'
const ast = parser.parse(sourceCode, {
    sourceType: 'unambiguous'
});

astexplorer.net生成的一致
image.png

转换(transform)

第一步: 我们需要定义Visitors(访问者)对象并且给其添加CallExpression方法。然后通过@babel/traverse遍历上面的AST语法树。在遍历中,每当树遇到了CallExpression就会调用CallExpression()方法。

  1. 访问者模式是将对数据的操作数据结构进行分离,将对数据中各元素的操作封装成独立的类,使其在不改变数据结构的前提下可以拓展对数据新的操作。
  2. 如果对visitors不了解的,可以阅读官网
const traverse = require('@babel/traverse').default;

traverse(ast, {

    CallExpression(path, state) {
    }
});

第二步: 编写CallExpression方法,进行代码转换添加aruguments参数其类型为StringLiteral, 添加。这里需要结合@babel/types@babel/generator

  1. @babel/types提供了手动构建AST检查AST类型的方法。例如接下用到t.StringLiteral创建一个StringLiteral的节点,也可以通过t.isStringLiteral来判断某个节点是不是isStringLiteral
  2. babel节点类型很多,我们不可能完全记住节点的数据结构。可以通过类型声明文件来查看

方式一: 判断条件过于复杂

const calleeName = ['log', 'info', 'error', 'debug'];
traverse(ast, {

    CallExpression (path, state) {
        if (types.isMemberExpression(path.node.callee) 
            && path.node.callee.object.name === 'console' 
            && calleeName. includes(path.node.callee.property.name) 
           ) {
            const { line } = path.node.loc.start;
            path.node.arguments.unshift(types.stringLiteral(`(${line}:`))
        }
    }
});

方式二: 结合@babel/generator直接生成console.log进行比较,而无需先比较console再比较是否为log。一次判断直接搜哈(推荐)

const traverse = require('@babel/traverse').default;

const t = require('@babel/types');
const generate = require('@babel/generator').default;

const targetCalleeName = ['log', 'info', 'error', 'debug'].map(item => `console.${item}`);
traverse(ast, {
    CallExpression(path, state) {
        const calleeName = generate(path.node.callee).code; // 生成console.~字符串
        
        if (targetCalleeName.includes(calleeName)) {
            const { line } = path.node.loc.start;
            path.node.arguments.unshift(t.stringLiteral(`${line}:`))
        }
    }
});

生成(generate)

通过@babel/generator将修改后的AST生成最终的代码

const code = generate(ast).code;
console.log('code: ', code);

image.png

插件化

上面已经实现了需求设定接下来只需要把它进行插件化即可.
定义一个函数并且exports出去,该函数的第一个参数为babel对象。

const generate = require('@babel/generator').default;
const targetCalleeName = ['log', 'info', 'error', 'debug'].map(item => `console.${item}`);
module.exports = function ({ types: t }) {
    console.log('babel: ', babel);
    return {
        visitor: {
            CallExpression(path, state) {
                const calleeName = transformFromAst(path.node.callee).code; // 生成console.~字符串
                if (targetCalleeName.includes(calleeName)) {
                    const { line } = path.node.loc.start;
                    path.node.arguments.unshift(t.stringLiteral(`${line}:`))
                }
            }
        }
    };
}

在@babel/core中使用上面编写的自定义babel插件

const { transformSync } = require("@babel/core");
const customPlugin = require('./custom-plugin');

const sourceCode = `
function AKclown(){
  console.log('AKclown')
}
`
const { code } = transformSync(sourceCode, {
    plugins: [customPlugin],
    parserOpts: {
        sourceType: 'unambiguous',
    }
});

console.log('code: ', code);

image.png

总结

通过上面步骤我们对babelAST以及babel插件开发有了初步的认识。如果想更加深入了解建议阅读Babel 插件通关秘籍babel-handbook官网文档

文献链接

工程化思维:主题切换架构
Babel 插件通关秘籍
babel-handbook
Abstract syntax tree
AST详解与运用
What is a Polyfill

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

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

昵称

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