Description
盛夏到初秋,这次没有坐在窗边看不到磅礴的雨,只是这个夏天也少了烈日的暴晒,就这么过去了。去年台风天的黄昏,很好看,还拍了不少照片,到了今年,左等右等的,结果台风一刮,夏天就走了。2021好像也快要过去了。今年没有专门学习新的技术,中途换工作的,从2月拖到了6月,中间又忙了工作,梳理业务的,难得现在又空总结一下自己做的多语言。
多语言与 eslint
多语言为什么和 eslint
相关?通过 eslint
规则来限制业务开发,凡是涉及到中文的,都应该有多语言的约束,否则提示 error
。这样就很强约束了,在多人协作的时候尤其重要,保持团队的统一风格。
这里说一下这个功能: 凡是涉及到中文的,都应该有多语言的约束。在 eslint
里面,这样的规则称之为 rule
,比如我们经常看到的 no-debugger
禁止使用 debugger,就是这样的。最后实现效果是这样的:当输入中文变量包括 jsx
里面存在非法中文的时候,都会提示报红:
加上代码提交之前的 lint-staged
检查就可以统一规范了。
通用规则实现
在看多语言的 eslint
的规则实现之前可以看一下普通规则是如何的,我们就按上面的 no-debugger
来看看,这个是 eslint
内部的实现,用户只需要配置就好了,代码如下:
module.exports = {
meta: {
type: "problem",
docs: {
description: "disallow the use of `debugger`",
recommended: true,
url: "https://eslint.org/docs/rules/no-debugger"
},
fixable: null,
schema: [],
messages: {
unexpected: "Unexpected 'debugger' statement."
}
},
create(context) {
return {
DebuggerStatement(node) {
context.report({
node,
messageId: "unexpected"
});
}
};
}
};
eslint/lib/rules/no-debugger.js
这个文件蛮简单的,可以看出下面有个 meta
和 create
,前者不难猜出是配置信息,包括提示信息以及是否推荐这些;后面的则是出现 debugger
语句的时候就提示报错 context.report
。是不是很简单?
回过头来可以看一下 eslint rule
官方教程,可以看到 recommended
就是是否在 eslint:recommended
扩展里面启用。最重要的 create
方法,eslint
会去遍历代码的抽象语法树 AST,调用 rule 里面注册的方法检测节点,上面则是 degugger 节点。
函数名是自己随便定义的?不是的,需要是对应的节点,可以通过 esprima 来解析来获得需要的函数名,比如下图,先有变量声明 VariableDeclaration
后有 DebuggerStatement
。
如果你把 no-debugger
中的 DebuggerStatement
换成 VariableDeclaration
,那每次声明变量都会异常。提到 AST 解析还是要说一下,espree
是 eslint
的官方解析器,基于 esprima
,并且输出结构相似。
钩子实现上面的 no-debugger
比较简单,遇到 DebuggerStatement
直接 context.report
,就完成了整个上报过程了。
其他需要主要的是规则代码的位置需要在 lib/rules/
下面,同时要有 tests/lib/rules
以及对应的文档。
多语言规则实现
通过上文 create
提示的实现,先是梳理常见的中文都是哪些节点,比如最常见的 let a = '中文'
,在 esprima
里面查询到的节点类型为 Literal
;其他的还有两大块一个是 jsx
里面的文字,比如 <div>中文</div>
这样的,还有一个是字符串模版变量,比如 let a = 中${name}文
。前者可以通 JSXText
获取,后则稍微麻烦点,下面重点介绍一下:
模版字符串
模版字符串解析的下图:
可以看到存在 quasis
和 expressions
两个数组,前者是字符串信息,后面是表达式。i18next
本身是支持变量传入的,比如 i18next.t('中{{value}}文', { value: name })
,变量从第二个参数里面传入,生成文案替换。由于表达式不一定是变量,可以采用 value
名字自动生成名称。
字段名称是生成了,后续还要获取到字段名称对应的 value
也就是 name
这个字符串。存在变量名的可以直接获取,其他的比如 i18next.t('中{{value}}文', { value: 1 + 1 })
,要如何获取到后面的 1 + 1
呢?可以有如下写法
const value = context.getSourceCode().getText().slice(expression.range[0], expression.range[1]);
在 eslint
规则里面会传入 context
上下文,通过上下文可以获取到非常多的信息,比如上面的获取全文字符串,再通过 range
,定位到具体的内容。
忽略与修复
正常代码里面中文也是会有需要的地方,比如上报日志,埋点信息这些,甚至更加根本的如何识别 i18next.t('中文')
这样的表达式不报错呢?这样的表达式已经是正常写法了。所以这里需要 ignore
正常表达式。
进入节点通过匹配白名单的形式可以过滤掉,只是就上文的 i18next.t('中文')
匹配了函数,但是文案又是另外一个节点,如何做到两个关联到一起呢?这里就需要额外数组来记录当前关系,比如进入 i18next.t
函数的时候,标记为 true
,后面进入函数形参的时候,检查标记,为 true
就忽略了。只是什么时候退出呢?这里有另外一个钩子 CallExpression:exit
。由于存在多个嵌套的可能,所以用的是一个数组来维护,当 exit
的时候退出。
上面的例子,是遇到 debugger
的时候报错,那如何 fix
呢,eslint
有修复功能。context.report
里面的传参 fixer
函数,提供了不少方法。
context.report({
node,
message,
fix(fixer) {
return fixer.replaceText(
node,
`${useCallee}('${textReplacer(node.value)}')`
);
}
});
还用到了 fixer.replaceTextRange
,来精准替换。
存量代码处理
对于老项目,直接用多语言的主要问题是,现有文案如何处理,要一个个替换显然不合理。最先的 i18next
提供了 i18next-scanner
工具,只是起更多的是将现有的 i18n._('Loading...')
这样的实现提出文案到 json
文件里面。还有其他思路比如通过 babel
的形式来解析,还有通过正则的形式来处理,两个方案都比较麻烦,容易出问题,于是我们这边有新的方案,本来也是通过 eslint
的规则来处理,那为什么不用 eslint
的解析器来提取词条呢?
require('eslint').CLIEngine(options).executeOnFiles
获取到 eslint
的分析结果,当然需要提前配置好规则,比如 i18next/no-literal-string
这个。后续获取到结果后,将其输出为转换为 json
文件输出就可以了,可以说这个规则一举两得。
单测
用的是 require("eslint").RuleTester
比如下面:
var ruleTester = new RuleTester({
parser: require.resolve("babel-eslint"),
parserOptions: {
sourceType: "module",
ecmaFeatures: {
jsx: true
}
}
});
ruleTester.run("多语言规则测试", rule, {
// 写 case valid 和 invalid
})
debug 的时候采用 create javascript debug terminal
的方式就可以了。
总结
这次算是深入的研究 eslint
了,从单个规则到引擎扫描,还有测试,打开了自己了新天地,尤其是 eslint
对团队规范的统一还是有很大帮助,后续的还可以添加 typescript
帮助,将 json
文件改为 ts
,在文案里面缺少该词条就报错的,建立更加完整的机制。
在三亚的酒店的窗边,眺望着远处的海岸线,觉得写写博客,旅游,生活可好了,只是心中想的还是想要进击,想要不断的提升自己,偶得半天安宁,还是好好偷闲。
惭愧这边文章是 2021.10 就写好了,结果拖到今天才发表,不过说来也奇怪感觉时间好像没有过多少一样,好像去三亚还是昨天的事情。