打造自定义 eslint
起因
在多人合作项目中, 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
:
效果如图:
-
What is your name?
作者名称, 填自己名字就行 -
Where will this rule be published? (Use arrow keys)
选择哪个生成的文件都一样, 所以回车即可 -
What is the rule ID?
rule 的名称, 如:no-inner-style
-
Type a short description of this rule:
随便填就行, 也可以直接回车 -
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
属性呢
这里我们需要一些工具了, 下面我推荐两个网址(他们的功能都是一样的)
工具的使用
首先我们需要两段符合场景的代码:
// 这就是特殊的情况, 这种情况下我们不会禁止行内样式 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: "不要使用行内样式", }] } ] });
填充 code
和 errors
中的 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/eslint-plugin-rn
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧