前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >接到“网站动态换主题”的需求,我是如何踩坑的

接到“网站动态换主题”的需求,我是如何踩坑的

作者头像
viktor
发布2022-08-04 11:10:02
1.4K0
发布2022-08-04 11:10:02
举报
文章被收录于专栏:程序员自习室

需求背景

随着业务的发展,客户的需求也会变得更加多样化,产品后期就需要有自定义界面的能力,于是出现了“动态换主题”的需求。

设计部门的同事让我们可以参考Ant Design色板生成算法演进之路

后面我们动态计算色板也是采用了目前 Ant Design 的算法, @ant-design/colors

但是切换主题的方式,经验证并不能很完美的适用于我们微前端项目。

设计标准

以上色系变量表是我们本次最终需要的全部变量

其中每种色系分为两种,h开头的和a开头的,a开头的通过调整透明度来生成,h 开头的一组由 base 色通过ant-design 的动态计算生成

本色系设计由合思设计团队 出品,中性色为直接定义死的,不做计算;

可配置的基础色分为

  • 品牌色(brand-base):#22B2CC
  • 警告色(warning-base):#FAAD14
  • 危险色(danger-base):#F5222D
  • 提示色(info-base):#1890FF
  • 成功色(success-base):#52C41A

前端方案

我在接到需求后,经过和公司架构师及其他同事的探讨后,渐渐产出了以下几种方案,一步步踩坑过来。

方案一:

两种主题模式(light/dark),需要分别两个 less 文件来定义这两套颜色变量

Light-colors.less

dark-colors.less

两种模式下,值固定不变的颜色变量单独定义一个文件 common-colors.less ,然后我选择将三个文件引入到同一个index 中输出使用,需要使用的地方只需要引入index.less 即可。

但是问题来了

1、如何在index.less 中来判断使用light-colors 还是 dark-colors 呢?

@import 只能定义在文件顶部,也没有任何可以做条件引入的方法

2、如何根据品牌色动态计算色系变量值呢?

计算为色系变量值是通过js产出一个数组,想要导入到一个less文件中,再引入使用,想要动态切换的话,需要用到 less的modifyVars方法, 也是Ant Design 官方提供的方式,接着我们尝试

方案二:

lessmodifyVars方法是是基于 less 在浏览器中的编译来实现。所以在引入less文件的时候需要通过link方式引入,然后基于less.js中的方法来进行修改变量

代码语言:javascript
复制
less.modifyVars({
  '@themeColor': '#22B2CC'
});
代码语言:javascript
复制
<link rel="stylesheet/less" type="text/css" href="./src/less/theme-colors.less" />
代码语言:javascript
复制
// color 传入颜色值
changeTheme (color) {
    less.modifyVars({  // 调用 `less.modifyVars` 方法来改变变量值'
         @themeColor':color
         })
    .then(() => {
         console.log('修改成功');
    });
};
  • 需要引入less编译器,太大了,严重影响性能;
  • 需要webpack 配置,无法多个进程间共享变量,不适用于微前端项目。
  • 这种方法仅限于用less的项目才能使用,如果你项目使用的是sass,是没有类似 less.modifyVars 这种解决方案的。

方案三:

1、在webpack构建时,通过 webpack-theme-color-replacer这个插件从所有输出的css文件中提取主题颜色样式,并创建一个仅包含颜色样式的'theme-colors.css'文件。在网页的运行时,客户端部分下载此css文件,然后将颜色动态替换为新的自定义颜色,能够满足更灵活丰富的功能场景,性能出色。

2、@ant-design/colors 来动态计算出品牌色系和功能色系。

3、可以动态的切换品牌色来获取整个主题的切换。

色系通过 提供的基准色, 自动计算及输出的颜色集合:

通过计算就可以输出整个色系数组如下:

需要设置颜色的地方就可以直接使用定义的这些变量,需要切换主题或者颜色的时候,传入主题模式、品牌色重新计算,就可以实现动态切换主题了。

看似没啥问题,但是在我们的系统里,问题来了。

因为我们是微前端项目,拆包出大概二三十个项目,创建一个仅包含颜色样式的theme-colors.css文件这一步是运行在编译时的,那么每个子项目如果没有配置这个webpack,就无法共享该变量,在开发编译阶段就会报错!即使每个项目都配置了这样的webpack构建,也会创建各自的 theme-colors.css 文件,更改主题时候也无法同步切换,一样的坑爹!!!

由此可见,即使一个方案很好很成熟,也不是满足所有项目的。落实一个方案的时候,要根据自己的项目情况做分析,做出一个符合自身项目的解决方案才是硬道理,而不是一味的生搬硬套。

于是该方案毙掉,继续思考下一个方案。

方案四:

时代好了,浏览器普遍支持Css3变量了,基于Css3 Variable 共享全局主题变量看起来就是一个很通用的方案了。

首先定义一个全局变量,改变这个变量的值,页面中所有引用这个变量的元素都会进行改变,既没有 less 的编译过程,也不存在什么性能问题,这不就是我们最期望的动态换肤方案吗?

Css3 Variable的用法就是给变量加--前缀,涉及到主题色的都改成var(--themeColor)这种方式

我们先查一下兼容性

主流浏览器基本全部兼容,对于大多数互联网企业产品完全够用了,但是对于某些还在使用IE 浏览器的产品就需要ponyfill 方案兼容了。

也确实有这样一个 polyfill 能兼容IE: css-vars-ponyfill

这个polyfill 只会在不支持Css3 Variable 的环境会生效

我们开始写代码了:

1、建一个存放公共css变量的js文件(variable.js),将需要定义的css变量存放到该js文件,品牌色及功能色等通过antd算法计算获得;

代码语言:javascript
复制
import { getAlphaColor } from "./themeUtils";
const { generate } = require("@ant-design/colors");
import baseTheme from "./baseTheme";
import lightTheme from "./lightTheme";
import darkTheme from "./darkTheme";
import { functionalColorsBase, grayBase } from "./colors";

const themeModes = {
  light: undefined,
  dark: {
    theme: "dark",
    backgroundColor: grayBase,
  },
};

// 获取品牌色系
export const getBrandColors = (color, mode) => {
  let options = themeModes[mode];
  return generate(color, options);
};

// 获取功能色系
export const getFunctionalColors = (mode) => {
  let options = themeModes[mode];
  let { success, warning, danger, info } = functionalColorsBase;
  const successColors = generate(success, options);
  const warningColors = generate(warning, options);
  const dangerColors = generate(danger, options);
  const infoColors = generate(info, options);
  return {
    success: successColors,
    warning: warningColors,
    danger: dangerColors,
    info: infoColors,
  };
};

// 输出色板
export const modifyVars = (color, mode) => {
  const brandColors = getBrandColors(color, mode);
  const { success, warning, danger, info } = getFunctionalColors(mode);
  const colors = {
    ...baseTheme,
    "--brand-base": brandColors[5],
    "--success-base": success[5],
    "--warning-base": warning[5],
    "--danger-base": danger[5],
    "--info-base": info[5],
    "--h-brand-1": brandColors[0],
    "--h-brand-2": brandColors[1],
    "--h-brand-3": brandColors[2],
    "--h-brand-4": brandColors[3],
    "--h-brand-5": brandColors[4],
    "--h-brand-6": brandColors[5],
    "--h-brand-7": brandColors[6],
    "--h-brand-8": brandColors[7],
    "--h-brand-9": brandColors[8],
    "--h-brand-10": brandColors[9],
    "--h-success-1": success[0],
    "--h-success-2": success[1],
    "--h-success-3": success[2],
    "--h-success-4": success[3],
    "--h-success-5": success[4],
    "--h-success-6": success[5],
    "--h-success-7": success[6],
    "--h-success-8": success[7],
    "--h-success-9": success[8],
    "--h-success-10": success[9],
    "--h-warning-1": warning[0],
    "--h-warning-2": warning[1],
    "--h-warning-3": warning[2],
    "--h-warning-4": warning[3],
    "--h-warning-5": warning[4],
    "--h-warning-6": warning[5],
    "--h-warning-7": warning[6],
    "--h-warning-8": warning[7],
    "--h-warning-9": warning[8],
    "--h-warning-10": warning[9],
    "--h-danger-1": danger[0],
    "--h-danger-2": danger[1],
    "--h-danger-3": danger[2],
    "--h-danger-4": danger[3],
    "--h-danger-5": danger[4],
    "--h-danger-6": danger[5],
    "--h-danger-7": danger[6],
    "--h-danger-8": danger[7],
    "--h-danger-9": danger[8],
    "--h-danger-10": danger[9],
    "--h-info-1": info[0],
    "--h-info-2": info[1],
    "--h-info-3": info[2],
    "--h-info-4": info[3],
    "--h-info-5": info[4],
    "--h-info-6": info[5],
    "--h-info-7": info[6],
    "--h-info-8": info[7],
    "--h-info-9": info[8],
    "--h-info-10": info[9],
  };
  const darkConfigableTheme = {
    "--a-brand-1": getAlphaColor(brandColors[5], 0.04),
    "--a-brand-2": getAlphaColor(brandColors[5], 0.08),
    "--a-brand-3": getAlphaColor(brandColors[5], 0.16),
    "--a-brand-4": getAlphaColor(brandColors[5], 0.24),
    "--a-brand-5": getAlphaColor(brandColors[5], 0.32),
    "--a-brand-6": getAlphaColor(brandColors[5], 0.4),
    "--a-brand-7": getAlphaColor(brandColors[5], 0.52),
    "--a-brand-8": getAlphaColor(brandColors[5], 0.64),
    "--a-brand-9": getAlphaColor(brandColors[5], 0.76),
    "--a-brand-10": getAlphaColor(brandColors[5], 0.88),

    "--a-success-1": getAlphaColor(success[5], 0.04),
    "--a-success-2": getAlphaColor(success[5], 0.08),
    "--a-success-3": getAlphaColor(success[5], 0.16),
    "--a-success-4": getAlphaColor(success[5], 0.24),
    "--a-success-5": getAlphaColor(success[5], 0.32),
    "--a-success-6": getAlphaColor(success[5], 0.4),
    "--a-success-7": getAlphaColor(success[5], 0.52),
    "--a-success-8": getAlphaColor(success[5], 0.64),
    "--a-success-9": getAlphaColor(success[5], 0.76),
    "--a-success-10": getAlphaColor(success[5], 0.88),

    "--a-warning-1": getAlphaColor(warning[5], 0.04),
    "--a-warning-2": getAlphaColor(warning[5], 0.08),
    "--a-warning-3": getAlphaColor(warning[5], 0.16),
    "--a-warning-4": getAlphaColor(warning[5], 0.24),
    "--a-warning-5": getAlphaColor(warning[5], 0.32),
    "--a-warning-6": getAlphaColor(warning[5], 0.4),
    "--a-warning-7": getAlphaColor(warning[5], 0.52),
    "--a-warning-8": getAlphaColor(warning[5], 0.64),
    "--a-warning-9": getAlphaColor(warning[5], 0.76),
    "--a-warning-10": getAlphaColor(warning[5], 0.88),

    "--a-danger-1": getAlphaColor(danger[5], 0.04),
    "--a-danger-2": getAlphaColor(danger[5], 0.08),
    "--a-danger-3": getAlphaColor(danger[5], 0.16),
    "--a-danger-4": getAlphaColor(danger[5], 0.24),
    "--a-danger-5": getAlphaColor(danger[5], 0.32),
    "--a-danger-6": getAlphaColor(danger[5], 0.4),
    "--a-danger-7": getAlphaColor(danger[5], 0.52),
    "--a-danger-8": getAlphaColor(danger[5], 0.64),
    "--a-danger-9": getAlphaColor(danger[5], 0.76),
    "--a-danger-10": getAlphaColor(danger[5], 0.88),

    "--a-info-1": getAlphaColor(info[5], 0.04),
    "--a-info-2": getAlphaColor(info[5], 0.08),
    "--a-info-3": getAlphaColor(info[5], 0.16),
    "--a-info-4": getAlphaColor(info[5], 0.24),
    "--a-info-5": getAlphaColor(info[5], 0.32),
    "--a-info-6": getAlphaColor(info[5], 0.4),
    "--a-info-7": getAlphaColor(info[5], 0.52),
    "--a-info-8": getAlphaColor(info[5], 0.64),
    "--a-info-9": getAlphaColor(info[5], 0.76),
    "--a-info-10": getAlphaColor(info[5], 0.88),
  };
  const lightModeColors = { ...lightTheme, ...colors };
  const darkModeColors = { ...darkTheme, ...darkConfigableTheme, ...colors };
  console.log(lightModeColors, "=====", darkModeColors);
  return mode == "light" ? lightModeColors : darkModeColors;
};

2、页面使用css变量,无论是web主项目,还是各个plugin子项目都可以共享变量,不需要引入任何依赖,设计图标注与代码对应关系:

UI

CODE

h-brand-1

var(--h-brand-1)

3、封装切换主题的js,在项目入口做初始化调用,支持更改light和dark模式,及变更品牌色基准色

代码语言:javascript
复制
import { brandBase, modifyVars } from "./variable";
import cssVars from "css-vars-ponyfill";

const key = "data-theme";

// 获取当前主题
export const getTheme = (mode, color) => {
  const localTheme = localStorage.getItem(key);
  const dataTheme = localTheme
    ? JSON.parse(localTheme)
    : {
        color: color || brandBase,
        mode: mode || "light",
      };
  return dataTheme;
};

// 初始化主题
export const initTheme = (mode, color) => {
  const dataTheme = getTheme(mode, color);
  document.documentElement.setAttribute("data-theme", dataTheme.mode);
  cssVars({
    watch: true,
    // 当添加,删除或修改其<link>或<style>元素的禁用或href属性时,ponyfill将自行调用
    variables: modifyVars(dataTheme.color, dataTheme.mode), // variables 自定义属性名/值对的集合
    onlyLegacy: false, // false  默认将css变量编译为浏览器识别的css样式  true 当浏览器不支持css变量的时候将css变量编译为识别的css
  });
};

// 变更主题
export const changeTheme = (mode, color) => {
  const dataTheme = {
    color: color || brandBase,
    mode: mode || "light",
  };
  localStorage.setItem(key, JSON.stringify(dataTheme));
  document.documentElement.setAttribute("data-theme", dataTheme.mode);
  cssVars({
    watch: true,
    variables: modifyVars(dataTheme.color, dataTheme.mode),
    onlyLegacy: false,
  });
};

4、在切换主题的按钮组件中调用 changeTheme切换主题

最终效果,目前只有部分扫雷了部分页面,控制开关为临时征用侧边栏:

总结

至此,一个微前端项目的动态换肤方案已经实现,大家如果有更好的方案,欢迎补充哦~

注:该方案出自合思大前端团队 ,北京和南昌均有技术团队

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-02-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员自习室 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档