Stylelint自定义规则并不是只能依赖官方提供的几十条内置规则,当团队内部存在特殊规范时,完全可以编写自己的规则来约束CSS代码。这背后的机制是Stylelint的插件系统,它允许开发者用JavaScript函数定义一个规则,然后在配置文件中像使用内置规则一样启用它。本文会从配置方式开始,逐步深入到规则函数内部,最后展示如何编写带自动修复能力的自定义规则。

一、配置Stylelint以加载自定义规则
自定义规则通常以npm包的形式发布,这样可以在多个项目中复用。一个标准的Stylelint插件包至少需要包含一个package.json文件和一个主入口文件,比如index.js。package.json中的name字段就是插件名,Stylelint会用这个名称作为规则名前缀。例如插件名为stylelint-plugin-my-rules,那么规则名就是stylelint-plugin-my-rules/no-disallowed-property这种格式。当然你也可以使用npm包名更短的形式,但前缀必须与插件模块名一致。
在项目的Stylelint配置文件里,通过plugins字段引入这个插件包,然后在rules对象中启用具体规则。下面是一个典型的配置示例,它加载了一个假设的插件包,并开启了一条名为no-disallowed-property的规则,同时传入了一个选项对象:
module.exports = {
plugins: ['stylelint-plugin-my-rules'],
rules: {
'stylelint-plugin-my-rules/no-disallowed-property': [true, {
disallowed: ['float', 'clear']
}]
}
};
配置文件支持多种格式,包括.stylelintrc、.stylelintrc.json、.stylelintrc.js和package.json中的stylelint字段。使用JavaScript格式的好处是可以通过require动态引入插件模块,避免依赖全局安装。如果你正在本地开发一个插件,还没有发布到npm,可以使用相对路径或绝对路径加载插件文件,但此时规则名前缀会变成文件路径的某些形式,因此更推荐的做法是先在本地创建node_modules目录,然后通过npm link或直接复制到node_modules中模拟正式安装。
还需要注意,插件包的入口文件必须导出Stylelint能够识别的规则对象。这个对象通过stylelint.createPlugin方法创建,并且必须为每条规则设置ruleName和messages属性。下面是一个最小化的插件入口文件结构,展示了如何导出一条自定义规则:
const stylelint = require('stylelint');
const ruleName = 'no-disallowed-property';
const messages = stylelint.utils.ruleMessages(ruleName, {
rejected: (prop) => `Unexpected disallowed property "${prop}"`,
});
const ruleFunction = function(primaryOption, secondaryOptionObject, context) {
return function(root, result) {
// 规则逻辑将在下一节实现
};
};
module.exports = stylelint.createPlugin(ruleName, ruleFunction);
module.exports.ruleName = ruleName;
module.exports.messages = messages;
插件包中还可以导出多个规则,只需要在入口文件中创建多个createPlugin调用,然后分别指定不同的规则名即可。Stylelint会遍历插件模块导出的所有属性,把每个规则函数注册到自己的规则表中。
二、自定义规则的执行模型与API
每一条Stylelint规则在运行时本质上是一个函数,它接收三个参数:primaryOption是规则配置中的第一个值,比如布尔值或字符串;secondaryOptionObject是配置中的第二个值,通常是一个选项对象;context则包含了当前lint会话的上下文信息,其中最常用的是fix属性,它表示是否处于自动修复模式。这个函数需要返回另一个函数,返回的函数会接收PostCSS的root节点和Stylelint的result对象。
在内部逻辑中,最常用的遍历方式是root.walkDecls,它会访问样式表中的每一条声明,比如color: red或margin: 0。每个声明节点包含prop和value属性,以及指向父规则的选择器信息。如果需要检查选择器,可以使用root.walkRules;如果需要检查@规则,可以使用root.walkAtRules。遍历时发现的违规项需要通过stylelint.utils.report方法报告给Stylelint,报告时需要指定消息文本、违规节点、结果对象和规则名。
下面是一个完整的规则函数实现,它检查所有声明,如果属性名出现在禁用列表中,就报告一条警告:
const stylelint = require('stylelint');
const ruleName = 'no-disallowed-property';
const messages = stylelint.utils.ruleMessages(ruleName, {
rejected: (prop) => `Unexpected disallowed property "${prop}"`,
});
module.exports = stylelint.createPlugin(ruleName, function(primaryOption, secondaryOptionObject, context) {
return function(root, result) {
const validOptions = stylelint.utils.validateOptions(result, ruleName, {
actual: primaryOption,
possible: [true, false],
}, {
actual: secondaryOptionObject,
possible: {
disallowed: [Array],
},
});
if (!validOptions) {
return;
}
const disallowed = secondaryOptionObject.disallowed || [];
root.walkDecls((decl) => {
if (disallowed.includes(decl.prop)) {
stylelint.utils.report({
message: messages.rejected(decl.prop),
node: decl,
result,
ruleName,
});
}
});
};
});
module.exports.ruleName = ruleName;
module.exports.messages = messages;
在这个实现中,validateOptions用于校验用户传入的配置是否合法,避免因为选项类型错误而导致难以调试的问题。校验通过后取出disallowed数组,遍历所有声明节点,用includes判断属性名是否被禁用。报告消息时传入违规节点,Stylelint会自动提取该节点的源码位置和片段,最终输出带行列号的警告信息。这个模式几乎适用于所有“禁用某类值”的规则,只要替换判断条件和消息文本即可。
三、扩展规则:支持自动修复和上下文信息
很多Stylelint规则不仅能够报告问题,还可以在--fix模式下自动修改代码。要实现自动修复,需要在规则函数中检查context.fix的值。当它为true时,不调用report方法,而是直接修改AST节点。例如把颜色值统一转换成小写,或者把属性值中的冗余空格清理掉。修改完成后,Stylelint会重新输出样式,生成修复后的文件内容。
自动修复的逻辑必须保证幂等性,也就是说对同一个文件连续执行两次修复,第二次不应该再产生任何修改。这要求你在修改节点后,不再对同一个节点重复报告问题。下面这个例子定义了一条规则,检查声明中颜色值是否使用小写的十六进制格式,如果检测到大写字母,在修复模式下直接将其转换为小写:
const stylelint = require('stylelint');
const ruleName = 'color-hex-case';
const messages = stylelint.utils.ruleMessages(ruleName, {
expected: (actual, expected) => `Expected "${actual}" to be "${expected}"`,
});
module.exports = stylelint.createPlugin(ruleName, function(primaryOption, secondaryOptionObject, context) {
return function(root, result) {
root.walkDecls(/^color$/, (decl) => {
const value = decl.value;
if (/^#[0-9A-F]{6}$/i.test(value)) {
const normalized = value.toLowerCase();
if (normalized !== value) {
if (context.fix) {
decl.value = normalized;
} else {
stylelint.utils.report({
message: messages.expected(value, normalized),
node: decl,
result,
ruleName,
});
}
}
}
});
};
});
module.exports.ruleName = ruleName;
module.exports.messages = messages;
上面代码中,walkDecls可以接收一个正则表达式作为过滤条件,只处理属性名为color的声明。通过修改decl.value直接改写了声明值,PostCSS会在底层更新对应的源码字符串。值得注意的是,自动修复模式只有在用户显式传入--fix参数时才生效,普通的lint执行仍然会按报告模式运行。这种分支设计保证了规则既可用于检查,也可用于修复。
四、调试与测试自定义规则
自定义规则在正式投入使用之前,必须经过充分的测试。Stylelint官方推荐使用jest-preset-stylelint这个测试预设,它封装了常见的断言逻辑,让你可以专注于规则行为的验证。测试文件通常位于插件包的__tests__目录下,每条规则对应一个测试文件。测试用例直接调用stylelint.lint方法,传入一段样式的字符串和配置对象,然后断言输出警告的数量和文本。
下面是一个使用Jest编写的简单测试,它验证了前面提到的禁用属性规则:
const stylelint = require('stylelint');
test('rejects disallowed property', () => {
return stylelint.lint({
code: 'a { float: left; }',
config: {
plugins: ['./index.js'],
rules: {
'my-plugin/no-disallowed-property': [true, { disallowed: ['float'] }]
}
}
}).then((data) => {
const warnings = data.results[0].warnings;
expect(warnings).toHaveLength(1);
expect(warnings[0].text).toBe('Unexpected disallowed property "float"');
});
});
test('accepts allowed property', () => {
return stylelint.lint({
code: 'a { display: block; }',
config: {
plugins: ['./index.js'],
rules: {
'my-plugin/no-disallowed-property': [true, { disallowed: ['float'] }]
}
}
}).then((data) => {
expect(data.results[0].warnings).toHaveLength(0);
});
});
调试时如果发现规则没有按预期工作,可以先打印一下root节点的结构。PostCSS提供了root.toString()方法输出格式化后的CSS,也可以使用JSON.stringify(root.toJSON())查看节点的完整属性。但更实用的做法是在规则函数内部使用console.log(decl.prop, decl.value),观察遍历到了哪些声明以及它们的值。由于Stylelint底层会捕获并重组输出,简单的console输出不会干扰lint结果。
另外,Stylelint提供了一个stylelint.utils.checkAgainstRule的辅助方法,用于在规则内部调用其他规则,方便复用已有的逻辑。不过对于自定义规则开发来说,这个API用得较少,更常见的是直接在规则函数中实现全部逻辑。测试通过后,就可以把插件包发布到npm,然后在团队项目的Stylelint配置中统一引入,让所有成员共享同一套样式规范。