公司站点作前端架构改造,须要把历史代码中,全部用到的色值替换成变量,便于作主题化和样式迭代。
项目一期经过 nodejs 脚本,扫代码并人工作替换。
考虑到新代码的后期保障和后续其余改造工做,决定编写 Lint 并整合到项目的 CI 脚本中。
由于采用的是 react,涉及到的色值,一部分在 jsx 代码中,一部分在自定义的 css 中,因此须要分别开发eslint和stylelint插件。javascript
在开发 eslint 插件前,先简单理一下下面几个概念:css
规则是 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 工做的原理,是利用解析器将 javascript 代码解析成 AST(抽象语法树),并对 AST 作从上至下和从下至上的两次遍历。同时,生效的规则会对 AST 中某些节点的选择器作监听,并触发回调。
所谓的 AST,其实就是一个树状的数据结构,每一个节点都有对应的选择器。选择器不少,能够经过mdn或estree查看不一样 js 版本的 AST 选择器。
这里推荐一个在线工具:astexplorer.net/ 能够对 js 代码片断在线解析,对后面开发插件带来很大的帮助。
eslint 官方默认的解析器是espree,其余用的比较多的还有babel-eslint,比官方支持更多最新的语法特性。node
官方提供的可配置的规则,都是内置在 eslint 包中的。若是想自定义规则,好比开始提到的查找色值这类特殊需求,就必须开发 eslint 插件。一个 eslint 插件,一般是若干规则和处理器的集合,好比写 react 项目,经常会用到的eslint-plugin-react。
下面就正式介绍,开发一个 eslint 插件的主要过程。react
安装官方推荐的脚手架工具Yeoman和对应的generator-eslint:git
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
打开入口文件: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 能够参考官方文档
接下来分析下需求,须要”检测全部 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
,显示运行结果:
若是以为编写测试用例太过麻烦,能够直接在真实项目中安装测试:
"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" ] } 复制代码
或者使用在线工具:
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 文件。
eslint 是用来解析 javascript 的,但项目中,还有部分硬编码的色值在.css 文件中,那么有没有办法检测这些文件呢?答案就是使用 stylelint。
stylelint 的设计大致上与 eslint 很是相似,因此这里重点只就它们的差别点作介绍。主要差别体如今如下几点:
与 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: red
、atrule
,好比@media
、comment
等。
对于咱们检测 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 终究是一种协助工具,实际开发中,测试仍是必不可少的,有条件的话能够上自动化单元测试。