開發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 終究是一種協助工具,實際開發中,測試仍是必不可少的,有條件的話能夠上自動化單元測試。

相關文章
相關標籤/搜索