本篇文章,我们从零开发一个自定义的 Stylelint插件工程。
完整项目地址参见
一、准备阶段
- PostCss官网 (Stylelint使用PostCss先将css内容解析成AST抽象语法树,再通过对AST进行分析处理,达成想要的结果。没错Eslint也是这样);
- Stylelint官网 (想学习一项技术,先看看它的官网)
- StylelintGitHub (照猫画虎)
- 开发环境:macOs-12.1 Apple M1 Pro(不太重要,可能跟后期的插件有关)
二、开发阶段
1. 创建目录
打开终端工具(我用的iterm
),cd 到平时存储项目的目录;
键入mkdir stylelint-plugin
创建一个名为 stylelint-plugin
的文件夹;
2. npm 初始化
cd 进 stylelint-plugin
目录,键入npm init
,接着一路回车,根据提示 输入内容即可;
这一步后,项目中会新增一个package.json
文件;
3. 构造项目结构
仿照StylelintGitHub 构造项目结构如下:
├── lib // 规则相关内容都放在`lib`目录
│ ├── index.js
│ ├── rules
│ │ ├── index.js
│ │ └── selector-class-no-elements // 每个文件夹下是一个规则,文件夹名即为规则名
│ │ └── index.js // 规则内容
│ └── utils
├── package.json
└── README.md
4. 安装部分依赖
npm install stylelint postcss-selector-parser -D
5. 我们以开发 selector-class-no-elements
规则为例,编写代码。
这个规则意义为:class选择器不能以’elements-'开头
因为第三方组件库(ElementUI、Ant Design等),组件名称是以’elements-'开头,开发过程中不要对这些三方组件覆盖样式,以免造成难以预料的bug。
======================================
这里我们插一下,先看看css解析后的内容是什么样的
步骤4中 安装的postcss-selector-parser
插件,就是解析css中选择器的,
先在根目录中创建一个 test.js
文件,内容如下:
const SelectorParser = require('postcss-selector-parser');
const transform = selectors => {
selectors.walk(node => {
console.log(node);
})
}
SelectorParser(transform).processSync('#app');
然后运行 node test.js
, log结果如下:
前者 “Selector” 类型 表示 选择器;
后者 “ID” 类型 表示 ID选择器
======================================
6. 正式开发:
lib/rules/selector-class-no-elements/index.js
内容如下:
/*
* @Desc: class选择器不能以"elements-" 开头
* @Author: 前端卡卡西
* @Date: 2022-06-25 17:10:14
*/
const { nameSpace, newMessage, validateOptions, report } = require('../../utils');
const parseSelector = require("../../utils/parseSelector");
const ruleName = nameSpace('selector-class-no-elements');
const messages = newMessage(ruleName, {
expected: (selector => `Expected CLASS selector ".${selector}" not to contain "elements-"`)
})
const rule = primary => {
return (postcssRoot, postcssResult) => {
const validOptions = validateOptions(postcssRoot, postcssResult, {
actual: primary
})
if (!validOptions) return;
// walkRules 为遍历所有 Rule类型节点
postcssRoot.walkRules(ruleNode => {
const selector = ruleNode.selector;
parseSelector(selector, postcssResult, ruleNode, (fullSelector) => {
// walk为遍历,同forEach
fullSelector.walk(selectorNode => {
// 筛选class选择器
if (selectorNode.type !== 'class') return;
const {value, sourceIndex} = selectorNode;
// 筛选以 "elements-" 开头的选择器
if (value.indexOf('elements') !== 0) return;
// 标记提示结束为止
const endIndex = sourceIndex + value.length;
// 提示用户信息
report({
result: postcssResult,
ruleName,
message: messages.expected(value),
node: ruleNode,
index: sourceIndex,
endIndex
})
})
})
})
}
}
rule.ruleName = ruleName;
rule.messages = messages;
module.exports = rule;
lib/utils/index.js
内容如下
const stylelint = require("stylelint")
function nameSpace(ruleName) {
// 前缀视个人情况而定
return `stylelint-plugin/${ruleName}`;
}
function newMessage(ruleName, options) {
return stylelint.utils.ruleMessages(ruleName, options);
}
function validateOptions(result, ruleName, options) {
return stylelint.utils.validateOptions(result, ruleName, options);
}
function report(problem) {
console.log({problem});
return stylelint.utils.report(problem);
}
module.exports = {
nameSpace,
newMessage,
validateOptions,
report
}
lib/utils/parseSelector.js
内容如下:
const SelectorParser = require("postcss-selector-parser");
module.exports = function parseSelector(selector, result, node, callback) {
try {
return SelectorParser(callback).processSync(selector);
} catch(err) {
result.warn(`Cannot parse selector (${err})`, { node, stylelintType: 'parseError' });
return undefined;
}
}
lib/rules/index.js
内容如下:
const selectorClassNoElements = require('./selector-class-no-elements');
const rules = {
"selector-class-no-elements": selectorClassNoElements
}
module.exports = rules;
lib/index.js
内容如下:
const { createPlugin } = require("stylelint");
const { nameSpace } = require("./utils");
const rules = require("./rules");
// 通过createPlugin 创建 plugin
const rulesPlugins = Object.keys(rules).map(ruleName => {
return createPlugin(nameSpace(ruleName), rules[ruleName])
})
module.exports = rulesPlugins;
7. 编写测试用例
测试用例 借助 jest
工具。
- 先安装对应依赖:
npm i jest jest-preset-stylelint -D
- 再配置jest,在
package.json
中增加如下配置:
...
"jest": {
"clearMocks": true,
"collectCoverage": false,
"collectCoverageFrom": [
"lib/**/*.js"
],
"coverageDirectory": "./.coverage/",
"coverageReporters": [
"lcov",
"text"
],
"coverageThreshold": {
"global": {
"branches": 75,
"functions": 75,
"lines": 75,
"statements": 75
}
},
"setupFiles": [
"./jest-setup.js"
],
"testEnvironment": "node",
"roots": [
"lib"
],
"testRegex": ".*\\.test\\.js$|lib/.*/__tests__/.*\\.js$"
},
...
- 根目录下创建
jest-setup.js
文件,内容如下:
const getTestRule = require('jest-preset-stylelint/getTestRule');
global.testRule = getTestRule({ plugins: ["./lib"] });
-
lib/rules/selector-class-no-elements/__tests__/index.js
中编写测试代码:(每个规则下边都加一个__tests__/index.js 测试用例文件)
const { messages, ruleName} = require("..");
testRule({
ruleName,
config: true,
// 可通过用例
accept: [
{ code: '.app {}' }
],
// 不可通过用例
reject: [
{ code: '.elements-app {}',
message: messages.expected('elements-app'),
line: 1, // 提示开始行
column: 1, // 提示开始位置
endLine: 1, // 提示结束行
endColumn: 13 // 提示结束位置
}
]
})
8. npm run test
package.json
配置:
...
"scripts": {
"test": "jest"
},
...
运行 npm run test
结果如上,大功告成。
9. 最终目录结构
├── jest-setup.js
├── lib
│ ├── index.js
│ ├── rules
│ │ ├── index.js
│ │ └── selector-class-no-elements
│ │ ├── __tests__
│ │ │ └── index.js
│ │ └── index.js
│ └── utils
│ ├── index.js
│ └── parseSelector.js
├── package-lock.json
├── package.json
├── test.js
└── README.md
三、项目覆盖测试
规则开发完成,需要找项目进行覆盖测试。一般这种工具类项目都是直接发布到 NPM仓库,使用时通过 npm install xxx
安装。
要想不发布npm包,本地测试 可以通过 npm link
命令,在本地的 node_modules
中生成一个软链接,链接到开发目录。
- 在上边的目录中执行
npm link
生成软链接
如果 npm link 之后报错:npm ERR! Error: EACCES: permission denied
,是因为权限原因,可使用 sudo npm link
重试。
- cd 到测试工程,“安装” stylelint-plugin 依赖
- 打开测试工程的
node_modules
, 可以看到: - 配置测试工程
.stylelintrc.js
module.exports = {
...
"plugins":[
"stylelint-plugin"
...
],
rules: {
'stylelint-plugin/selector-class-no-elements': true,
...
},
};