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));
});
});