
盛夏到初秋,这次没有坐在窗边看不到磅礴的雨,只是这个夏天也少了烈日的暴晒,就这么过去了。去年台风天的黄昏,很好看,还拍了不少照片,到了今年,左等右等的,结果台风一刮,夏天就走了。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 就写好了,结果拖到今天才发表,不过说来也奇怪感觉时间好像没有过多少一样,好像去三亚还是昨天的事情。
盛夏到初秋,这次没有坐在窗边看不到磅礴的雨,只是这个夏天也少了烈日的暴晒,就这么过去了。去年台风天的黄昏,很好看,还拍了不少照片,到了今年,左等右等的,结果台风一刮,夏天就走了。2021好像也快要过去了。今年没有专门学习新的技术,中途换工作的,从2月拖到了6月,中间又忙了工作,梳理业务的,难得现在又空总结一下自己做的多语言。
多语言与 eslint
多语言为什么和
eslint相关?通过eslint规则来限制业务开发,凡是涉及到中文的,都应该有多语言的约束,否则提示error。这样就很强约束了,在多人协作的时候尤其重要,保持团队的统一风格。这里说一下这个功能: 凡是涉及到中文的,都应该有多语言的约束。在
eslint里面,这样的规则称之为rule,比如我们经常看到的no-debugger禁止使用 debugger,就是这样的。最后实现效果是这样的:当输入中文变量包括jsx里面存在非法中文的时候,都会提示报红:加上代码提交之前的
lint-staged检查就可以统一规范了。通用规则实现
在看多语言的
eslint的规则实现之前可以看一下普通规则是如何的,我们就按上面的no-debugger来看看,这个是eslint内部的实现,用户只需要配置就好了,代码如下: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呢?可以有如下写法在
eslint规则里面会传入context上下文,通过上下文可以获取到非常多的信息,比如上面的获取全文字符串,再通过range,定位到具体的内容。忽略与修复
正常代码里面中文也是会有需要的地方,比如上报日志,埋点信息这些,甚至更加根本的如何识别
i18next.t('中文')这样的表达式不报错呢?这样的表达式已经是正常写法了。所以这里需要ignore正常表达式。进入节点通过匹配白名单的形式可以过滤掉,只是就上文的
i18next.t('中文')匹配了函数,但是文案又是另外一个节点,如何做到两个关联到一起呢?这里就需要额外数组来记录当前关系,比如进入i18next.t函数的时候,标记为true,后面进入函数形参的时候,检查标记,为true就忽略了。只是什么时候退出呢?这里有另外一个钩子CallExpression:exit。由于存在多个嵌套的可能,所以用的是一个数组来维护,当exit的时候退出。上面的例子,是遇到
debugger的时候报错,那如何fix呢,eslint有修复功能。context.report里面的传参fixer函数,提供了不少方法。还用到了
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比如下面:debug 的时候采用
create javascript debug terminal的方式就可以了。总结
这次算是深入的研究
eslint了,从单个规则到引擎扫描,还有测试,打开了自己了新天地,尤其是eslint对团队规范的统一还是有很大帮助,后续的还可以添加typescript帮助,将json文件改为ts,在文案里面缺少该词条就报错的,建立更加完整的机制。在三亚的酒店的窗边,眺望着远处的海岸线,觉得写写博客,旅游,生活可好了,只是心中想的还是想要进击,想要不断的提升自己,偶得半天安宁,还是好好偷闲。
惭愧这边文章是 2021.10 就写好了,结果拖到今天才发表,不过说来也奇怪感觉时间好像没有过多少一样,好像去三亚还是昨天的事情。