我们是怎么使用AST提升工作效率的?
AST 是什么
抽象语法树 (Abstract Syntax Tree),简称 AST,它是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
都遇到了些什么需求
突然的国际化: 产品在迭代两年多后,我们开始开拓海外市场,于是所有项目都要支持多语言切换。问题是我们在前期开发中,根本没有预留多语言的拓展能力,也就是说我们需要在一个版本的开发周期内,将代码中的中文文案全部提取出来,翻译,再以的形式替换。
丢弃babel的后果: 为了提升编译速度,我们使用swc替换babel来编译ts&tsx代码,导致基于插件:babel-plugin-react-css-modules 的 styleName 形式的css modules 写法不再被支持,需要切换回 className 写法。
本地化改造: 项目本地化部署时,大概率是局域网环境,因此需要将项目中引用到的CDN上的资源(图片,字体等)全部下载到本地,再将所有的引用切换为本地相对路径。
为什么会用到AST
上述提到的需求都是些耗时耗力的体力活,作为一个合格的程序员,肯定是拒绝手动的去做这些重复工作的;于是,如何写一个高效率的,能尽可能的覆盖更多case的工具就成了我们首要问题。不出意外的联想到了AST,而AST也是处理这类需求最好的且没有之一的工具(如果有,请狠狠地打我脸)。
在完成需求的同时,我们基于AST,实现了以下工具:
- 自动提取&替换国际化文案的工具;
- 自动下载项目中引用到的远程资源&替换资源引用链接的工具;
- 自动将styleName转换为className工具;
案例实操 -- 将styleName切换为className
代码
源码请看这里:style2class
背景
在日常开发中,启用css modules后,给开发带来了很多便利,例如通过css scoped为每个类名都加上hash,很好的避免了样式污染。但是,原生的css modules也有一些缺点:
- You have to use camelCase CSS class names.
- You have to use styles object whenever constructing a className.
- Mixing CSS Modules and global CSS classes is cumbersome.
- Reference to an undefined CSS Module resolves to undefined without a warning.
基于以上原因,我们项目的css modules都是基于babel-plugin-react-css-module的babel插件,通过styleName来定义css类名。
当我们将bebel替换为swc后,因为babel-plugin-react-css-module插件不再被支持,导致所有的css样式不再生效;因此需要将插件支持的styleNames形式的写法全部切换回原生的className写法。
分析
对于单个tsx或jsx文件,单独使用className或者styleName方案时,主要差别有以下几点:
- 样式文件导入方式不同:styleName并没有将样式导入为指定名称的样式对象,className则导入为指定名称的样式对象。
- 样式引用不同:styleName直接引用类名,className需要从样式对象中获取。
因此,在将styleName切换回className时,可将过程拆分为两个步骤:
- 转换样式导入,即:
// 转换前 import 'index.less'; // 转换后 import styles from 'index.less' - 转换样式取值,即:
// 转换前 <div styleName="container"/> // 转换后 <div className={styles.container}/>
标准的使用AST修改代码的流程如下图:
工具
以下是过程中用到的重要的工具:
jscodeshift: 基于babel的AST工具包,它提供API让遍历或者修改AST更丝滑;
astexplorer: AST可视化平台;
@babel/types: AST节点类型文档;
实现
工具函数
在开始处理实际需求前,先实现了以下工具函数。
getEntryPath: 遍历文件系统,返回路径名。该方法用于扫描项目中所有的ts|tsx文件;
const fg = require('fast-glob'); //fast-glob: https://github.com/mrmlnc/fast-glob
const getEntryPath = (entry) => {
const entries = fg.sync(entry, {
dot: true,
cwd: process.cwd(),
});
return entries;
};
getAstFromSource: 将源代码转换为AST;用于将单个ts|tsx文件源码转换成AST。
const jscodeshift = require('jscodeshift');
const j = jscodeshift.withParser('tsx');
const fs = require('fs-extra');
const getAstFromSource = (entry) => {
let source = fs.readFileSync(entry, { encoding: 'utf8' });
const ast = j(source);
return ast;
};
getSourceFromAst: 将AST转换为源代码;
const getSourceFromAst = (ast) => {
return ast.toSource({ lineTerminator: '\n', quote: 'single' });
};
createMemberExpression:创建对象取值表达式,如:style.content
const createMemberExpression = (key, value, computed) = > {
const isValid = !value.includes('-'); // 属性名包含“-”是非法的,需要以这种方式取值style["xxxx"]
const isRmComputed = !computed && isValid;
const memberExpression = j.memberExpression(
j.identifier(key),
isRmComputed ? j.identifier(value) : j.stringLiteral(value),
!isRmComputed
);
return memberExpression;
}
步骤一: 转换Import语句
在转换前,我们得知道Import语句对应的AST结构是怎么样的,此时就可以通过 astexplorer 来查看,更详细的内容,可以在babel的types文档中查看ImportDeclaration。
从文档中可以看到,import语句对应的AST节点类型定义如下:
type ImportDeclaration = {
type:"ImportDeclaration"
specifiers: Array<ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier> (required)
source: StringLiteral (required)
assertions: Array<ImportAttribute> (default: null, excluded from builder function)
attributes: Array<ImportAttribute> (default: null, excluded from builder function)
importKind: "type" | "typeof" | "value" (default: null, excluded from builder function)
module: boolean (default: null, excluded from builder function)
phase: "source" | "defer" (default: null, excluded from builder function)
}
现在只需要特别关注specifiers和source两个属性。source表示从哪个包或者文件路径导入;specifiers表示被导入后内容的标识符数组,每个数组项可以是ImportSpecifier ,ImportDefaultSpecifier,ImportNamespaceSpecifier(分别代表什么,文档写的很清楚);
import "index.module.less" 的AST结构如下图:
import style from "index.module.less" 的AST结构如下图:
与源代码导入语句的AST相比,目标代码的AST节点的specifiers属性不为空,有一个ImportDefaultSpecifier类型的节点,其代表:导入样式后指定样式对象的名称,如:import styles from 'index.module.less' 语句中的 styles。
因此转换逻辑大致为:
- 找到所有的Import语句;
- 通过AST节点的source属性过滤出所有的样式导入语句;ImportDefaultSpecifier
- 找到specifiers为空数组的样式导入语句;
- 计算出一个安全样式对象名称后,构造ImportDefaultSpecifier节点;
- 将ImportDefaultSpecifier节点push到该导入语句的specifiers数组中;
核心代码如下:
originData.find(j.ImportDeclaration).forEach((path) => { // originData表示整个tsx文件的AST;找到所有Import语句
const importValue = get(path, 'value.source.value');
const isLess = importValue.endsWith('.less');
if (isLess) { // 判断是否是样式导入(我们全部less)
const isImportDefaultSpecifier =
get(path, 'value.specifiers[0].type') === 'ImportDefaultSpecifier';
if (isImportDefaultSpecifier) {
const names = getImportVariableNames(path.value);
specifier = names[0]; // 如果已经是默认导入,则记录specifierName
} else {
const globalVars = getGlobalVariableNames(originData);
specifier = getGlobalStyleId(globalVars); // 如果不是默认导入,生成一个合法的标识符并记录
j(path).replaceWith((p) => {
p.value.specifiers = [ // specifiers 插入一个 ImportDefaultSpecifier 类型节点
{
type: 'ImportDefaultSpecifier',
local: {
type: 'Identifier',
name: specifier,
},
},
];
return p.value;
});
}
}
});
步骤二:将JSX的styleName属性转换为className属性
styleName是jsx节点的一个属性,AST结构如下图:
因此第一步就是遍历AST找到所有拥有styleName属性的JSX节点,并获取styleName属性的属性值的AST节点。
originData.find(j.JSXOpeningElement).forEach((path) => {
j(path)
.find(j.JSXAttribute)
.forEach((p) => {
if (p.value.name.name === 'styleName') {
// ......
}
});
});
找到styleName属性并且拿到属性值了,要怎么将其转换成className形式呢?
我们知道styleName类型为字符串,可在实际开发过程中,它不可能只是形如styleName="container"这样的简单字符串形式,当业务复杂度起来后,这个字符串可以是由任意的表达式生成的。
经过分析,styleName的属性值可能有以下几种情况进行赋值:
- 简单字符串,如:
styleName="container"; - 带空格的简单字符串,如:
styleName="container small"; - 逻辑表达式,如:
styleName={true && "container"}; - 三元表达式,如:
styleName={true ? "show" : "hide"}; - 变量,如:
styleName={styles}; - 函数调用,如:
styleName={fn()}; - 模版字符串,如:
styleName={'container ${ true ? "show" : "hide" }'} - 上述7种类型嵌套形式;
但仔细想想,不管什么类型的表达式,只需要保证表达式的结构不改变的同时,将表达式中表示类名的字符串”XXX“转换为style.XXX。
基于此,将以上多种形式的表达式,分为三类:
- 基础形式(简单字符串,带空格的简单字符串);
- 简单表达式形式(没有嵌套其他表达式,直接返字符串);如:
styleName={true ? "show" : "hide"}就认为是简单表达式,styleName={true ? false && "show" : "hide"}则不是,因为在条件表达式的基础上还嵌套了逻辑表达式; - 嵌套表达式形式(1~7形式任意嵌套);
基础形式
简单字符串的AST节点类型如下:
type Literal = {
value:string (required)
}
简单字符串形式:
在转换这种形式时,目标是将styleName="container" 转换为 className={styles.container}。
需要注意的是,在JSX中,当属性值为字符串时,可以直接给属性赋值;当属性值为非字符串时,都需要使用 {} 包裹。
带空格的简单字符串形式
当styleName属性值出现空格时,说明是多个类名,例如:styleName="container small" 就不能转换成className={styles["container small"]} 。
多个类名时,一般使用模版字符串来处理,此时应该转换成:className={'${styles.container} ${styles.small}'}。
以上两种形式是后续转换的基础,因为无论结构多复杂,目标都是将表示css类名的字符常量转换为样式对象取值。
代码实现如下:
/**
* @param {string} content
* @param {boolean} [needContainer=false] 是否需要用 {} 包裹
*/
function literalHandler(content, needContainer = false) {
if (!content) { // 空字符时
const node = j.stringLiteral(content);
return needContainer ? j.jsxExpressionContainer(node) : node; // 如果转换后的值要直接复制给JSX属性,则需要使用jsxExpressionContainer类型的节点({})来包裹转换后的值
}
const splits = content.split(' ').filter(Boolean);
const { specifier, computed } = global.style2class;
if (splits.length === 1) { // 只有一个类名 - 直接转换成样式对象取值
return needContainer
? j.jsxExpressionContainer(createMemberExpression(specifier, splits[0], computed))
: createMemberExpression(specifier, splits[0], computed);
} else { // 多个类名时 - 需要将每个类名使用样式对象取值后,再用模版字符串来拼接
const templateElements = [
createTemplateElement(''),
...Array(splits.length - 1).fill(createTemplateElement(' ')),
createTemplateElement(''),
];
const compressions = splits.map((split) => {
return createMemberExpression(specifier, split, computed);
});
const templateLiteral = j.templateLiteral(templateElements, compressions);
return needContainer ? j.jsxExpressionContainer(templateLiteral) : templateLiteral;
}
}
简单表达式
这部分的代码实现放到后文。
逻辑表达式形式: 条件表达式的AST结构如下:
type LogicExpression = {
operator: "||" | "&&" | "??" (required)
left: Expression (required)
right: Expression (required)
}
通常,使用条件表达式来计算styleName的值时,条件表达式的右表达式(AST节点的right属性)作为取值表达式;
三元表达式形式 三元表达式的AST结构为:
type ConditionalExpression = {
test: Expression (required)
consequent: Expression (required)
alternate: Expression (required)
}
通常,使用条件表达式来计算styleName的值时,consequent节点和alternate节点均可作为取值表达式;
变量形式 变量类型的值,我们直接忽略,处理起来略显复杂。因为它并不是个静态值,需要结合上下文(上下文甚至是夸文件的)来分析变量最终取值形式;
函数调用形式 函数调用本来也不是一个静态值,但是有一种特殊的且大量使用的情况是可以处理的。
社区提供了classnames包,用于有条件地将ClassNames加在一起。当使用classnames计算styleName的值,classnames函数的行为是可预测的,因此这种情况是可以被处理的。
先看下函数调用表达式的AST结构:
type CallExpression = {
callee: Expression | Super | V8IntrinsicIdentifier (required)
arguments: Array<Expression | SpreadElement | JSXNamespacedName | ArgumentPlaceholder> (required)
}
针对这种特殊情况的处理方式为:
- 判断表达式是否为函数调用表达式,是的话则获取函数名,即callee节点的字面量值;
- 全局所有的Import语句,判断该函数名是否是从
classnames导入的函数(更严谨的话应该分析作用域链); - 如果是,则处理;不是,则记录为异常;
从classNames的官方文档可以看见,该函数的入参类型和返回值如下:
classNames('foo', 'bar'); // => 'foo bar'
classNames('foo', { bar: true }); // => 'foo bar'
classNames({ 'foo-bar': true }); // => 'foo-bar'
classNames({ 'foo-bar': false }); // => ''
classNames({ foo: true }, { bar: true }); // => 'foo bar'
classNames({ foo: true, bar: true }); // => 'foo bar'
// lots of arguments of various types
classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux'
// other falsy values are just ignored
classNames(null, false, 'bar', undefined, 0, 1, { baz: null }, ''); // => 'bar 1'
//Arrays will be recursively flattened as per the rules above:
const arr = ['b', { c: true, d: false }];
classNames('a', arr); // => 'a b c'
因此classNames函数入参的转换规则为:
- 当入参为字符串时,按照 简单字符串 转换规则处理即可;
- 当入参为非字符串的基本类型时(number,boolean,null,undefined),falsy的值被过滤掉,truly的值转换成字符串后,再按照 简单字符串 转换规则处理即可;
- 当入参为Object类型时,Object的value保持不变,key转换为样式对象取值,例:
{container: true} => { [style.container]: true}; - 当入参为数组时,每个数组项按前三个步骤分别处理即可;
模版字符串形式 模版字符串对应的AST节点是这么定义的:
type TemplateLiteral = {
quasis: Array<TemplateElement> (required)
expressions: Array<Expression> (required)
}
type TemplateElement = {
value: { raw: string, cooked?: string } (required)
tail: boolean (default: false)
}
以'abc ${window}def'为例,其中:
expressions表示表达式集合:对应的是 ${window};
quasis表示字符集合:一个TemplateElement节点对应一段字符,如abc 或者def;
两个集合在映射成模板字符串的规则为:以TemplateElement节点开始,TemplateElement和Expression节点交替排列且以TemplateElement结束,末尾的TemplateElement节点的tail为true,表示一个模版字符串表达式结束。
嵌套表达式
使用递归来转换嵌套表示,递归的终止条件为:当前表达式的取值节点(如:逻辑表达式的right节点,条件表达式的alternate和consequent节点)类型是Literal或则StringLiteral类型;
前文的简单表达式部分只是讲述了各个表达式的AST结构,以及在理想状态下如何去转换。
现在开始,讲述如何结合递归去解决各个表达式的转换逻辑。
expressionHandler
经分析,我们需要处理的表达式类型有:LogicalExpression,ConditionalExpression,ObjectExpression,ArrayExpression,CallExpression,TemplateLiteral;
声明了名为expressionHandler方法作为递归入口,入参为Expression节点,返回值类型为{node:Expression;transfered:boolean},其中node表示Expression节点,transfered表示入参传入的节点是否被转换,代码如下:
各个类型表示的handler的返回值均与expressionHandler函数的返回值类型一致,在递归过程中,只要有handler返回的transfered为false,就表示改节点无法被转换,会被记录为异常。
function expressionHandler(node) {
if (node.type === 'LogicalExpression') {
return logicalHandler(node);
}
if (node.type === 'ConditionalExpression') {
return conditionalHandler(node);
}
if (node.type === 'ObjectExpression') {
return objectExpressionHandler(node);
}
if (node.type === 'ArrayExpression') {
return arrayExpressionHandler(node);
}
if (node.type === 'CallExpression') {
return callExpressionHandler(node);
}
if (node.type === 'TemplateLiteral') {
return templateLiteralHandler(node);
}
// 除了上述表达式类型,被认为无法处理,记录为异常;
return {
transfered: false,
node: node,
};
}
logicalHandler
通过logicalHandler方式来处理逻辑表达式,实现如下:
/**
* @param {LogicalExpression} node
*/
function logicalHandler(node) {
const logicalRight = node.right;
const logicalRightType = logicalRight.type;
const logicalRightValue = logicalRight.value;
const isLiteral = isLiteralType(logicalRightType);
let newLogicalRight = logicalRight;
if (isLiteral) { // 如果逻辑表达式的right节点是字符常量,则由 literalHandler 来转换
const expression = literalHandler(logicalRightValue);
newLogicalRight = expression;
return {
transfered: true,
node: j.logicalExpression(node.operator, node.left, newLogicalRight),
};
} else { // 如果逻辑表达式的right节点是表达式,则由expressionHandler方法递归处理
const { node: newNode, transfered } = expressionHandler(logicalRight);
return {
transfered,
node: transfered ? j.logicalExpression(node.operator, node.left, newNode) : node,
};
}
}
conditionalHandler
通过conditionalHandler方式来转换三元逻辑表达式,实现如下:
/**
* @param {ConditionalExpression} compressionNode
*/
function conditionalHandler(compressionNode) {
const consequentNode = compressionNode.consequent;
const alternateNode = compressionNode.alternate;
//转换 consequent 部分
const { node: newConsequentNode, transfered: consequentTransfered } = conditionalSideHandler(
consequentNode
);
//转换 alternate 部分
const { node: newAlternateNode, transfered: alternateTransfered } = conditionalSideHandler(
alternateNode
);
return {
transfered: consequentTransfered && alternateTransfered,
node: j.conditionalExpression(compressionNode.test, newConsequentNode, newAlternateNode),
};
}
function conditionalSideHandler(node) {
let newNode = node;
if (isLiteralType(node.type)) { // 如果是字符常量,由 literalHandler 来转换
newNode = literalHandler(node.value);
return { transfered: true, node: newNode };
}
return expressionHandler(node); // 如果是表达式,则由 expressionHandler 方法递归处理
}
objectExpressionHandler
ObjectExpression类型的表达式一般出现在classNames函数调用时,也一块加入到递归分支中,通过objectExpressionHandler来转换。
/**
* @param {ObjectExpression} node
*/
function objectExpressionHandler(node) {
const properties = node.properties.map((property) => {
const propertyKeyNode = property.key;
const propertyKeyNodeType = propertyKeyNode.type;
// propertyKeyNodeType === 'Identifier' ==> { name:"haoqidewukong"}
// isLiteralType(propertyKeyNodeType) ==> {["name"]:"haoqidewukong"}
if (propertyKeyNodeType === 'Identifier' || isLiteralType(propertyKeyNodeType)) {
const keyString = isLiteralType(propertyKeyNodeType)
? property.key.value
: property.key.name;
const { specifier, computed } = global.style2class;
const key = createMemberExpression(specifier, keyString, computed);
const objectProperty = j.objectProperty(key, property.value, 'init', true);
objectProperty.computed = true;
return { node: objectProperty, transfered: true };
} else {
return {
transfered: false,
node: property,
};
}
});
const isModified = properties.every((v) => v.transfered);
return {
transfered: isModified,
node: j.objectExpression(properties.map((v) => v.node)),
};
}
arrayExpressionHandler
同样的,使用arrayExpressionHandler来转换ArrayExpression类型的表达式,实现如下:
/**
* @param {ArrayExpression} node
*/
function arrayExpressionHandler(node) {
const elements = node.elements.map((element) => {
if (isLiteralType(element.type)) {
return {
transfered: true,
node: literalHandler(element.value),
};
}
return expressionHandler(element); // 递归处理表达式类型
});
const isModified = elements.every((v) => v.transfered);
return {
transfered: isModified,
node: j.arrayExpression(elements.map((v) => v.node)),
};
}
callExpressionHandler
实现callExpressionHandler来转换CallExpression类型的表达式:
/**
* @param CallExpression node
*/
function callExpressionHandler(node) {
let transfered = false;
const isPass = classnamesIdentifierChecker(node); // 检查改函数是否是从 classNames 包导入的
if (!isPass) {
return {
transfered,
node,
};
}
const arguments = node.arguments;
const newArguments = arguments.map((argument) => {
if (isLiteralType(argument.type)) {
return {
transfered: true,
node: literalHandler(argument.value),
};
}
return expressionHandler(argument); // 递归处理表达式类型
});
const isModified = newArguments.every((v) => v.transfered);
return {
transfered: isModified,
node: j.callExpression(
node.callee,
newArguments.map((v) => v.node)
),
};
}
templateLiteralHandler
观察下面两个例子,就能发现TemplateLiteral类型不能独立的去处理expressions和quasis两部分。
str1会转换成 "container show"的字符串,继而再转换成 '${style.container} ${style.show}' ;这种情况是可以分别 expressions和quasis;
str2 可能希望生成 'card-2' 这样的字符串,因此最终期望被转换成 'style['card-${index}']'的形式;如果此时分别去处理expressions和quasis的话,最后会被转换成'${style['card-']} ${style[index]}',明显与实际期望的效果有偏差。
const str1 = `container ${true? "show" : "hide"}`;
const str2 = `card-${index}`;
那么要如何处理呢?
关键点在quasis每个节点的value属性代表的字符常量首尾是否存在空格。
例如:
str1的quasis的第一个TemplateElement的value属性对应的字符常量为"container ",他是quasis的第一个节点且有尾空格,所以可以认为他是一个完整的css 类名;
而str2的quasis的第一个TemplateElement的value属性对应的字符常量为"card-",没有尾空格,说明"card-"实在与某个表达式一起拼接成新css 类名。
因此,可以实现一个工具函数来判断一个TemplateElement是否是完成的css 类名或者某个动态css 类名一部分组成,实现如下:
/**
* @param {string} value
* @param {boolean} isStart 是否是第一个TemplateElement节点
* @param {boolean} isEnd 是否是最后一个TemplateElement节点
* @returns
*/
function checkQuasisValueValid(value, isStart, isEnd) {
if (isEnd && isStart) return true;
if (isStart && value.endsWith(' ')) return true;
if (isEnd && value.startsWith(' ')) return true;
return value.startsWith(' ') && value.endsWith(' ');
}
检测到某个TemplateElement节点非法时,并不是不能处理,只是成本较高,所以记录为异常,后续手动处理即可。
quasisHandler
通过quasisHandler方法来处理模版字符串的quasis部分,实现代码如下:
/**
* @param {TemplateElement} node
*/
function quasisHandler(node, index, length) {
const value = node.value.raw;
if (!value) {
return {
transfered: true,
node,
};
}
const isStart = index === 0;
const isEnd = index === length - 1;
if (checkQuasisValueValid(value, isStart, isEnd)) { // 判断该节点是否合法
const trimmedValue = value.trim();
if (!trimmedValue) {
// 若过滤掉收尾空格后是空字符的话,说明是分隔空格,需要返回一个值为一个空格的TemplateElement节点
return {
transfered: true,
node: j.templateElement({ raw: ' ', cooked: ' ' }, false),
};
}
return {
transfered: true,
node: literalHandler(trimmedValue),
};
} else { // 不合法
return {
transfered: false,
node,
};
}
}
expressions部分很明细是个表达式数组,使用expressionHandler方法递归处理。
templateLiteralHandler
当expressions和quasis两部分都处理完后,就可以生成新的TemplateLiteral节点。实现如下:
/**
* @param {TemplateLiteral} node
*/
function templateLiteralHandler(node) {
const expressions = node.expressions;
const quasis = node.quasis;
const elementLength = quasis.length + expressions.length;
// 转换quasi部分
const quasisNodes = quasis.map((quasi, index) => {
return quasisHandler(quasi, index * 2, elementLength);
});
// 转换expressions部分
const expressionNodes = expressions.map((expression) => {
return expressionHandler(expression);
});
// 所有被处理的节点是否都合法
const isModified =
quasisNodes.every((v) => v.transfered) && expressionNodes.every((v) => v.transfered);
if (isModified) {
const mergedExpressionNodes = [];
for (let i = 0; i < quasisNodes.length; i++) {
const expressionNode = expressionNodes[i];
const quasisNode = quasisNodes[i];
mergedExpressionNodes.push(quasisNode.node);
if (i !== quasisNodes.length - 1) {
mergedExpressionNodes.push(expressionNode.node);
}
}
const filteredMergedExpressionNodes = mergedExpressionNodes.filter(
(v) => v.type !== 'TemplateElement'
);
const insertQuasisNode = [];
for (let idx = 0; idx < filteredMergedExpressionNodes.length; idx++) {
if (idx !== 0) {
insertQuasisNode.push(j.templateElement({ raw: ' ', cooked: ' ' }, false));
} else {
insertQuasisNode.push(j.templateElement({ raw: '', cooked: '' }, false));
}
}
insertQuasisNode.push(j.templateElement({ raw: '', cooked: '' }, true));
const newTemplateLiteral = j.templateLiteral(
insertQuasisNode,
filteredMergedExpressionNodes
);
return {
transfered: true,
node: newTemplateLiteral,
};
} else {
return {
transfered: false,
node,
};
}
}
以上就是各个形式的styleName属性值转换的核心代码;
效果
一下为部分case的转换效果;
// ------------- 转换前 ---------------------------
import '../styles/index.module.less';
import classnames from 'classnames';
const Home = () => {
return (
<div styleName="container">
<div styleName="content small sl-2"></div>
<div styleName={true ? 'show' : 'hide'}></div>
<div styleName={true && 'show'}></div>
<div styleName={`content ${true ? 'small' : ''}`}></div>
<div styleName={classnames('content', { show: true })}></div>
</div>
);
};
// ------------- 转换后 ---------------------------
import s from '../styles/index.module.less';
import classnames from 'classnames';
const Home = () => {
return (
<div className={s.container}>
<div className={`${s.content} ${s.small} ${s['sl-2']}`}></div>
<div className={true ? s.show : s.hide}></div>
<div className={true && s.show}></div>
<div className={`${s.content} ${true ? s.small : ''}`}></div>
<div className={classnames(s.content, {[s.show]: true})}></div>
</div>
);
};
对于不能转换的case,控制台也有相关提示:
文件"test/index.tsx"有脏东西,需要手动处理的代码为:
test/index.tsx:13
案例总结
前文废话说了这么多,其实最终实现被没有覆盖全部case,但是总实际使用来看,是能覆盖95%以上的case的;
至于为什么没有尽可能的去覆盖全部,有以下两点考虑:
- 实现难度比较大。例如当为变量形式的取值时,需要结合上下文(上下文环境甚至是跨文件的)去分析变量的终止取值;
- 实现成本比较高。例如在考虑className和styleName合并时,处理成本直接翻倍;
因此,在处理这些case时,都是将其记录为异常(文件名+代码行号)输出,提醒使用者手动处理即可。
最后的总结
前面的案例只用到了AST能力的冰山一角,更深奥的用法需要更多编译原理的知识,笔者功力尚浅,还不足以掌握。
文中若是有不对的地方,欢迎指正~