likes
comments
collection
share

VUE项目主题色切换

作者站长头像
站长
· 阅读数 11

前言

切换主题已经成为前端页面的标配功能,在方案的选择上考虑自身的优缺点同时还要考虑与现有项目的契合度,在契合现有项目方案上对其进行优化处理。

方案

类切换

准备几套主题样式文件,在首页全部加载,切换时改变对应的主题类。这种方式缺点就是页面渲染加载了所有的样式增加首屏加载时间,并且增加主题需要再单独写个对应主题的 css 样式,不是很方便。

动态样式引入

准备几套主题样式文件,在主题切换时动态引入对应主题样式文件,按需加载主题样式,这种方式不会影响首屏加载,但主题样式比较大时会出现切换卡顿,并且增加主题同样不方便。

css 变量

声明 css 变量,主题切换修改变量值。

:root{
  --color-primary: #409eff;
}

.dark{
  --color-primary: #eee;
}

.light{
  --color-primary: #fff;
}

.box {
  /* 使用变量 */
  color: var(--color-primary);
}

这种方式增加主题方便,而且切换不会卡顿,也不影响首屏加载。

css 变量的方式似乎是最好的方案,但是 css 变量需要项目样式基于使用声明的 css 变量。

实际项目中的实现

在项目中主题色切换往往需要色板选择自定义样式,并且 UI 组件库也要一起切换,因此不同组件库采用的方案也就不一样。

ant-design-vue

组件库 ant-design-vue 并未采用 css 变量,在主题色切换上更适合用动态样式的方式,那如何解决动态样式的缺点,以及如何抽取 ant-design-vue 组件的主题色样式代码来进行切换。 可以在构建工具中写个 Plugin 来抽取主题色相关的 css 代码存储在全局变量中,主题色切换时替换对应的颜色再作用到页面上。

vite 插件 vitePlginVueTheme

import type { Plugin } from "vite";
export function vitePlginVueTheme(): Plugin {
    return {
    name: "vue-theme",
    enforce: "post",
    transform(src, id) {}
    }
}

transform 钩子可以转译单个的模块。可以在这一步生成 ast,也可以给正在转译的模块增加一些参数。

vite 处理 css 原理

vite 在本地开发模式将 css 编译成 js 文件,在生产环境构建时仍然是编译成独立的 css 文件进行加载。vite 使用 serverPluginCss 插件来处理样式请求。

const id = JSON.stringify(hash_sum(ctx.path))
if (isImportRequest(ctx)) {
    const { css, modules } = await processCss(root, ctx) // 这里主要对 css 文件做一些预处理之类的操作如 less->css, postcss 之类的处理不在此处详细展开
    console.log(modules)
    ctx.type = 'js'
    ctx.body = codegenCss(id, css, modules)
}
export function codegenCss(
  id: string,
  css: string,
  modules?: Record<string, string>
): string {
  let code =
    `import { updateStyle } from "${clientPublicPath}"\n` +
    `const css = ${JSON.stringify(css)}\n` +
    `updateStyle(${JSON.stringify(id)}, css)\n`
  if (modules) {
    code += `export default ${JSON.stringify(modules)}`
  } else {
    code += `export default css`
  }
  return code
}

返回的是 js 代码,那么在 transform 钩子中先要抽取 css 代码。

抽取 css 代码

    if (!/\.(css|less|sass|scss)$/.test(id)) {
        return null;
      }
      let code = src.replace(/\\n/g, "");
      // console.log(src.lastIndexOf("\n"));
      // src = src.substring(src.lastIndexOf("\n"), src.length + 1);
      const cssPrefix = `css = "`;
      const cssPrefixLen = cssPrefix.length;

      const cssPrefixIndex = code.indexOf(cssPrefix);
      const len = cssPrefixIndex + cssPrefixLen;
      const cssLastIndex = code.indexOf("\n", len + 1);

      if (cssPrefixIndex !== -1) {
        code = code.slice(len, cssLastIndex);
      }

抽取与主题色相关的 css 代码

ant-design 的颜色是通过 generate 方法生成的。

import { generate } from "@ant-design/colors";
const primaryExtractColor = [
  ...generate("#1890ff"),
];
      const cssbock = code.match(/[^}]*\{[^{]*\}/g);
      let cssSelector: string = "";
      let cssCode: RegExpMatchArray | null = null;
      let extractedCss: string = "";
      cssbock?.forEach((css) => {
        if (new RegExp(`(${primaryExtractColor.join("|")})`).test(css)) {
          cssSelector = css.match(/[^{]*/)?.[0] ?? "";
          cssCode = css.match(
            new RegExp(
              `(\\w+-)*\\w+:[^:]*(${primaryExtractColor.join("|")})`,
              "g"
            )
          );
          if (cssCode) extractedCss += `${cssSelector} {${cssCode.join(";")}}`;
        }
      });

通过正则表达式将主题色先关 css 代码抽取。

        const retCode = [
          `window['extractedCss']=(window['extractedCss']||"")+\`${extractedCss}\``,
          src,
        ];
        return {
          map: null,
          code: retCode.join("\n"),
        };

将抽取的 css 代码赋值到 extractedCss 中。

完整代码

import type { Plugin } from "vite";
import { generate } from "@ant-design/colors";
import tinycolor from "tinycolor2";
const primaryColor = "#1890ff";
const arr = new Array(19).fill(0);
const alphaColors = arr.map((_t, i) => {
  return tinycolor(primaryColor)
    .setAlpha(i / 20)
    .toRgbString()
    .replace(/(\(|\))/g, "\\$1");
});
const primaryExtractColor = [
  ...generate(primaryColor),
  ...alphaColors
];
export function vitePlginVueTheme(): Plugin {
  return {
    name: "vue-theme",
    enforce: "post",
    transform(src, id) {
      if (!/\.(css|less|sass|scss)$/.test(id)) {
        return null;
      }
      let code = src.replace(/\\n/g, "");
      // console.log(src.lastIndexOf("\n"));
      // src = src.substring(src.lastIndexOf("\n"), src.length + 1);
      const cssPrefix = `css = "`;
      const cssPrefixLen = cssPrefix.length;

      const cssPrefixIndex = code.indexOf(cssPrefix);
      const len = cssPrefixIndex + cssPrefixLen;
      const cssLastIndex = code.indexOf("\n", len + 1);

      if (cssPrefixIndex !== -1) {
        code = code.slice(len, cssLastIndex);
      }

      const cssbock = code.match(/[^}]*\{[^{]*\}/g);
      let cssSelector: string = "";
      let cssCode: RegExpMatchArray | null = null;
      let extractedCss: string = "";
      cssbock?.forEach((css) => {
        if (new RegExp(`(${primaryExtractColor.join("|")})`).test(css)) {
          cssSelector = css.match(/[^{]*/)?.[0] ?? "";
          cssCode = css.match(
            new RegExp(
              `(\\w+-)*\\w+:[^:]*(${primaryExtractColor.join("|")})`,
              "g"
            )
          );
          if (cssCode) extractedCss += `${cssSelector} {${cssCode.join(";")}}`;
        }
      });
      if (extractedCss) {
        const retCode = [
          `window['extractedCss']=(window['extractedCss']||"")+\`${extractedCss}\``,
          src,
        ];
        return {
          map: null,
          code: retCode.join("\n"),
        };
      }
      return {
        map:null,
        code:src
      };
    },
  };
}

主题色切换

把抽取出来的 css 代码中颜色进行替换作用到页面上。

import { generate } from "@ant-design/colors";
import tinycolor from "tinycolor2";
const primaryColor: string = "#1EDF65";
const arr = new Array(19).fill(0);
const replaceCssColors = (css: string, colors: string[]) => {
  const alphaColors = arr.map((_t, i) => {
    return tinycolor(primaryColor)
      .setAlpha(i / 20)
      .toRgbString()
      .replace(/(\(|\))/g, "\\$1");
  });
  const primaryExtractColor: string[] = [
    ...generate(primaryColor),
    ...alphaColors,
  ];
  primaryExtractColor.forEach((color, index) => {
    css = css.replace(new RegExp(`${color}`, "g"), colors[index]);
  });
  return css;
};
export function renderTheme(color: string) {
  let style = document.getElementById("__VITE_PLUGIN_THEME__");
  const alphaColors = arr.map((_t, i) => {
    return tinycolor(color)
      .setAlpha(i / 20)
      .toRgbString();
  });
  const colorVariables = [...generate(color), ...alphaColors, color];
  if (!style) {
    style = document.createElement("style");
    style.setAttribute("id", "__VITE_PLUGIN_THEME__");
    style.innerHTML = replaceCssColors(window.extractedCss, colorVariables);
    document.body.appendChild(style);
  } else {
    style.innerHTML = replaceCssColors(window.extractedCss, colorVariables);
  }
}

小结

这种方式增加主题色方便,并且不用手动抽取主题色相关代码,缺点是改变了原选择器的优先级,ant-design-vue 中的一些特殊样式要单独处理。增加了个全局变量存储 css 代码。抽取 css 代码采用了正则表达式的方式实现,还可以用 ast 的方式抽取。

element-plus

element-plus 采用的是 css 变量的方式,实现主题色切换比较简单。

import { colord } from 'colord'
document.documentElement.style.setProperty("--el-color-primary", color)
for (let i = 0; i < 10; i++) {
  document.documentElement.style.setProperty(`--el-color-primary-light-${i}`,colord(color).lighten(i/10).toHex())
}