前言
2021年的今天,构建工具已经从 webpack 一家独大,变成了 vite, snowpack 等新工具不断涌现,逐渐有百花齐放的新形势。但不管使用哪一个构建工具,依然离不开babel:一方面,越来越多的项目采用了 typescript 语法,ts 语言需要被编译成 javascript 才能运行在浏览器端和 node 端;另一方面,我们需要通过 babel 将一些部分浏览器尚未广泛支持的语言新特性编译成浏览器支持的代码。
但对于普通前端从业者来说,babel 既熟悉又陌生,我们虽然天天都在使用babel功能,但大部分时候我都是直接使用一些babel插件。当我们有在编译阶段获取代码结构或者修改代码结构的需求时,该如何使用babel工具来实现呢?
本文将通过一个例子来介绍如何快速入门babel编译。
需求介绍
小鹿的业务仓库采用了react框架进行开发,由于组件升级,需要将所有的 Viewer 组件升级到 view 组件,且对修改过的组件新增一个className: new-view。
由于当前业务仓库的代码量非常大,如果人工手动修改,可能需要花费非常多的时间和精力,且无法保证修改的正确性。现在小鹿决定写一个转换脚本来实现这次升级。
实现准备
首先,这里先介绍几个babel包。
@babel/parser
文档地址:babeljs.io/docs/en/bab… 这个包将代码文本解析成 ast (Abstract syntax tree)
require("@babel/parser").parse("code", {
// parse in strict mode and allow module declarations
sourceType: "module",
plugins: [
// enable jsx and flow syntax
"jsx",
"flow",
],
});
@babel/traverse
文档地址:babeljs.io/docs/en/bab… 这个包将遍历 ast 节点,并可以更新 ast 节点信息。
import * as parser from "@babel/parser";
import traverse from "@babel/traverse";
const code = `function square(n) {
return n * n;
}`;
const ast = parser.parse(code);
traverse(ast, {
enter(path) {
if (path.isIdentifier({ name: "n" })) {
path.node.name = "x";
}
},
});
@babel/types
文档地址:babeljs.io/docs/en/bab… 这个包提供了 babel ast 的类型判断和各种类型的创建函数。
@babel/generator
文档地址:babeljs.io/docs/en/bab… 这个包会将 ast 生成代码文本,与 @babel/parser 刚好相反。
import { parse } from "@babel/parser";
import generate from "@babel/generator";
const code = "class Example {}";
const ast = parse(code);
const output = generate(
ast,
{
/* options */
},
code
);
开始实现
转换成AST
首先,我们需要将仓库代码解析成 ast.由于我们的代码使用react框架,因此在转换时,需要添加 jsx 插件。
const fs = require('fs');
const parser = require('@babel/parser').default;
const content = fs.readFileSync('这里是文件地址', 'utf-8');
const ast = parser.parse(content, {
sourceType: "module",
plugins: [
'jsx'
]
});
修改节点
我们需要使用@babel/traverse来遍历 ast 的节点,然后通过通过修改 ast 的节点来达到代码转换的目的。那么如何遍历又如何修改呢?
在 traverse 上,我们需要通过定义 vistor 的方法,当工具遍历到与我们定义的方法的类型一致时,就会调用我们定义的方法。
traverse(ast, {
FunctionDeclaration: function(path) {
path.node.id.name = "x";
},
});
比如在上述的例子中,如果某个节点的类型为 FunctionDeclaration,那么遍历到该节点的时候,就会调用上面的方法,将这个节点的name设置成x。
作为一个新手,很难一下子找到需要修改的节点的类型。这里介绍一个在线解析工具: astexplorer.net/ 。
将实例代码贴到编辑视图中,并将设置 @babel/parser 为解析器,其解析的 ast 结构就会展示在右边栏。
选中我们需要修改的节点后,右边的ast会自动高亮。根据右边的结构,我们来写转换代码。
我们选中 Viewer 的引入语句,可以看到我们需要修改的 viewer 在一个
ImportDeclaration 节点下,其 source 的 value 为 some-view。
traverse(ast, {
ImportDeclaration: function(path) { // import 语句
const node = path.node; // path.node 为当前节点
if (node.source.value === 'some-view') {
node.specifiers = node.specifiers.map(s => {
if (s.imported.name === 'Viewer') { // 找到 Viewer
return t.importSpecifier('View', 'View'); // 用 types 创建一个 importSpecifier 节点:https://babeljs.io/docs/en/babel-types#importspecifier
}
return s;
});
}
},
});
转换好依赖后,我们接着转换jsx中的节点。依旧是在左边选中我们需要更新的代码,然后在右侧观察结构。由于需求中还需要添加 className,我们在 demo 也需要添加一个类名,以帮助我们进行节点定位。
我们需要修改的节点在一个
JSXOpeningElement 下,节点类型为 JSXIdentifier, className 在attributes中。
traverse(ast, {
ImportDeclaration: function(path) { // import 语句
const node = path.node; // path.node 为当前节点
if (node.source.value === 'some-view') {
node.specifiers = node.specifiers.map(s => {
if (s.imported.name === 'Viewer') { // 找到 Viewer
return t.importSpecifier('View', 'View'); // 用 types 创建一个 ImportSpecifier 节点:https://babeljs.io/docs/en/babel-types#importspecifier
}
return s;
});
}
},
JSXOpeningElement: function(path) {
const node = path.node;
if (node.name.name === 'Viewer') { // 标签名为 Viewer
node.name.name = 'View';
node.name.attributes.push(t.jsxAttribute('className', 'new-view')); // 用 types 创建一个 JSXAttribute 节点:https://babeljs.io/docs/en/babel-types#jsxattribute
}
}
});
在实际处理时,还需要考虑 Viewer 组件上原本就存在 className 的情况,这里就不展开了。另外,我们还需要处理闭合标签中的 Viewer,还是同样的方法,大家可以参照 JSXOpeningElement 的方式进行实现。
输出文本
const generate = require('@babel/generator').default;
const newContent = generate(ast, {}, content);
结尾
本文的例子已经讲解完毕,需要对大家的工作和学习有帮助~鞠躬。