本文的次要背景:
- 心愿利用 css 变量实现 dark 和 light 模式的切换
- 原有的工程都是 less 模式定义的 css,并且还有 less 的函数,比方 fade 等,不想手动改 less 的函数,心愿该插件能反对解析 less 函数
- 须要反对部分不切换模式,比方某个区域是固定的 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
对象是函数名到函数体的映射,所以咱们将须要重写的函数重置成咱们自定义的即可。而函数的计算结果通过 calc
和var
两个 css 函数以及 css 变量进行示意,在页面中即可依据 css 变量进行实时计算!
上面只摘出来了咱们反对的其中两个函数——fade
和 darken
,fade
利用 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 步:【追加前缀款式】
- 生成款式时,通过 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。
- 第 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 步:【生成 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);
})
})
-
第 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…