设计了一个 CSS 终极解决方案!

Posted 小鹿动画学编程

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了设计了一个 CSS 终极解决方案!相关的知识,希望对你有一定的参考价值。

一、前言

随着产品的迭代,发现现有的产品架构上存在很多的问题,比如性能方面、业务方面、团队协作方面等。作为一个老项目,已经无法再做进一步的优化和完善了,所以这次我们团队打算对整个项目进行一次彻底的重构。

用了大约一个星期的时间,将有关的设计方案整理出来。本篇文章主要记录了项目中页面样式 CSS 最终采用的解决方案,当然,要提前说一下,这些方案最终是根据实际的业务场景、客户需求以及团队协作来决定的,并非适用其他所有项目。


二、技术栈

  • 老项目技术栈: Vue2 + Less + Webpack +组建库(内部开发)
  • 新项目技术栈: Vue3 + CSS + TypeScript + vite + Ant Design

三、面临的问题

所有的解决方案都是根据目前项目面临的实际问题来展开讨论的,如下:

1、 随着产品功能的不断完善,样式文件的体积是呈现增量上升的,达到一定程度,会导致打包速度和性能变慢;

2、 新需求涉及到在线换肤,老项目无法优雅的兼容。(可以做,但成本比较大);

3、 团队协作时,样式文件写的比较乱,每个人都有自己的一套风格,导致代码冗余,后期无法更好的维护。


四、CSS 解决方案

最终,我们决定对项目中用到的样式信息进行一个分层概念的处理。当然,你可能会有一系列的疑问,CSS 不就是写 class 样式么,然后将 class 挂在到相对应的 html 结构当中就行了。

为什么要分层?分层的意义何在?该如何分层?分层的依据又是什么?


1、CSS 的艰难险阻 ——使用 CSS 所遇到的问题**

在解决上述问题之前,我们需要先来探究在我们日常项目中遇到的一些有关 CSS 的问题。

  • 问题一:不同的 HTML 结构,相同的 CSS 样式
<!-- 组件一 -->
<div class="article-preview">
  <img class="article-preview__image" src="./image.png" alt="">
  <div class="article-preview__content">
    <h2 class="article-preview__title">文章标题:XXXXXXX</h2>
    <p class="article-preview__body"> 
      文章内容:XXXXXX XXXXXXXXXXXXXX,XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX。
    </p>
  </div>
</div>

如上,我写了一个有关文章预览的组件,所有的 class 采用的是 Block Element Modifer 的写法。但是由于需求的增加,需要写一个与文章预览 CSS 样式相同组件,我们称简历组件吧。

div>
  <imgsrc="./header.png" alt="">
  <div>
    <h2>姓名:小鹿</h2>
    <p>
      个人简介:熟练掌握 vue 开发,xxxxxxxxxxxxx
    </p>
  </div>
</div>

虽然上述的 HTML 的结构的内容是不同的,但是 CSS 样式完全是相同的。由于文章预览的组件 class 类名是语义化的(与有关文章预览所对应),如果我们直接使用文章预览的 class 到简历组件,违反了语义化的原则。

所以我们不得不复制一份与文章预览所有 class 相同的样式,只不过是将相对应的 class 名改成了与简历有关的语义化类名。

<div class="author-bio">
  <img class="author-bio__image" src="./header.png" alt="">
  <div class="author-bio__content">
    <h2 class="author-bio__name">姓名:小鹿</h2>
    <p class="author-bio__body">
      个人简介:熟练掌握 vue 开发,xxxxxxxxxxxxx
    </p>
  </div>
</div>

那么问题来了,明明是相同的样式,每次遇到相同的样式都要去复制一份,导致到代码冗余以及项目样式文件体积的增大。

到这,我们可能想到将 class 改成通用的语义化不就可以了,class 起名 media-card。

<div class="media-card">
  <img class="media-card__image" src="https://cdn-images-1.medium.com/max/1600/0*o3c1g40EXj65Fq9k." alt="">
  <div class="media-card__content">
    <h2 class="media-card__title">文章标题:XXXXXXX</h2>
    <p class="media-card__body">
      文章内容:XXXXXX XXXXXXXXXXXXXX,XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX。
    </p>
  </div>
</div>

<div class="media-card">
  <img class="media-card__image" src="https://i.vimeocdn.com/video/585037904_1280x720.webp" alt="">
  <div class="media-card__content">
    <h2 class="media-card__title">姓名:小鹿</h2>
    <p class="media-card__body">
       个人简介:熟练掌握 vue 开发,xxxxxxxxxxxxx
    </p>
  </div>
</div>

通过上述将 class 类名统一的形式,我们可以做到重用 CSS 样式,而不是让 CSS 样式依赖于特定的 HTML 结构。

  • 问题二:相同的 HTML 结构,不同的 CSS 样式

虽然上述重用了很多的 class 类,但是问题又来了,如果有一天,老板让我在文章预览的边框添加一个特殊的样式,而简历组件样式不变,我们只能使用内联样式(违反了 HTML 与 CSS 的低耦合原则)或者再声明一个 class 类将其覆盖掉(灵活度下降,代码冗余)。

由于 HTML 结构太过于依赖 CSS ,虽然 class 可以进行重用,但是 HTML 不能灵活的进行特殊样式的设计,那么这种解决方案看起来并不灵活。能不能有一种方案既能够重用 CSS 样式,又能对 HTML 进行特殊的处理呢?

对于 CSS 的发展历程,这里参考了一下 Adam Wathan 大佬所写的 CSS 个人使用经历,以及 CSS 各个写法的优缺点介绍。

英文链接:https://adamwathan.me/css-utility-classes-and-separation-of-concerns/

中文翻译:https://tailwindchina.com/translations/css-utility-classes-and-separation-of-concerns.html

这篇文章也是大名鼎鼎的 Tailwind CSS 项目的作者 Adam Wathan 写的。获取更多有关 Tailwind 的信息,可以戳官网(https://docs.tailwindchina.com/)。


2、CSS 的最新设计思想 —— Tailwind CSS

我们在探索解决方案时,正好发现了 Tailwind CSS 这个项目,根据它的设计思想,从而进一步完善了我们项目有关 CSS 的解决方案。

先看 Tailewind CSS 官方的一个例子。

<button class="py-2 px-4 font-semibold rounded-lg shadow-md text-white bg-green-500 hover:bg-green-700">
  Click me
</button>

/** tailwind class 部分源码 **/
.py-2 {
  padding-top: 0.5rem;
  padding-bottom: 0.5rem
}

.px-4 {
  padding-left: 1rem;
  padding-right: 1rem
}

.font-semibold {
  font-weight: 600
}

.rounded-lg {
  border-radius: 0.5rem
}

.shadow-md {
  --tw-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
  box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow)
}

.bg-green-500 {
  --tw-bg-opacity: 1;
  background-color: rgba(16, 185, 129, var(--tw-bg-opacity))
}

从使用上来看,同样使用了 class 的样式组合,表面上与我们的使用并没有两样,但是仔细分析就会发现,它的 class 类中的属性颗粒度更小,这样做的原因在于保证 class 的「灵活性」和「可重用性」—— 一个 class 类中声明的属性越多,就越难以重用。

也就是每个 class 中相关的属性尽可能的遵循最少原则,于此同时其不同的属性组合又能与 class 类名所对应,保留了 CSS 的语义化的规范 。

根据上述对 CSS 语义化的深入了解,我总结了几个有关语义化 CSS 的特性。

  • 语义化的 class —— 重用性
/***** font-weight *****/
.font-weight-normal {
  font-weight: normal;
}
.font-weight-bold {
  font-weight: bold;
}
.font-weight-100 {
  font-weight: 100;
}
.font-weight-200 {
  font-weight: 200;
}
.font-weight-300 {
  font-weight: 300;
}
.font-weight-400 {
  font-weight: 400;
}
......

/***** margin  *****/
.margin-auto {
  margin: auto;
}
.margin-0 {
  margin: 0;
}
.margin-5 {
  margin: 5px;
}
.margin-10 {
  margin: 10px;
}

......
  • 语义化的 class —— 局限性

上述我们提到过,语义化的 class 最大的问题就是在每个类中不要声明太多的 CSS 属性,那样会造成 class 变的难以重用以及灵活度大大降低。

如果想用上述的设计思想,那么就要保证这个原则 —— 一个 class 类中声明的属性越多,这个 class 就越难以重用

3、终极 CSS 设计方案

最终我们并没有直接用 Tailwind CSS 项目的原因在于项目中特殊的换肤需求,虽然可以用 Tailwind CSS 来实现换肤,但是不够优雅和方便,下文会具体聊聊换肤方案。下面就具体介绍一下我们基于 Tailwind CSS 的设计思想以及加入灵活运用 CSS 变量的方式,决定的 CSS 终极解决方案。

在文章的开头,我们提到过一个分层的概念,没错,我们要对 CSS 进行三个层级的分层,分别为变量原子层、变量应用层、class 应用层,我们接下来分析每一层的作用。

  • 变量原子层(atomicLayer)

我们主要在这一层声明 CSS 的原子化的变量,也就是页面中所有使用到的有关「大小」和「颜色」、「动画」等的属性值(颜色值、文字大小、边框大小等),我们页面用到的所有有关颜色、大小、动画等的属性值都应该在里声明。

/** 变量原子层 **/
<!-- atomicLayer.css -->
:root {
  /***** color *****/
  /* transparent */
  --color-transparent: transparent;

  /* black */
  --color-black: #000000;
  --color-black-1: rgba(0, 0, 0, 0.85);
  --color-black-2: rgba(0, 0, 0, 0.45);
  --color-black-3: rgba(0, 0, 0, 0.25);
  --color-black-4: rgba(0, 0, 0, 0.15);
  --color-black-5: rgba(0, 0, 0, 0.018);
  ....

  /* white */
  --color-white: #ffffff;
  --color-white-1: #f5f7fa;
  --color-white-2: #fafafa;
  ...

  /* blue */
  --color-blue-1: #1890ff;
  --color-blue-2: #40a9ff;
  --color-blue-3: #e6f7ff;
  --color-blue-4: rgba(24, 144, 255, 0.2);
  ...

  /* gray */
  --color-gray-1: #d9d9d9;
  ...

  /* red */
  --color-red-1: #ff4d4f;
  --color-red-2: rgba(255, 77, 79, 0.2);

  /***** font-size *****/
  --font-size-1: 12px;
  --font-size-2: 14px;
  --font-size-3: 16px;
  --font-size-4: 18px;

  /***** border-radius *****/
  --border-radius-1: 2px;
  --border-radius-2: 4px;
  --border-radius-3: 6px;
  --border-radius-4: 8px;

  /***** transition-duration *****/
  --transition-duration-1: 0.15s;
  --transition-duration-2: 0.3s;
  --transition-duration-3: 1s;

  /***** animation-duration *****/
  --animation-duration-1: 0.15s;
  --animation-duration-2: 0.3s;
  --animation-duration-3: 1s;
}
  • 变量应用层(applicationLayer)

这一层作为原则层的上一层应用,里边所有相关的值取自于变量原子层,主要将不带有语义的变量原子层附上一层语义化。当然,这一层还有一个特殊的作用,就是后续的换肤方案,改变的是变量应用层的变量来达到换肤的效果。

/**
 * 原子应用层
 */

:root {
	/***** color *****/
	--primary-color: var(--color-blue-1);
	--secondary-color: var(--color-blue-2);
	--third-color: var(--color-blue-3);
	--error-color: var(--color-red-1);

	/* text */
	--text-color: var(--color-black-1);
	--text-hover-color: var(--secondary-color);
	--text-disabled-color: var(--color-black-3);
	--text-error-color: var(--color-red-1);

	/* button */
	--btn-primary-color: var(--color-white);
	--btn-primary-hover-color: var(--color-white);
	--btn-primary-disabled-color: var(--text-disabled-color);
	--btn-plain-color: var(--text-color);
	--btn-plain-hover-color: var(--text-hover-color);
	--btn-plain-disabled-color: var(--text-disabled-color);
	--btn-text-color: var(--text-color);
	--btn-text-hover-color: var(--text-hover-color);
	--btn-text-disabled-color: var(--text-disabled-color);
	--btn-link-color: var(--primary-color);
	--btn-link-hover-color: var(--secondary-color);
	--btn-link-disabled-color: var(--text-disabled-color);

	/* tab */
	--tab-color: var(--text-color);
	--tab-hover-color: var(--secondary-color);
	--tab-actived-color: var(--primary-color);
	--tab-disabled-color: var(--text-disabled-color);

	/* menu */
	--menu-color: var(--text-color);
	--menu-hover-color: var(--primary-color);
	--menu-actived-color: var(--primary-color);
	--menu-disabled-color: var(--text-disabled-color);


	/***** bg-color *****/
	/* body */
	--body-bg-color: var(--color-white-1);
	--common-bg-color: var(--color-white-1);

	/* btn */
	--btn-primary-bg-color: var(--primary-color);
	--btn-primary-hover-bg-color: var(--secondary-color);
	--btn-primary-disabled-bg-color: var(--color-white-1);
	--btn-plain-disabled-bg-color: var(--color-white-1);
	--btn-text-hover-bg-color: var(--color-black-5);
	--btn-text-disabled-bg-color: var(--color-transparent);
	--btn-link-bg-color: var(--color-transparent);
	--btn-link-hover-bg-color: var(--color-transparent);
	--btn-link-disabled-bg-color: var(--color-transparent);

	/* tab */
	--tab-card-bg-color: var(--color-white-2);
	--tab-card-actived-bg-color: var(--color-white);

	/* table */
	--table-header-bg-color: var(--color-white-2);
	--table-row-hover-bg-color: var(--third-color);
	--table-row-striped-bg-color: var(--color-white-2);

	/* select */
	--select-hover-bg-color: var(--color-white-2);
	--select-selected-bg-color: var(--third-color);

	/* menu */
	--menu-item-selected-bg-color: var(--third-color);

	/* dialog */
	--dialog-mask-bg-color: var(--color-black-4);


	/***** border-color *****/
	--border-color: var(--color-gray-1);
	--border-hover-color: var(--secondary-color);
	--border-disabled-color: var(--color-gray-1);
	--border-error-color: var(--error-color);

	/* btn */
	--btn-primary-border-color: var(--primary-color);
	--btn-primary-hover-border-color: var(--secondary-color);
	--btn-primary-disabled-border-color: var(--border-color);
	--btn-plain-border-color: var(--border-color);
	--btn-plain-hover-border-color: var(--secondary-color);
	--btn-plain-disabled-border-color: var(--border-color);
	--btn-text-border-color: var(--color-transparent);
	--btn-text-hover-border-color: var(--color-transparent);
	--btn-text-disabled-border-color: var(--color-transparent);
	--btn-link-border-color: var(--color-transparent);
	--btn-link-hover-border-color: var(--color-transparent);
	--btn-link-disabled-border-color: var(--color-transparent);


	/***** box-shadow-color *****/
	--box-shadow-color: var(--color-black-4);

	/* input */
	--input-focus-box-shadow-color: var(--color-blue-4);
	--input-error-focus-box-shadow-color: var(--color-red-2);


	/***** font-size *****/
	--font-size-mini: var(--font-size-1);
	--font-size-small: var(--font-size-2);
	--font-size-middle: var(--font-size-3);
	--font-size-large: var(--font-size-4);

	/* text */
	--text-font-size: var(--font-size-mini);

	/* title */
	--title-font-size: var(--font-size-large);


	/***** border-radius *****/
	--border-radius-mini: var(--border-radius-1);
	--border-radius-small: var(--border-radius-2);
	--border-radius-middle: var(--border-radius-3);
	--border-radius-large: var(--border-radius-4);

	--border-radius: var(--border-radius-small);


	/***** transition-duration *****/
	--transition-duration-fast: var(--transition-duration-1);
	--transition-duration-normal: var(--transition-duration-2);
	--transition-duration-slow: var(--transition-duration-3);

	--transition-duration: var(--transition-duration-fast);


	/***** animation-duration *****/
	--animation-duration-fast: var(--animation-duration-1);
	--animation-duration-normal: var(--animation-duration-2);
	--animation-duration-slow: var(--animation-duration-3);

	--animation-duration: var(--animation-duration-fast);
}

  • class 应用层(applicationLayerClasses)

在 class 应用层中我们具体的写一些有关公共的 class(common.css)、组件相关class(component.css)、其他 class(other.css)等样式,接下来会具体介绍这些文件的内容和作用。

  • 通用样式 —— common.css

在 common.css 中的 class 样式,类似于Tailwind CSS 中声明的最小颗粒度的样式类,但是与 Tailwind CSS 不同的是,我们所有的变量值都是取自于「原子应用层」。

/***** color *****/
/* text */
.text-color {
  color: var(--text-color);
}
.hover:text-color:hover {
  --text-color: var(--text-hover-color);
}
.text-disabled-color,
.text-disabled-color:hover {
  --text-color: var(--text-disabled-color);
}
.text-error-color,
.text-error-color:hover {
  color: var(--text-error-color);
}

/***** border-color *****/
.border-color {
  border-color: var(--border-color);
}
.hover:border-color:hover {
  --border-color: var(--border-hover-color);
}
.border-disabled-color,
.border-disabled-color:hover {
  --border-color: var(--border-hover-color);
}
.border-error-color,
.border-error-color:hover {
  --border-color: var(--border-error-color);
}

/***** box-shadow *****/
.box-shadow {
  box-shadow: 0 2px 8px var(--box-shadow-color);
}

/***** font-size *****/
.font-size-mini {
  font-size: var(--font-size-mini);
}
.font-size-small {
  font-size: var(--font-size-small);
}
.font-size-middle {
  font-size: var(--font-size-middle);
}
.font-size-large {
  font-size: var(--font-size-large);
}
...

Tailwind CSS 源码中的属性值都是写死的值,如下:

<!-- border color -->
.border-white {
  --tw-border-opacity: 1;
  border-color: rgba(255, 255, 255, var(--tw-border-opacity))
}

.border-gray-50 {
  --tw-border-opacity: 1;
  border-color: rgba(249, 250, 251, var(--tw-border-opacity))
}

.border-gray-100 {
  --tw-border-opacity: 1;
  border-color: rgba(243, 244, 246, var(--tw-border-opacity))
}

.border-gray-200 {
  --tw-border-opacity: 1;
  border-color: rgba(229, 231, 235, var(--tw-border-opacity))
}

.border-gray-300 {
  --tw-border-opacity: 1;
  border-color: rgba(209, 213, 219, var(--tw-border-opacity))
}

<!-- font-size -->

.text-xs {
  font-size: 0.75rem;
  line-height: 1rem
}

.text-sm {
  font-size: 0.875rem;
  line-height: 1.25rem
}

.text-base {
  font-size: 1rem;
  line-height: 1.5rem
}

.text-lg {
  font-size: 1.125rem;
  line-height: 1.75rem
}
...

我们之所以这样设计的目的为了能够在实现在线换肤的时候,通过覆盖原子应用层的变量值,直接进行换肤,后续换肤章节会具体讲到。

  • 组件相关 —— component.css

由于我们使用的是 Ant Design 组件库,考虑到可能后续的业务无法被现有的组件库所支持,所以需要自己造轮子,那么所有组件库相关的应用层的 class 统一写到这个文件中。

  • 其他 class —— other.css

other.css 也是一些通用的 class,但是为什么不写到 common.css 中,原因在于 other.css 中的 class 类是由多个属性组成的最小功能类。如下我们常用到的:

<!-- other.css -- >

/* 垂直居中 */
.flex-middle {
  display: flex;
  justify-content: center;
  align-items: center;
}

/* 多余文本 ... */
.text-ellipsis {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

而 common.css 中是一些单个属性的最小功能类,所以在这里我们将其两者做了一个区分存储。

好了,到这里我们的 CSS 分层结构分享完了,是不是非常的简单和清晰,为了更加深入体验到分层以及 Tailwind CSS 设计思想的,我们来做一个 Demo,体验一下愉快的开发,顺便分享一下最终的在线换肤方案。


4、终极换肤方案

起初,我们团队中提出过很多的换肤方案,通过组内不断讨论和交流,最终选择我们统一了意见,选择了最终这种换肤方案。

我们最终采用了这种换肤方案,不是说其他的换肤方案不好,而是我们团队根据之前所遇到的问题以及现有的业务需求决定的,特定的项目环境以及产品需求,再加上人为因素的影响,决定了相同的问题,会出现不同的解决方案。

分别从以下三个方面是什么、怎么样、为什么来介绍采用的这个换肤方案。

  • 换肤原理 —— what

我们采用的用换肤原理是通过改变与页面样式相对应的 class 中的 CSS 的变量的值来实现换肤的功能。

注意,这里改变的是原子应用层中的 CSS 变量,而不是原子层的变量,因为我们如果改变的原子层,也就是最底层的变量值,那么页面中很多不需要换肤的元素也被改变了,这也是我们设计原子应用层的原因所在。

所以我们在写页面涉及到换肤元素的时候,涉及到换肤的相关变量的 CSS 值应该去原子应用层中获取。

有一个问题,我们用到的是第三方的组件库 Ant Design,涉及到组件库中的换肤如何实现?这也是我们换肤方案中的一个缺陷,应对方案是需要将组件库中所有的组件的上层样式做一层 class 覆盖,所覆盖的 class 中的属性值用我们原子应用层的 CSS 变量来代替,从而实现组件库的换肤。

虽然前期的工作量非常大,但是后续的维护和使用没有了心智负担,这也是我们团队中对这种方案的取舍。

  • 效果如何 —— How

这里我写了一个 Demo,已经上传至 Github,项目地址:https://github.com/luxiangqiang/peedingDemo

App.vue

<!--
 * @Author: xiaolu
 * @Date: 2021-08-22 14:34:41
 * @LastEditors: xiaolu
 * @LastEditTime: 2021-08-26 09:33:<

以上是关于设计了一个 CSS 终极解决方案!的主要内容,如果未能解决你的问题,请参考以下文章

设计了一个 CSS 终极解决方案!

炫酷 CSS 背景效果的 10 个代码片段

<div+css页面布局课堂笔记>11---页面布局网站首页设计实例__终极版(仿csdn首页)

HTML5期末大作业:餐饮美食网站设计——咖啡(10页) HTML+CSS+JavaScript 学生DW网页设计作业成品 web课程设计网页规划与设计 咖啡网页设计 美食餐饮网页设计...(代码片段

高效Web开发的10个jQuery代码片段

十条实用的jQuery代码片段