公司站點作前端架構改造,須要把歷史代碼中,全部用到的色值替換成變量,便於作主題化和樣式迭代。
項目一期經過 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 終究是一種協助工具,實際開發中,測試仍是必不可少的,有條件的話能夠上自動化單元測試。