arvinxx / less-plugin-dynamic-variable

(⚠️WIP)a less plugin to enable dynamic variable by comile less variable to css variable, compatible with less function
12 stars 0 forks source link

插件动机 & 功能需求点 #1

Open arvinxx opened 3 years ago

arvinxx commented 3 years ago

动机

简单来说,就是让 less 变量可以无痛迁移到 css variable 去。详见二楼 ⬇️

功能特性

RoadMap

预期使用方法

1. 配置

使用 cosmiconfig 式配置。

暂且将配置命名为 dynamic-theme, 那么配置文件就是dynamic-theme.config.js.dynamic-themerc等形式。

准备暂且只支持一个 variables 参数,其他需求后面再看

module.exports = {
  variables: ["primary-color","green-4"]
}

如此一来,插件就会识别到 primary-colorgreen-4 变量是需要提升为 css variables 的。

2. 引入插件

lessc 直接执行的话

lessc file.less --css-variable-theme

引入到配置项中

const LessPluginCssVariablesTheme = require('less-plugin-css-variable-theme');

{
  //...
  lessLoaderOptions: {
    plugins: [CssVariablesWithFunctions];
  }
}

3. 使用

如下声明

@primary-color: #1890ff;
.use {
  color: @primary-color;
}

会生成

:root {
  --primary-color: #1890ff;
}
.use {
  color: var(--primary-color);
}

参考对象

社区插件仓库

开发笔记

资料库

arvinxx commented 3 years ago

Ref: 实时主题切换方案研究 · 语雀

Why?

目前 css variables 和 less variable、less function 存在的问题:less function 视野里没有 css variable 的值 这就导致使用了 less function 的库,(例如 antd ),就没法用 css variable 来覆写 @primary-color这样的变量,进而失去了利用 css variables 动态切换主题方案的能力。

社区相关讨论:

解决思路:less 变量替换

使用 less 变量很重要的一个点是它支持 function。而 css 变量不支持 function。上述很让人头疼的一点是 less 是一个编译时,编译时的视野里是没有 css variable 的。 但是我们希望能把 less 和 css variable 结合起来,结合 less 静态编译的能力和 css variable 动态切换的能力,进而提升样式的实现灵活性。 换个角度来思考,如果 less 无法直接支持 css variable,那么让 less 先行编译,然后在得到具体值时再挂载到css variable 上,再将 css variable 赋予给 less 变量,这样是不是就可以解决这个问题了? 即让 less 做两次解析。第一次计算值,第二次再赋予变量。

用例思路

less 变量计算阶段

// color palettes
@blue-base: #1890ff;
@blue-1: color(~`colorPalette('@{blue-6}', 1) `);
@blue-2: color(~`colorPalette('@{blue-6}', 2) `);
@blue-3: color(~`colorPalette('@{blue-6}', 3) `);
@blue-4: color(~`colorPalette('@{blue-6}', 4) `);
@blue-5: color(~`colorPalette('@{blue-6}', 5) `);
@blue-6: @blue-base;
@blue-7: color(~`colorPalette('@{blue-6}', 7) `);
@blue-8: color(~`colorPalette('@{blue-6}', 8) `);
@blue-9: color(~`colorPalette('@{blue-6}', 9) `);
@blue-10: color(~`colorPalette('@{blue-6}', 10) `);

声明 css varable 阶段

@import '~antd/es/style/themes/default';

@brand-color: #233ad2;

:root {
  @blue-base: @brand-color;

  --blue-1: @blue-1;
  --blue-2: @blue-2;
  --blue-3: @blue-3;
  --blue-4: @blue-4;
  --blue-5: @blue-5;
  --blue-6: @blue-6;
  --blue-7: @blue-7;
  --blue-8: @blue-8;
  --blue-9: @blue-9;
  --blue-10: @blue-10;
}

@dark-mode-primary-color: mix(@brand-color, @component-background, 85%);

:root [theme^='dark'] {
  // 需要在 dark 下导入 dark 相关的变量
  // 这样就可以使得在不同的 theme scope 中
  // 同样的 @blue-base 会得到不同的值

  @import '~antd/es/style/themes/dark';
  @blue-base: @dark-mode-primary-color;

  --blue-1: @blue-1;
  --blue-2: @blue-2;
  --blue-3: @blue-3;
  --blue-4: @blue-4;
  --blue-5: @blue-5;
  --blue-6: @blue-6;
  --blue-7: @blue-7;
  --blue-8: @blue-8;
  --blue-9: @blue-9;
  --blue-10: @blue-10;
}

这样的话,就有了

亮色模式 暗色模式
image.png image.png

覆写样式阶段

最后这个阶段进行覆写,因此应该处于 less 编译周期的最后,在计算完值之后进行替换。因此需要找到 less 的编译声明周期方法,然后看是否能在相应的阶段注入替换。


@blue-base: var(--blue-base);
@blue-1: var(--blue-1);
@blue-2: var(--blue-2);
@blue-3: var(--blue-3);
@blue-4: var(--blue-4);
@blue-5: var(--blue-5);
@blue-6: var(--blue-6);
@blue-7: var(--blue-7);
@blue-8: var(--blue-8);
@blue-9: var(--blue-9);
@blue-10: var(--blue-10);

插件成型

上述的思路已经需要渗透进 less 的编译流程,因此需要用插件的方式来解决这个问题。(在https://github.com/less/less.js/issues/3600 提了个相关 issue)

其中需要用到 less AST 的能力,目前挺难办的,正在 How to replace a Declaration node By an array of Declaration and a Ruleset when using visitor to modify AST ? · Issue #3601 · less/less.js 寻求作者帮助。

leeyeh commented 3 years ago

哇哦,我正在给一个现有 less 项目增加 theme 支持,调研的时候找到了这里。我想问一下参考对象中的 https://github.com/miyamarisubs/less-plugin-custom-properties 有什么问题以至不能满足你的需求吗?

arvinxx commented 3 years ago

@leeyeh 上述插件只是将所有 less 变量变成了 css 变量。例如

// multiplyTwo 是一个 less function,将传入的值×2
@base-number: 10;
@multiply-number: multiplyTwo(@base-number);

.use {
  base: @base-number;
  multiply: @multiply-number;
}

会变成

:root {
--base-number: 10;
--multiply-number: multiplyTwo(var(--base-number));
}

.use {
  base: var(--base-number);
  multiply: var(--multiply-number);
}

而进入less function 计算环节时,就会抛出var(--base-number) is invalid 错误。 原因就是 less function 无法使用 css 变量进行计算。相关 Issue 我在二楼 Why 里有讲。

这就导致像 antd 这样的组件库就无法使用那个插件,因为它的色板系统是通过 less function 生成的,同时已经定义的 less 变量中有不少使用了 darken、light 等 less 内置 function,都会在直接转移成 css variable 时报错。

leeyeh commented 3 years ago

感谢明确问题。其实我疑惑的点是,如果按照设想的方式,在 less 编译阶段就将所有的 theme 对应的 variables 展开,那 css variable 动态的特性就大打折扣了。而且在我现在的项目里有非常多中间变量的定义,虽然对比纯 less 的 theme 方案最终编译的 css 大小可能是减少了,但好像还是会随着主题数量的增加增加。

可能我还是会先尝试一下运行时 calc + hsl 的方式来替代 darken 这样的 function(我的需求里没有特别多其他复杂的 function),也期待这个项目的进展,如果方便我也可以帮忙测试。

arvinxx commented 3 years ago

那 css variable 动态的特性就大打折扣了。

不是的,这反而能够更好地利用 less 的局部作用域的能力,动态性会更好。还是同样一个 case,我加一个 local 的局部状态覆写:

@base-number: 10;
@multiply-number: multiplyTwo(@base-number);

[scope='local'] {
  @base-number: 2;
}

.use {
  base: @base-number;
  multiply: @multiply-number;
}

这样在生成 css 时就可以去自动生成下面这样的结果:

:root {
  --base-number: 10;
  --multiply-number: 20;
}

:root[scope='local'] {
  --base-number: 2;
  --multiply-number: 4;
}

.use {
  color: var(--base-number);
  multiply: var(--multiply-number);
}

这样其实才是最大程度发挥 less 局部作用域 + css 变量动态性特点的方法。

leeyeh commented 3 years ago

我说的动态性指的是另一个角度,比如上面的例子里我就无法直接在客户端通过覆盖 --base-number 来看效果了(因为 multiply-number 被展开了),必须要通过重新编译才行。我的 use case 里有大量类似的中间变量,举个极端的例子:

https://github.com/Semantic-Org/Semantic-UI/blob/master/src/themes/default/elements/label.variables#L148-L174

另一方面就是本来在纯 less 的方案中这些中间变量是不会产生任何 CSS 的,但是把他们「编译」成 CSS variables 之后则会是实际的 CSS 代码,如果给每个主题都展开一份中间变量列表这个量就是我需要考虑的了。

上面的 case 对这个项目的价值可能在于,在不涉及到 function 的情况下,或许不展开会是更好的选择(或者是一个有用的选项)。

djkloop commented 3 years ago

这东西最近有啥进度吗,😃

laterinc commented 2 years ago

@text-color-secondary: var(--ant-ext-color-secondary); shade(@text-color-secondary, 40%) 转换为 --ant-ext-color-secondary-shade-40 变量, 有什么好的方案么?