每个人对设计的偏好会有不同。网站支持换肤,可以极大的提升用户体验。

最近,朋友的一个管理后台项目要支持换肤。不同皮肤,主要的不同是颜色。不同皮肤,也存在页面结构不同的情况。

项目用的 Vue2,UI 库用的 Ant Design Vue。页面已经很多了,而且很多页面写了 UI 库之外的自定义样式。

基于这些情况,设计换肤方案时,要考虑以下的点:

  1. 换肤导致每个页面的代码改动要聚集在一起,要少。因为页面多,每个页面多改一处,整个项目的多改动的是倍数级的。
  2. 样式不污染其他页面。还是用 Scoped CSS 来实现。

换肤方案

换肤方案要支持如下功能:

  1. 项目的所有页面和组件都能获取和修改皮肤值。
  2. 皮肤值变化时,样式 和 页面结构做对应的变化。

我们下面具体来实现这 2 个功能。

获取和修改皮肤值

这边用 provide / inject API 来实现页面和组件能获取和修改皮肤值。provide / inject 允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深。

在项目根组件上,提供皮肤值和修改皮肤值的方法。

  1. export default {
  2. provide () {
  3. return {
  4. // 皮肤值
  5. theme: this.theme,
  6. // 修复皮肤值的方法
  7. changeTheme: this.changeTheme
  8. }
  9. },
  10. data () {
  11. return {
  12. theme: {
  13. name: 'red-theme'
  14. }
  15. }
  16. },
  17. methods: {
  18. changeTheme (themeName) {
  19. this.theme.name = themeName
  20. }
  21. }
  22. }

上面代码 provide 提供的 theme 是一个对象,是为了保证皮肤是可响应的。因为 provide 和 inject 绑定并不是可响应的。传入了一个可监听的对象,那么其对象的 property 是可响应的。

页面和组件中可以获取和修改皮肤值。

  1. export default {
  2. inject: ['theme', 'changeTheme']
  3. }

当然也可以用 Vuex 来做。

皮肤值变化时的处理

页面和组件中都能拿到当前的皮肤值。皮肤值变化时,样式的变化,通过改组件根元素的类名为皮肤值来实现。

HTML:

  1. <div :class="theme.name">
  2. ...
  3. </div>

皮肤样式都以皮肤做为父选择器:

  1. /* 红色皮肤 */
  2. .red-theme .text{
  3. color: #f00;
  4. }
  5. /* 黑色皮肤 */
  6. .black-theme .text{
  7. color: #333;
  8. }

如果 CSS 这么写,有如下问题:

  • 某个皮肤值的样式变化后,需要改很多个文件,工作量大,还容易漏改。如红色皮肤的颜色值从 #f00 改成 #e00。该颜色值可能存在于 color,background-color,border-color 等地方。
  • 要做一个新的皮肤。可能要在很多页面加 CSS。

我们下面来谈谈怎么优化。

优化方案

优化方案主要用 Sass 来代替 CSS。Sass 支持变量,循环,函数等 CSS 不具备的特性(变量除外)。这些特性有助于让程序高内聚,低耦合。

1 抽取变量

我们将不同皮肤的不同的样式值抽取出来,单独放在不同的文件。用 Sass 的 Map 来存这些变量。

红色皮肤:

  1. // assets/theme/red/_var.scss
  2. $red-theme: (
  3. primary-color: #f00,
  4. ...
  5. )

黑色皮肤:

  1. // assets/theme/black/_var.scss
  2. $black-theme: (
  3. primary-color: #333,
  4. ...
  5. )

2 处理自定义 UI 组件库的皮肤

我们以改 Ant Design 的 Primary Btn 的背景色为例。

红色皮肤:

  1. // assets/theme/red/ant-design.scss
  2. @import './var';
  3. .red-theme {
  4. & /deep/ .ant-btn-primary {
  5. background-color: map-get($red-theme, primary-color);
  6. border-color: map-get($red-theme, primary-color);
  7. }
  8. }

黑色皮肤:

  1. // assets/theme/black/ant-design.scss
  2. @import './var';
  3. .black-theme {
  4. & /deep/ .ant-btn-primary {
  5. background-color: map-get($black-theme, primary-color);
  6. border-color: map-get($black-theme, primary-color);
  7. }
  8. }

其中 /deep/ 是 Scoped CSS 的深度作用选择器。深度作用选择器能作用到子组件中的样式。在这里用来重置 Ant Design 的样式。

3 处理具体页面: 同一个样式属性,不同皮肤设置不同值

我们可以循环皮肤,生成样式。这样,以后加了一个新的皮肤,只需改下配置,并不需要动页面的样式。

我们以改页面中的文字值为例。

  1. @import '@/assets/theme/page';
  2. @each $theme in $all-themes {
  3. .#{$theme} .text {
  4. color: getValue($theme, primary-color);
  5. }
  6. }

其中

  • $all-themes 是 Sass 中的数组。值是: (red-theme black-theme)
  • getValue 是封装的一个 Sass 函数。用来返回皮肤下的某个变量值。

上面的 Sass 会生成如下的 CSS 内容。

  1. .red-theme .text[data-v-a9956c70] {
  2. color: #f00;
  3. }
  4. .black-theme .text[data-v-a9956c70] {
  5. color: #333;
  6. }

4 处理具体页面: 某个皮肤有自定义的样式或页面结构不同

这个比较简单。直接写就好。改某个皮肤的自定义样式:

  1. .red-theme {
  2. .text {
  3. font-weight: bold;
  4. }
  5. }
  6. .black-theme {
  7. .text {
  8. font-style: italic;
  9. }
  10. }

改结构:

  1. <div v-if="theme.name === 'red-theme'">
  2. 红色皮肤时才显示
  3. </div>

到这里,我的换肤方案就介绍完了。你有更好的方案吗?推荐给我吧~

关注我们的公众号,回复“换肤方案”,获取源码~
0.jpeg