关于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…

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理