stylelint实现原理

avatar
FE @字节跳动

lint类工具是我们在前端开发工作中经常的工具,比如eslint和stylelint。lint可以帮助我们发现代码中可能存在的错误,有时候我们也会利用其提供的api来实现一些自定义的代码检查。但我们其实很少了解lint的实现原理,只是为用而用,这篇文章就来简单探究下stylelint的使用与实现原理。

使用

  1. 首先安装stylelint,所有样式文件的校验都是通过stylelint完成的
yarn add -D stylelint
  1. 然后在项目的根目录下创建配置文件.stylelintrc.json,主要的配置项如下
{
    "extends": ["xx"], // 继承另外的stylelint配置
    "plugins": ["xxx"], // 一系列自定义的规则
    "customSyntax": xxx, // 自定义解释器
    "rules": {
        "xxxxx": true,
    } // 一系列官方提供的规则
}
  1. 最后在package.json中加入校验的脚本,可以配合git hooks和CI来加强校验
{
    "scripts": {
        "stylelint-fix": "stylelint **/*.{css,less,scss} --fix"
    }
}

执行后效果如下图

基础的配置建议直接extends以下三个config

stylelint-config-recommended: 此config打开了官方规则中提供所有可能导致错误的规则

stylelint-config-rational-order: 此config是用来决定css书写顺序的,可以提供用户良好的编码习惯

stylelint-config-prettier: 此config关闭了官方规则中可能与prettier冲突的规则

实现原理

入口

直接从stylelint执行命令开始分析

npx stylelint "**/*.css" --fix

这段命令即是对项目下所有后缀为css的文件进行stylelint的校验并进行自动修复,其余较重要的可指定参数为

--config
指定stylelint的配置文件地址,如果没有指定,则会按具体规则寻找
--config-basedirconfig中extends和plugins是相对路径时可用,指定config目录
--ignore-path
指定哪些文件不需要进行lint检查
--fix
是否自动修复检测出来的问题
--custom-syntax
指定postcss解释器
--stdin
直接输入需要lint检查的语法
--formatter
将stylelint结果以默认方式序列化输出
--custom-fomatter
指定方式格式化stylelint的结果

接下来直接进入代码逻辑

可以看到执行lint操作的时候其实是执行了stylelint文件中导出的standalone方法,传入options就是我们指定的参数

核心逻辑

standalone主要是下面的步骤:

  1. 将参数处理并用来创建stylelint对象

  1. 运用stylelint对象上的_lintSource方法对文件或输入的类css语法进行校验,_lintSource是stylelint的核心方法,就是在这个方法中完成了对css的校验,我们详细拆解下这个方法

    a. 首先通过语法解析拿到AST(可以在ast explore查看AST结果),这里使用的是postcss解释器,可以通过customSyntax属性来配置使用的是哪个解释器
    b. 拿到AST之后就用lintPostcssResult方法来对AST进行相关规则的校验,该方法的核心逻辑为使用用户配置的rule对应的function来校验相关语法

  1. 校验完成后如果有配置自动修复,则会对有问题的代码尝试自动

  1. 调用方法对lint之后的结果进行处理输出,其中包含resultProcessor和fomatter等处理

流程图

image.png

实现自定义lint

有了上面的基础,我们就可以很轻松的实现一个最简单的lint工具。这个工具接受一段代码输入,分析之后返回lint结果。

这里拿graphql来举例,下面是一段graphql查询语句

query{
  ListAZ(version:"yyy", version:"zzz"){
    AZ{
      RegionAZDesc
    }
  }
}

一般来说,lint规则需要提供两种能力

  • 找出可能出错的语法,避免出现预期外的错误
  • formatter相关的能力,规范代码的写法

这里我们简单点,默认只有一个规则

  • 找出重复的参数,将第一个参数去掉

首先我们需要通过parser(词法分析和语法分析)将之转换为AST,这里我们可以直接使用graphql提供的parser

const {parse} = require('graphql');

const ast = parse(code)

然后我们需要把规则注册好,在AST上查询是否有多余参数的实现

module.exports = (root) => {
    // 遍历ast查询重复的参数
    const results = root.definitions.map(item => item?.selectionSet?.selections?.map(item => {
        const result = {};
        const dupArgs = [];
        item?.arguments?.forEach(args => {
            /** 不存在参数则存储,存在参数则先把旧参数存入重复集中,再delete旧参数,最后存入新参数*/
            if (!result[args.name.value]) {
                result[args.name.value] = args
            } else {
                dupArgs.push(result[args.name.value]);
                delete result[args.name.value];
                result[args.name.value] = args
            }
        })
        return dupArgs;
    }));
    const dupArgs = (results || []).flat(2);
    return !Boolean(dupArgs.length);
}

根据用户配置的规则使用相应的规则来校验,最后返回这次lint的结果

const performRules = [];
const rules = Object.keys(config.rules);
for (const rule of rules) {
    performRules.push(ruleFunctions[rule](ast))
}
const result = performRules.every(Boolean);
console.log('lint结果:', result);
return result;

运行结果示例

总结

如果看完了上面的实现原理以及自定义lint,应该很清楚lint类工具的大概原理和步骤了,简单来说只有三步:

  1. 通过parser拿到相关AST
  1. 调用ruleFunction对AST进行检验
  1. 修复相关的代码并处理结果

最后

如果有时间,大家也可以想一下如何实现别的编程语言的lint工具,或者进一步探索如何给上文自定义lint实现自动修复的功能。