为什么要使用eslint
原因很简单,使用 eslint 好处 > 缺点;
优点如下:
- 有的规则可以帮我们避免错误;
- 有的规则可以帮我们写出最佳实践的代码;
- 有的规则可以帮我们规范变量的使用方式;
- 有的规则可以帮我们规范代码格式;
- 有的规则可以帮我们更合适的使用新的语法;
- …
缺点:不能放飞自我,代码不能随便写;
如下图:
eslint使用
eslint只需要配置.eslintrc.js文件即可;
常见配置字段如下:(下面的配置主要是介绍字段,非正常配置)
extends: [
'airbnb-typescript',//相当于eslint-config-airbnb-typescript ,会从这个包读取导出的配置,是对默认规则使用的配置
'plugin:react-hooks/recommended',// 使用eslint-plugin-react-hooks这个插件包的config.recommended配置
'plugin:prettier/recommended',//同上,从eslint-plugin-prettier 的配置中读取config.recommend配置,
],
plugins:[
'import',//相当于添加eslint-plugin-import 中所有的自定义rule
],
parser: '@typescript-eslint/parser',//表示eslint会使用'@typescript-eslint/parser'作为解析器来解析代码,生成ast树
parserOptions:{
},//设置解析器选项
ignorePatterns:['pathPatterns'],//eslint会忽略匹配到的文件
rules:{
'linebreak-style': 'off',//表示eslint的默认规则内 linebreak-style这条规则关闭
'react/require-default-props': 'off',//表示eslint-plugin-react的自定义规则内 require-default-props这条规则关闭
}
eslint 禁用
eslint 原理
第一步:ESLint使用指定的JavaScript解析器把JS代码解析成AST,默认是Espree
第二步:深度遍历生成的 AST,将每一个 node 传入 nodeQueue 队列中,每个会被传入两次。(node: 节点)
第三步:遍历所有给定的规则,创建 rule 对象,执行 rule 对象的 create 方法,返回 ruleListeners 对象(这个对象里面包含了 rule 的选择器和回调函数),遍历 ruleListeners 对象(每个 rule 可以有多个选择器),为规则中所有的选择器添加监听事件。
第四步:遍历 nodeQueue 队列,触发匹配到当前 node 的选择器的监听事件,执行相应的回调函数。
eslint 实践
0. 开发目标
目标:禁止umi history.push方法传字符串
原因:因为我们项目中的guid里有"#",用这种姿势跳转会自动把"#"encode
1. 开发准备
eslint推荐Yeoman脚手架生成器来创建项目,
第一步:安装yo、generator-eslint
npm install yo generator-eslint -g
第二步:生成脚手架
yo eslint:plugin
第三步:生成rule模板文件
把指令添加到package.json 的scripts中 "createRule": "yo eslint:rule”
npm run createRule
此时,项目的目录结构是这样的
.
├── README.md
├── docs
│ └── rules
│ └── history-push-no-string.md
├── lib
│ ├── index.js
│ └── rules
│ └── history-push-no-string.js
├── tests
│ └── lib
│ └── rules
│ └── history-push-no-string.js
├── node-modules
├── package.json
└── package-lock.json
开发
第一步:编写测例
在我们创建的 tests/lib/rules/history-push-no-string.js 模板文件中,按照模板填入自己设定的有效使用和无效使用:
ruleTester.run("history-push-no-string", rule, {
valid: [
{
code: `
const history = [];
history.push('/some/path');
`,
parserOptions: { ecmaVersion: 6, sourceType: "module" }
},
{
code: `
import {history} from 'umi';
history.push({
path: '/some/path'
});
`,
parserOptions: { ecmaVersion: 6, sourceType: "module" }
}
],
invalid: [
{
code: `
import {history as h} from 'umi';
h.push('/some/path');
`,
parserOptions: { ecmaVersion: 6, sourceType: "module" },
errors: [{ message: "umi history的[push]方法必须使用对象参数" }],
},
{
code: `
import {history} from 'umi';
function test() {
history.replace('/some/path');
}
`,
parserOptions: { ecmaVersion: 6, sourceType: "module" },
errors: [{ message: "umi history的[replace]方法必须使用对象参数" }],
},
],
});
第二步:开发规则
我们需要把校验规则填在生成的lib/rules/history-push-no-string.js模板文件中
/**
* @fileoverview desc
* @author xiuji
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/**
* @type {import('eslint').Rule.RuleModule}
*/
module.exports = {
meta: {
type: null, // `problem`, `suggestion`, or `layout`
docs: {
description: "desc",
category: "Fill me in",
recommended: false,
url: null, // URL to the documentation page for this rule
},
fixable: null, // Or `code` or `whitespace`
schema: [], // Add a schema if the rule has options
},
create(context) {
// variables should be defined here
//----------------------------------------------------------------------
// Helpers
//----------------------------------------------------------------------
// any helper functions should go here or else delete this section
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
return {
// visitor functions for different types of nodes
};
},
};
rule规则开发文档详情
第三步:把测例解析成AST
语法解析
eslint官方:cn.eslint.org/parser/
如下图可以看到,history.push的关键调用类型为CallExpression,我们可以通过这个节点的type,name以及arguments的类型,来做判断
create(context) {
// variables should be defined here
//----------------------------------------------------------------------
// Helpers
//----------------------------------------------------------------------
// any helper functions should go here or else delete this section
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
return {
// visitor functions for different types of nodes
CallExpression: function (node) {
const callee = node.callee;
const { type, object, property } = callee;
if (type === 'MemberExpression' && object.type === 'Identifier' && property.type === 'Identifier') {
if (property.name==='history'&&['push', 'replace'].includes(property.name)) {
if (node.arguments[0].type !== 'ObjectExpression') {
//到这里就可以判断调用了history.push('string'),通过context.report报告问题
context.report({
node,
message: `umi history的[${property.name}]方法必须使用对象参数`
})
}
}
}
},
};
},
第四步:增加history和umi的关联
思考以下场景:
import { history } from 'someApi'
import {history as xxx } from 'umi'
function (){
history.push('abc');
xxx.push({
path:'path/route'
})
}
按照第三步的判断,一定是会报错的,所以,我们要把xxx和umi关联起来,这个时候,凭借查看AST树是没法做到的。
在上面的规则文档中,中提供了两个跟踪对变量的引用的api
1. context.getDeclaredVariables(node)
2. context.getScope()
因为我们的node节点是CallExpression类型,使用1拿不到相关数据,所以我选择了getScope(),拿到了海量深层次数据,这个时候,我们需要配置vscode来调试我们运行的程序
按照以下步骤添加调试
1. 如下图,创建launch.json (已存在可以忽略)
2. 文件中填入以下内容,name 和program路径可根据实际修改。
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "调试history-push-no-string", // 调试界面的名称
// 运行项目下的这个文件:
"program": "${workspaceFolder}/tests/lib/rules/history-push-no-string.js",
"args": [] // node 文件的参数
},
]
}
3. 在项目中打断点,并启动调试,如下图
通过调试,我们找到了history和 umi 的关联,通过编写一个函数,判断push("string")的调用者是不是来自于umi 的history;
create(context) {
// variables should be defined here
//----------------------------------------------------------------------
// Helpers
//----------------------------------------------------------------------
// any helper functions should go here or else delete this section
//----------------------------------------------------------------------
// Public
//----------------------------------------------------------------------
const isDefinedByUMI = (scope, identifierName) => {
const { block } = scope;
const isDefined = (block) => {
if (block.type === 'Program') {
let body = block.body;
for (let i = 0; i < body.length; i++) {
let { source, specifiers } = body[i];
if (source && source.value === 'umi') {
for (let j = 0; j < specifiers.length; j++) {
let { local } = specifiers[j];
if (local.name === identifierName) {
return true;
}
}
}
}
return false;
} else {
return isDefined(block.parent);
}
}
return isDefined(block);
}
return {
// visitor functions for different types of nodes
CallExpression: function (node) {
const callee = node.callee;
const { type, object, property } = callee;
if (type === 'MemberExpression' && object.type === 'Identifier' && property.type === 'Identifier') {
if (['push', 'replace'].includes(property.name)) {
if (node.arguments[0].type !== 'ObjectExpression') {
// 走到这里已经确定是(someIdentifier).push('string')调用,最后判断是全局是否有umi的history对象
const scope = context.getScope();
const identifierName = object.name;
const exists = isDefinedByUMI(scope, identifierName);
if (exists) {
context.report({
node,
message: `umi history的[${property.name}]方法必须使用对象参数`
})
}
}
}
}
},
};
},
最后,查看单测运行结果
至此,我们完成了eslint插件及规则的开发
项目接入
在我们创建的eslint插件项目中执行
npm link
在workstation项目中执行
npm link eslint-plugin-aloudata-lint
在.eslintrc.js 文件中集成插件:
// .eslintrc.js
plugins: ['aloudata-lint'],
rules: {
...
'aloudata-lint/history-push-no-string': ['error'],
}
执行校验命令
npm run lint-js
参考资料
团队介绍
以上便是本次分享的全部内容,来自团队 @修己 分享,希望对你有所帮助^_^
喜欢的话别忘了 分享、点赞、收藏 。
我们是来自大应科技(Aloudata, www.aloudata.com )的前端团队,负责大应科技全线产品前端开发工作。在一家初创公司,前端团队不仅要快速完成业务迭代,还要同时建设前端工程化的落地和新技术的预研,我们会围绕产品品质、开发效率、创意与前沿技术等多方向对大前端进行探索和分享,包括但不限于性能监控、组件库、前端框架、可视化、3D 技术、在线编辑器等等。