前言
本文主要描述的组织 CSS 的问题, 文章内容整理自《前端架构设计》的读书笔记.
继承与特性之争
- h2{
- }
- #sidebar h2{
- background:red;
- }
- #sidebar .calendar h2{
- background:green;
- }
上面这样写样式有很大的问题, 主要是我们在样式书写时默认的继承到了之前的样式, 但在特殊化的样式时, 为了让自己的样式生效, 又要新写样式进行覆盖.
所以列举一下可能的问题是下面的:
选择器的优先级: 选择合适的优先级, 查询效率高, 方便其他人样式覆盖的需求
颜色重置: 要恢复到原来的颜色, 需要进行不断的重置或者赋值
位置依赖: 如果我们的样式移到其他位置, 那么样式代码就会失效, 因为严格依赖 dom 的结构和顺序, 不能很好的解耦
多重继承: 最终元素的样式可能是层层嵌套得到的最终样式代码, 非常不可复用, 当改变主体的样式时, 子元素都会受到影响
深层嵌套: 效率非常低
现代化的模块方案
在之前的章节中, 我们讲到了 CSS 不同的模块化方案, 那么对于上面描述的问题, 其实可以很简化的去解决.
案例代码
你根据以上的需求按照模块的方案和命名, 去除各种依赖, 最大程度使用 class 命名方式, 减少继承, 更大程度的使用独立样式代码块, 轻松解决了以上的问题.
- <h2 class="content_title"></h2>
- <div class="sidebar">
- <h2 class="content_title--reversed"></h2>
- <div class="calendar">
- <h2 class="calendar_title"></h2>
- </div>
- </div>
- // 组件文件夹
- .content_title{
- }
- // 不用恢复重置样式
- .contitle_title--reverse{
- }
- // 特殊组件的 title 定义
- .calendar_title{
- }
单一职责原则
比如我们在定义标题的时候, 可能有两个模块都用到了标题.
- <div class="calendar">
- <h2 class="primary-title"></h2>
- </div>
- <div class="blog">
- <h2 class="primary-title"></h2>
- </div>
- <style>
- .primary-title{
- }
- </style>
可能的问题, 是我们需要在日历或者博客的时候需要去修改这个样式, 于是你的样式可能变成这样的.
- .primary-title{
- }
- .blog .primary-title{
- }
这样的问题是会导致选择器过多, 于是我们按照 bem 进一步优化.
- .calendar_header{
- }
- .blog_header{
- }
这样维护之后, 每个样式块都只负责自己的内容, 除了导致代码的重复之外没有任何坏处. 好处是如果项目严格执行单一职责方式, 当我们改动任何代码的时候, 不会担心对全局造成的影响. 那么重复的代码怎么解决, 这个其实 webpack 的部分和 gzip 的部分已经能把我们的代码进行优化了.
延伸思考: 如果压缩打包的角度可以去掉重复代码的部分, 那么更多角度考虑代码的可持续, 可维护就好了.
单一样式来源
简单来讲, 就是你的样式代码应该来源于尽可能少的组件, 最好是维护在一个 class 中, 保证整个样式代码是可追溯的, 也可以预想效果的.
案例
- <div class="blog">
- <div class="blog-header"></div>
- </div>
- <div class="calendar">
- <div class="calendar-header"></div>
- </div>
- /* calendar CSS*/
- .calendar-header{
- }
- /* blog CSS*/
- .blog-header{
- }
- .blog .blog-header{
- }
以 blog 进行 title 的限定, 主要是限制让字号小一点, 这种的主要问题是当项目积累下来, 会有很多样式代码散落在各个组件, 这样导致不可追溯.(补充个常识, 以父元素为什么进行修饰子元素, 这个称为样式上下文)
那么建议的方式是将散落在各处的代码完全控制在一个组件内.
组件修饰符
虽然单一组件的原则让我们对组件的样式代码封装的很好, 但还是有一些特殊的需求, 那我们如何设计这方面的代码呢? 通过皮肤或者子模块来实现, 也可以称为组件修饰符.
比如我们针对日历的组件, 我们需要追加一个特殊的样式, 可以这样实现.
- .calendar-header{
- }
- .calendar--nested .calendar-header{
- }
通过这样的方式, 我们不但可以实现皮肤的需求, 也可以实现对上下文的解耦, 我们只要关注组件需要什么样的特殊样式, 进行组件修饰即可, 而不用依赖耦合在父容器里.
element-ui 的 CSS
以下以 element 的 2.4.11 版本为例, 根据其 API 文档以及源码的角度为大家分析 element-ui 是如何解决这些问题的.
el-button 分析
el-button 文档地址
API 设计规范: 可以看到 button 的暴露的 API 主要是基于属性的
参数 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
size | 尺寸 | string | medium / small / mini | — |
type | 类型 | string | primary / success / warning / danger / info / text | — |
plain | 是否朴素按钮 | boolean | — | false |
round | 是否圆角按钮 | boolean | — | false |
circle | 是否圆形按钮 | boolean | — | false |
loading | 是否加载中状态 | boolean | — | false |
disabled | 是否禁用状态 | boolean | — | false |
icon | 图标类名 | string | — | — |
autofocus | 是否默认聚焦 | boolean | — | false |
native-type | 原生 type 属性 | string | button / submit / reset | button |
class 的设计, 虽然组件的设计是基于属性的, 但实际不同的属性最终都是表现为 class 的. 那么基于这样的角度去设计样式是 element-ui 的首创或者是 vue 组件的创新么? 其实早在 Bootstrap 里就是这样设计的.
class | 说明 |
---|---|
el-button-group | 按钮组的样式,作为外部容器,不对 el-button 产生任何样式代码,但对其内含有的 el-button 会产生垂直居中的效果,还有向右的间距,.el-button-group .el-button--primary:first-child{} |
el-button el-button--default | 基本样式,修饰符, 生效规则 el-button--default 也是直接定义其样式规则,不依赖于 el-button |
el-button el-button--medium | el-button--medium 修饰符中直接定义好尺寸的全部代码 |
el-button el-button--default is-circle | 基本样式,smacss 状态样式 ,其中 is-circle 包含这个原型的全部样式,其生效的条件是与 el-button 同时生效,is-disabled 同理 |
icon="el-icon-edit" | 带 icon 的部分不在 button 里做特殊样式,其样式属于基本样式,但是 dom 结构是通过组件进行添加的,那么其图标的字号来源于哪里呢?来源于 el-button 的基本样式 14px,所以这部分进行了解耦,不用单独设置 |
组件内如何根据属性进行返回对应的 class.
- class="el-button"
- // 可以看到其根据传入的各个 type 属性分别将 class 追加到 class 数组之中
- :class="[type ?'el-button--'+ type :'',buttonSize ? 'el-button--' + buttonSize : '',{'is-disabled': buttonDisabled,'is-loading': loading,'is-plain': plain,'is-round': round,'is-circle': circle}]"
- // 根据为 loading 显示出固定的 loading 图标
- <i class="el-icon-loading" v-if="loading"></i>
- // 根据传入的 icon 以及没有 loading 显示出对应 type 的 icon
- <i :class="icon" v-if="icon && !loading"></i>
轻度定制的 element
经常有场景我们需要引入 element-ui 之后需要对其 ui 进行皮肤化的开发样式, 虽然 element-ui 有暴露其对应的组件样式, 我们也可以进行对应的详细的源码的 fork 并开发, 但在大多数中小公司其实不用小题大做. 我们只需要根据自己的情况, 针对一样的样式进行全局样式覆盖即可. 那么, 我们项目中针对按钮的皮肤化改变会是这样的.
- // 定义 variables.SCSS 的主题变量
- // 主题色相关变量
- $color-thin:rgba(60,191,196,0.1);
- $color:rgba(60,191,196,1);
- // customer-element.SCSS 主要定义已使用饿了么组件样式的修改
- @import './variables';
- // 一个按钮样式的全局覆盖
- .el-button--primary{
- &:hover,&:focus{
- background:$color;
- border-color:$color;
- }
- background:$color;
- border-color:$color;
- }
Bootstrap 的 CSS
我们同样也是分析 Bootstrap 里的样式使用, 基本也是使用 class 拼盘理论的.
- components#btn-groups 文档地址 https://v3.bootcss.com/components/#btn-groups
- <div class="btn-group" role="group" aria-label="...">
- <button type="button" class="btn btn-default">Left</button>
- <button type="button" class="btn btn-default">Middle</button>
- <button type="button" class="btn btn-default">Right</button>
- </div>
button.Less 样式文件节选, 可以看到其基本样式, 嵌套内伪类的样式, button-size 的方法类, 修饰符的 (子模块的) 维护方式, 这都是值得我们借鉴的科学规范思想.
- // Base styles
- // --------------------------------------------------
- .btn {
- display: inline-block;
- ......
- .button-size(@padding-base-vertical; @padding-base-horizontal; @font-size-base; @line-height-base; @btn-border-radius-base);
- .user-select(none);
- &,
- &:active,
- &.active {
- &:focus,
- &.focus {
- .tab-focus();
- }
- }
- &:hover,
- &:focus,
- &.focus {
- color: @btn-default-color;
- text-decoration: none;
- }
- a& {
- &.disabled,
- fieldset[disabled] & {
- pointer-events: none; // Future-proof disabling of clicks on `<a>` elements
- }
- }
- }
- // Alternate buttons
- // --------------------------------------------------
- .btn-default {
- .button-variant(@btn-default-color; @btn-default-bg; @btn-default-border);
- }
- .btn-primary {
- .button-variant(@btn-primary-color; @btn-primary-bg; @btn-primary-border);
- }
小结
到此为止总结的技巧点如下: 当然肯定还有更多的技巧等待你补充
分离容器与内容
单一样式来源
区分布局与组件的角色
在标记上使用单一的, 扁平的标识
组件修饰符
通过本文我们了解并总结了 CSS 代码的继承与特殊化的问题, 并通过上面的技巧解决了这样的痛点, 并用来显著提升自己的代码水平.
来源: https://juejin.im/post/5c34b176518825258513f9be