对于前端开发者来说,ESLint 是比较常用的代码规范和错误检查工具,ESLint 非常强大不仅提供了大量的常用 rules,还有许多实用的 ESLint 插件可以满足各样需求。但随着项目不断迭代发展,可能会遇到已有 ESLint 插件不能满足现在团队开发的情况。那么这时候,我们就需要自定义 Eslint 开发 ESLint Shareable Config、ESLint Plugins。
- ESLint Shareable Config,可分享的扩展配置(
eslint-config-<config-name>)。 - ESLint Plugins,插件(
eslint-plugin-<plugin-name>) 。
ESLint Shareable Config 开发
可分享的扩展配置(eslint-config-<config-name>) 是一个 ESLint 配置对象 npm 包,模块名称以 eslint-config-<config-name> 、@<scope>/eslint-config-<config-name> 命名,创建比较简单导出配置规则即可。
创建扩展配置
创建扩展配置非常简单,创建一个新的 index.js 文件并 export 一个包含配置的对象即可:
module.exports = {
globals: {
MyGlobal: true
},
rules: {
semi: [2, "always"]
}
}
更多配置字段,参考 Configuring ESLint
使用扩展配置
npm 发布扩展包,引入 ESLint 配置:
module.exports = {
// extends: ['antife', 'myconfig'],
extends: ['eslint-config-antife', 'eslint-config-myconfig'],
globals: {
'EVENT': true,
'PAGE': true,
'SCENE': true,
'AlipayJSBridge': true,
},
plugins: [
'babel',
// 'html', // eslint-plugin-html 从 <script> 标记中提取内容,eslint-plugin-vue 需要 <script> 标记和<template> 标记,两者同时存在会冲突
'vue',
]
}
Eslint plugin 开发
插件(eslint-plugin-<plugin-name>) 是一个命名格式为 eslint-plugin-<plugin-name> 的 npm 包,模块名称以 eslint-plugin-<plugin-name> 、@<scope>/eslint-plugin-<plugin-name> 命名。
Eslint plugin 目录
我们可以利用 yeoman 和 generator-eslint 来构建插件的目录结构进行开发,这里我们选用自定义目录,如下:
├── README.md
├── _tests__
├── docs
├── index.js
└── rules
└── my-rule.js
插件主入口组成部分
-
Rules - 插件必须输出一个
rules对象,包含规则 ID 和对应规则的一个键值对。 -
Environments - 插件可以暴露额外的环境以在 ESLint 中使用。
-
Processors - 定义插件如何处理校验的文件。
-
Configs - 可以通过配置指定插件打包、编译方式,还可提供多种风格校验配置。
module.exports = {
rules: {
"my-rules": {
create: function (context) {
// rule implementation ...
}
}
},
env: {
jquery: {
globals: {
$: false
}
}
},
configs: {
myConfig: {
parser: require.resolve('vue-eslint-parser'),
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures: {
jsx: true
}
},
plugins: ["myPlugin"],
env: ["browser"],
rules: {
"myPlugin/my-rule": "error",
}
},
myOtherConfig: {
plugins: ["myPlugin"],
env: ["node"],
rules: {
"myPlugin/my-rule": "off",
}
}
},
processors: {
'.vue': {
// takes text of the file and filename
preprocess: function(text, filename) {
// here, you can strip out any non-JS content
// and split into multiple strings to lint
return [string]; // return an array of strings to lint
},
// takes a Message[][] and filename
postprocess: function(messages, filename) {
// `messages` argument contains two-dimensional array of Message objects
// where each top-level array item contains array of lint messages related
// to the text that was returned in array from preprocess() method
// you need to return a one-dimensional array of the messages you want to keep
return messages[0];
},
supportsAutofix: true // (optional, defaults to false)
}
}
}
Rules 创建
在开始编写新规则之前,请阅读官方的 ESLint指南,了解下 ESLint 的特点:
- ESLint 使用 Espree 进行JavaScript解析。
- ESLint 使用 AST 评估校验代码。
- ESLint 是完全可插入的,每个规则都可以是一个插件。
- ESLint 每条规则相互独立,可以设置禁用
off、警告warn⚠️和报错error❌,当然还有正常通过不用给任何提示。
我们可以通过使用 astexplorer.net, 去了解 ESLint 如何使用 AST 评估校验代码,astexplorer.net 非常强大,还支持 Vue 模板。
规则组成部分
-
meta对象包含规则的元数据 -
create函数返回 ESLint 调用方法对象,通过该方法访问 JavaScript 代码的抽象语法树(由ESTree定义的AST)节点 -
context对象包含与规则上下文相关的信息-
属性:
-
方法:
-
getAncestors()- 返回当前遍历的节点的祖先数组,从AST的根部开始,一直到当前节点的直接父级 -
getCwd()- 将cwd传递的内容返回给Linter, 为当前工作目录 -
getDeclaredVariables- 返回给定节点声明的 -
getFilename()- 返回与源关联的文件名 -
getScope()- 返回当前遍历的节点的 scope ,用于跟踪对变量的引用 -
getSourceCode()- 返回一个SourceCode对象,可以使用该对象来处理传递给ESLint的源 -
markVariableAsUsed(name)- 在当前作用域中使用给定名称标记变量 -
report(descriptor)- 报告代码中的问题
-
-
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "disallow unnecessary semicolons",
category: "Possible Errors",
recommended: true,
url: "https://eslint.org/docs/rules/no-extra-semi"
},
fixable: "code",
schema: [] // no options
},
create: function(context) {
return {
Identifier(node) {
if (node.name === "foo") {
context.report({
node,
messageId: "avoidName",
data: {
name: "foo",
}
})
}
},
ExportDefaultDeclaration(node){
context.report({
node,
message: "test",
})
}
}
}
}
若我们需要校验 Vue 模板,这里要注意由于Vue中的单个文件组件不是普通的 JavaScript,因此无法使用默认解析器,因此引入了新的解析器 vue-eslint-parser。
要了解更多 vue AST 知识,可以查看
自定义 Processors
ESLint 插件开发,支持自定义处理器来处理 JavaScript 之外的文件,自定义处理器含有两个过程:preprocess 和postprocess。自定义处理器大体结构如下:
module.exports = {
processors: {
// assign to the file extension you want (.js, .jsx, .html, etc.)
".ext": {
// takes text of the file and filename
preprocess: function(text, filename) {
// here, you can strip out any non-JS content
// and split into multiple strings to lint
return [string]; // return an array of strings to lint
},
// takes a Message[][] and filename
postprocess: function(messages, filename) {
// `messages` argument contains two-dimensional array of Message objects
// where each top-level array item contains array of lint messages related
// to the text that was returned in array from preprocess() method
// you need to return a one-dimensional array of the messages you want to keep
return messages[0];
},
supportsAutofix: true // (optional, defaults to false)
}
}
};
插件测试
ESLint 提供了 RuleTester 实用工具可以轻松地测试你插件中的规则,在 peerDependency 指向 ESLint 0.8.0 或之后的版本。
{
"peerDependencies": {
"eslint": ">=0.8.0"
}
}
peerDependencies 目的是提示宿主环境去安装满足插件peerDependencies所指定依赖的包,然后在插件import或者require所依赖的包的时候,永远都是引用宿主环境统一安装的npm包,最终解决插件与所依赖包不一致的问题。
// in the file to lint:
var foo = 2;
// ^ error: Avoid using variables named 'foo'
// In your tests:
var rule = require("../rules/no-avoid-name")
var RuleTester = require("eslint").RuleTester
var ruleTester = new RuleTester()
ruleTester.run("no-avoid-name", rule, {
valid: ["bar", "baz"], // right data
invalid: [ // error data
{
code: "foo",
errors: [
{
messageId: "avoidName"
}
]
}
]
})
实践开发 Vue 模版 Eslint plugin
在开发之前,这里要注意由于 Vue 中的单个文件组件并不是普通的 JavaScript,导致无法使用默认解析器,因此引入了新的解析器 vue-eslint-parser。
{
"parser": "vue-eslint-parser",
"parserOptions": {
"parser": "babel-eslint",
"sourceType": "module",
"allowImportExportEverywhere": false
}
}
开发 vue eslint 规则
这里要注意,涉及到自定义的解析器的,需要使用context.parserServices 访问该解析器解析的抽象语法树内容。
- Vue 插件规则,示例
module.exports = {
meta: {
docs: {
description: 'disallow unnecessary `v-bind` directives',
url: 'https://eslint.vuejs.org/rules/no-useless-v-bind.html'
},
fixable: 'code',
type: 'suggestion'
},
create(context) {
if (context.parserServices.defineTemplateBodyVisitor == null) {
context.report({
loc: { line: 1, column: 0 },
message:
'Use the latest vue-eslint-parser. See also https://eslint.vuejs.org/user-guide/#what-is-the-use-the-latest-vue-eslint-parser-error'
})
return {}
}
return context.parserServices.defineTemplateBodyVisitor({
VElement(node){
if(node.name === "template"){
context.report({
node,
message: "template标签",
})
}
},
Identifier(node){
console.error('Identifier.name', node.name)
}
})
}
}
若是校验 js,无需通过 context.parserServices.defineTemplateBodyVisitor 获取语法树信息
- Vue 插件入口文件示例:
module.exports = {
configs: {
base: {
parser: require.resolve('vue-eslint-parser'),
plugins: ['boilerplate'],
rules: {
'boilerplate/no-avoid-name': 'error',
'boilerplate/no-useless-v-bind': 'error'
}
}
},
env: {
browser: true,
es6: true
},
rules: {
'no-avoid-name': require('./rules/no-avoid-name'),
'no-useless-v-bind': require('./rules/no-useless-v-bind'),
}
}
- 配置使用
module.exports = {
parser: 'vue-eslint-parser',
parserOptions: {
parser: 'babel-eslint',
ecmaVersion: 2018,
sourceType: 'module'
},
// vue 插件要放在 extend 前面,防止出现覆盖,"eslint:recommended" 是默认推荐的规则
extends: ['plugin:vue/recommended', 'plugin:boilerplate/base', "eslint:recommended"],
plugins: [
'babel',
],
}
了解更多 vue AST 知识,可以查看
eslint-plugin-boilerplate
eslint-plugin-boilerplate —— 快速开发 eslint plugins 模版样例。
写到最后 - 招贤纳士
蚂蚁国际事业群深圳无线前端团队大量社招岗位 hc,详细请关注 Alipay Payment Services Hong Kong Limited-高级前端研发工程师/专家,小伙伴们,可以加我微信 gluuu1 内推,机会更大~~