写了一个修改Less变量在服务端渲染Css文件的功能,可以作为Express中间件。也可以自己写成webpack插件。
起初是使用了antd-theme-webpack-plugin,但是这个东西实在太麻瓜了,复杂的一批。搞到最后根本没用,心态去了一半。搞了github上的demo,结果demo也不行,可太难受了。想着或许也不是什么难事,还不如自己安排一个。
根本上是利用了Less自带的parse方法生成css,这当然是很简单的。
但是对于我们的项目来说,生成的css文件过于庞大了,比如我们只需要修改less文件中的某个变量,如antd中的@primary-color,但是却需要把整个Antd给打包出来,就很呆板。还有更关键的问题在于,我如果修改了颜色,那么在应用中,如果已经使用了dark(黑暗)或者compact(紧密)主题,那么我就必须在打包@primary-color生成css文件之前再先指定样式为某个主题,同时引入该主题变量或者文件。这太愚蠢了,所以shake是必须的。
方案一、
先遍历所有的文件,然后筛选需要改变的样式。最后通过less.render渲染出css。
方案二、
其实就是直接放弃方案一了。好家伙,因为太复杂了,我就想是否有更便捷的方式能够完成这一步骤,于是我想Less不管怎么样它自己的包里总是有对自己语法的解析。不管它怎么做必定会解析出AST,我在这个基础上做shake不好吗。
基于方案二下千辛万苦的去阅读了源码(其实没有什么用),却可以知道虽然less的声明文件只有提供render方法,但是实际上less包中暴露了许多方法以及结构。使用到的就是parse方法以及对象ParseTree,parse方法会调用底层结构生成AST树,ParseTree对象提供了转换css代码的功能。
// 获取index.less文件所在文件夹的路径,确保index.less中@import的相对路径都是从该目录开始的
const dir = path.dirname(input);
// 读取index.less文件内容
let data = fs.readFileSync(input, "utf-8");
// 增设内容
if (data && setInputData) {
data = setInputData(data);
}
// @ts-ignore
less.parse(
data,
{
paths: [dir], // Specify search paths for @import directives
compress: true, // Minify CSS output
modifyVars,
javascriptEnabled: true,
// rootpath: imagesDir, //url替换路径
},
(err: any, root: any, imports: any, options: any) => {
if (!err) {
if (shake) {
root.rules = shaking(
root.rules,
options.modifyVars
);
}
// @ts-ignore
const parseTree = new less.ParseTree(root, imports);
const result = parseTree.toCSS(options);
fs.writeFileSync(output, result.css);
resolve(true);
} else {
reject(false);
}
}
);
代码中可以看到,parse方法提供了一个回调函数,在回调函数中暴露了root对象,最后也是通过root对象来生成css的。也就是root中存有解析过的AST,在测试过后发现root中的rules属性就是我们需要的AST,也就是说我们只需要对这个rules进行shaking一下,即剔除那些不必要的样式就好了。
这是less样式对应的AST上的对象,在测试的时候做了一点总结。
/**
* less类型
*
* Import 导入 (递归
* Circular 样式被复用 (递归
* Ruleset 无复用样式 (递归
* Comment 注释
* AtRule 带 @ 的样式 -> Ruleset.rules -> [ Ruleset(from),Ruleset(to) ]
* AtRule -> Ruleset.rules -> Expression
*
* JavaScript 代码段
* Definition 自定义函数
* MixinCall 自定义函数调用
*
* Definition 函数定义 -> Declaration
* Declaration -> Value 单行样式值
* Declaration -> Keyword 单行样式名 Keyword.name
* Value -> Expression.value -> Variable (变量)
* Value -> Expression.value -> Operation.operands -> Variable (计算表达式)
* Value -> Expression.value -> Call.args -> Variable (less函数)
* 通用嵌套均为Ruleset.rules -> [各个样式]
*
* 如果是进入Import.root.rules -> [各个样式] root为Ruleset类型
*/
如图所示,只需要把多余的样式删除就好。
但是事实上我们的变量也可能会依赖其他的变量,比如
所以我们也需要去变量中找出相关的依赖变量才行。
除了这种直接依赖的变量,还有存在于形参和实参中的变量,同时还需要注意实参的变量只作用于该样式块,不能污染到了全局的变量。 比如
图中@color或者@background都有可能依赖于@primary-color,甚至还有多级嵌套,这也是需要考虑到的地方。
由于声明与调用是一对多的关系,假设这样一个场景样式块A中调用.button-variant-primary(@pirmary-color,red)时,对形参@color存在变量依赖,而在样式块B中.button-variant-primary(blue,@pirmary-color),形参依赖变量的位置发生转变了,甚至时可能直接不依赖变量,也可能都依赖变量。由于过于复杂,在解析时面对这种情况直接视作全部依赖。
shaking解析分为三个步骤,前两次用来查找所有的依赖变量,最后再进行shaking。
// shaking函数
// 拿到需要更新的变量的命名列表
const vars = Object.keys(modifyVars);
// 去除所有不需要的样式
const allVars: any = {};
// 收集函数中的变量
const map = new Map();
// 第一次遍历找出所有的变量
shake(false,rules,
(v) => {
allVars[v.name] = v;
},
undefined, map
);
// 判断变量依赖
const newVars = filterVars(Object.values(allVars), vars);
// 第二次变量找到所有函数形参与实参的关系,判断哪些实参与变量存在依赖,在函数中应该保留这些依赖。
// 比如改变了变量@primary-color,在函数.btn-color()中形参叫做@color,而实参则是@primary-color,
// 如果直接按着已经查找出的依赖变量做shaking,就会检测不到@color与@primary-color的关系
// 所以需要保留这个关系,但是却不能把它直接添加到全部变量中,因为不确保全部变量中是否存在@color,
// 污染到全局数据,所以存放在map中,用到的时候再做判断
shake(false, rules, undefined, newVars, map);
// 第一此与第二次均为查找变量的过程,第三次真正的shaking,剔除所有与依赖变量无关的叶子节点。
shake(true, rules, undefined, newVars, map);