liyongning/blog

在线主题切换

liyongning opened this issue · 0 comments

在线主题切换

在线主题切换的本质就是通过 JS 替换主题 link 标签的 href 属性,加载对应主题的样式包。

样式包可以是多套 CSS 样式,也可以是由 CSS 变量组成的主题包。

多套 CSS 样式

优点 是简单、易于理解,缺点 也很明显,可维护性差、扩展性差、开发工作量大(需要研发同学为系统开发多套样式)。可阅读下面的示例代码感受一下

index.html

<!DOCTYPE html>
<html lang="en">

<head>
	<meta charset="UTF-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>multi theme</title>
    <!-- 主题包,通过 JS 动态切换 link 的 href 值 -->
	<link rel="stylesheet" class="theme" href="./blue.css">
</head>

<body>
	<div class="theme div-ele">
		multi theme
	</div>
	<button onclick="toggleRedTheme()">red theme</button>
	<button onclick="toggleBlueTheme()">blue theme</button>

	<script>
        // 切换红色主题
		function toggleRedTheme() {
			document.querySelector('.theme').setAttribute('href', './red.css')
		}

        // 切换蓝色主题
		function toggleBlueTheme() {
			document.querySelector('.theme').setAttribute('href', './blue.css')
		}
	</script>
</body>
</html>

blue.css

/* 蓝色主题 */
.div-ele {
	width: 200px;
	height: 200px;
	line-height: 200px;
	text-align: center;
	color: #fff;
	margin-bottom: 20px;
	background-color: blue;
}

red.css

/* 红色主题 */
.theme {
	width: 200px;
	height: 200px;
	line-height: 200px;
	text-align: center;
	color: #fff;
	margin-bottom: 20px;
	background-color: red;
}

当有一天你接手了类似的一个老旧系统,产品过来跟你说,我们现在需要给系统新增一套样式,全新的 UI 设计稿已经出来了,你抽时间做一下吧。这里大家要搞清楚的是,这不是一个抽时间就能完成的简单需求,这意味着你需要为系统重写一套新的样式,比如叫 yellow.css

首先你需要复制已有样式,然后在浏览器中对照设计稿挨个去修改相关样式代码,并将修改同步到代码文件中,对于一个大型系统来说,这个工作量真的是......

针对上面的问题,有没有什么优化办法呢?一个呼之欲出的答案就是样式抽离。

当你新增或修改已有 UI 样式时,其实修改的只是部分样式,比如背景色、字体颜色、边框色等,这部分经常被修改的样式我们称为主题样式,你需要将这些样式找出来。这里难在寻找样式,就像大海捞针,那能否把相关样式抽出来呢?就像写代码一样,将公共逻辑抽离,然后在各个地方复用。

Sass 变量

这里我们需要借助 CSS 预编译语言去实现,比如 Sass。将多套 CSS 样式中的公共样式(主题样式)抽离,通过 Sass 变量维护公共样式,每次新增或修改主题时,只需要修改主题变量文件,然后重新编译生成新的 CSS 样式。也就是说这里的样式需要通过 Sass 或 Less 语言编写,然后编译成 CSS,因为浏览器只认识 CSS。

这样就进一步提升了系统的可维护性和扩展性,也降低了主题样式的开发工作量。

index.html

<!DOCTYPE html>
<html lang="en">

<head>
	<meta charset="UTF-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>multi theme</title>
    <!-- 主题包,通过 JS 动态切换 link 的 href 值 -->
	<link rel="stylesheet" href="./blue.css">
</head>

<body>
	<div class="div-ele">
		multi theme
	</div>
	<button onclick="toggleRedTheme()">red theme</button>
	<button onclick="toggleBlueTheme()">blue theme</button>

	<script>
        // 切换红色主题
		function toggleRedTheme() {
			document.querySelector('.theme').setAttribute('href', './red.css')
		}

        // 切换蓝色主题
		function toggleBlueTheme() {
			document.querySelector('.theme').setAttribute('href', './blue.css')
		}
	</script>
</body>

</html>

var.scss

// blue theme
$backgroundColor: blue;

// red theme
// $backgroundColor: red;

index.scss

@import './var.scss';

.div-ele {
	width: 200px;
	height: 200px;
	line-height: 200px;
	text-align: center;
	color: #fff;
	margin-bottom: 20px;
	background-color: $backgroundColor
}

index.scss 编译后生成如下两套样式

blue.css

/* 蓝色主题 */
.div-ele {
  width: 200px;
  height: 200px;
  line-height: 200px;
  text-align: center;
  color: #fff;
  margin-bottom: 20px;
  background-color: blue;
}

red.css

/* 红色主题 */
.div-ele {
  width: 200px;
  height: 200px;
  line-height: 200px;
  text-align: center;
  color: #fff;
  margin-bottom: 20px;
  background-color: red;
}

这时候当产品说新增一套黄色样式的时候,我只需要在 var.scss 文件中修改对应的主题样式,然后编译生成 yellow.css 样式文件即可。

像 ElementUI、Ant Design 就是这样的思路,样式包中内置主题样式变量文件,比如 var.scss,文件中维护了大量的样式变量,如果你需要定制自己的主题样式,只需要修改这个变量文件,然后重新编译组件库,发版就可以了。

当然了,这里抛开技术之外,有一个跨团队合作的问题(研发、设计、产品),你需要协调三个团队的资源去完成这件事,让产品和设计同学合作,根据业务和产品特点为团队出一套 UI 规范,研发同学根据 UI 规范完成多主题样式的研发。

但这里存在一个问题,组件库都支持按需打包,只打包使用到的组件和组件的样式。但是当业务系统需要支持多主题时,组件库就没办法再提供样式的按需打包了。

首先,组件库多主题需要配置不同的样式变量(var.scss)文件,然后编译生成多套样式,将样式包独立发布。

业务系统在使用组件库时,手动引入样式包,不能再使用组件库的样式按需打包能力,因为业务系统切换主题样式是发生在运行时,而按需打包是发生在编译时。

运行时切换主题的方式和 多套 CSS 样式 一样,也是通过 JavaScript 操作 link 标签,完成样式的替换,所以该方案算是第一个方案的一个优化。

CSS 变量

Sass 变量的方案虽然提升了可维护性和可扩展性,但是却导致另外一个问题,组件库丢失了样式按需打包的能力。

而丢失的原因是因为主题切换发生在运行时,但是组件库的样式却需要在编译期将 Sass 编译为 CSS,两者具有不同的运行时段,所以结合起来使用就导致无法使用组件库样式按需打包的能力。

这时候就需要想有没有什么办法能让两者发生在同一时刻,比如都发生在运行时或编译时,可惜编译时暂时还没什么好的方案,但是运行时可以使用 CSS 变量的方式。

CSS 变量是 CSS 的新功能,可以简单理解为原生支持像 Sass、Less 语言的变量能力,从 2017 年 3 月份之后,所有主要浏览器已经都支持了 CSS 变量。

所以这里的方案就是 CSS 变量,将所有主题样式抽离到独立的主题样式文件中,然后在运行时通过 JavaScript 动态替换 link 标签。

CSS 变量基本使用

/* 最佳实践是将样式变量定义在根伪类 :root 下,这样就可以在 HTML 文档的任何地方访问到定义的样式变量了,相当于全局作用域 */
:root {
	--backgroundColor: red;
}

.div-ele {
	/* 通过 var 函数来获取指定变量的值 */
	background-color: var(--backgroundColor);
}

index.html

<!DOCTYPE html>
<html lang="en">

<head>
	<meta charset="UTF-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>multi theme</title>
    <!-- 主题包,通过 JS 动态切换 link 的 href 值 -->
	<link rel="stylesheet" class="theme" href="./blue.css">
    <!-- 系统样式,其中的主题样式使用 CSS 变量 -->
	<link rel="stylesheet" href="./index.css">
</head>

<body>
	<div class="div-ele">
		multi theme
	</div>
	<button onclick="toggleRedTheme()">red theme</button>
	<button onclick="toggleBlueTheme()">blue theme</button>

	<script>
        // 切换红色主题
		function toggleRedTheme() {
			document.querySelector('.theme').setAttribute('href', './red.css')
		}

        // 切换蓝色主题
		function toggleBlueTheme() {
			document.querySelector('.theme').setAttribute('href', './blue.css')
		}
	</script>
</body>

</html>

index.css

/* 系统样式,其中的主题样式使用 CSS 变量 */
.div-ele {
	width: 200px;
	height: 200px;
	line-height: 200px;
	text-align: center;
	color: #fff;
	margin-bottom: 20px;
    /* 使用 CSS 变量声明的主题样式 */
	background-color: var(--backgroundColor);
}

red.css

/* 红色主题 */
:root {
	--backgroundColor: red;
}

blue.css

/* 蓝色主题 */
:root {
	--backgroundColor: blue;
}

所以,当产品需要为系统新增一套黄色主题时,只需要增加一个 yellow.css 文件即可

yellow.css

/* 黄色主题 */
:root {
	--backgroundColor: yellow;
}

总结

以上就是常见的在线主题切换方案:开发多套 CSS 样式、基于 Sass 变量的多套 CSS 样式、CSS 变量。其本质就是在切换主题时通过 JS 替换主题 link 标签的 href 属性,加载对应主题的样式包。

  • 多套 CSS 样式
    • **优点:**简单、易理解,就是写多套主题
    • **缺点:**开发工作量大、维护难度大、扩展性差
  • 基于 Sass 变量优化后的多套 CSS 样式
    • **优点:**通过主题样式的抽离,开发工作量小,维护难度中等,扩展性好
    • **缺点:**组件库样式丢失了按需打包的能力,因为在线切换的整个主题包,维护难度中等,后期每次新增和修改主题样式都需要重新编译生成对应的主题样式
  • CSS 变量
    • **优点:**开发工作量小、易维护、扩展性好,浏览器原生支持
    • **缺点:**虽然主流浏览器都支持了,单相对上面两个方案来说是劣势,性能稍微优点没那么优秀

所以如果你的业务复杂度没那么高(一个页面有上万个 DOM 节点),浏览器兼容性也还好,CSS 变量可以成为你的首选方案,结合 Sass 等预编译语言去实现在线主题切换。

拓展

虽然现在有了多主题方案,但是在团队内如何很好的落地呢?

看到这个问题,你可能会想这还不简单?主题肯定是内置到组件库啊。嗯,没问题,这是一种应用场景,但这只是冰山一角。

大家要知道 CSS 主题样式的应用场景不止是组件库(基础 UI 库、业务组件库、物料库),更多的其实是在你的业务代码中,可以仔细想想,你平时开发时是不是需要写很多 CSS 代码,这些 CSS 代码中也会包含很多主题相关的样式。

你如果将相关样式直接写死成设计稿上给定的数据,在主题切换时,这部分写死的样式就无法被切换,这是切换的只是组件库中相关 UI 的样式。你的业务代码怎么办?

  1. 原始方案,将主题变量全部通过文档记录,要求每个开发同学熟记这些主题变量,并在业务代码中使用。这个方法一听就很变态(变量那么多)
  2. vscode 插件,将主题变量封装成 vscode 插件,或者代码片段,拿代码片段举例来说,比如背景主题色,输入 background-color 直接生成 background-color: var(--backgroundColor);。这里只给大家提供一个思路,具体实现可以自己探索探索,有好的实现可以在评论区和大家的分享分享

链接

多主题切换的示例代码:liyongning/multi-theme


当学习成为了习惯,知识也就变成了常识。 感谢各位的 关注点赞收藏评论

新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn

文章已收录到 github 仓库 liyongning/blog,欢迎 Watch 和 Star。