前端的平常开发离不开各类 lint 的支持,使用 lint 的一种误解是:我的能力不足,必须 lint 规范才能写出规范的代码,实际上规范的定义主要取决于开源项目做者的习惯,或者公司团队编码的习惯,即便两个前端专家,写出的代码规范也会有差异。前端
今天主题聊聊 eslint,做为最主流的 JavaScript lint 工具深受你们喜好,而 JSHint 却逐渐淡出了你们的视线,使用的比较少了vue
经常使用的 eslint 扩展有 standard,airbnb 等node
扩展无非就做两个事情react
原理就是利用 eslint 的继承模式,理论上能够无限继承并覆盖上一级的规则git
第一条不详细介绍了,eslint 官网说的十分详细,基本每一条规则都支持自定义参数,覆盖面也特别广,基本上全部的语法都有 rulegithub
第二条的自定义 rule 才是本文的重头戏,由于特殊的业务场景靠 eslint 自身配置已经没法知足业务需求了,如:算法
通常特殊场景的自定义规则都使用 eslint-plugin-*
的命名,使用时能够方便的写成express
{ plugins: [ 'vue', 'react', 'jest' ] }
固然 eslint-config-* 同理,不过配置时须要写成npm
{ extends: 'standard' }
下面介绍下开发流程json
官方推荐使用 yeoman 生成项目,感受生成的项目比较守旧,推荐下习惯个人项目结构
eslint-plugin-skr |- __tests__ | |- rules | |- utils | |- lib | |- rules | |- utils | |- index.js | |- jest.config.js | |- package.json | |- README.md
总体看下来发现多了 jest 配置文件,是的 yeoman 生成的项目默认采用 Mocha 做为测试框架,我的感受调试起来麻烦,没有 jest 灵活,vscode 轻松搞定调试
教程一搜一大把哈,给伸手党一个连接 debugging-jest-tests
关于 jest 的 config 文件也po出来一下,都是些基本的配置,复杂的用不到,下面会详细介绍测试部分
module.exports = { testEnvironment: 'node', roots: ['__tests__'], resetModules: true, clearMocks: true, verbose: true }
自定义的规则所有在 lib/rules 下面,每条规则单独一个文件足以
下面一个简单的例子打通任督二脉
前期准备
这个官方文档写的密密麻麻,好几十个属性,其实只是冰山一角,有不少复杂场景须要考虑
有人疑问:必定须要精通 AST?
个人回答是固然不须要,简单了解即是,最起码知道解析出来的语法树大致结构长什么样子
那就随便给本身一个命题写吧!写个超级简单的
module.exports = { meta: { docs: { description: '禁止块级注释', category: 'Stylistic Issues', recommended: true } }, create (context) { const sourceCode = context.getSourceCode() return { Program () { const comments = sourceCode.getAllComments() const blockComments = comments.filter(({ type }) => type === 'Block') blockComments.length && context.report({ message: 'No block comments' }) } } } }
具体写法官方文档有介绍哈,就不赘述了,例子也十分简单,调用了环境变量 context 中的方法获取所有注释
如须要 lint bar
对象中属性的顺序,以下假设一个规则
// good const bar = { meta: {}, double: num => num * 2 } // bed const bar = { double: num => num * 2, meta: {}, }
这个第一次些会有些蒙,官网没有提供具体的例子,解决办法很简单,推荐一个利器 astexplorer
点进去别着急复制代码查看 AST 结果,首先选择 espree(eslint 使用的语法解析库),以下
这短短的四行代码会对应着一个抽象语法树,以下图:
因为全展开太长了哈,感兴趣的自行尝试,会发现层级嵌套的特别深,找到 bar
的属性须要 Program.body[0].declarations[0].init.properties
固然不至于每次都从最顶级的 Program
找下来,从上面的例子能够看出 create
方法的 return
返回的是一个 object,里面能够定义不少检测类型,如官网的例子:
function checkLastSegment (node) { // report problem for function if last code path segment is reachable } module.exports = { meta: { ... }, create: function(context) { // declare the state of the rule return { ReturnStatement: function(node) { // at a ReturnStatement node while going down }, // at a function expression node while going up: "FunctionExpression:exit": checkLastSegment, "ArrowFunctionExpression:exit": checkLastSegment, onCodePathStart: function (codePath, node) { // at the start of analyzing a code path }, onCodePathEnd: function(codePath, node) { // at the end of analyzing a code path } } } }
这里可使用 VariableDeclarator
类型做为检察目标,从下面的解析树能够分析出筛选条件
以 VariableDeclarator
对象做为当前的 node
当变量名为 bar
,即 node.id.name === 'bar'
,且值为对象,即 node.init.type === 'ObjectExpression'
,代码以下:
module.exports = { meta: { ... }, create (context) { return { VariableDeclarator (node) { const isBarObj = node.id.name === 'bar' && node.init.type === 'ObjectExpression' if (!isBarObj) return // checker } } } }
就这样成功取到 bar
对象后就能够检测属性的顺序了,排序算法一大把,挑一个喜欢的用就好了,这里不啰嗦了,直接上结果:
const ORDER = ['meta', 'double'] function getOrderMap () { const orderMap = new Map() ORDER.forEach((name, i) => { orderMap.set(name, i) }) return orderMap } module.exports = { create (context) { const orderMap = getOrderMap() function checkOrder (propertiesNodes) { const properties = propertiesNodes .filter(property => property.type === 'Property') .map(property => property.key) properties.forEach((property, i) => { const propertiesAbove = properties.slice(0, i) const unorderedProperties = propertiesAbove .filter(p => orderMap.get(p.name) > orderMap.get(property.name)) .sort((p1, p2) => orderMap.get(p1.name) > orderMap.get(p2.name)) const firstUnorderedProperty = unorderedProperties[0] if (firstUnorderedProperty) { const line = firstUnorderedProperty.loc.start.line context.report({ node: property, message: `The "{{name}}" property should be above the "{{firstUnorderedPropertyName}}" property on line {{line}}.`, data: { name: property.name, firstUnorderedPropertyName: firstUnorderedProperty.name, line } }) } }) } return { VariableDeclarator (node) { const isBarObj = node.id.name === 'bar' && node.init.type === 'ObjectExpression' if (!isBarObj) return checkOrder(node.init.properties) } } } }
这里代码有点多,耐心看完其实挺简单的,大体解释下
getOrderMap
方法将数组转成 Map 类型,方面经过 get
获取下标,这里也能够处理多纬数组,例如两个 key
但愿在相同的排序等级,不分上下,能够写成:
const order = [ 'meta' ['double', 'treble'] ] function getOrderMap () { const orderMap = new Map() ORDER.forEach((name, i) => { if (Array.isArray(property)) { property.forEach(p => orderMap.set(p, i)) } else { orderMap.set(property, i) } }) return orderMap }
这样 double
和 treble
就拥有相同的等级了,方便后面扩展,固然实际状况会有 n 个属性的排序规则,也能够在这个规则上轻松扩展,内部的 sort 逻辑就不赘述了。
开发就介绍到这里,经过上面安利的在线语法解析工具能够轻松反推出 lint 逻辑。
若是 rule 比较复杂,就须要大量的 utils 支持,否则每一个 rule 都会显得一团糟,比较考验公共代码提取的能力
如前面所讲建议使用 jest 进行测试,这里的测试和普通的单元测试还不太同样,eslint 是基于结果的测试,什么意思呢?
lint 只有两种状况,经过与不经过,只须要把经过和不经过的状况整理成两个数组,剩下的工做交给 eslint 的 RuleTester
处理就好了
上面的属性排序 rule,测试以下:
const RuleTester = require('eslint').RuleTester const rule = require('../../lib/rules/test') const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }) ruleTester.run('test rule', rule, { valid: [ `const bar = { meta: {}, double: num => num * 2 }` ], invalid: [ { code: `const bar = { double: num => num * 2, meta: {}, }`, errors: [{ message: 'The "meta" property should be above the "double" property on line 2.' }] } ] })
valid
中是但愿经过的代码,invalid
中是不但愿经过的代码和错误信息,到这里一个 rule 算是真正完成了。
最后写好的 rules 须要发一个 npm 包,以便于在项目中使用,这里就不赘述怎么发包了,简单聊聊怎么优雅的把 rules 导出来。
直接上代码:
const requireIndex = require('requireindex') // import all rules in lib/rules module.exports.rules = requireIndex(`${__dirname}/rules`)
这里使用了三方依赖 requireindex
,对于批量的导出一个文件夹内的全部文件显得简洁不少。
固然前提是保证 rules 文件夹下都是 rule 文件,不要把 utils 写进去哈
行文目的是国内外对于自定义 eslint rule 的相关资源较少,但愿分享一些写自定义规则的经验。
千万不要在学习 AST 上浪费时间,不一样的库对 AST 的实现是不一样的,下次写 babel 插件又要学其它的 AST 规则,再次安利一下 AST 神器 astexplorer,只要把须要验证的代码放到 astexplorer
里跑一遍,而后总结出规律,逻辑其实十分简单,对 AST 结果进行判断就好了。
从团队层面讲,但愿全部的团队都有本身的 eslint 规则库,能够大大下降 code review 的成本,又能保证代码的一致性,一劳永逸的事情。