开发ESLint & Stylelint插件实践

公司站点作前端架构改造,须要把历史代码中,全部用到的色值替换成变量,便于作主题化和样式迭代。
项目一期经过 nodejs 脚本,扫代码并人工作替换。
考虑到新代码的后期保障和后续其余改造工做,决定编写 Lint 并整合到项目的 CI 脚本中。
由于采用的是 react,涉及到的色值,一部分在 jsx 代码中,一部分在自定义的 css 中,因此须要分别开发eslintstylelint插件。javascript

开发 ESLint 插件

在开发 eslint 插件前,先简单理一下下面几个概念:css

  • eslint 规则
  • eslint 解析器
  • eslint 插件

eslint 规则

规则是 eslint 基础配置之一,每条规则都用来检测符合某种特征的代码。一条规则能够配置是否开启以及错误的级别,其中 0 或“off”表明关闭规则,1 或“warn”表明警告(warning),2 或“error“表明错误(error)。好比:前端

rules: {
        "no-unused-vars": 1 // 当存在没有使用却声明的变量时,给出warning
    }
复制代码

有些规则还有 option,则能够经过数组进行配置,好比:java

rules: {
        "no-unused-vars": ["warn", { "ignoreRestSiblings": true }] // 形如var { type, ...coords } = data;type未使用的话会ignore
    }
复制代码

eslint 解析器

eslint 工做的原理,是利用解析器将 javascript 代码解析成 AST(抽象语法树),并对 AST 作从上至下和从下至上的两次遍历。同时,生效的规则会对 AST 中某些节点的选择器作监听,并触发回调。
所谓的 AST,其实就是一个树状的数据结构,每一个节点都有对应的选择器。选择器不少,能够经过mdnestree查看不一样 js 版本的 AST 选择器。
这里推荐一个在线工具:astexplorer.net/ 能够对 js 代码片断在线解析,对后面开发插件带来很大的帮助。
eslint 官方默认的解析器是espree,其余用的比较多的还有babel-eslint,比官方支持更多最新的语法特性。node

eslint 插件

官方提供的可配置的规则,都是内置在 eslint 包中的。若是想自定义规则,好比开始提到的查找色值这类特殊需求,就必须开发 eslint 插件。一个 eslint 插件,一般是若干规则和处理器的集合,好比写 react 项目,经常会用到的eslint-plugin-react
下面就正式介绍,开发一个 eslint 插件的主要过程。react

建立项目

安装官方推荐的脚手架工具Yeoman和对应的generator-eslintgit

npm install -g yo generator-eslint

# 建立项目目录
mkdir eslint-plugin-console
cd eslint-plugin-console

# 生成项目
yo eslint:plugin
复制代码

项目目录结构以下:es6

── eslint-plugin-console
│   ├── CHANGELOG.md
│   ├── README.md
│   ├── lib
│   │   ├── index.js // 入口
│   │   ├── processors // 存放处理器
│   │   └── rules // 存放规则
│   ├── package.json
│   └── yarn.lock
复制代码

这里要注意两点:github

  • eslint 插件有固定的命名形式,以 eslint-plugin-开头,在配置时能够省略这个开头
  • 注意脚手架工具建立的默认 eslint 版本可能较老,这里须要与所应用的项目的 eslint 版本保持一致,避免不适配

打开入口文件:npm

// import all rules in lib/rules
module.exports.rules = requireIndex(__dirname + "/rules");

// import processors
module.exports.processors = {

    // add your processors here
};
复制代码

能够看到,一个最基础的 eslint 插件其实就是一个包含 rules 和 processors 的对象。其余的配置具体能够参考官方文档

建立规则

能够经过脚手架工具执行命令来建立规则:

yo eslint:rule
复制代码

固然也能够手动建立:因为入口文件经过requireindex引用了整个 rules 目录,因此能够直接在 rules 目录下以规则名为文件名建立一个规则文件:no-css-hard-code-color.js
这里须要注意,虽然官方没有限制规则的命名方式,但为了便于理解和维护,一般用于禁用某种形式的规则,能够以 no-开头,后面跟禁止的内容,而且单词以前以短横-分隔。

开发规则

module.exports = {
    meta: {
      type: "problem",
    },
    create: function(context) {
        return {
            // 返回AST选择器钩子
        };
    }
};
复制代码

一个规则导出一个对象,对象中最核心的功能部分,主要在 create 当中,用来监听 AST 选择器。
另外,create 回调中还返回了一个 context 对象,用的最多的就是它的 report 方法,用来给出报错提示。具体 API 能够参考官方文档

合理选择 AST 选择器

接下来分析下需求,须要”检测全部 js 中写死的 css 色值“,那么先要总结出 css 色值的全部形式。
根据mdn查到,大体有四类:内置命名色、hex 色值、rgb 色值和 hsl 色值。因而,针对这四种,分别作匹配:内置色值采用枚举的方式检查,后三种使用正则校验:

/^#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$/ //hex
/^rgba?\(.+\)$/ //rgb
/^hsla?\(.+\)$/ //hsl
复制代码

而后就是调用 AST 选择器钩子作检测。首先能想到的是最简单粗暴的方式,对全部的字面量作检查:

TemplateElement(node) {
  const { value } = node;

  if (value) {
    checkAndReport(value.raw, node);
  }
},
复制代码

然而通过测试,这种方式会存在大量的误检测,好比:

<a className="blue" href="/" />
复制代码

这里的 blue 不是色值,但也会一律被误检出来。
调整策略,针对对象的 key 须要作一层白名单过滤。同时,经过分析可知,色值可能存在于如下几种状况中:

  • 对象的值
  • 变量声明的值
  • 变量赋的值
  • 三元表达式的值
  • 模板字符串

因而修改代码以下:

// 对象的值
Property(node) {
  const whiteList = ["className", "type", "warn"];
  if (whiteList.indexOf(node.key.name) >= 0) return;

  if (node.value.type === "Literal") {
    checkAndReport(node.value.value, node.value);
  }
},

// 变量定义
VariableDeclarator(node) {
  if (!node.init) return;

  if (node.init.type === "Literal") {
    checkAndReport(node.init.value, node.init);
  }
},

// 变量赋值
AssignmentExpression(node) {
  if (node.right.type === "Literal") {
    checkAndReport(node.right.value, node.right);
  }
},

// 三元表达式
ConditionalExpression(node) {
  if (node.consequent.type === "Literal") {
    checkAndReport(node.consequent.value, node.consequent);
  }

  if (node.alternate.type === "Literal") {
    checkAndReport(node.alternate.value, node.alternate);
  }
},

// 模板字符串
TemplateElement(node) {
  const { value } = node;
  checkAndReport(value.raw, node);
},
复制代码

这样基本能检测出全部硬编码的色值。
不过规则可能仍是存在一些问题。一方面,一些特殊状况可能会误检,这时能够经过eslint 注释针对部分代码片断作过滤;另外一方面,规则仍是有漏洞的,好比若是经过模板字符串把内置色值名作了拆分、并赋值给新的变量,就检测不出来了。但这种状况通常不用考虑,若是为了绕过检测,直接用前面所述的 eslint 注释忽略掉就好了。

测试规则

测试插件规则的方式,我总结下来有三种:

eslint 的测试工具依赖mocha,因此须要先安装 mocha(脚手架搭建的话能够忽略这步):

npm install mocha --dev
复制代码

而后再 tests 目录下编写测试用例:

var rule = require("../../../lib/rules/no-css-hard-code-color"),
  RuleTester = require("eslint").RuleTester;

var ruleTester = new RuleTester();
ruleTester.run("no-css-hard-code-color", rule, {
  valid: [{ code: "var designToken = T_COLOR_DEFAULT" }],

  invalid: [
    {
      code: "var designToken = '#ffffff'",
      errors: [
        {
          message: "Please replace '#ffffff' with DesignToken. You can find in http://ui.components.frontend.ucloudadmin.com/#/Design%20Tokens?id=color",
        },
      ],
    },
  ],
});
复制代码

添加 npm 脚本:

"scripts": {
  "test": "mocha tests --recursive",
},
复制代码

运行npm run test,显示运行结果:

示例1

若是以为编写测试用例太过麻烦,能够直接在真实项目中安装测试:

"dependencies": {
  "eslint-plugin-console": "../eslint-plugin-console",
}
复制代码

而后添加 .eslintrc 配置:

{
  "parser": "babel-eslint",
  "env": {
    "browser": true,
    "es6": true,
    "node": true
  },
  "rules": {
    "console/no-css-hard-code-color": 2
  },
  "plugins": [
    "eslint-plugin-console"
  ]
}
复制代码

或者使用在线工具:

示例2

发布

eslint 插件通常都是以 npm 包的形式发布和引用的,因此能够在 package.json 中添加发布脚本:

"scripts": {
  "_publish": "npm publish",
  "publish:patch": "standard-version --release-as patch --no-verify && npm run _publish",
  "publish:minor": "standard-version --release-as minor --no-verify && npm run _publish"
},
复制代码

这里引入standard-version,能够实现自动生成 CHANGELOG 文件。

开发 stylelint 插件

eslint 是用来解析 javascript 的,但项目中,还有部分硬编码的色值在.css 文件中,那么有没有办法检测这些文件呢?答案就是使用 stylelint。

与 eslint 的差别

stylelint 的设计大致上与 eslint 很是相似,因此这里重点只就它们的差别点作介绍。主要差别体如今如下几点:

  • 解析器
  • 插件入口
  • 命名规则

stylelint 解析器

与 eslint 最核心的区别,无疑就是解析器。stylelint 所使用的解析器,是大名鼎鼎的postcss。若是开发过 postcss 插件就会发现,stylelint 的处理逻辑就相似于 postcss 插件。

具体实现上来讲,stylelint 经过stylelint.createPlugin方法,接收一个 rule 回调函数,并返回一个函数。函数中能够取到所检测 css 代码的 postcss 对象,该对象能够调用 postcss 的 api 对代码进行解析、遍历、修改等操做:

function rule(actual) {
  return (root, result) => {
    // root即为postcss对象
   };
}
复制代码

相比 eslint,css 的节点类型少不少,主要有rule,好比#main { border: 1px solid black; }decl,好比color: redatrule,好比@mediacomment等。

对于咱们检测 css 属性值是否含有色值的需求,能够调用root.walkDecls对全部 css 规则作遍历:

root.walkDecls((decl) => {
  if (decl) { ... }
});
复制代码

随后,再利用postcss-value-parser解析出规则中的值部分,经过枚举或正则,判断是否为色值:

const parsed = valueParser(decl.value);
parsed.walk((node) => {
  const { type, value, sourceIndex } = node;

  if (type === "word") {
    if (
      /^#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$/.test(value) ||
      colorKeywords.includes(value)
    ) {
      ...
    }
  }

  if (type === "function" && /^(rgba?|hsla?)$/.test(value)) {
    ...
  }
});
复制代码

最后,当检测到色值时,调用 stylelint 提供的report方法给出报错提示:

const messages = ruleMessages(ruleName, {
  rejected: (color) =>
    `Unexpected hard code color "${color}", please replace it with DesignToken.`,
});
report({
  message: messages.rejected(valueParser.stringify(node)),
  node: decl,
  result,
  ruleName,
});
复制代码

插件入口

与 eslint 不一样的是,stylelint 插件经过stylelint.createPlugin建立。若是一个插件包含多个规则,则能够返回数组:

const requireIndex = require("requireindex");
const { createPlugin } = require("stylelint");
const namespace = require("./lib/utils/namespace");
const rules = requireIndex(__dirname + "/lib/rules");

const rulesPlugins = Object.keys(rules).map((ruleName) => {
  return createPlugin(namespace(ruleName), rules[ruleName]);
});

module.exports = rulesPlugins;
复制代码

这里参照了 eslint 插件相似的目录结构,经过 requireIndex 一块儿倒入进入口文件。

命名规则

相比 eslint,stylelint 官方对规则的命名作了建议,通常由两部分组成,即检测的对象+检测的内容,好比咱们检测硬编码的色值,就能够命名为color-no-hard-code。具体规则可见:stylelint.io/user-guide/…

总结

eslint 和 stylelint 能够帮助团队代码风格统1、减小 bug,而经过自定义插件和规则,能够根据业务和框架状况,定制化一些特性,这点在架构迭代中颇有帮助,好比要下线某个组件或 组件的 api。可是 lint 终究是一种协助工具,实际开发中,测试仍是必不可少的,有条件的话能够上自动化单元测试。

相关文章
相关标签/搜索