Vue3 黑神话:悟空版 eslint: eslint-plugin-wukong搭建一个前端项目,代码规范配置必不可少
搭建一个前端项目,代码规范配置必不可少,但琳琅满目、形形色色的ESlint NPM包,让人无从下手,为什么一个eslint就有几十个包?
eslint要识别出问题代码,需要经过的处理阶段:文件识别、代码提取、生成AST、问题分析、代码修复。js、json、vue、ts、markdown等不同类型的文件,再加上vue、react、angular等前端框架,要生成AST,需提供不同的processor、parser能力,因此少不了各种各样的plugin去支撑。
常用的eslint解析器、配置、插件有哪些?
- 解析器
- espree
- Esprima
- @babel/eslint-parser
- @typescript-eslint/parser
- 配置
- eslint
- eslint-plugin-unicorn
- eslint-config-airbnb-base
- eslint-config-airbnb
- eslint-plugin-vue
- eslint-plugin-react
- typescript-eslint
- @antfu/eslint-config
- @element-plus/eslint-config
- 插件
- eslint-config-prettier
- eslint-plugin-babel
- eslint-plugin-import
- eslint-plugin-promise
- @typescript-eslint/eslint-plugin
- eslint-import-resolver-webpack
- eslint-import-resolver-typescript
代码规范往期介绍:
如果以上罗列的规范都无法完全覆盖提出的"30个代码规范",那就尝试为这30个代码规范提供定制版eslint插件,我将其取名为:eslint-plugin-wukong。是不是有强行蹭热度的嫌疑!
使用eslint-plugin-wukong必须头铁,因为不满足规则的直接error。
开搞:搭建eslint-plugin-wukong项目
创建eslint-plugin-wukong目录,进入该目录后执行如下指令,初始化package.json,安装ts,生成tsconfig.json文件。
npm init -y && npm i typescript -D && npx tsc --init
ts配置如下:
{
"compilerOptions": {
/* Language and Environment */
"target": "es2016",
/* Modules */
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
/* Type Checking */
"strict": true,
"skipLibCheck": true,
"outDir": "lib"
},
"exclude": [
"node_modules",
"lib"
]
}
安装Node ts类型
npm i @types/node -D
安装eslint,选项中仅使用javascript
npx eslint --init
生成的eslint配置如下,eslint<v9版的通过.eslintignore配置文件黑名单,而v9之后直接在配置文件中添加ignores选项即可。
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
export default [
{ignores: ["lib/"]},
{files: ["**/*.{js,mjs,cjs,ts}"]},
{languageOptions: { globals: globals.browser }},
pluginJs.configs.recommended,
...tseslint.configs.recommended,
];
安装prettier
npm i prettier -D
添加.prettierrc、.prettierignore文件,.prettierrc配置如下:
{
"semi": false,
"singleQuote": true,
"overrides": [
{
"files": ".prettierrc",
"options": {
"parser": "json"
}
}
]
}
结合VSCode IDE安装eslint、prettier插件,并在settings.json添加相应配置。
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
新生成的目录大致如下:
eslint-plugin-wukong插件
eslint官方在2024年4月份发布了eslint v9.0.0、@eslint/config-inspector。 eslint v9默认使用更简洁的flat config,@eslint/config-inspector工具提供可视化显示eslint包含的rules。
使用eslint V9版本
eslint V9.0.0开始,一个项目只用在根目录定义eslint.config.mjs配置文件。v9以前使用不同目录联级,比较难管理。V9配置默认采用flat config扁平化配置。
import customConfig from "eslint-config-custom";
export default [
{ignores: ["lib/"]},
customConfig,
{
files: ["**/*.js", "**/*.cjs"],
rules: {
"semi": "error",
"no-unused-vars": "error"
}
},
{
files: ["**/*.js"],
rules: {
"no-undef": "error",
"semi": "warn"
}
}
];
使用ignores
代替v9之前的.eslintignore
文件。扩展更简单,只需将插件对象(如customConfig)放置在合适位置即可。统一使用glob patterns
模式匹配文件,如**/*.js
匹配所有目录下的.js文件。
eslint开发好助手:@eslint/config-inspector
使用eslint的苦恼:使用eslint就像个黑盒,匹配哪些文件不知道,配置、规则肉眼不可见。而@eslint/config-inspector的出现,完全解决了这些问题。
当安装并配置完eslint.config.mjs文件后,执行指令:
npx @eslint/config-inspector@latest
本地启动端口为7777的web server,通过日志可查看配置条数、规则总数。
点击链接地址,打开页面,切换Configs、Rules、Files查看具体信息,当使用了新配置或者新增了rule,都可以通过该页面查看其信息。
插件需要的配置项
本节目的:了解eslint插件实现的不同形式:使用第三方config、自定义config、自定义rules。
按eslint官方介绍, 一个plugin包含meta、configs、rules和processors
属性。
const plugin = {
meta: {},
configs: {},
rules: {},
processors: {}
};
// for ESM
export default plugin;
元信息meta
meta包含name、version字段,两个字段的信息可以通过读取package.json文件获取,新建meta.ts
文件,添加代码:
import pkg from '../package.json';
const { name, version } = pkg;
export default {
name,
version,
}
268基础版配置config:base
定义两个config:base、recommended,base包含引入官方、三方常用的config, 而recommended将包揽自定义规则的所有rule。
在configs目录下新增base.ts, 导入@eslint/js
、typescript-eslint
、eslint-plugin-vue
, 最后一项配置引入定义的wukong
插件。
import pluginJs from '@eslint/js'
import pluginTs from 'typescript-eslint'
import globals from 'globals'
import wukong from '../'
// eslint-disable-next-line @typescript-eslint/no-require-imports
const pluginVue = require('eslint-plugin-vue')
export default [
pluginJs.configs.recommended,
...pluginTs.configs.recommended,
...pluginVue.configs["flat/essential"],
{
name: 'wukong/base',
plugins: {
get wukong() {
return wukong
},
},
languageOptions: {
sourceType: 'module',
globals: globals.browser
}
}
]
328升级版配置vue-recommended
在configs目录下新增vue-recommended.ts文件,初始化内容:
import base from "./base";
export default [
...base,
{
name: 'wukong/vue',
rules: {}
}
]
30个规范自定义的rule统一放到name为wukong/vue
下的rules对象。由于规范比较多,暂且梳理几个具有代表性的规范。
- vue文件夹按kebab-case命名
[`${PLUGIN_CHECK_FILE}/folder-naming-convention`]: [
'error',
{ '**/': 'KEBAB_CASE' }
],
- components目录下的vue文件采用KEBAB_CASE
[`${PLUGIN_CHECK_FILE}/filename-naming-convention`]: [
'error',
{
'**/components/**/*.{vue,}': 'KEBAB_CASE',
},
]
- vue组件规范
{
// vue组件节点顺序: script、template、style
[`${PLUGIN_VUE}/block-order`]: ['error', { order: ['script', 'template', 'style'] }],
// vue组件命名采用kabab-case
[`${PLUGIN_VUE}/component-definition-name-casing`]: ['error', 'kebab-case'],
// defineProps中属性命名采用camelCase
[`${PLUGIN_VUE}/prop-name-casing`]: ['error', 'camelCase'],
// defineEmits中事件命名
[`${PLUGIN_VUE}/define-emits-declaration`]: ["error", 'runtime'],
// template每行显示属性个数为1
[`${PLUGIN_VUE}/max-attributes-per-line`]: ["error", {
"singleline": {
"max": 1
},
"multiline": {
"max": 1
}
}],
// b-bind使用简写
[`${PLUGIN_VUE}/v-bind-style`]: ["error", "shorthand"],
// b-on使用简写
[`${PLUGIN_VUE}/v-on-style`]: ["error", "shorthand"],
//v-slot使用简写
[`${PLUGIN_VUE}/v-slot-style`]: ["error", {
"atComponent": "shorthand",
"default": "shorthand",
"named": "shorthand",
}]
}
- ts规范
{
[`@typescript-eslint/naming-convention`]: [
"error",
// 枚举选项使用UPPER_CASE
{
"selector": "enumMember",
"format": ["UPPER_CASE"]
}
]
}
如何实现Rule:vue必须按script、template、style顺序排列
Rule结构
eslient官方对rule的定义:
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "Description of the rule",
},
fixable: "code",
hasSuggestions: true,
messages
schema: [] // no options
},
create: function(context) {
return {
// callback functions
};
}
};
想要实现一个rule, 得知道要配置哪些属性:
- meta
- type: 值为
"problem"
|"suggestion"
|"layout"
,problem为代码问题,需要修复;suggsetion为建议,不是问题;layout为空额、分号、风格类提示。 - docs:包含属性description、categories、url、等信息,描述rule是干什么的。
- flexable:值包含code、whitespace,code表示可修复的代码类错误,whitespace表示可修复的风格类错误。
- hasSuggestions: boolean,指定规则是否返回建议。
- messages: 定义错误、警告消息模板
- schema: 定义使用规则时的选项,防止传入无法识别的选项。
- deprecated:表示规则是否过期,过期的规则可能在后续版本淘汰。
- type: 值为
- create(context): 最核心的方法,定义和ESTree节点名称一样的方法,当ESTree遍历代码时被执行,因此在这些方法中能够拿到所有节点(Node)信息。下表为eslint官方提供的ESTree节点类型,重点关注有哪些节点可供我们使用。
Program
FunctionDeclaration
FunctionExpression
ArrowFunctionExpression
ClassDeclaration
ClassExpression
BlockStatement
※1SwitchStatement
※1ForStatement
※2ForInStatement
※2ForOfStatement
※2WithStatement
CatchClause
其他
初步了解实现一个rule需要的属性、方法,接下来就以Vue提供的vue/block-order为例,说明如何限制Vue文件中<script>
、<template>
、 <style>
三个标签的顺序。
meta信息配置如下,类型定义为suggestion
,fixable设置为code
,支持自动修复。messages下定义unexpected消息占位符,其中elementName等占位符哪来?在create方法可一探究竟。
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'enforce order of component top-level elements',
url: 'https://eslint.vuejs.org/rules/block-order.html'
},
fixable: 'code',
schema: [...],
messages: {
unexpected:
"'<{{elementName}}{{elementAttributes}}>' should be above '<{{firstUnorderedName}}{{firstUnorderedAttributes}}>' on line {{line}}."
}
},
create(context) {
...
return {
Program(node) {
}
}
}
}
当读取文件时create方法被自动调用,从context提供的getSourceCode
方法可获取源代码信息。
Program
由于仅需判断script、template、style顺序,而这些信息可以在ESTree的Program节点获取,因此可以在Program节点触发规则判断逻辑。
Program处理逻辑分为3步:
Program(node) {
// 1. 获取节点列表
// 2. 根据rule选项,判断节点顺序
// 3.生成错误报告
}
假如源代码、rule配置如下,接下来分析Program的具体实现。
// 源代码
<script></script><template></template><style></style>
// 规则配置
{
"vue/block-order": ["error", {
"order": ["template", "script", "style" ]
}]
}
获取节点列表
调用getTopLevelHTMLElements方法获取源代码节点列表:
const elements = getTopLevelHTMLElements()
查看elements信息,节点列表分别为script、template、style
getTopLevelHTMLElements是如何获取节点列表的?通过context的sourceCode获取代码片段DocumentFragment,fragment的children列表即为script、template、style节点。
const sourceCode = context.getSourceCode()
const documentFragment =
sourceCode.parserServices.getDocumentFragment &&
sourceCode.parserServices.getDocumentFragment()
function getTopLevelHTMLElements() {
if (documentFragment) {
return documentFragment.children.filter(utils.isVElement)
}
return []
}
拿到节点列表之后,下一步根据规则选项判断节点顺序是否匹配。
根据rule选项,判断节点顺序
判断节点顺序的核心逻辑是,将解析出来的代码节点列表和配置的rule选项顺序匹配,如果匹配失败则任务代码错误。
数据输入有两个来源:
- rule选项:
{ "order": ["template", "script", "style" ] }
- 代码节点:elements
比较前需要将这两份数据整合,整合后每一项包含order、element,order为节点在rule中的顺序,elmement为节点信息。
const elementsWithOrder = elements.flatMap((element) => {
const order = getOrderElement(element)
return order ? [{ order, element }] : []
})
查看elementsWithOrder数据,其中第一项element的name为script,而匹配的orderindex为1。初步猜测,由于节点索引index为0,而rule选项index为1,两个不匹配会报错?
有了排序后的列表,下一步遍历数组并对每一项中的order、element判断是否一致。如下代码,index为代码节点实际索引,而expected为rule期望的顺序索引。
for (const [index, elementWithOrders] of elementsWithOrder.entries()) {
const { order: expected, element } = elementWithOrders
...
}
如何判断顺序?取出当前节点的expected信息,然后使用.slice(0, index)
裁剪当前节点之前的节点列表(暂命名为prevElements),如果prevElements中有满足expected.index < order.index
,也就是应该在expected之后的节点出现在了其之前。
const firstUnordered = elementsWithOrder
.slice(0, index)
.filter(({ order }) => expected.index < order.index)
.sort((e1, e2) => e1.order.index - e2.order.index)[0]
通过以上逻辑找到的第一个不满足条件的节点firstUnrdered,节点名称为script
,rule规定的位置索引应该是第二项(template、script、style)。
生成错误报告
如果识别出有节点错误,则需要调用context的report方法打印报告, 其中messageId值unexpected
对应为定义rule时声明的消息占位符,而消息占位符变量定义在data对象下,上文meta中的elementName就来自data数据。
context.report({
node: element,
loc: element.loc,
messageId: 'unexpected',
data: {...},
*fix(fixer) {...}
})
如果需要自动修复问题,则必须实现*fix生成器函数。修复思路是,先按正确的顺序排好列表:
const fixedElements = elements.flatMap((it) => {
if (it === firstUnordered.element) {
return [element, it]
} else if (it === element) {
return []
}
return [it]
})
以上代码将无序的firstUnordered节点移动至element之后,使顺序变为template、script、style。
遍历代码原始节点elements,从后往前遍历,当相同索引位置节点不相等时elements[i] !== fixedElements[i]
,则使用replaceTextRange方法把fixedElements[i]的代码替换到elements[i]下。
for (let i = elements.length - 1; i >= 0; i--) {
if (elements[i] !== fixedElements[i]) {
yield fixer.replaceTextRange(
elements[i].range,
sourceCode.text.slice(...fixedElements[i].range)
)
}
}
至此,一个规则的流程执行完毕。
总结
写eslint-plugin-wukong的感悟:eslint有非常庞大的生态圈,需要支持js、ts、vue、react等不同语言不同框架的规范,功能离子化、插件模式能够让其生态逐渐庞大起来,目前也支持了java、sql等语言。通过本篇文章,我们可以了解如何从0到1开发一个eslint插件,有特殊规范需求时,也能够通过自定义Rule开发来满足需求,告别拿来主义。
wukong插件相关地址:
- npm: www.npmjs.com/package/esl…
- github: github.com/cnmapos/esl…
参考
我是
前端下饭菜
,原创不易,各位看官动动手,帮忙关注、点赞、收藏、评轮!
转载自:https://juejin.cn/post/7409238250042982412