在SaaS平台中需要实现千人千面,每个租户需要当前平台尽量跟客户公司其他系统在风格上保持一致。为了尽量减低成本,采取在线换肤的方式解决,做到同一系统线上同时支持任意套皮肤(主题)和新租户无需新的开发。本文探讨不同方式的换肤实现的区别。

预设多主题

经典方案:网站的所有样式代码提前预设一些颜色,在构建的时候生成多份皮肤样式。然后开放给用户有限的选择这几个皮肤,选择后,替换或者覆盖原来的样式文件。

该方案关键是如何生成多份皮肤样式文件和如何替换样式文件。生成多份样式文件可以利用less, scss等css预处理语言中的变量能力。在编译时,调整变量生成多份样式文件。生成的样式文件名中,可以携带皮肤名(一般会抽象成具有情感色的名次如落日黄),这样在用户选择新的“皮肤”时,可以替换或者加载(覆盖)新的样式链接,达到切换皮肤的目标。

该方案的缺点:

  • 线上只能支持有限数量的皮肤(取决于预埋数量),不能用户自定义色彩;
  • 样式文件替换非常复杂

    • 如果采用全部样式文件都放在一个文件中,可以简化很多操作。但会极大的增加首屏成本,并且不符合当前前端组件化开发把样式文件放在组件内容旁边的方式;
    • 如果是分散的样式文件,在切换时,除了要替换当前已经加载的样式文件,对于新页面的样式文件,也需要加载指定色彩样式文件,需要对webpack等打包工具做一些优化调整;

所以该方案比较适合组件库(当前流行的组件库的主题定制基本也就提供这个能力),不是很适合整体网站的换肤方案。

该方案可以参考知乎文章: 基于less、sass的在webpack或vite等的动态主题的实现方案

为了支持用户自定义颜色,可以在后端创建一个动态生成指定色彩的全样式文件的API,如element-ui@2中的在线主题编辑器。但这需要依赖后端能力,且样式文件为动态之后,缓存会受到极大的影响,进而影响加载性能。

对于直接在浏览器中使用less或者scss等,这一般用于实验性质,会消耗大量客户端性能,不推荐使用。

css in js

如果你的网站使用css in js编写,那么换肤功能将会非常容易实现。只需要将主题色从用户上下文中获取后设置到组件上下文中,其他组件通过获取上下文中主题色的值来设置颜色即可。

该方案的缺点:

  • 国内很少有团队使用css in js开发,很有可能你当前的项目不是采用css in js开发;
  • 生态不足,很多组件库不是采用css in js, 需要额外的特殊处理;

这里提供一个对没有使用css in js的组件库的特殊处理方案:将组件库的全量样式文件包裹在一个样式组件中,对其中的主题色全部换成js变量。
上述组件改造方案缺点:

  • 极大增加了组件库升级成本,每次升级都要重新将组件库全量的样式文件进行css in js改造
  • 丢失组件库样式的按需引入能力。如果对每个组件的样式文件都进行改造,那第一轮改造和后续升级的工作量会翻几倍。

    css变量

    css很早以前已经支持了css变量。在使用色彩的地方,替换为使用css变量,然后控制css变量即可换肤。具体方案可以参照知乎文章:基于Css Variable的主题切换完美解决方案

该方案可能存在以下问题:

  • 技术还是比较新,可能当前的项目并不是使用css变量,改造起来成本太高;
  • 不兼容ie,虽然有垫片,但谁知道有多少坑在前面等着你;

如果是一个新项目,且不需要过多的考虑兼容ie, 强烈建议使用css变量进行开发,跟less,scss等预处理工具一起使用也没有冲突。

颜色内容替换

假设我们能够接受,换肤就是把所有(或者绝大多数)的颜色A的地方都换成颜色B的话(除颜色外,其他限定的一些样式值也可以),那么换肤只需要有一种方式找到目标样式值,把它替换成另一个值即可。

浏览器的的document.styleSheets,可以查询到当前网站上所有的样式内容并且可以修改。如:

// document.styleSheets.item(*).cssRules[*].style[{styleName}] = {updateValue}
document.styleSheets.item(0).cssRules[0].style['color'] = 'red';

该方式在运行时动态设置,不需要调整任何服务器上的源码,计算逻辑都在客户端,且实现原理非常简单。但存下以下致命缺陷:

  • 每一次的调整只针对于网站已经加载后的样式,对于后续加载的新样式,还需要额外的逻辑去处理。比如可以每个页面加载完成后的钩子函数中,或者调整构建逻辑(定制style-loader)在样式资源文件加载成功后修改样式内容;
  • 额外消耗客户端计算资源,可能会有一段响应时间,造成了换肤客户在访问页面已开始显示默认色彩,短暂时间后,使用目标色彩,可能会出现闪屏现象;
  • 由于采取样式比对方式去修改主题色,会造成一个问题:当第一次修改主题色为一个页面已有的其他颜色(假设为颜色A)后,下一次修改主题色时因为此时已经将颜色A也当作时主题色,会把不是主题色含义的色彩也调整;
  • 如果万一出现某一个跟主题色一样的值的固定颜色值,需要特殊处理;

虽然该方案存在严重的缺陷,但它也有极大的优势:

  • 对老项目极度友好,几乎无改造成本;几乎兼容老项目的任何样式开发方式,也几乎兼容任何第三方组件库。
  • 对业务开发者非常友好,几乎做到了透明化,除直接写在元素style的方式外,不需要增加额外的样式开发规范。

构建时替换成css变量

虽然“颜色内容替换”方案有严重缺陷,但可以按照其思路进行另一种实现:在构建时就将颜色样式内容替换对应的css变量,然后利用css变量的能力,支持整个项目换肤。

利用postcss的技术,可以对项目产生的css代码进行值的替换。只需要开发postcss插件,对css内容进行匹配转换。如对使用了主题色颜色的样式,替换成对应的css变量。

该方案延续上文中“颜色内容替换”中的优点:兼容老项目和第三方组件库。对于其缺点,1-3已经不存在,第四点可以支持特定的源码注释语法(类似eslint-disable-xxx方式),忽略一些情况下的替换,用来适配解决极端情况。

除颜色的替换外,也可用于对于一些其他样式的调整为变量,如字体大小,字体等。让换肤的内容更加丰富。

存在缺点:

  • 不兼容ie,虽然有垫片,但还是建议有限效果兼容。

    总结

    如果不需要兼容ie浏览器(或者ie浏览器受限访问),并接受换肤是全局同样颜色值都替换的话,建议使用“构建时替换成css变量”换肤方案。

“构建时替换成css变量”可以兼容已有项目代码和第三方组件库,几乎无任何改造成本。并且也不会新增额外的开发规范,可以直接复制使用ued切图出来的样式,提高开发效率。而使用css变量等方案,如同国际化,需要整个团队在持续迭代时刻遵循使用变量的规范,一旦管理不到位,容易失控和造成历史债。


joyerli
158 声望5 粉丝

前端搬砖一枚,会分享一些对技术的个人理解和思考,还会分享一些自己解决实际碰到的业务需而设计的奇葩技术方案。