一、前情提要
在不久前的一天,师兄跑来对我说: “天覇,你对国际化熟悉吗?”。 我心想,不就是context + hook吗?
但是不能骄傲,于是谦虚的跟师兄说用过。果然事情没那么简单,师兄接着说:“我们的ant-design-pro有国际化的部分,但是使用场景不多,现在要支持在不需要的时候可以处理掉国际化的代码”。
此时我脑袋极速运转,闪现两个方案:
- 改写formatMessage
- 通过AST干掉相关代码 但是改写formatMessage依然会将国际化代码打到包里,作为一个正经程序员怎么会忍受这样的事情?
于是乎方案还是落定为AST。而此时,师兄已经通过AST处理了一部分代码,现在需要的是彻底处理国际化出现的各种场景。诶?处理了一部分代码?不对劲
二、AST
AST是什么
AST是抽象语法树(Abstract Syntax Tree),它是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。通俗易懂的说话就是AST是通过JSON的方式来表示代码。
AST作用
- 编辑器的错误提示、代码格式化、代码高亮、代码自动补全;
- elint、pretiier 对代码错误或风格的检查;
- webpack 通过 babel 转译 javascript 语法;
AST生成
目前主要是通过babel-parse将代码转换成AST,babel-parse主要通过词法解析和语法解析生成AST
const ast = parser.parse(code, {
sourceType: 'module',
plugins: ['jsx', 'typescript', 'dynamicImport', 'classProperties', 'decorators-legacy'],
});
- 词法解析 词法分析阶段把字符串形式的代码转换为 令牌(tokens)流。你可以把令牌看作是一个扁平的语法片段数组。
// 输入
n * n
// 输出
/**
[
{ type: { ... }, value: "n", start: 0, end: 1, loc: { ... } },
{ type: { ... }, value: "*", start: 2, end: 3, loc: { ... } },
{ type: { ... }, value: "n", start: 4, end: 5, loc: { ... } },
]
*/
/**
{
type: {
label: 'name',
keyword: undefined,
beforeExpr: false,
startsExpr: true,
rightAssociative: false,
isLoop: false,
isAssign: false,
prefix: false,
postfix: false,
binop: null,
updateContext: null
},
...
}
*/
- 语法解析 语法分析就是根据词法分析的结果,也就是令牌tokens,将其转换成AST。
{
"type": "File",
"start": 0,
"end": 3,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 3
}
},
"errors": [],
"program": {
"type": "Program",
"start": 0,
"end": 3,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 3
}
},
"sourceType": "module",
"interpreter": null,
"body": [
{
"type": "ExpressionStatement",
"start": 0,
"end": 3,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 3
}
},
"expression": {
"type": "BinaryExpression",
"start": 0,
"end": 3,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 3
}
},
"left": {
"type": "Identifier",
"start": 0,
"end": 1,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 1
},
"identifierName": "n"
},
"name": "n"
},
"operator": "*",
"right": {
"type": "Identifier",
"start": 2,
"end": 3,
"loc": {
"start": {
"line": 1,
"column": 2
},
"end": {
"line": 1,
"column": 3
},
"identifierName": "n"
},
"name": "n"
}
}
}
],
"directives": []
},
"comments": []
}
三、AST遍历
根据需求,我们需要把代码中的国际化代码都删掉,比如
import {useIntl, FormatedMessage} from 'umi'
function App() {
const Intl = useIntl()
alert(Intl.formatMessage({id: 'key'}))
alert(useIntl().formatMessage({id: 'key'}))
return (
<>
<FormatedMessage />
<div>{Intl.formatMessage({id: 'key'})}</div>
<div data-lang>{useIntl && Intl.formatMessage({id: 'key'})}</div>
</>
)
}
// Format相关的都要删除,情况非常复杂....
当然,babel这么高的下载量当然会提供一个方式让我们处理AST,说白了AST就是一个JSON,处理的本质还是遍历,babel提供了babel-traverse
traverse.default(ast, {
enter(path) {
// 遍历入口
},
exit(path) {
},
CallExpression(path) {}
// ...more
})
四、AST处理
AST节点本身具备一些api 遍历到目标节点后,需要对目标节点进行删除处理,babel-type提供了一整套能力 github.com/jamiebuilds… 例子:
traverse.default(ast, {
CallExpression(p) {
if (p.container?.property?.name === 'formatMessage') {
const parent = p.parentPath
const { arguments: formatMessageArguments } = parent.container;
if (arguments && arguments.length) {
const params = {};
formatMessageArguments.forEach(node => {
node.properties.forEach(property => {
params[property.key.name] = property.value.value;
});
});
const message = genMessage(params, localeMap);
parent.parentPath.replaceWith(t.identifier(`'${message}'`));
}
}
},
enter(p) {
// import {useIntl} form 'umi'
if (path.isImportDeclaration()) {
const { specifiers } = path.node;
if (path.node.specifiers) {
path.node.specifiers = specifiers.filter(({ imported }) => {
if (imported) {
return imported.name !== 'useIntl';
}
return true;
});
}
}
if (path.node.source?.value === 'umi' && !path.node.specifiers.length) {
path.remove()
return
}
}
})
五、踩坑
- node.container 表示的是node所在的容器节点的内容
- 无法直接删除节点的属性节点
- 先执行enter,在执行其他callback,最后执行exit,不要尝试在父节点处理子节点
- api的入参基本都是节点类型
t.JSXOpeningElement(t.JSXIdentifier('span'), [t.JSXAttribute(t.JSXIdentifier('data-lang-tag'))], true)
- 无法处理动态国际化
六、最佳实践
虽然通过AST可以处理一些代码,但是都是针对某一些具体语法去做匹配,往往在开发过程中会出现n多种语法,无法保证能覆盖到所有需要处理的代码 可以通过给标签加上一个唯一标识,比如
<div data-lang>{formatMessage({id: key})}</div>
<FormatedMessage data-lang/>
所以针对删除国际化这个需求而言,可以对代码编程制定一些规范