网上关于设置网站主题的文章很多,很多文章还都介绍了很多种方法,咱也不整那么多花里胡哨的,本文就介绍我自己在公司项目中使用的方法。达到的效果是让一套给多个商户使用的前台网站,不同的商户可以通过后台管理系统动态地设置其前台网站的主题色。另外,对于涉及到的 sass 知识点我也会做尽可能详细的说明。
整体思路
- 根据实际需求,与后端约定好几个可选的颜色供管理后台设置,比如有红、蓝和深蓝:

- 管理员选择的某个颜色,会通过接口将对应颜色的名称传递给前台网站,比如选择了蓝色,那么就会传递
domainSetting.site.theme:"blue"; - 对于前台网站,则是想办法在页面的诸如
<html>或<body>这样的外层节点,添加上一个能表明主题色信息的类名,比如蓝色对应的类名就是theme-blue,红色对应的就是theme-red; - 准备好所有在第 1 步中约定的可选颜色相应的类名,并在其中使用 css 变量定义主题色,那么页面之中所有的子节点就都可以使用 css 变量来表示主题色了。
具体实现
整体思路中的前两步没啥好说的,下面说说第 3 步的具体实现。
给外层节点添加类名
SSR 网站
前台的 PC 端我使用的是 nuxt 开发的 ssr 网站,因为 nuxt 项目里所有页面都会加载在 layouts 目录下的布局页面中的 <nuxt /> 标签内,所以我直接在 layouts\default.vue 这样的布局页面的最外层节点添加对应主题颜色的类名:
vue
<template>
<div class="layout-default" :class="'theme-' + domainSetting.site.theme">
<!-- ... -->
</div>
</template>
domainSetting.site.theme 就是从接口获取的关于网站的主题色信息,比如为 'blue'。渲染后的结果如下所示:

CSR 网站
前台的移动端我使用的是 uniapp 开发的 csr 网站,直接通过 document.querySelector() 获取 <html> 标签添加类名:
javascript
const html = document.querySelector('html')
html.classList.add('theme-' + domainSetting.site.theme)
编译后得到结果如下:

使用 sass 编写样式文件
对于整体思路的第 4 步,最终效果大概是要定义如下这样的 css 代码:

但是如果我们直接使用 css 写,将来要添加新的颜色时,就会比较麻烦(因为每种主题色还有其它一些 css 代码要写,比如对框架的处理等,而不是只有 2 个 css 变量属性)。所以我将主题相关的样式代码使用 sass 编写在了 theme.scss,借助 sass 的编程能力来方便之后的维护工作。
theme.scss
css
@import './base';
$color-theme: (
'blue': #3d82d1,
'dark_blue': #163b8e,
'red': red
);
@each $theme, $color in $color-theme {
.theme-#{$theme} {
--primary-color: #{$color};
--primary-color-light: #{mix(#fff, $color, 10%)};
@include theme($color, mix(#fff, $color, 10%));
}
}
下面对 theme.scss 中的语法做一些解释:
导入 sass 文件
@import './base';导入了 _base.scss 文件,导入时后缀名 .scss 可以省略。虽然文件名中还有个_前缀,但是导入时可以省略不写。_的作用在于让 sass 编译器不要把 base.scss 单独编译生成 base.css,因为该文件会被导入到 theme.scss 内;- 如果
@import导入的是 .css 文件,或者是采用@import url('./base.scss');写法,则都会将这一句代码作为普通的 css 语句原封不动地复制在编译后的 css 文件中。
定义变量
$color-theme是使用$定义了名为color-theme的 sass 变量,变量名注意不要使用数字开头,可以包含字母、数字、_和-,使用_和-定义的同名变量是同一个变量,比如$color-theme和$color_theme是相等的;- 由于是定义在最外层的,所以为全局变量;如果定义在某个选择器内,则为局部变量,但是可以在值后面添加
!global提升为全局变量:.wrap { $red: red !global; }; - 其值为
:后边的('blue': #3d82d1, 'dark_blue': #163b8e, 'red': red),是类型为 maps 的值,用圆括号包围,相当于 js 中的 object。sass 中变量的值支持 6 种类型:- 数字;
- 字符串;
- 颜色;
- 布尔型:
true或false,可以结合流程控制指令@if、@else if与@else做判断,比如设置暗黑模式的时候:
css
$dark-mode: true;
.container {
@if $dark-mode {
background-color: #000;
} @else {
background-color: #fff;
}
}
- 空值:
null; - 数组(用空格或逗号作分隔符):比如
$theme: red blue green;。可以结合@each使用,通过函数index()获取某个值在数组中的位置(从 1 开始,而不是 js 中数组的下标从 0 开始):
css
@each $color in $theme {
$index: index($theme, $color);
.text-#{$index} {
color: $color;
}
}
// 编译成 css 后得到的为
.text-1 {
color: red;
}
.text-2 {
color: blue;
}
.text-3 {
color: green;
}
- maps:maps 中的值除了可以像 theme.scss 那样使用
@each遍历,也可以使用map-get()获取(如果获取不到,则直接忽略该属性):
css
.container {
background-color: map-get($color-theme, 'blue');
// $color-theme 中 获取不到 key 为 'yellow' 的值,编译成 css 时会直接忽略 color
color: map-get($color-theme, 'yellow');
}
// 编译成 css 后得到的为
.container {
background-color: #3d82d1;
}
- 定义变量时还可以使用
!default表示默认值。在 elementUI 的源码中就能看到这样的写法:

我们可以自己定义一个 $--color-primary,然后导入 elementUI 的样式文件,elementUI 的组件有用到 $--color-primary 的地方就会使用我们自己定义的值。如果我们没有自定义 $--color-primary 则使用默认的 #409EFF。
@each 指令
@each 与 @import 同为指令,其格式是 $var in <list>,$var 为变量,<list> 为值列表。在 theme.scss 中,<list> 为 maps 类型的 $color-theme,而 $var 就是 $theme, $color。 @each $theme, $color in $color-theme {} 是对 $color-theme 的值进行遍历,$theme 分别为 'blue'、'dark_blue' 和 'red',对应的 $color 就是 #3d82d1、#163b8e 与 red。
插值语句 #{}
- 在 theme.scss 中的
@each指令内,我定义了类名.theme-#{$theme},其中#{$theme}为插值语句,从而能够在选择器的名称中使用变量$theme。编译成 css 后得到的就是对应的.theme-blue、.theme-dark_blue和.theme-red; - 在定义 css 变量
--primary-color时,其值也使用了插值语句#{$color},如果写成--primary-color: $color;则编译后的 css 会直接照搬过去,也是--primary-color: $color;而导致无效。
mix()
通过 mix(#fff, $color, 10%),将 #fff 和变量 $color 所对应的颜色进行混合,从而得到变量 --primary-color-light 的值。第 3 个参数 10% 表示颜色混合的权重比例,如果不传则为默认的 50%,该值需要是一个介于 0% 和 100%(包括 0% 和 100%)之间的数字。如果这个数字越大,则表示 #fff 使用的就越多,越小则表示 $color 使用的越多。elementUI 中也是这样根据某一主色确定相应色簇的:

混合指令 @mixin 与引用混合样式 @include
在 theme.scss 中使用 @include 引用了在 _base.scss 中,使用混合指令 @mixin 定义的混合名称为 theme 的混合样式。
参数
@mixin theme()是接收 2 个参数的,在 @include theme() 时,如果直接传递 2 个值,则会按顺序作为参数 $--primary-color 和 $--primary-color-light 的值。如果你浑身反骨,不想按顺序传递,也可以像下面这样指定参数:
css
@include theme(
$--primary-color-light: #{mix(#fff, $color, 10%)},
$--primary-color: $color
);
默认值
在定义 @mixin 时,也可以指定默认值,这样就不需要在 @include 传递所有参数了:
css
@mixin theme($--primary-color: blue, $--primary-color-light: skyblue) {}
参数变量
对于有些样式属性可能有多个值的情况,比如 border: 1px solid #efefef;,在定义参数时可以使用 ...:
css
@mixin border-style($color, $border...) {
div {
color: $color;
border: $border;
}
}
通过 @include border-style(red, 1px solid #efefef); 引用时,1px solid #efefef 都会传递给 $border。
_base.scss
在 _base.scss 中,则是使用混合指令,定义了一些诸如 theme-primary 和 theme-primary-bg 这种方便项目中使用的样式类,以及对一些框架的样式的重置:
css
@mixin theme($--primary-color, $--primary-color-light) {
.theme-primary,
.nuxt-link-exact-active {
color: $--primary-color !important;
}
.theme-primary-bg {
background-color: $--primary-color;
}
// elementUI ↓
.el-button--primary {
background-color: $--primary-color;
border-color: $--primary-color;
}
// ...
// elementUI ↑
}
对于 _base.scss 的说明如下:
注释
在 sass 中,注释可以使用 /* */ 或者 //:
css
/*
我是注释,
并且可以写多行
*/
// 我是单行注释
它们除了多行与单行的区别外,使用 /* */ 注释的内容,在编译后生成的 css 文件中也会被保留,而使用 // 写的注释则会被删除。
设置 elementUI 的主题色
似乎由于 sass 需要在编译期间转换成 css 文件的工作原理,对于 elementUI 主题色的动态设置,我目前只能采取用到了哪个组件,如果该组件涉及到了主题色,就对应的添加样式进行重置的办法。
小结
之后如果添加了某种主题色,只需在 theme.scss 文件的 $color-theme 中添加键值对即可。当然也别忘了在项目中合适的地方引用 theme.scss,比如 nuxt 项目就是在 nuxt.config.js 里配置 styleResources.scss:
javascript
styleResources: {
scss: [
// ...
'~/assets/scss/theme/theme.scss'
]
},
对于 uniapp 项目则是在 uni.scss 文件中导入 @import '@/assets/scss/theme/theme.scss';。
Sass 与 Scss
最后顺带介绍下 Sass 与 Scss 的关系。在一段时间内,我一直有个疑问 ------ 为什么同为 css 预处理器的 less, 其文件的后缀名就是 .less,而 sass 文件的后缀名却是 .scss?后来才知道,这是因为 sass 是由 ruby 开发的,借用 sass 中文网的一句话:
它使用"缩进"代替"花括号"表示属性属于某个选择器,用"换行"代替"分号"分隔属性。
这对于习惯了 css 语法的人而言就不是很适应,而且也不能直接将 css 的代码加到 sass 文件中去。所以从 sass3 开始,sass 对语法做出了改变,也开始采用"花括号"和"分号"等写法,并且完全兼容了 css 代码,是为 scss(sassy css)。

