CSS | 使用 PostCSS 插件让 Web 应用支持暗黑模式

1 概述

通过使用 CSS 变量,可以实现暗黑模式。比如使用媒体查询器:

:root {
    color-scheme: light dark;
    background: white;
    color: black;
}

@media (prefers-color-scheme: dark) {
    :root {
        background: black;
        color: white;
    }
}

如果有很多颜色需要处理,此时就要用到 CSS 变量。

:root {
    color-scheme: light dark;
    --nav-bg-color: #F7F7F7;
    --content-bg-color: #FFFFFF;
    --font-color: rgba(0,0,0,.9);
}

@media (prefers-color-scheme: dark) {
    :root {
        --nav-bg-color: #2F2F2F;
        --content-bg-color: #2C2C2C;
        --font-color: rgba(255, 255, 255, .8);
    }
}

:root {
    color: var(--font-color)
}

.header {
    background-color: var(--nav-bg-color);
}

.content {
    background-color: var(--content-bg-color);
}

可以看到这种办法代码简单易懂,问题是var关键字不兼容 IE 浏览器。
在这里插入图片描述
还有一个办法,就是使用 less.js 实时编译 CSS 代码,比如:

<script src="less.js"></script>
<script>
less.modifyVars({ '@text-color': '#fff', '@bg-color': '#000' });
</script>

... 

var less = require("less");
var fs = require("fs");

fs.readFile("./index.less", "utf-8", (err, str) => {
  less.render(
    str,
    {
      paths: [".", "./components"], // 搜寻由 @import 指向的目录
      compress: true, 
      modifyVars: {
        "@text-color": "#fff",
        "@bg-color": "#000",
      },
    },
    function (e, output) {
      console.log(output.css);
    }
  );
});

使用 less 的缺点是,当点击切换按钮时,会导致卡顿。

当然,也可以通过DOM动态更新 CSS 代码。

function changeTheme(theme) {
    const styleCss = document.querySelector("#styleCss");
    if (styleCss) {
        styleCss.href = `/assets/css/${theme}.css`;
    } else {
        const head = document.getElementsByTagName("head")[0];
        const link = document.createElement("link");
        link.id = "styleCss";
        link.type = "text/css";
        link.rel = "stylesheet";
        link.dataset.type = "theme";
        link.href = `/assets/css/${theme}.css`;
        head.appendChild(link);   
    }
    localStorage.setItem("theme", theme);
}

使用 DOM 动态更改的办法,需要把颜色单独作成样式文件,导致配置繁琐。

2 什么是 PostCSS ?

PostCSS 核心包含一个生成 CSS 抽象语法树的AST 解析器,能将 CSS 代码抽象成一个节点树。如果代码发生更改,节点树也会更新。

核心过程是 解析 => 转换 => 生成,类似 Babel。形式如下图所示:

在这里插入图片描述

3 举个例子

比如下面的 .less 代码:
在这里插入图片描述
我们要将其转换为:
在这里插入图片描述
Wepack 的配置如下:

module: {
    rules:[
        //...
        {
           test: /\.less$/i,
           use: ['style-loader', 'css-loader', 'postcss-loader', 'less-loader'],
        },
        //...
    ]
}

3.1 编写 PostCSS 插件

我们可以使用 postcss-plugin-boilerplate脚手架,这个脚手架可以创建一个 postcss-plugin,并且还可以配置jest单元测试。流程图如下:
在这里插入图片描述

当然,也可以在项目根目录下手动创建一个 PostCSS 插件。

// test-plugin.js
var postcss = require("postcss");

module.exports = postcss.plugin("pluginname", function (opts) {
  opts = opts || {}; // plugin 参数
  return function (root, result) {
    // Transform the CSS AST
  };
});

然后,将其放入 postcss.config.js 配置文件中。

module.exports = {
  plugins: [
    require('./test-plugin'),
    require('autoprefixer')
  ]
};

编写代码,完成插件功能。

var postcss = require("postcss");

module.exports = postcss.plugin("postcss-backwards", function (opts) {
  opts = opts || {};
  return function (root, result) {
    // Iterate over all style nodes
    root.walkDecls((declaration) => {
      declaration.value = declaration.value.split("").reverse().join("");
    });
  };
});

这是一个没有实际意义的插件,仅作教学使用。

3.2 如何使用 PostCSS 实现暗黑模式支持 ?

less文件导入项目。

import "./default-theme.less";
import "./dark-theme.less";
component.less
.box{
  width: 100px;
  height: 100px;
  border: 1px solid @border;
  background-color: @bg;
  color: @color;
}
default-theme.less
@import "./component";

@border: #333;
@color: #000;
@bg: #fff;
dark-theme.less
@import "./component";

@border: #999;
@color: #fff;
@bg: #000;
转换模块
function isEmpty(arr) {
  return Array.isArray(arr) && arr.length === 0;
}

const hasColorProp = (colorProps, declProp) =>
  colorProps.some((prop) => declProp.includes(prop));

module.exports = (opts = {}) => {
  if (!opts.colorProps) {
    opts.colorProps = ["color", "background", "border", "box-shadow", "stroke"];
  }
  return (root) => {
    let theme;
    const file = root.source.input.file || "";

    const matched = file.match(
      /(?<theme>[a-zA-Z0-9]+)-theme.(less|css|scss|sass)/
    );
    if (matched && matched.groups.theme !== "default") {
      theme = matched.groups.theme;
    } else {
      if (process.env.NODE_ENV == "test") {
        theme = "test";
      }
    }
    if (theme) {
      root.walkRules((rule) => {
        rule.walkDecls((decl) => {
          if (!hasColorProp(opts.colorProps, decl.prop)) {
            decl.remove();
          }
        });

        if (isEmpty(rule.nodes)) {
          rule.remove();
        } else {
          rule.selector = rule.selector
            .replace(/\n/g, "")
            .split(",")
            .map((s) => `.${theme} ${s}`)
            .join(",\n");
        }
      });
    }
  };
};

4 实施步骤

4.1 通过文件名来确定是否需要生成皮肤样式

const file = root.source.input.file || "";

const matched = file.match(
  /(?<theme>[a-zA-Z0-9]+)-theme.(less|css|scss|sass)/
);

4.2 删除不包含颜色的样式

只保留边框颜色、背景颜色和包含颜色的 CSS 属性。并且,删除不包含 CSS 属性的选择器。

["color", "background","border","box-shadow","stroke",]

4.3 配置规则

module.exports = [
  {
    prop: ["background-color", "background"],
    from: ["#fff", "#ffffff", "@white"],
    to: "@component-background",
  },
  {
    prop: ["border", "border-color"],
    from: ["#D3D9E4", "#D3D9E2"],
    to: "@border-color",
  },
  {
    prop: ["color"],
    from: ["#666E79", "#5C6268"],
    to: "@text-color",
  }
];

4.4 执行转化

const syntax = require("postcss-less");
var fs = require("fs");
const path = require("path");
const rules = require("./rule.js");

var glob = require("glob");

function log(file, node, to) {
  console.log(
    "\x1b[32m",
    `convert ${file} ${node.source.start.line}:${node.source.start.column}  ${node.parent.selector} ${node.prop} from ${node.value} to ${to}`
  );
}

let codes = {};

// options is optional
glob("./src/**/*.less", function (er, files) {  
  files.forEach((file) => {
    var ast = syntax.parse(file);

    // traverse AST and modify it
    ast.walkDecls(function (node) {
        rules.forEach((item) => {
          if (item.prop.includes(node.prop) && item.from.includes(node.value)) {
              node.value = item.to;
              log(file, node, item.to);
          }
        });
    });
    fs.writeFileSync(path.resolve(file), syntax.nodeToString(ast));
  });
});
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

孟华328

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值