2021升职加薪,了解下这个 CSS 变量

avatar
@https://www.tuya.com/

本文由团队成员 BingLee1994 撰写,已授权涂鸦大前端独家使用,包括但不限于编辑、标注原创等权益。

变量大家都再熟悉不过了,为啥要代码需要变量?因为相同的数值被多个地方引用,我们将其提取为变量,这样只需要修改该变量,那么所有引用的地方都会同步修改。

CSS自定义变量

一直以来CSS只是简单的UI描述语言,无法像js那样使用更高级的特性,后期CSS变量的诞生赋予CSS更多的能力,让我可以实现“一次修改,多处生效”,不仅节省了开发时间,还可让我们将更多的UI变量代码(全局context, state等等)从js挪到CSS代码里,方便维护,主题换肤功能(暗色模式/深色模式/护眼模式也属于)就是CSS变量的一个使用场景。

如果你还没接触CSS变量,你可以前往教程来学习一下,她真的没有任何学习难度,只需花费你几分钟。

兼容问题

如果你还在硬性地兼容老旧的IE11,那么很遗憾IE11并未支持,因此应禁止使用该特性,尤其是你将其应用到布局样式,诸如margin, flex, position等等,那会导致这些样式直接失效,从而使页面崩坏掉。

这个变量没有兼容性问题

不兼容的硬伤我们无法修复,不过好在下文所述的这个CSS变量没有兼容性问题。

不要小看她,就算是仅存的一个变量,但是还是在很多使用场景都有所帮助!

初体验

很多地方都在用这个变量,只是你没察觉到,客官往下看:

让我们创建两个普通的超链接,为了让你更好理解,我们创建两个不同样式的下划线:

<a href="https://www.tuya.com">访问官网</a>
<a style="text-decoration-style: wavy;" href="https://developer.tuya.com/cn">
  访问开发者
</a>

显示效果如下:

截屏2021-03-06 上午10.27.27.png

我们发现下划线和波浪线看起来和字体颜色一样,让我们继续做实验:

我们分别将两个超链接的字体颜色改为主题色涂鸦橙:rgb(255 72 0),官网活力蓝:rgb(78 133 254)

<a style="color: rgb(255 72 0);" href="https://www.tuya.com">
    访问官网
</a>
<a style="color: rgb(78 133 254);text-decoration-style: wavy;" href="https://developer.tuya.com/cn">
  访问开发者
</a>

截屏2021-03-06 上午10.32.25.png 我们惊喜的发现,不管是下划线还是波浪线,都在视觉上与字体颜色同步,但这些线可能并不是用字体画出来的

我们甚至可以通过js获取计算样式的真实颜色值来验证:

getComputedStyle(firstLink).textDecorationColor

截屏2021-03-06 上午10.35.48.png

最终应用的色值就是字体颜色本身,验证了我们的猜测。其实textDecorationColor的默认值正是currentColor,现在主角登场~

currentColor

CSS变量值顾名思义就是当前color值,你可以这样理解:currentColot = color,该属性值用于读取和同步字体颜色,用以其他地方,比如上述波浪颜色,而我们熟知的border-color的默认值也是currentColor

由于color可以继承父元素或者祖先元素的变量,因此对父元素只需设置一次颜色,即可穿透至底部元素。

完整真实案例

让我们制作这样的彩色按钮,默认是镂空按钮,hover变为实心按钮。

截屏2021-03-06 上午10.56.27.png

截屏2021-03-06 上午10.56.53.png

这对你来说没有一点难度,让我们按照旧思路来写:

<button class="btn">
  <span class="btn-txt">访问官网</span>
  <span class="btn-icon">
    <!-- 图标(如果需要) -->
  </span>
</button>
button {
  margin: 0;
  padding: 0;
  outline: none;
  border: none;
  background: none;
} /* reset css用于清除button自带样式 */

.btn {
  color: rgb(78 133 254); /* 默认颜色 */
  height: 30px;
  line-height: 30px; /* 高度 */
  padding: 0 8px; 
  border-radius: 6px; /* 圆角 */
  border-style: solid;
  border-width: 1px; /* 边框 */
  cursor: pointer;
}

/* hover时变实心 */
.btn:hover {
  background-color: rgb(78 133 254);
}

.btn:active {
  opacity: .8;
}

.btn:hover > .btn-txt {
  color: white;
}

这对你来说没有任何难度,别高兴太早,往下看:

需求增加

现我们需要增加额外两种颜色主题,表示成功的绿色,和警示性的红色。

截屏2021-03-06 下午2.34.26.png

截屏2021-03-06 下午2.34.18.png

这也是小菜一碟,我们在基础样式上,增加两个特性样式去覆盖字体颜色和hover背景色即可:

/* 保持源代码不动,增加如下代码 */
/* 成功色 */
.btn.btn-success {
  color: #7cb305;
}
.btn.btn-success:hover {
  background-color: #7cb305;
}

/* 警示色 */
.btn.btn-warning {
  color: #ff4d4f;
}
.btn.btn-warning:hover {
  background-color: #ff4d4f;
}

然后我们在需要使用的地方添加新增样式即可:

<!-- 应用警告色 -->
<button class="btn btn-warning">
  <span class="btn-txt">访问官网</span>
  <span class="btn-icon">
    <!-- 图标(如果需要) -->
  </span>
</button>

如果后续我们新增1个,2个或多个颜色主题,只需将上述代码无聊的复制粘贴后改改颜色即可(高级的可以用scss mixin或者循环去生成)

封装组件

我们需要将其封装为React组件,下面写个伪代码示范下:

const colorTheme = {
  success: 'btn-success',
  warning: 'btn-warning',
}

// 出于时间原因,代码全程省略非空控判断,请自觉添加;请自觉forwardRef.
const Button = (props) => {
  const { children, theme, className: customerClassName, ...restAsHTMLAttr } = props
  const themeClassName = colorTheme[theme] || ''
  const classNames = ['btn', themeClassName, customerClassName].join(' ')

  return (
    <button className={classNames} {...restAsHTMLAttr}>
      <span className="btn-txt">{children}</span>
      <span className="btn-icon">
        {/* 图标,如果需要 */}
      </span>
    </button>
  )
}

然后我们可以通过theme属性来轻松切换我们想要的按钮:

<Button theme="success">登录</Button>
<Button theme="warning">注销账号</Button>

难度升级

现要求我们组件支持自定义颜色值,因为有可能我们用于不同项目,不同项目使用不同色系,就需要对该基础组件进行自定义颜色后再二次封装为业务组件。

由于我们无法预知未来有多少种颜色,因此在基础组件中,我们无法通过写样式来配置themeColor,鬼知道未来有多少种颜色,因此我们需要使用者直接将颜色值通过props传进来。

你当然可以完全不做这个需求,转而让使用者通过添加自定义class名去hack样式来实现自定义颜色,但这明显是偷懒或者逃避的行为,我们是专业的开发,怎能就此放弃。

接受颜色属性

现在我们通过propscolor获取自定义颜色值,然后将其应用到按钮的color style中:

const colorTheme = {
  success: 'btn-success',
  warning: 'btn-warning',
}

const Button = (props) => {
  const {
    children,
    theme,
    color,
    className: customerClassName,
    style: customerStyle,
    ...restAsHTMLAttr
  } = props
  const themeClassName = colorTheme[theme] || ''
  const classNames = ['btn', themeClassName, customerClassName].join(' ')

  const colorStyle = {
    color, //接受自定义颜色
    ...customerStyle,
  }

  return (
    <button style={colorStyle} className={classNames} {...restAsHTMLAttr}>
      <span className="btn-txt">{children}</span>
      <span className="btn-icon">
        {/* 图标,如果需要 */}
      </span>
    </button>
  )
}

支持hover

目前我们通过color来修改了按钮颜色,但是我们需要处理hover态,但js没有hover事件,我们可以通过两个事件来模拟,比如mouseovermouseleave,而且我们需要颜色改变,这里可以使用state,思路如下:

//以下均为伪代码
const { color } = props
const normalStyle = { color }
const hoverStyle = {
  backgroundStyle: color,
}

const [colorStyle, setColorStyle] = useState({
  ...normalStyle
  //...用户传入的style这里略过,请自行处理
})

接下来在mouseover事件中,将style改变为背景色:

//切换到背景色
onMouseOver = () => {
  setColorStyle(hoverStyle)
  //其他代码省略
}

最后在mouseleave中切换为字体色:

//切换回字体色
onMouseLeave = () => {
  setColorStyle(normalStyle)
  //其他代码省略
}

当然如果你不想用state,你可以在事件回调中直接拿到dom,通过event.target.style.xxx = XXX,修改样式,不过同样不友好。

因为写这么多事件竟然只是为了改个背景色,原本纯U组件变得不纯粹,这样做很恶心,我是非常讨厌写一堆这样的代码,要是能挪到css就好了。

使用currentColor优化

注意到了吗?上述背景色只是为了同步字体颜色,那么我们就使用currentColor来优化,修改我们的CSS如下:

.btn {
  color: rgb(78 133 254);
  height: 30px;
  line-height: 30px;
  padding: 0 8px;
  border-radius: 6px;
  border-style: solid;
  border-width: 1px;
  cursor: pointer;
}

.btn:hover {
  background-color: currentColor; /* 同步color */
}

.btn:active {
  opacity: .8;
}

.btn:hover > .btn-txt {
  color: white;
}

那么接下来我们只需要操作color就可以同步背景色了,所以我们要把以前无用的,操作背景的代码统统删掉!

/* 成功色 */
.btn.btn-success {
  color: #7cb305;
}
/* 通通删掉 */
/* .btn.btn-success:hover {
  background-color: #7cb305;
} */

/* 警示色 */
.btn.btn-warning {
  color: #ff4d4f;
}
/* 通通删掉 */
/* .btn.btn-warning:hover {
  background-color: #ff4d4f;
} */

最后只留下了两个颜色。接着修改我们的组件,将mouseover, mouseleavestate等一堆杂七杂八的东西全删掉,只留下修改color的代码,最后代码精简到如下:

const colorTheme = {
  success: 'btn-success',
  warning: 'btn-warning',
}

const Button = (props) => {
  const {
    children,
    theme,
    color,
    className: customerClassName,
    style: customerStyle,
    ...restAsHTMLAttr
  } = props

  const themeClassName = colorTheme[theme] || ''
  const classNames = ['btn', themeClassName, customerClassName].join(' ')
  const colorStyle = {
    color, //接受自定义颜色
    ...customerStyle,
  }


  return (
    <button style={colorStyle} className={classNames} {...restAsHTMLAttr}>
      <span className="btn-txt">{children}</span>
      <span className="btn-icon">
        {/* 图标,如果需要 */}
      </span>
    </button>
  )
}

小技巧

借助color可以继承的特性,如下的场景卡片,会有一个主题色(下图所示的黄色),并且需要根据配图来变化。
多个标题和按钮的颜色可以继承卡片,我们只需要将颜色设置在卡片最大容器上,然后将Button颜色设置为inherit即可继承至卡片的颜色,从而不需要在js代码里将颜色层层往下传递或者使用context

截屏2021-03-06 下午2.56.14.png

更多场景

颜色还可以用于box-shadow,甚至是svgfill属性,做到svg icon和字体颜色自动同步,以省去我们通过自定义样式层层修改颜色值的时间。antd的部分svg图标也用到currentColot

截屏2021-03-06 下午3.03.59.png