由于项目的历史原因,项目中同一个功能的依赖就会有多个,比如用于excel解析和导出的依赖就有excel.js和xlsx.js,为了提高项目的提高项目的可维护性和减少项目的体积,通过调研决定采用自定义eslint来进行依赖规范。本文章的目的是记录并介绍如何编写一个适用于团队风格的
eslint插件和规则并在项目中应用。
创建项目
ESLint 官方为了方便开发者开发插件,提供了使用 Yeoman 模板 (generator-eslint) 。 Yeoman 是一个为 web 应用提供脚手架的工具,它提供了三种能力:
yo——脚手架,自动生成工具
Grunt、gulp——构建工具
Bower、npm——包管理工具 这里就用到了 Yeaman 脚手架插件市场中的 eslint 项目生成工具
//全局安装一下
npm install -g yo generator-eslint
//创建个文件夹
mkdir eslint-plugin
cd eslint-plugin
//创建项目结构
yo eslint:plugin
//按照需求填写即可
? What is your name? @oec-pearl-kits/eslint-plugin
? What is the plugin ID? import
? Type a short description of this plugin: bytedance 国际化电商自定义插件
? Does this plugin contain custom ESLint rules? Yes
? Does this plugin contain one or more processors? YES(是否需要处理除js以外的文件例如.vue)
create package.json
create lib/index.js
create README.md
这里有个坑需要特别注意,不然到时候再引入eslint的时候,会导致找不到插件或者找不到规则。即:plugin的命名, 我这里的命名是
@oec-pearl-kits/eslint-plugin, 意思@oec-pearl-kits这个scope下的eslint插件。在命名的时候,建议采用@xxxx/eslint-plugin,这个命名规则在官方的文档也没有找到规范,确实是一个坑!
上面的代码主要是创建了 eslint 的项目模板,现在我们来创建 rule 相关的文件:
yo eslint:rule
//按照需求填写即可
? What is your name? pearl-import-validate
? Where will this rule be published? ESLint Plugin
? What is the rule ID? pearl-import-validate(规则的名称)
? Type a short description of this rule: 规范项目的导入依赖
? Type a short example of the code that will fail:
//上面的步骤生成了 rule 对应的 说明文件、模板和单元测试文件,现在安装一下依赖
npm i
生成代码结构如下:
├── README.md
├── docs // 使用文档
│ └── rules
│ └── pearl-import-validate.md
├── lib // eslint 规则开发
│ ├── index.js
│ └── rules // 此目录下可以构建多个规则
│ └── pearl-import-validate.js
├── package.json
└── tests // 单元测试
└── lib
└── rules
└── pearl-import-validate.js
Eslint运行原理
-
代码解析: eslint 通过解释器把我们写的代码转换成对应的 AST 抽象语法树,不同的语法需要用不同的解析器来解析,如Vue、TSX
-
深度遍历AST,监听匹配过程: 解析完 AST 后,eslint 会以 从上层到下层、从下层到上层的方式,遍历选择器两次
-
触发监听选择器rule回调: 在深度遍历的过程中,生效的每条规则(自己写的规则)都会对其中的某一个或多个选择器(ast树中的选择器)进行监听,每当匹配到选择器,监听该选择器的rule,都会触发对应的回调。
开发一个规则
前期准备
{
"type": "Program",
"start": 0,
"end": 34,
"body": [
{
"type": "ImportDeclaration",
"start": 0,
"end": 34,
"specifiers": [
{
"type": "ImportDefaultSpecifier",
"start": 7,
"end": 12,
"local": {
"type": "Identifier",
"start": 7,
"end": 12,
"name": "React"
}
},
{
"type": "ImportSpecifier",
"start": 16,
"end": 18,
"imported": {
"type": "Identifier",
"start": 16,
"end": 18,
"name": "FC"
},
"local": {
"type": "Identifier",
"start": 16,
"end": 18,
"name": "FC"
}
}
],
"source": {
"type": "Literal",
"start": 26,
"end": 33,
"value": "react",
"raw": "'react'"
}
}
],
"sourceType": "module"
}
根据AST树,对于依赖相关的,只需要在 ImportDeclaration 进行处理就好了。
- 先写个简单的例子
rule 代码
// lib/rules/pearl-import-validate.js
module.exports = {
meta: {
type: 'suggestion', // `problem`, `suggestion`, or `layout`
docs: {
description: "规范项目的导入依赖",
recommended: false,
url: null, // URL to the documentation page for this rule
},
// 是否自动格式化
fixable: 'code',
// 获取配置文件传递的属性
schema: [], // Add a schema if the rule has options
messages: {
// 提示语
avoidImportPackage: "Avoid using import named '{{ sourcePackage }}', advise to use import named '{{ finalPackage }}'",
}
},
create(context) {
return {
ImportDeclaration(node) {
if(node.source.value.includes('bad-import-declaration')) {
context.report({
node,
// 指定提示语
messageId: 'avoidImportPackage',
data:{
// 提供给上面的messages
sourcePackage: 'bad-import-declaration',
finalPackage: 'good-import-declaration'
},
// 自动fix
fix: fixer => fixer.replaceText(node, `import somepackage from 'good-import-declaration';`)
});
}
},
};
},
};
单元测试
// tests/lib/rules/pearl-import-validate.js
const rule = require("../../../lib/rules/pearl-import-validate"),
RuleTester = require("eslint").RuleTester;
var config = {
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
}
}
const ruleTester = new RuleTester(config);
ruleTester.run("pearl-import-validate", rule, {
valid: [
{
code: "import somepackage from 'good-import-declaration';",
},
],
invalid: [
{
code: "import somepackage from 'bad-import-declaration';",
errors: [
{
messageId: 'avoidImportPackage'
}
],
output: "import somepackage from 'good-import-declaration';",
},
],
});
让我们一步一步来看看这里的所有东西:
-
create 方法将接受一个上下文参数,该参数稍后将用于创建有关已发现问题的报告。
-
此方法将返回一个新的 ImportDeclaration 规则。 如果您对其他规则感兴趣,请查看官方文档
-
我们正在检查某个节点导入是否包含查询(在我们的例子中是 bad-import-declaration)
-
在此 if 语句中,我们通过使用以下参数从上下文对象调用方法来生成新报告:
- node:触发规则的实际节点(类似于堆栈跟踪)位置。
- message:运行规则并发现问题后应显示的消息。
- 将用于修复导入语句的修复方法。 在这种情况下,它是一种使用修复器作为参数的方法,然后使用当前节点和应该添加的新值而不是节点来调用称为 replaceText 的修复器方法。
集成项目
- 发布npm 包
这里参考文档。
-
项目集成
npm install @oec-pearl-kits/eslint-plugin -D// .eslintrc.js module.exports = { root: true, extends: ['@jupiter-app'], parserOptions: { tsconfigRootDir: __dirname, project: ['../tsconfig.json'], }, plugins: ['@oec-pearl-kits'], rules: { '@oec-pearl-kits/pearl-import-validate': 'error' }, };
本地调试
现在已经开发完了,如果每次都发版本校验会很麻烦,可以使用 npm link 命令。它会把包依赖映射到本地,从而避免等待远端的最新代码。
-
cd 进入 eslint-plugin 目录,执行 npm link。
-
cd 进入 引入插件的项目目录,执行 npm link @oec-pearl-kits/eslint-plugin
-
当 eslint-plugin 有修改后,重新在eslint-plugin中执行npm link。然后在实际引入的项目中关闭编辑器重新打开,插件即生效
获取配置参数
-
配置文件传参
context 可以通过配合schema获取配置文件传递的参数 adviseForbidPackages :
module.exports = { root: true, extends: ['@jupiter-app'], parserOptions: { tsconfigRootDir: __dirname, project: ['../tsconfig.json'], }, plugins: ['@oec-pearl-kits'], rules: { 'react-hooks/exhaustive-deps': [ 'warn', { additionalHooks: 'useRecoilCallback', }, ], '@oec-pearl-kits/pearl-import-validate': [ 'error', { adviseForbidPackages: { 'query-string': 'qs', lodash: 'lodash-es', }, }, ] }, };rule中Create获取 adviseForbidPackages :
create(context) { const { adviseForbidPackages } = context.options[0] return { ImportDeclaration(node) { if(node.source.value in adviseForbidPackages) { context.report({ node, // 指定提示语 messageId: 'avoidImportPackage', data:{ // 提供给上面的messages sourcePackage: 'bad-import-declaration', finalPackage: 'good-import-declaration' }, // 自动fix fix: fixer => fixer.replaceText(node, `import somepackage from 'good-import-declaration';`) }); } }, }; },schema配置:
schema: [{ type: 'object', properties: { adviseForbidPackages: { type:"object" } } }] -
单元测试适配配置参数
const rule = require("../../../lib/rules/pearl-import-validate") const RuleTester = require("eslint").RuleTester; var config = { parserOptions: { ecmaVersion: 2020, sourceType: 'module', } } const option = { adviseForbidPackages: { }, } const ruleTester = new RuleTester(config); ruleTester.run("pearl-import-validate",rule,{ valid: [ { code: "import somepackage from 'good-import-declaration';", options:[option], }, ], invalid: [ { code: "import somepackage from 'bad-import-declaration';", errors: [ { messageId: 'avoidImportPackage' } ], output: "import somepackage from 'good-import-declaration';", }, ], });