javascript - 使用 TypeScript 强制 React 组件命名

标签 javascript reactjs typescript

有React+TypeScript的应用,所有的组件类都要大写,有Component后缀,例如:

export class FooBarComponent extends React.Component {...}

应用程序被弹出 create-react-application 应用程序,即使用 Webpack 构建。

如何强制组件命名与样式指南保持一致,至少对于组件类而言,当存在不一致时会在构建时抛出错误?

我相信这不能单独使用 TSLint/ESLint 来实现。如果应该对 TypeScript 和 JavaScript 使用不同的方法,那么针对这两种语言的解决方案会很有帮助。

最佳答案

我只能为您提供 typescript 的解决方案。

I believe this cannot be achieved with TSLint/ESLint alone.

有一个所谓的规则class-name可以部分解决您的问题,但似乎您需要为这种情况编写自定义规则。

所以让我们试着写这样的 custom tslint rule .为此,我们需要在 tslint 配置中使用 rulesDirectory 选项来指定自定义规则的路径

"rulesDirectory": [
    "./tools/tslint-rules/"
],

因为我要在 typescript 中编写自定义规则,所以我将使用在 tslint@5.7.0 中添加的一项功能

[enhancement] custom lint rules will be resolved using node's path resolution to allow for loaders like ts-node (#3108)

我们需要安装ts-node

npm i -D ts-node

然后在tslint.json中添加fake规则

"ts-loader": true,

并在我们的规则目录中创建文件 tsLoaderRule.js:

const path = require('path');
const Lint = require('tslint');

// Custom rule that registers all of the custom rules, written in TypeScript, with ts-node.
// This is necessary, because `tslint` and IDEs won't execute any rules that aren't in a .js file.
require('ts-node').register({
    project: path.join(__dirname, '../tsconfig.json')
});

// Add a noop rule so tslint doesn't complain.
exports.Rule = class Rule extends Lint.Rules.AbstractRule {
    apply() {}
};

这基本上是一种广泛用于 Angular 包(如 Angular Material 、通用等)的方法

现在我们可以创建将用 typescript 编写的自定义规则(class-name 规则的扩展版本)。

myReactComponentRule.ts

import * as ts from 'typescript';
import * as Lint from 'tslint';

export class Rule extends Lint.Rules.AbstractRule {
  /* tslint:disable:object-literal-sort-keys */
  static metadata: Lint.IRuleMetadata = {
    ruleName: 'my-react-component',
    description: 'Enforces PascalCased React component class.',
    rationale: 'Makes it easy to differentiate classes from regular variables at a glance.',
    optionsDescription: 'Not configurable.',
    options: null,
    optionExamples: [true],
    type: 'style',
    typescriptOnly: false,
  };
  /* tslint:enable:object-literal-sort-keys */

  static FAILURE_STRING = (className: string) => `React component ${className} must be PascalCased and prefixed by Component`;

  static validate(name: string): boolean {
    return isUpperCase(name[0]) && !name.includes('_') && name.endsWith('Component');
  }

  apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
    return this.applyWithFunction(sourceFile, walk);
  }
}

function walk(ctx: Lint.WalkContext<void>) {
  return ts.forEachChild(ctx.sourceFile, function cb(node: ts.Node): void {
    if (isClassLikeDeclaration(node) && node.name !== undefined && isReactComponent(node)) {
      if (!Rule.validate(node.name!.text)) {
        ctx.addFailureAtNode(node.name!, Rule.FAILURE_STRING(node.name!.text));
      }
    }
    return ts.forEachChild(node, cb);
  });
}

function isClassLikeDeclaration(node: ts.Node): node is ts.ClassLikeDeclaration {
  return node.kind === ts.SyntaxKind.ClassDeclaration ||
    node.kind === ts.SyntaxKind.ClassExpression;
}

function isReactComponent(node: ts.Node): boolean {
  let result = false;
  const classDeclaration = <ts.ClassDeclaration> node;
  if (classDeclaration.heritageClauses) {
    classDeclaration.heritageClauses.forEach((hc) => {
      if (hc.token === ts.SyntaxKind.ExtendsKeyword && hc.types) {

        hc.types.forEach(type => {
          if (type.getText() === 'React.Component') {
            result = true;
          }
        });
      }
    });
  }

  return result;
}

function isUpperCase(str: string): boolean {
  return str === str.toUpperCase();
}

最后,我们应该将新规则放入 tsling.json:

// Custom rules
"ts-loader": true,
"my-react-component": true

所以这样的代码

App extends React.Component

将导致:

enter image description here

我还创建了 ejected react-ts 可以试用的应用程序。

更新

I guess tracking class names in grandparents won't be a trivial task

我们确实可以处理继承。为此,我们需要创建从类 Lint.Rules.TypedRule 扩展的规则,以访问 TypeChecker:

myReactComponentRule.ts

import * as ts from 'typescript';
import * as Lint from 'tslint';

export class Rule extends Lint.Rules.TypedRule {
  /* tslint:disable:object-literal-sort-keys */
  static metadata: Lint.IRuleMetadata = {
    ruleName: 'my-react-component',
    description: 'Enforces PascalCased React component class.',
    rationale: 'Makes it easy to differentiate classes from regular variables at a glance.',
    optionsDescription: 'Not configurable.',
    options: null,
    optionExamples: [true],
    type: 'style',
    typescriptOnly: false,
  };
  /* tslint:enable:object-literal-sort-keys */

  static FAILURE_STRING = (className: string) =>
    `React component ${className} must be PascalCased and prefixed by Component`;

  static validate(name: string): boolean {
    return isUpperCase(name[0]) && !name.includes('_') && name.endsWith('Component');
  }

  applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
    return this.applyWithFunction(sourceFile, walk, undefined, program.getTypeChecker());
  }
}

function walk(ctx: Lint.WalkContext<void>, tc: ts.TypeChecker) {
  return ts.forEachChild(ctx.sourceFile, function cb(node: ts.Node): void {
    if (
        isClassLikeDeclaration(node) && node.name !== undefined &&
        containsType(tc.getTypeAtLocation(node), isReactComponentType) &&
        !Rule.validate(node.name!.text)) {
      ctx.addFailureAtNode(node.name!, Rule.FAILURE_STRING(node.name!.text));
    }

    return ts.forEachChild(node, cb);
  });
}
/* tslint:disable:no-any */
function containsType(type: ts.Type, predicate: (symbol: any) => boolean): boolean {
  if (type.symbol !== undefined && predicate(type.symbol)) {
    return true;
  }

  const bases = type.getBaseTypes();
  return bases && bases.some((t) => containsType(t, predicate));
}

function isReactComponentType(symbol: any) {
  return symbol.name === 'Component' && symbol.parent && symbol.parent.name === 'React';
}
/* tslint:enable:no-any */

function isClassLikeDeclaration(node: ts.Node): node is ts.ClassLikeDeclaration {
  return node.kind === ts.SyntaxKind.ClassDeclaration ||
    node.kind === ts.SyntaxKind.ClassExpression;
}

function isUpperCase(str: string): boolean {
  return str === str.toUpperCase();
}

另见提交:

关于javascript - 使用 TypeScript 强制 React 组件命名,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49196812/

相关文章:

javascript - 在 Scala.js 中,如何切换类中元素的可见性?

javascript - 使用 React.js 从 map 函数中删除动态添加的项目

reactjs - 使用 Formik 降档自动完成 onBlur 重置值

reactjs - 未捕获的类型错误 : Cannot read property 'mountComponent' of undefined error when running create-react-app build version

javascript - 找不到名称“withStyles”

angular - 在 Ionic 中传递来自不同页面的数据

typescript - 如何推断实现接口(interface)的类类型

javascript - 在 Google API 上使用 LatLng 获取街景图像

javascript - 负十进制数和正十进制数的正则表达式

javascript - 动画性能 jQuery、css transition ?我怎样才能在这里有更好的表现?