非常规 - VUE 实现特定场景的主题切换

实现页面皮肤切换,常见的方案有几种:替换 css 链接、替换 className、改变 css 原生变量值、使用 less.modifyVars、props 参数下发等;
不同的业务场景,我们一般会选择不同的方法来实现目标。最近在公司运营活动平台上的主题功能的实现 ,我们尝试了一种新的解决方案,实现了页面主题的切换,目标是为了提高项目的可维护性、可扩展性,以及降低接入复杂度。

“主题”需求

在了解主题功能之前,我们先来解下业务场景:在运营活动后台中,编辑活动配置页面,拖拽选择所需对应组件,并设置组件相应配置项,点击保存,既完成活动页面发布活动,前台就能访问对应生成活动。
编辑页面如下图:



在如上前提,我们的需求就是:在运营后台配置页面中,实现全局切换主题功能,具体需求如下:

  1. 在配置页面,初始化页面时,实现主题一键切换所有组件的样式;
  2. 页面中的组件的配置,可配置对应组件样式,覆盖主题样式;
  3. 再次点击设置主题,可以覆盖已经设置样式的组件样式;

实现效果如下图所示:



那在了解完需求之后,对于 VUE 项目,要实现主题功能,一般想到的实现方式就是 theme 参数通过 prop 下发来实现。
那我们就先来聊下常规实现方式:prop 下发实现方式。

常规实现方式

定义主题

首先我定义 theme.js 为主题相关参数,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const DEFAULT_THEME = {
primary: '#2F54EB',
subPrimary: '#D6E4FF',
error: '#F5222D',
success: '#52C41A',
warning: '#FAAD14',
background: '#FFFFFF',
text: '#222222'
}
export default {
DEFAULT: DEFAULT_THEME,
FIRST: {
...DEFAULT_THEME,
background: '#2590ff'
}
}

主模块下发 theme 给予组件

接着需要在主模块中,下发 theme 参数,和组件相关配置参数 给到组件,点击按钮,切换主题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
<div>
<div @click="changeTheme">换主题</div>
<Component
v-for="(item, index) in componentList"
:theme="theme"
:key="index"
...item.config // 业务相关参数都在config中
/>
</div>
</template>
<script>
import theme from 'theme.js'
export default {
name: "themeChange",
data() {
return {
theme: theme['DEFAULT']
}
},
methods: {
changeTheme() {
this.theme = theme['FIRST']
}
}
}
</script>

组件监听 theme 改变组件样式

组件中,获取上级组件传递下来的配置参数及主题参数,并监听 theme 的变化,当发生改变,重置样式参数值为主题样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<template>
<div>
<div :style="{ background: config.bgColor }">主题</div>
</div>
</template>

<script>
import theme from 'theme.js'
const initTheme = theme['DEFAULT']
export default {
name: "themeSwitch",
props: {
theme: {
type: Object,
default: () => ({})
},
bgColor: {
type: String,
default: initTheme.background
},
...
},
data() {
return {
config: {
bgColor: this.bgColor,
...
}
}
},
watch: {
'theme' (to, from) {
this.config.bgColor = this.theme.bgColor
}
}
}
</script>

看到这里大家会说,为什么需要在** **watch 中监听主题的变化,而不是在组件初始化的时候参数就直接指向主题对应的参数呢?

因为主题需求里面所说的,在组件里面也是可以改变组件相关样式的,上述 demo 代码中的 bgColor 参数,既可以通过点击切换主题可以设置 ,也可以是组件自己设置的,有多个来源(这里不对组件的配置实现做详细展开);要做到设置主题的时候,组件的样式会设置相应的主题色,就需要在 watch 中进行监听 theme 参数的变化,发生变化,重置相应参数,但是这种方式在每个组件都需要有相同代码片段,监听参数,达到我们的效果,代码非常冗余。

综上,我们对代码进一步优化,把监听 theme 参数的方法统一封装,这里会有另一个问题:每个组件对应颜色的参数是不可定的,且参数层级也是不可定的,几乎每个组件需要维护一整个变量数组。这样定义的规则会相对复杂,维护成本过高,且极易弄错。

很显然这样的实现方式并不是一种很好的方法,那要如何实现?

“非常规”实现方式

在尝试上面的方式之后,我在想我的思路是否正确,是不是切入角度有点问题,那我们换一个角度去切入。

配置参数入手

当我**发现页面整个 this.componentList 参数 ( 里面存储了所有组件的相关配置 ) 我是可以拿到的时候,我**是不是可以从数据入手?
ok,说到这里,那其实思路就出现了, this.componentList 里面的参数规则:

1
2
3
4
5
6
7
8
9
10
11
[
{
componentName: 'xx',
config: {
color: 'xxx',
background: 'xxx',
...
}
},
...
]

我们会发现,在开发组件的时候就已经是把颜色相关参数提取到配置里面了,那也就是说我修改配置参数的值,其实就可以达到设置主题的效果?
因为所有组件的配置参数都是由this.componentList 参数下发的。

参数给予特殊标识

定义 theme.js 相关参数,和上面一致,故不在多说,主要做的就是,在组件中,我们把相关参数进行修改,改为有特殊标示的参数, 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>
<div :style="{ background: this['bgColor.t.background']}">主题</div>
</div>
</template>

<script>
import theme from 'theme.js'
const initTheme = theme['DEFAULT']

export default {
name: "themeSwitch",
props: {
'bgColor.t.background': { // .t.: 为特殊标识 ;background: 为主题里面对应的字段名 background: '#FFFFFF'
type: String,
default: initTheme.background
}
}
}
</script>

遍历参数替换特殊标识参数值

当点击主题切换的时候,会去遍历 this.componentList 参数,修改有特殊标示的参数为新主题对应的参数,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/*
* 根据主题重制componentsConfig
* @method changeTheme
* */
changeTheme () {
this.theme = theme['FIRST']
this.componentList.forEach(component => {
this._setThemeChangeConfig(component.config || {})
})
},
_setThemeChangeConfig (obj) {
Object.keys(obj).map(name => {
if (Object.prototype.toString.call(obj[name]) === '[object Object]') {
this._setThemeChangeConfig(obj[name])
} else {
const themeColorArr = name.match(/\.t\.(\S*)/)
if (themeColorArr && this.isThemeColorName(themeColorArr[1])) {
this.$set(obj, name, this.theme[themeColorArr[1]])
}
}
})
},
/*
* 判断颜色name是否在主题里面
* @method isThemeColorName
* */
isThemeColorName (name) {
let has = false
Object.keys(this.theme).forEach((paramsName) => {
if (paramsName === name) has = true
})
return has
}

最终实现了最终的主题切换的效果。

该方式带来的优势:

  1. 对组件代码几乎无侵入性,组件只需要修改样式相关参数带上特殊标示既可,规则相对简单;
  2. 参数无需一层层下发,易于维护;
  3. 主题与主线功能相对独立,可以轻易移除主题功能,项目也可以正常运行;

总结

主题的实现,不管是常见的方式,还是上述项目中的主题的实现方式,我们往往需要了解业务特性,去寻找最合适的解决方案。不同项目,有不同的实现方式,但目标都是为了提高项目的可维护性、可扩展性,以及降低接入复杂度。

项目目前的实现方案,尚不失为一个好的解决方案,或者可以作为一种新的思路,供大家参考。

原创声明:本文为阅文前端团队 YFE 成员出品,请尊重原创,转载请联系公众号 ( id: yuewen_YFE ) 获取授权,并注明作者、出处和链接。