項(xiàng)目中原來有好多_.get(a, `b.c.d`, [])
樣式的代碼房待,看著很不爽践盼,因?yàn)轫?xiàng)目用的Typescript思瘟,這種語(yǔ)法直接把Typescript的類型約束破壞掉了。但又不能把代碼換成:
(a && a.b && a.b.c && a.b.d) || []
所以一直無技可施囚似,當(dāng)然了赏酥,也不是全無辦法,但強(qiáng)推谆构,總會(huì)遇到動(dòng)力,所以想著有沒有更優(yōu)雅的方式解決這個(gè)問題框都。
同事前幾天說Typescript3.7開始支持Optional Chaining和Nullish Coalescing搬素,原來的代碼就可以改成:
a?.b?.c?.d ?? []
具體文檔請(qǐng)查詢Typescript官網(wǎng)。是不是優(yōu)雅了很多魏保,而且完美的保留了類型約束熬尺。但升級(jí)有兩個(gè)問題:
- 但項(xiàng)目上原來的
_.get
怎么辦?只能手工把所有代碼替換掉谓罗,全局搜索后發(fā)現(xiàn)總計(jì)300多處使用粱哼,但為了推一波,沒辦法檩咱。替換的過程中確實(shí)發(fā)現(xiàn)好多地方直接把類型約束去掉了揭措,類型中無這個(gè)字段胯舷,_.get
可以跳過類型約束直接獲取值。 - 怎么防止再增加
_.get
? 團(tuán)隊(duì)每天都進(jìn)行Code Review绊含,但靠人為約束桑嘶,總歸不是個(gè)辦法。所以思考能不能在tslint里加一個(gè)規(guī)則躬充,禁掉一部分lodash的上帝函數(shù)逃顶?再結(jié)合husky,在git commit
階段禁止提交。
注意:盡量不自己造輪子充甚,偶爾折騰一下也是可以的以政,本人不是自己造輪子的激進(jìn)愛好者
查資料
TSLint是如何工作的?
TSLint 用的是 TypeScript AST伴找,語(yǔ)法樹的每個(gè)節(jié)點(diǎn)對(duì)應(yīng)原文件的一小段文字盈蛮,并包含了一些額外信息。學(xué)習(xí)AST(Abstruct Tree)可以查詢相關(guān)書籍疆瑰,或者到A handbook for making programming languages學(xué)習(xí)如何利用AST實(shí)現(xiàn)一門編程語(yǔ)言眉反。可以在AST Explorer上查看相關(guān)語(yǔ)法生成的Tree穆役。
大概意思是寸五,TSLint會(huì)基于整個(gè)Tree去檢查每個(gè)節(jié)點(diǎn)上的語(yǔ)法是否符合規(guī)范。
官方文檔
以下內(nèi)容摘自官網(wǎng)耿币,略作刪減
重要事項(xiàng)
- Rule 標(biāo)識(shí)遵循kebab-cased梳杏。例如:no-lodash-functions
- Rule 文件名遵循camel-cased。 例如:camelCasedRule.ts
- Rule 文件名必須以Rule名為后綴淹接。例如:noLodashFunctions.ts
- Rule 文件必須導(dǎo)出一個(gè)名為Rule的類十性,并且該類繼承
Lint.Rules.AbstractRule
。
示例代碼
import * as Lint from "tslint";
import * as ts from "typescript";
export class Rule extends Lint.Rules.AbstractRule {
public static FAILURE_STRING = "import statement forbidden";
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithWalker(new NoImportsWalker(sourceFile, this.getOptions()));
}
}
// The walker takes care of all the work.
class NoImportsWalker extends Lint.RuleWalker {
public visitImportDeclaration(node: ts.ImportDeclaration) {
// create a failure at the current position
this.addFailure(this.createFailure(node.getStart(), node.getWidth(), Rule.FAILURE_STRING));
// call the base version of this visitor to actually parse this node
super.visitImportDeclaration(node);
}
}
上面是一個(gè)禁止Import
語(yǔ)法的代碼示例塑悼,Typescript中的解析器訪問每個(gè)AST節(jié)點(diǎn)的方式是Visitor模式劲适,所以自定義的Walker只需要重載對(duì)應(yīng)的visitor
(例如visitImportDeclaration
)方法,在里面實(shí)現(xiàn)自己的檢查語(yǔ)法厢蒜。
-
Lint.RuleWalker
提供的基礎(chǔ)方法可以在syntaxWalker中查詢霞势。 - 可以在AST Explorer調(diào)試代碼。
如何生效
其實(shí)有兩種方式斑鸦,但官方只給出了直接編譯導(dǎo)入的方式愕贡,后面我們會(huì)介紹另一種方式。
- 由于TSLint不支持直接ts類型的文件巷屿,所以我們需要先將代碼編譯為es2015版本:
tsc noImportsRule.ts
- 在tslint的配置文件中導(dǎo)入生成的文件, 并配置相應(yīng)規(guī)則:
{
"extends":[],
"rules": {
"no-import-rule": true
},
//省略其它配置固以,只要知道在什么層級(jí)配置就好
//./tslint-rules/lib是tsc編譯后輸出的目錄
"rulesDirectory": ["./tslint-rules/lib"]
}
如何添加相應(yīng)的Fix代碼
// create a fixer for this failure
const fix = new Lint.Replacement(node.getStart(), node.getWidth(), "");
// create a failure at the current position
this.addFailure(this.createFailure(node.getStart(), node.getWidth(), Rule.FAILURE_STRING, fix));
在addFailure時(shí),添加對(duì)應(yīng)的fix規(guī)則
最后提示
- 核心規(guī)則不能被自定義規(guī)則覆蓋(overwritten)
- 自定義規(guī)則可以通過
this.getOptions()
獲取相關(guān)配置 - 在TSLint 5.7.0版本之后可以不編譯
.ts
文件了嘱巾。但需要指明如何加載.ts
文件憨琳。例如使用ts-node:
ts-node node_modules/.bin/tslint <your options>
# 或者
node -r ts-node/register node_modules/.bin/tslint <your options>
# 或者
NODE_OPTIONS="-r ts-node/register" tslint <your options>
實(shí)戰(zhàn)
第一版 仿官方版
Rule文件 noLodashFuctions.ts
將Rule文件放置到tslint-rules/src目錄下诫钓。因?yàn)橹皇墙靡徊糠趾瘮?shù),所以規(guī)則很好寫栽渴,大概思路是在調(diào)用函數(shù)時(shí)尖坤,判斷是不是調(diào)用的_.get
函數(shù)(文件中做了增強(qiáng),把functionNames做為了參數(shù))闲擦。具體的AST Node怎么找到的慢味,可以直接在AST Explorer上調(diào)試。
export class Rule extends Lint.Rules.AbstractRule {
public static metadata: Lint.IRuleMetadata = {
description: 'Disallows the functions in the lodash library.',
options: {
oneOf: [{ type: 'array', items: { type: 'string' } }, { type: 'string' }]
},
optionsDescription: 'the function name(s).',
requiresTypeInfo: true,
ruleName: 'no-lodash-functions',
type: 'maintainability',
typescriptOnly: false
};
public static GET_FAILURE_MESSAGE = (functionName: string) =>
`The "${functionName}" in the lodash library is not allowed to be used in this project.`;
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithWalker(new NoLodashFunctionsRuleVisitor(sourceFile, this.getOptions()));
}
}
// tslint:disable-next-line:max-classes-per-file
class NoLodashFunctionsRuleVisitor extends Lint.RuleWalker {
private functionNames: Set<string>;
constructor(sourceFile: ts.SourceFile, options: IOptions) {
super(sourceFile, options);
this.functionNames = new Set<string>(options.ruleArguments);
}
protected visitCallExpression(node: ts.CallExpression): void {
if (node.expression.kind === ts.SyntaxKind.PropertyAccessExpression) {
const propertyAccessExpression = (node as ts.CallExpression).expression as ts.PropertyAccessExpression;
if (
this.functionNames.has(propertyAccessExpression.name.text) &&
(propertyAccessExpression.expression as ts.Identifier).text === '_'
) {
this.addFailureAtNode(propertyAccessExpression.name, Rule.GET_FAILURE_MESSAGE(propertyAccessExpression.name.text));
}
}
}
}
編譯
在tslint-rules目錄下添加tsconfig.json, 主要是為了將代碼編譯為es2015的版本:
{
// 省略n多配置
"compilerOptions": {
"target": "es2015",
"outDir": "./lib"
},
"include": [
"./src/"
]
}
在項(xiàng)目的package.json中添加編譯命令墅冷,主要是為了復(fù)用:
"lint-rules-compile": "tsc -p ./tslint-rules"
運(yùn)行編譯命令
yarn run lint-rules-compile
成功編譯后纯路,可以看到tslint-rules/lib中已經(jīng)存在編譯后的文件
導(dǎo)入并配置
在tslint.json中添加如下配置:
{
"rules": {
"no-lodash-functions": [true, "get"]
},
"rulesDirectory": ["./tslint-rules/lib"]
}
效果
在webStorm中可以看到:
問題
- 經(jīng)過多次嘗試,發(fā)現(xiàn)增加規(guī)則后寞忿,打開ts(x)等文件速度明顯變慢驰唬,webstorm反應(yīng)變慢。
- 查詢?cè)创a發(fā)現(xiàn)Lint中的
RuleWalker
已經(jīng)標(biāo)記棄用腔彰。文檔:
/**
* @deprecated
* RuleWalker-based rules are slow,
* so it's generally preferable to use applyWithFunction instead of applyWithWalker.
* @see https://github.com/palantir/tslint/issues/2522
*/
- 官網(wǎng)給出了一些性能優(yōu)化方面的提示:Performance Tip(https://palantir.github.io/tslint/develop/custom-rules/performance-tips.html)
第二版
解決第一版問題
官網(wǎng)中的Performance Tip中有重要提示Implement your own walking algorithm, 文檔如下:
Convenience comes with a price. When using SyntaxWalker or any subclass thereof like RuleWalker you pay the price for the big switch statement in visitNode which then calls the appropriate visitXXX method for every node in the AST, even if you don’t use them.
Use AbstractWalker instead and implement the walk method to fit the needs of your rule. It’s as simple as this:
大意是說叫编,visitXXXX類的方法會(huì)訪問每一個(gè)AST節(jié)點(diǎn),可以用AbstractWalker
去定義適合自己的算法霹抛,防止遍歷所有節(jié)點(diǎn)搓逾。
認(rèn)真思考了下該問題,做為一個(gè)TSLint新手杯拐,可不可以借鑒TSLint核心規(guī)則中的一些寫法霞篡,來優(yōu)化當(dāng)前代碼呢?例如端逼,no-console-log和現(xiàn)在的算法就很相近朗兵,能不能抄一抄?畢竟抄代碼是程序員生存的第一技能顶滩。
注意:作者不是無腦抄選手余掖,也不建議大家無腦抄。我們要抄的有理有據(jù)礁鲁,并稱其為“借鑒”盐欺。
翻閱TSLint 核心Rule的源碼, 找到noConsoleRule.ts文件,動(dòng)手實(shí)現(xiàn)第二版代碼救氯。noConsoleRule的代碼請(qǐng)自行查閱。
實(shí)現(xiàn)(仿noConsoleRule)
import * as ts from 'typescript';
import * as Lint from 'tslint';
import { isPropertyAccessExpression, isIdentifier } from 'tsutils';
export class Rule extends Lint.Rules.AbstractRule {
// 省略metadata和GET_FAILURE_MESSAGE
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithFunction<Set<string>>(sourceFile, walk, new Set<string>(this.getOptions().ruleArguments));
}
}
function walk(ctx: Lint.WalkContext<Set<string>>) {
return ts.forEachChild(ctx.sourceFile, function cb(node): void {
if (
isPropertyAccessExpression(node) &&
isIdentifier((node as ts.PropertyAccessExpression).expression) &&
((node as ts.PropertyAccessExpression).expression as ts.Identifier).text === '_' &&
ctx.options.has((node as ts.PropertyAccessExpression).name.text)
) {
ctx.addFailureAtNode(
(node as ts.PropertyAccessExpression).name,
Rule.GET_FAILURE_MESSAGE((node as ts.PropertyAccessExpression).name.text)
);
}
return ts.forEachChild(node, cb);
});
}
延伸閱讀
另一種導(dǎo)入模式:發(fā)布到repository
其實(shí)可以把rule文件單獨(dú)放到一個(gè)工程里歌憨,然后編譯發(fā)布到私有或者公有的npm repository中, 在tslint.json中可以像如下方式導(dǎo)入:
{
"extends": ["tslint:latest", "tslint-eslint-rules", "tslint-react", "tslint-config-prettier"]
}
開源項(xiàng)目 You-Dont-Need-Lodash-Underscore
在我實(shí)現(xiàn)自定義的Lint規(guī)則后着憨,我的同事(就前面那個(gè)同事,沒錯(cuò)务嫡,還是他甲抖,項(xiàng)目上的好兄弟-強(qiáng)哥)給我推了這個(gè)項(xiàng)目You-Dont-Need-Lodash-Underscore, 項(xiàng)目介紹如下:
Lodash and Underscore are great modern JavaScript utility libraries, and they are widely used by Front-end developers. However, when you are targeting modern browsers, you may find out that there are many methods which are already supported natively thanks to ECMAScript5 ES5 and ECMAScript2015 ES6. If you want your project to require fewer dependencies, and you know your target browser clearly, then you may not need Lodash/Underscore.
大意是漆改,Lodash和Underscore是很好的JavaScript庫(kù),但是其中的好多方法在ES5/ES6/Typescript中已經(jīng)實(shí)現(xiàn)准谚,本著更少依賴和可讀性的原則挫剑,我們可能不再需要Lodash/Underscore中的部分函數(shù)。
Readme中還列舉了一些大牛開發(fā)者的聲音柱衔,感興趣的可以上去看一下樊破。
思考:Typescript和Lodash
首先說明Typescript和Lodash并不沖突,而且Lodash也有type定義唆铐,lodash中確實(shí)提供了一些不錯(cuò)的方法哲戚,例如chain
可以優(yōu)化一些流式計(jì)算速度,例如a.filter().map().filter()
艾岂。
但為什么我們有時(shí)候會(huì)不提倡lodash顺少?
我們引入Typescript的一個(gè)很大原因就是類型約束,但loadsh中一些過于方便的上帝方法會(huì)將這層約束破壞掉王浴。在這個(gè)層面上我們認(rèn)為類型約束要比便利性更重要脆炎。抉擇本來就是一個(gè)Balance,如果在Team上對(duì)于一些技術(shù)的認(rèn)知能達(dá)到一定的共識(shí)氓辣,那么我們就要遵守秒裕。而且Typescript也在向著便利性邁進(jìn),比如支持Optional Chaining和Nullish Coalescing筛婉。
lodash中的很多函數(shù)簇爆,ts中本來就支持,我們沒必要引入過多的依賴爽撒,主要是代碼也不太優(yōu)雅入蛆,一屏幕的"_"。有些過于便利的函數(shù)硕勿,可能本身就是個(gè)黑盒哨毁,由于我們對(duì)其的不了解,在一些特殊情況下源武,會(huì)產(chǎn)生一些莫名其妙的Bug扼褪,例如_.isEmpty(2)
。
在某些情況下引入lodash可能是為了更語(yǔ)義化粱栖,但有時(shí)候真的更語(yǔ)義化嗎话浇?還是說引入了一個(gè)黑盒?我們不得不承認(rèn)在引入“語(yǔ)義化”的同時(shí)闹究,增加了“認(rèn)知成本”幔崖。