2

最近在做less的一些语法上的处理,想把less的变量在编译时自动转为css的变量使用方式,进而学习了一下less的预处理插件开发。

less支持的插件分为两种模式,一种是普通插件,主要是扩展less的运行时语法(less默认函数)插件,通过@plugin语法使用。这种方式的主要使用请参考官网文档,本文不做探讨。

预处理插件可以对编译的源码在编译前,编译后和编译中进行一些代码调整。也可以添加一些对less的扩展内容,如文件系统的扩展等。由于官方还没有对这一块有对应的文档描述,所以本文在官网文档出来之前,根据自己的使用经验记录的一些最佳实践。

预处理插件代码结构

一个简单的预处理插件的格式为:

class LessPlugin {
  constructor(options) {
    this.minVersion = [3, 0, 0];
    this._options = options;
  }

  install(less, pluginManager, functions) {
    // 插件初始化逻辑
  }

  setOptions() {}

  printUsage() {
    return '';
  }
}

插件需要是一个拥有install方法的对象,会被注入三个参数less, pluginManager, functions

  • less: 当前less对象;
  • pluginManager: 插件管理器,用于注册具体的插件;
  • functions:内部方法管理器,@plugin使用的普通插件就是通过functions.add添加上去的

预处理插件主要是通过pluginManager对象添加,它的关键API有:

  • addVisitor: 添加ast语法访问器,可以在编译过程中修改或者检查语法操作;
  • addPreProcessor: 添加预处理器,可以配置源码在经过less处理前进行预处理的hook
  • addPostProcessor: 添加后处理器,可以配置源码在经过less处理后进行后续加工的hook
  • addFileManager: 添加一个文件管理器,可以扩展文件解析,主要是解决@import的资源导入解析问题
可以查看官方举例的插件列表.

另外插件对象中请尽量设置minVersion属性,用来表明该插件兼容的最低的less版本;setOptions是用于接受less命令行方式执行时传入的参数,在构造函数中接受的选项是由于创建插件对象采取类的方式,所以可以在构建函数中接受这个对象创建的前置参数,也就是可以用于使用代码的方式配置插件时的传入的选项;printUsage也是在使用命令行方式注入插件(--plugin=xxxx),打印帮助的时候输出的提示信息。

构建过程中修改源码的插件

addVisitor可以加一个less的ast语法处理器,在编译过程中对语法进行处理(如将less变量的使用改为css变量的使用)。一个简单的语法访问器的插件创建为:

class LessPlugin {
  // ...

  install(less, pluginManager, functions) {
    // 注册访问器,访问器为一个对象
    pluginManager.addVisitor(new ExampleVisitor(less, this._options));
  }
  // ...
}

class ExampleVisitor {
  constructor(less, options) {
    this._less = less;
    this._options = options;
    this._visitor = new less.visitors.Visitor(this);
  }
  
  run(root) {
    return this._visitor.visit(root);
  }
  
  // ...其他代码
}

对于访问器对象,我们同样采取class方式创建。一般情况下,都需要将less对象传入,用于访问less相关api。

在访问器对象中,最关键的就是run函数了,在语法被解析后,生成了ast结构后,就会执行run函数。此时传入的root就是整个代码的ast语法树,run函数执行完毕后需要返回一颗新的ast语法树。可以直接在run函数中对整个root进行遍历和修改,但更加推荐使用less自带的语法树访问工具(new less.visitors.Visitor(this)),在run函数中启动访问语法树this._visitor.visit(root);,这样就可以像babel一样,写一些节点访问函数更加优雅的遍历节点树:

class ExampleVisitor {
  constructor(less, options) {
    this._less = less;
    this._options = options;
    this._visitor = new less.visitors.Visitor(this);
  }
  
  run(root) {
    return this._visitor.visit(root);
  }
  
  visitRuleset(node, visitArgs) {
    // 修改逻辑
    return node;
  }
}

visitRuleset就是在遍历节点树时,碰到的每一个声明集({}包裹的代码,整个文件虽然没有被{}包裹也属于一个声明集下)都会被调用。如果你想要在访问这个节点内部节点之后,也就是即将离开的时候函数被触发(冒泡的概念),只需要在函数后加Out即可:

visitRulesetOut(node, visitArgs) {
  // 修改逻辑
  return node;
}

less中拥有的节点类型主要有:AtRule, RuleSet, Declaration,所有的类型可以查看源码中关于节点的定义目录下的所有节点和每个节点的属性定义。

由于css这一块的ast规范没有找到,并且看了下postcss跟less差异很大,可能没有标准的,所以就不去定义每一个节点的名称,这里可以说明下AtRule@规则的解析,在less中除变量外的一些配置,如@charset;RuleSet为声明集,Declaraion为一个声明,如font-size: 200px

我们可以在访问节点函数中,对节点进行改造:

visitDeclaration(node) {
  const { name, value: valueNode } = node;
  const { value } = valueNode;
  if (name === 'font-size' && value.endsWith('px')) {
    valueNode.value = `${Number.parseFloat(value) / fontBase}rem`;
  }
  return node;
}

node对象的属性进行改造,就是调整了代码,如上面的代码中,就是将设置为px单位字体大小自动调整为支持响应式的rem单位。

在改造时,需要注意,less的ast中,值会被多层的Value节点包括。开发时,可以debugger一下,看一下这些value节点的结构。

less中会对声明的值进行解析,这点跟postcss是有区别的,并且值的解析出来的ast结构非常丰富。

上面的案例都是对node进行小小的改造,如果是需要替换节点类型(替换整个节点),可以这样:

visitDeclaration(node) {
  const { name, value: valueNode } = node;
  const { value } = valueNode;
  if (value === 'red') {
    return new this._less.tree.Call(
      'var',
      [new this._less.tree.Keyword('--red')],
    );
  }
  return node;
}

上文代码含意为如果一个样式的值是red,将其替换成css变量var(--red)

如果需要替换节点,直接根据已有节点的一些值创建一个新的less.tree.*节点对象返回即可。创建新的节点类型需要传入的参数和旧节点类型(可以通过type属性查看每个节点的类型)包含的值,可以查看源码中节点的定义目录下的节点列表和每个节点的属性定义。

最后,一个自动将font-size的值转为支持响应式rem的插件简单实现完整代码为:

class ExampleVisitor {
  constructor(less, options) {
    this._less = less;
    this._options = options;
    this._visitor = new less.visitors.Visitor(this);
  }
  
  run(root) {
    return this._visitor.visit(root);
  }
  
  visitDeclaration(node) {
    const { name, value: valueNode } = node;
    const { value } = valueNode;
    if (name === 'font-size' && value.endsWith('px')) {
      valueNode.value = `${Number.parseFloat(value) / fontBase}rem`;
    }
    return node;
  }
}

class LessPlugin {
  constructor(options) {
    this.minVersion = [3, 0, 0];
    this._options = options;
  }

  install(less, pluginManager, functions) {
    pluginManager.addVisitor(new ExampleVisitor(less, this._options));
  }

  setOptions() {}

  printUsage() {
    return '';
  }
}
更多的visitor的写法,可以参考less中的内置访问器中代码的写法和一个官方实现的插件中非用法

前置处理插件

addPreProcessor可以添加一个前置处理插件。预处理插件一般在less构建之前执行,可用于代码的预处理和其他类型插件的前置准备工作。

一个简单的前置处理插件为:

class LessPlugin {
  // ...

  install(less, pluginManager, functions) {
    // 注册访问器,访问器为一个对象
    pluginManager.addPreProcessor(new ExamplePreProcessor());
  }
  // ...
}

class ExamplePreProcessor {
  process(src, processArgs) {
    // 
  }
}

前置处理器为一个带process函数的对象。process函数会被注入一个源码内容和上下文对象,需要返回一个新的字符串。在上下文对象中,可以拿到如当前处理的文件信息等。

由于前置处理器插件需求场景特别少,这里就不写案例。可以参考less-plugin-sass2less

后置处理器

可以通过addPostProcessor添加后置处理器,用于对less生成的css代码做后续处理,如压缩等。less的后置处理器为一个携带process方法的对象,可以使用类创建。一个简单的后置处理器代码示意:

class LessPlugin {
  // ...

  install(less, pluginManager, functions) {
    // 注册访问器,访问器为一个对象
    pluginManager.addPostProcessor(new ExamplePreProcessor());
  }
  // ...
}

class ExamplePreProcessor {
  process(src, processArgs) {
    // 
  }
}

process函数会被注入一个源码内容和上下文对象,需要返回一个新的字符串。在上下文对象中,可以拿到如当前处理的文件信息等。

后置处理器跟前置处理器类似,但比前置处理器使用场景更广,官方提供的插件也基本上为后置处理器。后置处理器中,可以集成其他工具,进行一些css代码后续操作:比如集成clean-csscsswring的进行压缩处理;使用csscomb进行格式化处理;还可以集成postcss使用整个postcss生态,如Autoprefixer

由于处理器的process只接受同步处理,所以在集成第三方框架的时候,需要使用他们的同步方式。但postcss官网都是异步使用案例,可以参照下文这样同步使用:

class ExamplePreProcessor {
  process(src, processArgs) {
    const result = postcss([...postcssPlugins]).process(src, {
      from: filename,
      // ....
    });
    return result.css;
  }
}

文件管理器

可以通过addFileManager添加文件管理器,用于扩展@import的处理。一个标准的结构可以为:

class LessPlugin {
  // ...

  install(less, pluginManager, functions) {
    // 注册访问器,访问器为一个对象
    pluginManager.addFileManager(buildExampleFileManager(less));
  }
  // ...
}

function buildExampleFileManager(less) {
  class ExampleFileManager extends less.FileManager {
    supports(filename, currentDirectory, options, environment) {
      // ...
    }
    supportsSync(filename, currentDirectory, options, environment) {
      // ...
    }
    resolve(filename, currentDirectory) {
    },
    loadFile(filename, currentDirectory, options, environment){
    },
    loadFileSync(filename, currentDirectory, options, environment){
    },
    tryAppendExtension(path, ext) {
    },
    tryAppendLessExtension(path){
    },
  }
  return new ExampleFileManager();
}

中间的关键函数接口的实现,可以参照内置的less.FileManager实现,或者官方提供的less-plugin-npm-import实现。

写在最后

虽然less提供了多种预处理插件,但是如果是在类webpack等构建框架中,有其他的方式替换掉前置/后置处理器,如webpackless-loader前或者后添加其他loader就可以实现同等效果。而语法访问器插件,只有在有自己的less语法糖/自动化处理流程在less原生无法提供时,需要开发插件。对于css的一些语法糖或者自动化处理(如自动将px替换成rem)建议使用postcss开发插件。


joyerli
158 声望5 粉丝

前端搬砖一枚,会分享一些对技术的个人理解和思考,还会分享一些自己解决实际碰到的业务需而设计的奇葩技术方案。


引用和评论

0 条评论