基于Less的动态主题切换,用于切换Antd主题简直好极了

时间:2021-1-18 作者:admin

写了一个修改Less变量在服务端渲染Css文件的功能,可以作为Express中间件。也可以自己写成webpack插件。

项目地址:github.com/Hahahahx/ux…

起初是使用了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);

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。