浅谈自定义组件样式覆盖的方式
简介
有时候,基础组件提供的样式不够,需要在不同模块中使用不同的样式,而且差异大到无法通过一两个属性完成。比如组件的颜色,最常见的有三个区域(文字/图标、背景、边框,常用但不一定经常修改的还有一个阴影,包括文字阴影和盒子阴影)、三种状态(基础、激活、禁用),组合一下最多可能需要 9 种不同的颜色。
若想要通过完全组件化的方式传递颜色参数,那么必须给所有可能的颜色设置一个单独的 Prop。
颜色状态
以分页器为例,其内部包括:总计信息、前进/后退键、页码跳转键、省略位等。根据自定义样式需求,至少需要指定以下颜色:
- 基础颜色(必须):color, backgoundColor, borderColor
- 激活颜色(主要针对页码):activeColor, activeBackgroundColor, activeBorderColor
- 禁用颜色(主要针对页码和前进/后退键,可能还有快速跳转到首页和尾页的按钮):disabledColor, disabledBackgroundColor, disabledBorderColor
总计信息使用基础颜色,省略位使用基础颜色的 color 和 backgroundColor (通常无边框)。
子组件颜色
若还需要区分页码和前进/后退键的颜色,则还需要再分别设置 6 种颜色( 3 种基本颜色和 3 种禁用颜色):
- 针对页码设置:pageColor, pageBackgoundColor, pageBorderColor,disabledPageColor, disabledPageBackgroundColor, disabledPageBorderColor
- 针对前进设置(后退同理):prevColor, prevBackgoundColor, prevBorderColor,disabledPrevColor, disabledPrevBackgroundColor, disabledPrevBorderColor
默认颜色
同时,还需要在某些颜色未提供时设置默认值:由于总计信息和省略位只有基础颜色,因此无需考虑其他情况。
针对各区域的基础状态:
- 首先,取各区域的局部颜色,如:pageColor
- 其次,取全区域的通用颜色,如:color
针对各区域的激活和禁用状态(这里的步骤 3 和 4 就是上面的步骤 1 和 2,因此可以复用计算属性):
- 首先,取各状态下各区域的局部颜色,如:activePageColor
- 其次,取各状态下全区域的通用颜色,如:activeColor
- 然后,取各区域的局部颜色,如:pageColor
- 最后,取全区域的通用颜色,如:color
页码颜色示例(以下仅使用文字颜色作为示例,背景色和边框色同理;前进/后退键同理):
- 基础颜色:realPageColor = pageColor ?? color
- 激活颜色(由于只有页码跳转键存在激活状态,因此省去了步骤 2):realActivePageColor = activePageColor ?? realPageColor
- 禁用颜色:realDisabledPageColor = disabledPageColor || disabledColor || realPageColor
问题
这种方式虽然能避免使用 css 时需要知道组件内部结构、类名、所属的上级元素等(这破坏了组件的封装性),以及选择器权重不好计算的问题(可能为了一个样式属性,就需要重写所有嵌套的选择器)。但缺陷也很明显,过多的样式变量导致整个组件变得异常臃肿。
这是完全使用组件化封装了样式变量后生成的一个分页器,渲染成 html 后带上了一大堆变量。更别说在组件文件中还需要定义这些变量并传给根元素,至少存在两倍的变量。
该问题可以抽象为两个模块通信,需要传递大量字段,而这些字段的值由外部提供;其中只有一个模块可以接受值,另一个模块对于外部不可见的,而就是这个不可见的模块才是字段的实际使用者;并且接受值的模块还负责给某些字段设置默认值。
这里的问题其实就是维护字段的模块过于臃肿,想要瘦身,要么是减少维护部分的代码,要么是字段的使用者减少需要的字段(通过重用/共用部分字段)。若要减少前者的话,就只能把默认值的设置直接移到模板的根元素上;而后者与实现自定义样式的需求相悖,也不可行。暂时还想不到很好的方式能解决这个问题。
总结
整理所有可能出现的问题:
- 避免使用组件需要知道内部所有结构和类名
- 避免需要计算选择器的权重来保证覆盖默认样式
- 避免声明大量样式变量
对比两种方式:
- 样式覆盖:优点是只要知道了组件的结构,就能完全自定义样式,而且不需要原基础组件的支持,也不需要维护大量的默认值规则;但缺点也是必须知道组件结构才能修改,而且可能会受到原基础组件各种属性的影响,使用时需要较多测试。对基础组件的开发者友好。
- 样式变量:缺点是前期需要定义和维护大量样式变量,开发期需要较多测试;而一旦变量设置好了,后期只需要根据规则提供变量值即可,对基础组件的使用者友好。
目前想到的优化方法是,编写一个构建函数,用于生成样式对象,该对象包含真实样式值的 css 变量;使用时只要将需要生成的子组件的名称和状态作为参数传入,构建函数就能自动生成所有所需的样式对象,之后赋予根元素即可。这样,虽然根元素还是会带上很多变量,但由于不需要手动生成,因此只是 html 看上去有点丑,组件体积并不会过分增加;而且避免了手动维护大量样式变量。
之后如果完成了优化,可能会再写一篇实现思路的介绍。