乐趣区

关于javascript:如何利用css-var做一个dark-mode方案

本文的次要背景:

  1. 心愿利用 css 变量实现 dark 和 light 模式的切换
  2. 原有的工程都是 less 模式定义的 css,并且还有 less 的函数,比方 fade 等,不想手动改 less 的函数,心愿该插件能反对解析 less 函数
  3. 须要反对部分不切换模式,比方某个区域是固定的 light 模式

步骤

第一步:less 变量转换成 css 变量

这一步比较简单,less 曾经提供了字段用于转换,只须要增加一个配置项就能够,就是 globalVars 属性。
能够查看 example 代码参考

{
    loader: 'less-loader',
    options: {globalVars: LessGlobalCSSVars,}
}

LessGlobalCSSVars 大略长这个样子

{"bg-body": "var(--bg-body)",
    "static-white": "var(--static-white)",
    ...
}

less 会将 LessGlobalCSSVars 的映射关系追加到 less 文件前,在进行变量查找的时候就会替换成相应的 css 变量
比方,上面的 less 文件

div {color: @bg-body;}

less 理论解析的文件内容是

bg-body: "var(--bg-body)";
static-white: "var(--static-white)"
div {color: @bg-body;}

最初下面的文件就会被编译成

div {color: var(--bg-body);}

(这也是为什么,没有定义 less 变量,然而应用 css 变量的形式编译 less 文件还不会报错)

第二步:less 函数如何解析?【应用 less 插件】

然而还有一个问题是 less 函数应该如何解析呢?比方 fade(@bg-body, 20%),如果不通过任何解决,这个函数会抛出异样,因为var(--bg-body) 并不是 less 可能解析的节点类型,会提醒 var(--bg-body) 不能被转换成 Color 类型(less 的一个节点类型),这是 less 的语法树解析,须要将 fade 函数的第一个参数解析成 Color 节点类型,否则就会抛 异样 。所以,咱们须要对 less 函数进行改写,具体通过 less 插件的形式实现。批改less-loader 的配置如下,减少一个插件。

{
    loader: 'less-loader',
    options: {
      globalVars: LessGlobalCSSVars,
      plugins: [new LessSkipVarsPlugin ()
      ]  
    }
}

该插件的次要作用就是重写 less 特有的函数!!!
less 的所有函数会被注册到 functions 中,插件裸露了该 functions,因而能够通过批改响应的 less 函数,实现函数的笼罩。该插件的实现源代码如下,functions 对象是函数名到函数体的映射,所以咱们将须要重写的函数重置成咱们自定义的即可。而函数的计算结果通过 calcvar两个 css 函数以及 css 变量进行示意,在页面中即可依据 css 变量进行实时计算!

上面只摘出来了咱们反对的其中两个函数——fadedarkenfade 利用 rgba 的函数示意,而 darken 利用的是 hsl 的函数示意,次要是用 rgba 的表示法无奈用 css 反对的函数示意进去,所以咱们用了 hsl 函数。这里能够看到咱们须要一些非凡的 css 变量,比方--bg-body-SA--bg-body-raw--bg-HS--bg-body-L, 这些变量次要来源于一个原始的变量进行转换。咱们利用原始的色值(bg-body)进行转换
(上面代码做了省略,次要是示意)

class LessSkipVarsPlugin {install(less, pluginManager, functions) {functions.add('fade', function (color, percent) {if (color.type === 'Call') {if (color.name === 'var') {const key = color.args[0].value.substring(2);
                return `rgba(var(--${key}-raw), calc(var(--${key}-SA) * ${parseLessNumber(percent)}))`;
              }
            }
            ......
        
            return `rgba(${red},${green},${blue},${alpha * parseLessNumber(percent)})`;
        });             
        functions.add('darken', function (color, amount, method) {
            .......
            if (color.type !== 'Color')
              throw new Error(`fade function parameter type error: except Color, get ${color.type}`);
            const hsl = (new Color(color.rgb, color.alpha)).toHSL();
            if (typeof method !== 'undefined' && method.value === 'relative') {hsl.l = hsl.l * (1 - parseLessNumber(amount));
            } else {hsl.l = hsl.l - parseLessNumber(amount);
            }
            return `hsl(${hsl.h},${hsl.s},${hsl.l})`;
        })
    }
}

通过该插件的编译,咱们应用了 fade 函数编译的代码会被转化成 css 变量示意。

第三步:部分 light 模式如何反对?【应用 postcss 插件】

能够在 dom 节点上增加 classname 前缀,用来标注该 dom 下的款式都应用动态的亮色模式,不随主题切换。这里须要做的次要分为 3 步:

1. 第 1 步:【增加 dom 前缀 classname】

在相应的 dom 节点增加 classname 前缀,比方static-light

// 原来的 dom 构造
<div className="test">aaa</div>
// 新的 dom 构造
<div className="static-light">
    <div className="test">aaa</div>
</div>
2. 第 2 步:【追加前缀款式】
  1. 生成款式时,通过 postcss 为所有的款式增加 static-light 前缀;

这一步实际上是在 css-loader 的处理过程中退出了一个 postcss 插件,对每条规定 rule 额定生成一条动态款式。
举个例子, 我定义了如下的 less 款式

.test {background-color: @static-white;}

通过该 postcss 插件之后,生成的产物会变成

.test {background-color: var(--static-white);
}
.static-light .test {background-color: var(--static-white);
}

所以要怎样才能追加生成这样的 css 呢?
能够看到 css-loader 的源码中,节点都通过了 postcss 插件的解决,咱们只须要在插件列表中,加上咱们的插件即可

result = await postcss([...plugins, new colorPlugin({staticEx: {prefix:'.static-light'},
})]).process(content, {...});

所以接下来能够实现咱们的 postcss 插件

var postcss = require('postcss');
module.exports = postcss.plugin('postcss-color-and-function', function (options) {const { staticEx} = options;
    function processNode(node, type) {
        let staticNode;
        switch (node.type) {
            ......
            case 'rule':
                staticNode =  node.clone();
                staticNode.selectors = staticNode.selectors.map(i => {return `${options.staticEx.prefix} ${i}`
                });
                break;
            default:
                break;
        }
        return staticNode;
    }
    return function (css) {let last = [];
        css.each((node, type) => {const staticNode = processNode(node, type);
            if (staticNode) {last.push(staticNode);
            }
        });
        css.nodes = css.nodes.concat(last);
    };
});

次要的实现思路是:

  • 通过以后节点克隆一个一样的节点,在最初返回的时候拼接该节点,这样能够生成两份款式;
  • 对于克隆的那份节点,追加选择器,

    staticNode.selectors = staticNode.selectors.map(i => {return `${options.staticEx.prefix} ${i}`
    });

    这样就能够实现追加节点和部分 css 变量定义了。
    注:css-loader 会校验参数,所以如果须要批改传入的参数格局,还须要批改 options.json 和 normalizeOptions。

  1. 第 3 步:插入一套制订 classname(这里是 static-light)的 css var 变量。
    这里咱们借助 webpack 的插件来实现,具体内容看下一部分

第四步:追加全局 css 变量定义【webpack 插件】
咱们能够定义一下 css 变量,就能够失效了,增加 @media (prefers-color-scheme: dark)能够在零碎模式变动的时候切换 css 变量,就能够实现款式的切换。

:root {
 --bg-body: "#1f1f1f";
 --static-white: '#fff'
}
@media (prefers-color-scheme: dark) {
    :root {
     --bg-body: "#2f2f2f";
     --static-white: '#fff'
    }
}

下面一大节中,咱们还须要追加部分 light 款式对应的 css 变量,须要在上述变量的根底上追加上面的一段代码。

:root {
 --bg-body: "#1f1f1f";
 --static-white: '#fff'
}
@media (prefers-color-scheme: dark) {
    :root {
     --bg-body: "#2f2f2f";
     --static-white: '#fff'
    }
}
.static-light {
 --bg-body: "#1f1f1f";
 --static-white: '#fff'
}

这样咱们手动追加变量就会变得复杂,并且容易出错,所以咱们能够利用 webpack 插件进行追加,webpack 提供了各种钩子,咱们能够利用这些生命周期钩子在适合的机会执行相应的逻辑。

  1. 第 1 步:【生成 css 文件】
    咱们须要保障生成 css 文件只会执行一次,并且保障生成文件在插入 link 标签之前,HtmlWebpackPlugin 插件提供的生命周期钩子函数 alterAssetTags,返回以后所有的资源列表,用户能够在此追加一些资源链接,所以咱们能够在此生命周期钩子处,触发生成文件。
compiler.hooks.compilation.tap('LarkThemePlugin', compilation => {HtmlWebpackPlugin.getHooks(compilation).alterAssetTags.tapAsync('LarkThemePlugin', (data, cb) => {
        const source = xxx;
        compilation.assets['theme.css'] = {source: () => source, size: () => Buffer.byteLength(source, 'utf-8')};
      cb(null,data);
    })

})
  1. 第 2 步:生成 link 标签援用:
    上一步生成了 css 资源文件,咱们须要在 html 中追加一个 link 标签,援用该 css 资源,在理论利用中,咱们往往会有很多资源标签插入到 html 中,而咱们又心愿该标签能够插入到所有资源文件之前进行加载

    compiler.hooks.compilation.tap('LarkThemePlugin', compilation => {HtmlWebpackPlugin.getHooks(compilation).alterAssetTags.tapAsync('LarkThemePlugin', (data, cb) => {const { assetTags: { styles}} = data;
     styles.unshift({
       tagName: 'link',
       voidTag: true,
       attributes: {
         href: 'theme.css',
         rel: 'stylesheet'
       }
     })
     cb(null,data);
     })
    
    })

    残缺的 webpack 插件代码在下方:

    const fs = require('fs');
    const path = require('path');
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const {getRootCSSVarMap} = require('../util');
    class InjectThemeWebpackPlugin {constructor({ lessVarsSet, darkTokens, lightTokens}) {
       this.darkTokens = darkTokens;
       this.lightTokens = lightTokens;
     }
     // 生成 css 变量的 css 款式
     generateResult() {const generateCss = (cssObj) => {
         let css = '';
         for (let key in cssObj) {const value = cssObj[key];
             css += `${key}: ${value};`
         }
         return `:root{${css}}`;
       }
       const darkCSSObj = getRootCSSVarMap(this.darkTokens, 'DARK');
       const lightCSSObj = getRootCSSVarMap(this.lightTokens, 'LIGHT');
       return `${generateCss(lightCSSObj)}\n@media (prefers-color-scheme: dark) {${generateCss(darkCSSObj)}}`;
     }
      
     apply(compiler) {
       // 追加 link 标签
       compiler.hooks.compilation.tap('LarkThemePlugin', compilation => {HtmlWebpackPlugin.getHooks(compilation).alterAssetTags.tapAsync('LarkThemePlugin', (data, cb) => {const source = this.generateResult();
           compilation.assets['theme.css'] = {source: () => source, size: () => Buffer.byteLength(source, 'utf-8')};
           const {assetTags: { styles}} = data;
           styles.unshift({
             tagName: 'link',
             voidTag: true,
             attributes: {
               href: 'theme.css',
               rel: 'stylesheet'
             }
           })
           cb(null,data);
         })
    
       })
     }
    }
    module.exports = InjectThemeWebpackPlugin;

    注:这里须要留神,如果是多仓须要保障 node_modules 中只有一个 html-webpack-plugin 插件,不然会呈现无奈追加上 link 标签的状况

🧭技术点疾速导航:

本文波及了 less 插件、postcss 插件、和 webpack 插件的内容,对于没有插件背景的敌人会有点生疏,能够学习理解以下内容,不便补齐各个插件应用形式的信息。
一个 less 插件的例子
less.js 的 pluginMananger 源码
PostCSS 罕用插件与语法介绍
PostCSS API 文档 | PostCSS 中文网
https://www.webpackjs.com/api…
html-webpack-plugin 插件的生命周期钩子

附录
https://github.com/webpack-co…
https://www.postcss.com.cn/ap…

退出移动版