【问题标题】:Force React component naming with TypeScript使用 TypeScript 强制 React 组件命名
【发布时间】:2018-08-18 04:55:49
【问题描述】:

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

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

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

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

我相信仅使用 TSLint/ESLint 无法实现这一点。如果对 TypeScript 和 JavaScript 使用不同的方法,两种语言的解决方案都会有所帮助。

【问题讨论】:

    标签: javascript reactjs typescript


    【解决方案1】:

    我只能为您提供打字稿的解决方案。

    我认为仅使用 TSLint/ESLint 无法实现这一点。

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

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

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

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

    [增强] 自定义 lint 规则将使用节点路径解析 允许像 ts-node (#3108) 这样的加载器的分辨率

    我们需要安装ts-node

    npm i -D ts-node
    

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

    "ts-loader": true,
    

    并在我们的 rulesDirectory 中创建文件 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() {}
    };
    

    这基本上是一种广泛用于有角材料、通用等角包的方法

    现在我们可以创建将用打字稿编写的自定义规则(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
    

    将导致:

    我还创建了 ejected react-ts 应用程序,您可以在其中试用。

    更新

    我想跟踪祖父母的班级名称不会是一件小事

    我们确实可以处理继承。为此,我们需要创建从 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();
    }
    

    参见提交:

    【讨论】:

    • 谢谢,这是 TSLint 插件的一个很好的例子。我猜想在祖父母中跟踪类名并不是一件容易的事(这将是一个较小的问题,因为它们在 React 中并不常见)。
    【解决方案2】:

    这在eslint 中更容易做到。自定义插件要简单得多。所以我创建了一个展示相同内容的插件。为了测试插件,我创建了以下文件

    import React from "react"
    
    class ABCComponent extends React.Component {
    
    }
    
    class ABC2component extends React.Component {
    
    }
    
    class TestComponent {
    
    }
    
    
    class FooBarComponent extends React.Component {
    
    }
    
    class fooBazComponent extends React.Component {
    
    }
    
    class FooBazing extends React.Component {
    
    }
    

    然后在相同的地方运行插件

    我在编写插件时遵循了以下指南

    https://flexport.engineering/writing-custom-lint-rules-for-your-picky-developers-67732afa1803

    https://www.kenneth-truyers.net/2016/05/27/writing-custom-eslint-rules/

    https://eslint.org/docs/developer-guide/working-with-rules

    我想出的最终代码如下所示

    /**
     * @fileoverview Check that proper naming convention is followed for React components
     * @author Tarun Lalwani
     */
    "use strict";
    
    //------------------------------------------------------------------------------
    // Rule Definition
    //------------------------------------------------------------------------------
    var toPascalCase = require('to-pascal-case');
    
    module.exports = {
        meta: {
            docs: {
                description: "Check that proper naming convention is followed for React components",
                category: "Fill me in",
                recommended: false
            },
            fixable: "code",  // or "code" or "whitespace"
            schema: [
                // fill in your schema
            ]
        },
    
        create: function(context) {
    
            // variables should be defined here
    
            //----------------------------------------------------------------------
            // Helpers
            //----------------------------------------------------------------------
    
            // any helper functions should go here or else delete this section
    
            //----------------------------------------------------------------------
            // Public
            //----------------------------------------------------------------------
    
            return {
    
                ClassDeclaration: function(node) {
                    var isReactComponent = false;
                    if (node.superClass && node.superClass && node.superClass)
                    {
                        if (node.superClass.object && node.superClass.object.name == 'React' && node.superClass.property.name === 'Component')
                            {
                                isReactComponent = true;
                            }
                        else if (node.superClass && node.superClass.name === 'Component') {
                            // if you want to suppot extends Component instead of just React.Component
                            isReactComponent = true;
                        }
                    }
    
                    if (isReactComponent) {
                        var className = node.id.name;
                        if (className[0] !== className[0].toUpperCase() || !className.endsWith("Component"))
                             context.report({
                                node: node, 
                                message: "Please use Proper case for the React Component class - {{identifier}}",
                                data: {
                                    identifier: className
                                }, fix: (fixer) => {
                                    var newClassName = className.toLowerCase().replace('component', '') + 'Component';
                                    newClassName = toPascalCase(newClassName);
                                    return fixer.replaceTextRange(node.id.range, newClassName)
                                }
                            });
    
                    }
                }
    
            };
        }
    };
    

    关键是理解 AST 树,我使用astexplorer 完成了这项工作。休息代码是非常自我解释的。

    我已经在下面的 repo 上托管了插件,以防你想直接给它一个简短

    https://github.com/tarunlalwani/eslint-plugin-react-class-naming

    使用以下命令安装插件

    npm i tarunlalwani/eslint-plugin-react-class-naming#master
    

    然后将其添加到您的 .eslintrc

    {
        "plugins": [
           "react-class-naming"
        ]
    }
    

    然后在.eslintrc中添加规则

    "rules": {
       "react-class-naming/react-classnaming-convention": ["error"],
       ....
    }
    

    【讨论】:

    • 很好的例子。感谢您在 NPM 上发布它,希望对您有用。
    猜你喜欢
    • 1970-01-01
    • 2020-07-28
    • 2018-08-08
    • 2021-08-30
    • 2021-04-19
    • 2020-11-16
    • 2018-11-16
    • 2021-06-17
    • 2019-06-27
    相关资源
    最近更新 更多