关于javascript:打造自定义-eslint

4次阅读

共计 9759 个字符,预计需要花费 25 分钟才能阅读完成。

起因

在多人合作项目中, eslint 和 prettier 是不用能短少的, 他能帮忙你, 对立标准某一事物, 某一个办法的应用

然而有时候也并不尽如人意, 有些标准官网包没提供, 然而咱们也须要进行肯定的标准, 这个时候靠人工 code review 是不牢靠的了

所以咱们须要的是自定义 eslint , 来标准咱们的开发

原理

ast 语法树

其实 eslint 的原理就是根据于 ast 语法树, 对于他这一块 能够参阅我之前写的文章: babel 与 ast

初始化

首先咱们须要创立一个仓库来存储咱们的代码, 我取名为: @grewer/eslint-plugin-rn

应用指令来初始化我的项目:

npx -p yo -p generator-eslint yo eslint:plugin

在这咱们能够在 package.json 批改咱们的项目名称和配置, 比方我就将我的项目名改成这样:
eslint-plugin-rn => @grewer/eslint-plugin-rn

再执行 yarn 指令(或者 npm i) 下载咱们须要的依赖

这时能够将 generator-eslint 放入我的项目的 devDependencies 中不便咱们应用,

yarn add -D generator-eslint

到这为止我的项目的初始化正式实现

创立

将指令 npx yo eslint:plugin 放入 scripts 中:

{
  "scripts": {
    "test": "mocha tests --recursive",
+    "create:rule": "npx yo eslint:rule"
  }
}

开始执行指令: yarn create:rule:

成果如图:

  1. What is your name? 作者名称, 填本人名字就行
  2. Where will this rule be published? (Use arrow keys) 抉择哪个生成的文件都一样, 所以回车即可
  3. What is the rule ID? rule 的名称, 如: no-inner-style
  4. Type a short description of this rule: 轻易填就行, 也能够间接回车
  5. Type a short example of the code that will fail: 失败的代码状况, 因为在终端里打比拟麻烦, 间接回车跳过, 前面补充

创立实现后的文件目录:

├── README.md
├── docs
│   └── rules
│       └── no-inner-style.md
├── lib
│   ├── index.js
│   └── rules
│       └── no-inner-style.js
├── package.json
├── tests
│   └── lib
│       └── rules
│           └── no-inner-style.js
└── yarn.lock

例子

咱们须要在 lib/rules/no-inner-style.js 文件中填写咱们的逻辑

module.exports = {
    meta: {
        docs: {
            description: "jsx render cannot write style",
            category: "Fill me in",
            recommended: false
        },
        fixable: null,  // 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 {// give me methods};
    }
};

首先要有一个思路, 我的目标是 render 中的 jsx 不能写行内款式, 咱们要做的就是在 jsx 的属性中检测 style 这个属性是否存在, 他是否是一个 object 格局

当然也存在非凡状况, 比方 style 中的 width 会依据变量变动 {width:flag?100:0}, 或者动画方面的一些变动, 所以在判断的时候须要留神

那问题来了, 咱们怎么寻找 jsx 中的 style 属性呢

这里咱们须要一些工具了, 上面我举荐两个网址(他们的性能都是一样的)

  • https://lihautan.com/babel-as…
  • https://astexplorer.net/

工具的应用

首先咱们须要两段合乎场景的代码:

// 这就是非凡的状况, 这种状况下咱们不会禁止行内款式
class TestView extends Component {render() {
        const mode = 'dark';
        return (<View style={{flex: 1, width: 200, color: mode === 'dark' ? '#000' : '#fff'}}>
            </View>
        )
    }
}

// 这是须要禁止的状况
class TestView extends Component {render() {
        const mode = 'dark';
        return (<View style={{flex: 1, width: 200}}>
            </View>
        )
    }
}

将须要禁止的状况放入, 上述的网站中(如果报错, 须要批改配置, 使得他反对 jsx)

将鼠标光标放到 style 标签上, 如图:

通过网站的解析能够看到, style 在 ast 中的属性是叫 JSXAttribute

所以咱们将它作为键, 增加办法:


module.exports = {
    meta: {// 省略, 不扭转},
    create: function (context) {
        // 省略这里的正文
        return {
            // 将 JSXAttribute 作为键
            JSXAttribute: node => {// 留神 如果这里写不上来了, 能够间接跳到下一步 (调试) 中

                const propName = node.name && node.name.name;
                // console.log(propName)

                // 先判断 style 是否存在
                if (propName === 'style') {

                    // node.value.expression.type 这个门路, 在 ast 网站中能够疾速找到, ObjectExpression 也一样
                    // 意思是 当 style 的值是一个 Object 的时候, 抛出信息
                    if (node.value.expression.type === 'ObjectExpression') {

                        // context.report 的作用就是抛出信息, node 参数 就是抛出信息的节点在那个地位
                        context.report({
                            node,
                            message: "不要应用行内款式",
                        });
                        // TODO 留神!  这里咱们还没思考非凡状况
                    }
                }
            }
        }
    }
};

到这一步咱们曾经抓到了 ast 的精华所在了

调试 & 测试

这里咱们就须要测试文件来调试咱们的规定了, 关上文件 tests/lib/rules/no-inner-style.js:

"use strict";

var rule = require("../../../lib/rules/no-inner-style"),

    RuleTester = require("eslint").RuleTester;

var ruleTester = new RuleTester();
ruleTester.run("no-inner-style", rule, {

    valid: [// give me some code that won't trigger a warning],

    // 先将咱们刚刚的例子代码上到这里的 code 中
    invalid: [
        {
            code: `
            class TestView extends Component{render() {
                    const mode = 'dark';
                    return (<View style={{flex:1, width: 200}} >
                        </View>
                    )
                }
            }
            `,
            errors: [{message: "不要应用行内款式",}]
        }
    ]
});

填充 codeerrors 中的 message

在终端中应用 node 指令运行: node tests/lib/rules/no-inner-style.js

然而 , 她会报出谬误:
AssertionError [ERR_ASSERTION]: A fatal parsing error occurred: Parsing error: The keyword 'class' is reserved
起因是在于执行的环境问题

test 配置

tests/lib 中增加文件 config.js, 门路为(tests/lib/config.js)

const testConfig = {
    env: {es6: true},
    parserOptions: {
        ecmaFeatures: {jsx: true,},
        ecmaVersion: 2021,
        sourceType: 'module',
    },
}

module.exports = {testConfig}

tests/lib/rules/no-inner-style.js 中引入和应用:

var rule = require("../../../lib/rules/no-inner-style"),

    RuleTester = require("eslint").RuleTester;

// 引入
const {testConfig} = require("../config.js");

// 应用
var ruleTester = new RuleTester(testConfig);
ruleTester.run("no-inner-style", rule, {valid: [],
    invalid: [
        {// 因为没变动所以此处省略}
    ]
});

到此, 咱们再度执行指令: node tests/lib/rules/no-inner-style.js

如果验证胜利 (即 context.report 抛出的 message 和 测试用例中的 message 相等) 就不会有任何返回信息

如果验证失败则会打印出起因

增加非凡状况

之前咱们就说到了 style 的非凡状况, 在这种有变量的状况下, 咱们是不会再抛出错误信息的

当初将代码复制到测试用例文件中的 valid 中:

var rule = require("../../../lib/rules/no-inner-style"),

    RuleTester = require("eslint").RuleTester;

// 引入
const {testConfig} = require("../config.js");

// 应用
var ruleTester = new RuleTester(testConfig);
ruleTester.run("no-inner-style", rule, {

    valid: [
        `
        class TestView extends Component {render() {
                const mode = 'dark';
                return (<View style={{flex: 1, width: 200, color: mode === 'dark' ? '#000' : '#fff'}}>
                    </View>
                )
            }
        }
        `
    ],
    invalid: [
        {// 因为没变动所以此处省略}
    ]
});

这时候咱们再执行指令: node tests/lib/rules/no-inner-style.js 就会发现报错了, 当然这是咱们曾经预料到的非凡状况:

AssertionError [ERR_ASSERTION]: Should have no errors but had 1: [
  {
    ruleId: 'no-inner-style',
    severity: 1,
    message: '不要应用行内款式',
    line: 6,
    column: 27,
    nodeType: 'JSXAttribute',
    endLine: 6,
    endColumn: 98
  }
]

这时候再来批改咱们的规定文件 lib/rules/no-inner-style.js :


module.exports = {
    meta: {// 省略没有变动的内容},
    create: function (context) {
        return {
            JSXAttribute: node => {
                const propName = node.name && node.name.name;
                if (propName === 'style') {
                    // 能够通过执行  `node tests/lib/rules/no-inner-style.js` 和 console 来调试以后程序
                    // console.log(node.value.expression)
                    if (node.value.expression.type === 'ObjectExpression') {
                        // const arr = node.value.expression.properties
                        // 如果 style 中有表达式, 则不判断
                        for (let i = 0, len = arr.length; i < len; i++) {
                            // ConditionalExpression 当然是能够在 ast 网站中找到, 也能够通过 console 缓缓调试进去
                            if (arr[i].value.type === 'ConditionalExpression') {
                                // 如果有表达式则间接返回, 不抛出任何信息
                                return
                            }
                        }
                        context.report({
                            node,
                            message: "不要应用行内款式",
                        });
                    }
                }
            }
        }

    }
};

再度执行指令, 能够发现失常通过了

再增加一些函数的组件, 扩充测试范畴, 保障咱们的规定十拿九稳, 这部分能够看我结尾的仓库, 文章里就不赘述了

测试工具

在咱们我的项目中初始化的时候, 他内置了测试工具 mocha, 咱们能够间接运行:

yarn test
// 或者
npm run test

如果我的项目中报错 sh: mocha: command not found
能够应用这个指令: "test": "node_modules/.bin/mocha tests --recursive", 代替之前的旧指令

它能够测试 tests 文件中的所有事例, 当然调试也能够, 只有你不嫌他麻烦

文档

在咱们应用指令新建规定的时候, 他还有一个新建文件就是文档文件: docs/rules/no-inner-style.md

在此文件中, 咱们能够具体写一下须要留神的事项, 有些待填写区域能够删除

# jsx render cannot write style (no-inner-style)

## Rule Details

Examples of **incorrect** code for this rule:

```js

function TestView(){
    return (<View style={{flex:1, width: 200}} >
        </View>
    )
}
```

Examples of **correct** code for this rule:

```js

function TestView() {
    const mode = 'dark';
    return (<View style={{flex: 1, width: 200, color: mode === 'dark' ? '#000' : '#fff'}}>
        </View>
    )
}
```

## When Not To Use It

标准我的项目的行内款式, 如果不须要能够敞开

并且同样地须要更新下 README.md 文档

我的项目集成

当初的我的项目能够间接就公布了

在我的我的项目公布之后, 能够看到他的全名是: @grewer/eslint-plugin-rn

在主我的项目中增加之后, package.json 这样退出:

  "eslintConfig": {
    "extends": [// 省略],
+    "plugins": ["@grewer/rn"], // 将咱们的插件插入这边
    "rules": {
        // 编写规定的危险等级
+      "@grewer/rn/no-inner-style": 1
    },
    "env": {"react-native/react-native": true}
  },

在 eslint 中, 规定的值能够是以下值之一:

  • "off"0 – 敞开规定
  • "warn"1 – 开启规定,应用正告级别的谬误:warn (不会导致程序退出)
  • "error"2 – 开启规定,应用谬误级别的谬误:error (当被触发的时候,程序会退出)

当然上述配置在 .eslintrc 文件中也一样配置

如果你的插件全名没有前缀(scoped), 则是这样增加:

(如果插件全名是: eslint-plugin-rn)

{
  "plugins": ["rn"],
  "rules": {"rn/rule-name": 2}
}

就须要这样增加, 差距也就是一个前缀

留神

在更改 eslint 配置之后, 想要起效, 须要重启下 eslint
比方在 webstorm 中, 须要关上配置中的 eslint 模块, 显示 Disabled Eslint, 抉择 ok 敞开之后

再度关上该模块回复原状, 当然重启编辑器也是能够解决的

规定默认值

问题降临: 当咱们的规定越来越多的时候, 咱们每次将插件接入我的项目中, 都须要增加 rules 这个属性.
这里咱们就须要优化下了

在我的项目中咱们须要写上默认值, 一种计划是能够间接写:
lib/index.js 文件中批改 module.exports

-module.exports.rules = requireIndex(__dirname + '/rules')

+module.exports = {+    rules: requireIndex(__dirname + '/rules'),
+    configs: {
+        recommended: {+            plugins: ["@grewer/rn"],
+            rules: {
+                "@grewer/rn/no-inner-style": 1
+            },
+        }
+    }
+}

当然主我的项目中 eslint 的配置也须要做出批改:

{
  "eslintConfig": {
    "extends": [
      "xxx 之前另外的 config",
+      "plugin:@grewer/rn/recommended"
    ],
-    "plugins": ["@grewer/rn"], // 删除 "@grewer/rn"
-    "rules": {
-        "@grewer/rn/no-inner-style": 1
-    },// 删除, 然而咱们也能够加上, 来笼罩默认值
    "env": {"react-native/react-native": true}
  }
}

当然, 因为规定的增多, 在 lib/index.js 文件中间接写也是比拟麻烦的, 咱们能够新建一个脚本来主动新增规定的默认值:

在根目录下创立文件 create.js

const requireIndex = require("requireindex");
const fs = require('fs')

const pluginName = '@grewer/rn'

const rules = requireIndex(__dirname + "/lib/rules")

const keys = Object.keys(rules)

const defaultLevel = keys.map(key => {
    // 这里能够进行更加具体的判断
    return `'${pluginName}/${key}': 1`
})


const data = `
const requireIndex = require("requireindex");

module.exports = {rules: requireIndex('./rules'),
    configs:{
        recommended: {plugins: ['${pluginName}'],
          rules: {${defaultLevel.join(',')}
          },
        }
    }
}`


fs.writeFileSync('./lib/index.js', data, 'utf8')

运行脚本: node create.js

这样生成的 lib/index.js 文件是这个样子的:

const requireIndex = require("requireindex");

module.exports = {rules: requireIndex('./rules'),
    configs:{
        recommended: {plugins: ['@grewer/rn'],
          rules: {'@grewer/rn/no-inner-style': 1},
        }
    }
}

更进一步优化

当初我的项目依赖于 requireindex 这个库, 有许多插件库都不依赖于这个库, 这个时候咱们也须要稍微优化下:

批改 package.json:

{
  "dependencies": {
    // 原来 requireindex 的地位, 删除
-     "requireindex": "~1.1.0",
  },
  "devDependencies": {
+    "requireindex": "~1.1.0", // 当初的地位
    "eslint": "^7.1.0",
    "generator-eslint": "^2.0.0",
    "mocha": "^8.3.0"
  },
}

批改刚刚的 create.js 脚本:

    const requireIndex = require("requireindex");
    const fs = require('fs')
    
    const pluginName = '@grewer/rn'
    
    const rules = requireIndex(__dirname + "/lib/rules")
    
    const keys = Object.keys(rules)
    
    const defaultLevel = keys.map(key => {
        // 这里能够进行更加具体的判断
        return `'${pluginName}/${key}': 1`
    })
    
    
+    const temp = keys.map(key => {+        return `'${key}': require('./lib/rules/${key}.js')`
+    })
    
    
    const data = `
-    const requireIndex = require("requireindex");
    
    module.exports = {
        rules:{+             ${temp.join(',')}
        },
        configs:{
            recommended: {plugins: ['${pluginName}'],
              rules: {${defaultLevel.join(',')}
              },
            }
        }
    }`
    
    
    fs.writeFileSync('./lib/index.js', data, 'utf8')

运行之后的文件:

module.exports = {
    rules:{'no-inner-style': require('./rules/no-inner-style.js')
    },
    configs:{
        recommended: {plugins: ['@grewer/rn'],
          rules: {'@grewer/rn/no-inner-style': 1},
        }
    }
}

当初的插件更 pure 了, 只依赖于 node

最初批改咱们的发包指令:

{
    "scripts": {
        "test": "node_modules/.bin/mocha tests --recursive",
        "create:rule": "npx yo eslint:rule",
-       "pub": "npm publish",
+       "pub": "node create.js && npm publish",          
    },
}

结语

本文介绍了 eslint 插件, 从我的项目创立到插件创立, 再到发包最初是优化

在团队里, 为了咱们会议的决定, 共识可能落实到我的项目中去, eslint 插件是必不可少的

本我的项目中创立的 eslint 插件库: https://github.com/Grewer/esl…

援用参考

  • https://lihautan.com/babel-as…
  • https://astexplorer.net/
  • http://eslint.cn/docs/user-gu…
  • https://www.zoo.team/article/…
正文完
 0