CSS 自定义属性在Web组件中的应用

发布于 大漠

今天我们不聊什么是**CSS自定义属性**,而把重点放在CSS自定义属性在组件中怎么使用,她又会给我们设计组件带来什么样的变化和相应的优势。在这篇文章中,我们主要会探讨CSS自定义属性为Web组件服务,以及怎么利用CSS自定义属性来维护组件。

设计背景

Web组件的设计对于Web从业人员来说是最常见的一部分了,随着JavaScript框架到来,除了给我们为组件设计带来了红利,但也给我们造成一定的约束性。比如说,我们设计的一个Web组件,应该提供什么样的方式给Web开发人员使用是最佳的姿势。不管是CSS方法论的基础上设计的UI组件,还是基于JavaScript框架上(比如Vue、React)设计的UI组件,在提供给开发者使用上来说还是存在一定的约束性。或者说可扩展性并不是非常的灵活。这也是令很多设计Web组件的同学来说头痛的地方,也在致力解决的一个点。

值得幸运的是,CSS Houdini的出现,给Web组件的设计带来了一些灵感,至少我在这方面得到较强的设计灵感。接下来,我们来阐述CSS Houdini给Web组件的设计带来什么灵感?感兴趣的同学请继续往下阅读。

CSS Houdini给组件带来的扩展性

在《提示框组件的实现给我带来的思考和探索》一文中的结尾处向大家演示了如何通过CSS Houdini来设计一个Tooltips的组件

同时给大家留下一个还需要继续探讨的话题:

如何将CSS自定义属性运用于Web组件中,来构建一个复用性,灵活性和可扩展性更强的Web组件

设计稿中的组件

接下来先拿一个最简单的组件来举例。在讲故事之前,我们先来看设计稿中对某个组件的理解,比如按钮(即Button

从Sketch设计面板上可以看到,对按钮在UI上有影响的属性,如果拿CSS来对比的话,可能会有:

  • 宽度 ─➤ width
  • 高度 ─➤ height
  • 边框 ─➤ border
  • 圆角 ─➤ border-radius
  • 阴影 ─➤ box-shadow
  • 背景 ─➤ background (可能是background-image,也可能是background-color
  • 字体 ─➤ font (比如font-size)
  • 文本颜色 ─➤ color

其中一些属性是用于UI风格(比如border-colorcolorbackgroundbox-shadow),一些属性是用于UI大小(比如widthheightborder-widthfont-size)。如果我们把这些都声明成一个自定义属性(可灵活调整):

--backgroundColor: #F2F3F7
--borderColor: #C4C6CF
--borderRadius: 3px
--fontSize: 16px
--color: #333
--padding: 5px 10px

CSS Framework中的UI组件

在社区中CSS Framework有很多,比如大家熟悉的Bootstrap就是其中之一,就我个人而言一直喜欢这个CSS框架。在Bootstrap中有很多UI组件,同样拿按钮来举例:

就上图而言,整个Bootstrap在设计UI组件的时候也采用了一些CSS方法论。2019年的CSS报告中统计了社区中常用的一些CSS方法论

有关于2019年CSS报告中更有趣的东西,可以阅读《从9102年的CSS状态报告中看CSS特性的使用》一文。

Bootstrap中就使用了OOCSS的相关特性。比如按钮他有一个基类btn,然后有一个扩展类btn-primary

.btn {
    display: inline-block;
    font-weight: 400;
    color: #212529;
    text-align: center;
    vertical-align: middle;
    user-select: none;
    background-color: transparent;
    border: 1px solid transparent;
    padding: .375rem .75rem;
    font-size: 1rem;
    line-height: 1.5;
    border-radius: .25rem;
    transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;
}

.btn-primary {
    color: #fff;
    background-color: #007bff;
    border-color: #007bff;
}

其他风格的按钮也是采用类似方式实现的。可以说,这种设计UI组件的方式是个古老的方式。

如果你对CSS方法论感兴趣的话,可以花点时间阅读《写CSS的姿势》一文。

CSS自定义属性给UI组件带来的变革

这些年CSS的技巧在不断的革新,特别是近几年,可以说是一种飞跃性的发展。其中CSS 自定义属性的进展就足以令我们目瞪口呆,发展神速

CSS的自定义属性给UI组件的设计带来更多的灵活性。

从设计软件中我们可以获知,设计的任何UI组件,都有很多参数可以用来控制组件的样式风格,比如说大小,皮肤等。而这些参数就可以和CSS自定义属性匹配起来。

继续拿Button来举例,我们可以在基类中声明按钮UI所需的参数,比如:

.button {
    // 声明CSS自定义属性
    --backgroundColor: #fff;
    --borderColor: #ccc;
    --borderRadius: 5px;
    --fontSize: 16px;
    --color: #333;
    --padding: 5px 10px;

    // 按钮的基本样式
    display: inline-flex;
    justify-content: center;
    align-items: center;

    // 调用CSS自定义属性
    background: var(--backgroundColor);
    border: 1px solid var(--borderColor);
    color: var(--color);
    font-size: var(--fontSize);
    padding: var(--padding);
    border-radius: var(--borderRadius);
}

效果如下:

其中--padding--fontSize用来控制按钮大小;--backgroundColor--color--borderColor--borderRadius用来控制按钮的皮肤。如果我们需要其他风格的按钮时,我们可以这样使用:

<!-- HTML -->
<div class="button primary" role="button">Primary Button</div>
<div class="button danger large" role="button">Danger && Large Button</div>

// CSS
.primary {
    --backgroundColor: #007bff;
    --borderColor: #007bff;
    --color: #fff;
}

.danger {
    --color: #fff;
    --backgroundColor: #dc3545;
    --borderColor: #dc3545;
}

.large {
    --padding: .5rem 1rem;
    --borderRadius: .3rem;
    --fontSize: 1.25rem;
}

看到上面的示例,你可能会问为什么不显式的设置widthheight来控制按钮的大小呢?就我个人经验来说,能不显式设置widthheight来控制元素大小就尽量不要使用,就算是要设置,也更应该考虑min-widthmin-height这样的属性。另外在CSS中,还有更为灵活来控制元素在大小的属性,即min-content()max-content()fit-content()函数和fill-availablewidth的一个属性值)等。有关于这几个函数的使用可以阅读W3C的**CSS Intrinsic & Extrinsic Sizing Module Level 3**规范或者@张鑫旭大师的《理解CSS3 max/min-contentfit-contentwidth》一文。

另外为了更好的向大家演示CSS自定义属性给UI组件带来的灵活性和可维护性,下面一个示例,借助CSSOM中的CSSStyleDeclaration中的setProperty()getPropertyValue()方法来重构一个CSS自定义属性构建的UI组件:

操作右侧表单项,就可以轻易的控制按钮的样式风格:

CSS自定义属性在Vue组件中的应用

时至今日,仅基于CSS来构建UI的场景越来越少了。随着类似Vue和React这样的优秀框架的诞生,在组件构建方面大部分都基于这两大体系。接下来就和大家一起探讨CSS自定义属性怎么在JavaScript框架中来构建组件。

同样拿Button组件来举例。在Vue的项目中构建一个Button组件,在样式的构建和维护上同样使用CSS自定义属性:

// Button.vue
<template>
    <div class="button">{{ text }}</div>
</template>

<script>
export default {
    name: 'Button',
    props: {
        text: String
    }
}
</script>

<style scoped>
    .button {
        --backgroundColor: #fff;
        --borderColor: #ccc;
        --borderRadius: 5px;
        --fontSize: 16px;
        --color: #333;
        --padding: 5px 10px;

        display: inline-flex;
        justify-content: center;
        align-items: center;
        background: var(--backgroundColor);
        border: 1px solid var(--borderColor);
        color: var(--color);
        font-size: var(--fontSize);
        padding: var(--padding);
        border-radius: var(--borderRadius);
    }
</style>

在调用组件的时候,我们就可以很灵活的像在CSS Framework中那样引用Button组件和构建自己的按钮风格:

// App.vue

<Button text="Default"/>
<Button text="Primary" class="primary" />

<style scoped>
    .primary {
        --backgroundColor: #007bff;
        --borderColor: #007bff;
        --color: #fff;
        --padding: .375rem .75rem;
        --borderRadius: .25rem;
        margin: 0 5px;
    }
</style>

看到的效果如下:

从上面的结果上我们可以看出,CSS自定义属性在Vue组件的使用上还是非常的灵活。就上面的示例而言,CSS自定属性在Button组件的样式上暴露出相应的API。开发者在引用Button组件的时候,我们只需要根据自己的需要给声明的自义定义属性赋予相应的值。比如primary按钮,我们只需要在.primary按钮上赋值相应的属性值:

.primary {
    --backgroundColor: #007bff;
    --borderColor: #007bff;
    --color: #fff;
    --padding: .375rem .75rem;
    --borderRadius: .25rem;
}

上面示例还是在样式上来通过再次给声明的自定义属性(我把这个定位于组件给开发者暴露的API),重而覆盖了按钮的基本样式,生成所需要的按钮样式风格。

在Vue中还可以使用props的特性来对数据进行通讯,给组件带来进一步的灵活性。在上面的示例基础上我们来做一下调整:

// ButtonProps.vue
<template>
    <div class="button" :style=coustomStyle>{{ text }}</div>
</template>

<script>
    export default {
        name: 'ButtonProps',
        props: {
            text: String,
            varCSS: {
                type: Object,
                default: function (){
                    return {
                        backgroundColor: '#fff',
                        borderColor: '#ccc',
                        borderRadius: '5px',
                        fontSize: '16px',
                        color: '#333',
                        padding: '5px 10px'
                    }
                }
            }
        },
        computed: {
            coustomStyle() {
                return {
                    '--backgroundColor': this.varCSS.backgroundColor,
                    '--borderColor': this.varCSS.borderColor,
                    '--borderRadius': this.varCSS.borderRadius,
                    '--fontSize': this.varCSS.fontSize,
                    '--color': this.varCSS.color,
                    '--padding': this.varCSS.padding
                }
            }
        }
    }
</script>

<style scoped>
    .button {
        --backgroundColor: #fff;
        --borderColor: #ccc;
        --borderRadius: 5px;
        --fontSize: 16px;
        --color: #333;
        --padding: 5px 10px;

        display: inline-flex;
        justify-content: center;
        align-items: center;
        background: var(--backgroundColor);
        border: 1px solid var(--borderColor);
        color: var(--color);
        font-size: var(--fontSize);
        padding: var(--padding);
        border-radius: var(--borderRadius);
    }
</style>

在Vue的props中声明了一个varCSS的对象,这个对象只做一件很简单的事情,就是把按钮组件中要用到的样式(决定按钮样式)的属性都在varCSS中定义。然后在computedvarCSS对象中的keyvalue赋予到按钮样式中的自定义属性中:

computed: {
    coustomStyle() {
        return {
            '--backgroundColor': this.varCSS.backgroundColor,
            '--borderColor': this.varCSS.borderColor,
            '--borderRadius': this.varCSS.borderRadius,
            '--fontSize': this.varCSS.fontSize,
            '--color': this.varCSS.color,
            '--padding': this.varCSS.padding
        }
    }
}

这样在组件调用的时候,我们就可以通过varCSS传递需要的值:

// App.vue中的<template>

<ButtonProps text="Default Button with Custom Property" :varCSS="coustomProperty" />

data () {
    return {
        coustomProperty: {
            backgroundColor: '#007bff',
            borderColor: '#007bff',
            color: '#fff'
        }
    }
}

这个时候你可以看到按钮的样式风格和primary的风格相同:

而且这种方式可以非常灵活的运用于任何其他的组件中。比如我们在Card组件中引用按钮组件:

// Card.vue
<template>
    <div class="card">
        <div class="card-thumbnail">
            <img :src=cardObject :alt=cardTitle />
        </div>
        <div class="card-body">
            <h5 class="card-title">{{cardTitle}}</h5>
            <div class="card-content">{{cardText}}</div>
        </div>
        <div class="card-footer">
            <ButtonProps text="Enter" :varCSS="coustomProperty"/>
            <ButtonProps text="Cancel" />
        </div>
    </div>
</template>

<script>
import ButtonProps from './ButtonProps';

export default {
    name: 'Card',
    props: {
        cardTitle: String,
        cardText: String,
        cardObject: String,
    },
    components: {
        ButtonProps
    },
    data () {
        return  {
            coustomProperty: {
                backgroundColor: '#dc3545',
                borderColor: '#dc3545',
                color: '#fff'
            }
        }
    }
}
</script>

上面的示例涉及到了一些有关于Vue组件数据通讯相关的知识,如果你初次接触的话,建议你花点时间阅读:

使用props方式虽然可以通过数据来传递,但如果同一个组件中有多个不同风格的按钮的样式时,会让事情变得复杂。但如果使用样式的覆盖会更为灵活,比如:

<Button text="Default" />
<Button text="Primary" class="primary" />
<Button text="Danger" class="danger" />
<Button text="Info" class="info" />
<Button text="Secondary" class="secondary" />
<Button text="Success" class="success" />

<style scoped>

    .primary {
        --backgroundColor: #007bff;
        --borderColor: #007bff;
        --color: #fff;
        --padding: .375rem .75rem;
        --borderRadius: .25rem;
    }

    .secondary {
        --color: #fff;
        --backgroundColor: #6c757d;
        --borderColor: #6c757d;
    }
    .success {
        --color: #fff;
        --backgroundColor: #28a745;
        --borderColor: #28a745;
    }

    .danger {
        --color: #fff;
        --backgroundColor: #dc3545;
        --borderColor: #dc3545;
    }
    .info {
        --color: #fff;
        --backgroundColor: #17a2b8;
        --borderColor: #17a2b8;
    }
</style>

在当初刚学习Vue组件相关的知识时,也尝试着使用Vue来构建一个按钮组件。感兴趣的可以拿来对比,大家也可以结合自己构建组件时,看看哪种方式更为灵活,更适合自己。

另外,上面的示例只向大家演示了一种状态,其实我们可以采用同样的方式为组件不同的状态设置不同的样式风格,比如下面这个示例:

回过头来再把CSS自定义属性在组件中的应用和CSS Houdini构建的组件对比,是不是有所相似之处。其实我们可以使用类似的方法在Vue中来构建Tooltips组件,要是您感兴趣的话,不仿可以尝试自己构建一个。

CSS 自定义属性在React组件中的应用

在React中使用CSS的方式相对而言要更复杂一点,因为在React中可选的方案更多,每种不同的方案都有着自己的利弊,并且使用和维护的方式都有所不同。有关于这方面更深入的介绍可以阅读《React中CSS Modules的使用》和《React中编写CSS的姿势》两篇文章。

在这一节中可能会多花点时间来聊CSS自定义属性在React组件中的应用。

CSS 自定义属性在React组件的样式文件中的使用

先来看简单,最基本的方式。也有点类似于其他的框架模式:样式文件和组件分离。同样拿按钮组件来举例:

// Button.js
import React from 'react';
import './Button.css';

function Button(props) {
    const { text , className , ...rest } = props;

    return (
        <div className={`button ${className}`} {...rest}>{text}</div>
    );
}

export default Button;

// Button.css
.button {
    --backgroundColor: #fff;
    --borderColor: #ccc;
    --borderRadius: 5px;
    --fontSize: 16px;
    --color: #333;
    --padding: 5px 10px;

    display: inline-flex;
    justify-content: center;
    align-items: center;
    background: var(--backgroundColor);
    border: 1px solid var(--borderColor);
    color: var(--color);
    font-size: var(--fontSize);
    padding: var(--padding);
    border-radius: var(--borderRadius);
}

这样我们创建了一个最简单,最基本的Button组件,有了这个组件之后可以在有需要的地方引用该组件,经如我们在App.js引用Button组件:

// App.js
import React from 'react';
import logo from './logo.svg';
import Button from './Button';
import './App.css';

function App() {
    return (
        <div className="App">
            <header className="App-header">
                <img src={logo} className="App-logo" alt="logo" />
                <div className="wrapper">
                    <Button text="Default" />
                    <Button text="Primary" className="primary" />
                </div>
            </header>
        </div>
    );
}

export default App;

App.css中可以为.primary按钮重置已声明的CSS自定义属性的值:

// App.css

.primary {
    --backgroundColor: #007bff;
    --borderColor: #007bff;
    --color: #fff;
    --padding: .375rem .75rem;
    --borderRadius: .25rem;
}

这样你在页面上看到的效果如下:

从上面的示例中来看,这种方式和使用CSS Framework来维护CSS类似。也就是说,平时大家使用CSS会碰到的问题,在这种方式之下同样也会是会碰到。这个和Vue有点不一样,因为在Vue中构建组件的时候,通过scoped来指定了组件样式只作用于组件自身,并不会造成全局的污染。

我们接着来看第二种方式,即Inline Style方式。

CSS自定义属性在Inline Style中的使用

在React中使用Inline Style也是常用方式之一。那么CSS自定义属性和React组件怎么结合,即如何通过内联方式来覆盖组件中给我们暴露的接口(即自定义属性)。

先回到CSS中,如查我们要在行内样式(元素style标签)覆盖,需要把自定义属性运用于其中,比如:

<div class="button " style="--color:#fff; --backgroundColor:#dc3545; --borderColor:#dc3545;">Danger</div>

在React中,我们可以把这些自定义属性用于一个对象中,比如:

// App.js

function App() {
    // 显式声明danger按钮要用的覆盖样式
    const dangerButton = {
        '--color': '#fff',
        '--backgroundColor': '#dc3545',
        '--borderColor': '#dc3545',
    } 

    return (
        <div className="App">
            <header className="App-header">
                {* ... *}
                <Button text="Danger" style={dangerButton} />
                </div>
            </header>
        </div>
    );
}

最终的效果如下:

如果有多个Button需要覆盖的时候,声明的Object可能会变得更复杂一些,更能维护一些,比如:

const buttonThemes = {
    danger: {
        '--color': '#fff',
        '--backgroundColor': '#dc3545',
        '--borderColor': '#dc3545',
    },
    info: {
        '--color': '#fff',
        '--backgroundColor': '#17a2b8',
        '--borderColor': '#17a2b8'
    }
} 

<Button text="Danger" style={buttonThemes.danger} className="" />
<Button text="Info" style={buttonThemes.info} className="" />

效果如下:

在React社区中,有一个react-custom-properties插件可以帮助我们更灵活的来使用自定义属性。在使用之前,先执行下面的命令安装插件:

» npm i react-custom-properties -D

我们可以这样来使用该插件的功能,覆盖Button组件默认的CSS自定义的属性值。使用该插件会涉及到CSS自定义属性作用域的问题(因为CSS自定义属性是有全局和局部作用域名的区分)。

上图来自于@una的《Locally Scoped CSS Variables: What, How, and Why》一文,详细介绍了CSS自定义属性作用域相关的知识。

在上面的示例基础上,根据**react-custom-properties**文档所描述的使用方式来引用Button组件:

// App.js
import CustomProperties from 'react-custom-properties';

<CustomProperties properties={buttonThemes.success}>
    <Button text="Success" />
</CustomProperties>

编译出来,并没有得到我们所期望的Success按钮:

如果了解CSS自定义属性的同学不难理解,出现上述的原因是CSS自定义属性作用域的影响。如果想要达到我们想要的Success按钮风格,或者说CustomPropertiesproperties生成的自定义属性能用于Button组件中,就需要对Button组件的样式做一个调整,即:

// Button.css
.button__coustom {
    /* --backgroundColor: #fff;
    --borderColor: #ccc;
    --borderRadius: 5px;
    --fontSize: 16px;
    --color: #333;
    --padding: 5px 10px; */

    display: inline-flex;
    justify-content: center;
    align-items: center;
    background: var(--backgroundColor);
    border: 1px solid var(--borderColor);
    color: var(--color);
    font-size: var(--fontSize);
    padding: var(--padding);
    border-radius: var(--borderRadius);
}

为了不影响前面示例的代码,我在这里重新创建一了个组件,叫作ButtonCustomProperty,唯独不同的就是引用的Button.css做了相应的调整,把.button显式声明的自定义属性去注释掉了(删除),只通过var()函数来引用按钮组件所需要的自定义属性,这样一来,他可以引用他任一级别父元素的自定义属性。

这个时候可以像下面这样引用新创建的ButtonCustomProperty组件:

<CustomProperties properties={buttonThemes.success}>
    <ButtonCustomProperty text="Success" />
</CustomProperties>

此时得到了想要的Success按钮:

从编译出来的结果我们可以看到,该插件会给组件外套一个div容器,如果你对代码有洁癖的话,估计会有点忍受不了。这里暂且不对这方面做过多的讨论和阐述。

react-custom-properties插件还可以在<CustomProperties>调用声明的自定义属性的时候带上global标识符。

<CustomProperties global properties={buttonThemes.warning}>
    <ButtonCustomProperty text="Warning" />
</CustomProperties>

带上global标识符之后,编译出来的CSS自定义属性就会是全局的,放置在<html>元素的style属性中,能作用于任何元素上(只要通过var()调用了buttonThemes.warning中声明的自定义属性)。比如上面的代码编译之后,在浏览器上就可以看到Warning按钮的效果:

使用global标识符之后,不会给组件添加额外的容器,但生成的CSS自定义属性是全局的,可以作用于任何引用相同自定义属性的元素上:

这个时候你可能会感到纳闷,为什么没有影响到别的按钮呢?那是因为在这个示例中,按钮都有着自己作用域的自定义属性,权重大于全局作用域名。如果把局部作用域名的干掉,就会看到全局作用域的自定义属性对其影响。我们可以在浏览器中尝试着删除某个按钮的自身作用域中的CSS自定义属性:

有关于在行内样式中添加CSS自定义属性更多的介绍,还可以阅读下面几篇文章:

CSS自定义属性在CSS Modules中的应用

接着我们再来看看CSS自定义属性在CSS Modules中的使用。

如果你想了解CSS Modules在React中的使用感兴趣的话,可以阅读前段时间整理的文章《React中CSS Modules的使用》。

示例的项目是由Create React App创建的,使用该CLI构建的React项目如果需要使用CSS Modules的话,有两种方式:

  • 创建的.css文件需要[name].module.css来命名样式文件
  • 使用npm run eject,重新开启有关于CSS Modules相关配置

接下来,咱们使用第一种方式来开启React中的CSS Modules的使用。同样拿Button组件来举例。首先创建一个ButtonModules组件,同时创建ButtonModules.module.css

// ButtonModules.js
import React from 'react';
import styles from './ButtonModules.module.css';

function ButtonModules(props) {
    const { text , className , ...rest } = props;
    return (
        <div className={`${styles.button} ${className}`} {...rest}>{text}</div>
    );
}

export default ButtonModules;

// ButtonModules.module.css
.button {
    --backgroundColor: #fff;
    --borderColor: #ccc;
    --borderRadius: 5px;
    --fontSize: 16px;
    --color: #333;
    --padding: 5px 10px;

    display: inline-flex;
    justify-content: center;
    align-items: center;
    background: var(--backgroundColor);
    border: 1px solid var(--borderColor);
    color: var(--color);
    font-size: var(--fontSize);
    padding: var(--padding);
    border-radius: var(--borderRadius);

    margin: 5px;
}

同时构建一个ModulesDemo组件和对应的样式文件ModulesDemo.module.css

// ModulesDemo.js
import React from 'react';
import ButtonModules from './ButtonModules';
import styles from './ModulesDemo.module.css';

function ModulesDemo() {
    return (
        <div className="wrapper">
            <ButtonModules text="Default" />
            <ButtonModules text="Primary" className={styles.primary} />
        </div>
    )
}

export default ModulesDemo;

// ModulesDemo.module.css
.primary {
    --backgroundColor: #007bff;
    --borderColor: #007bff;
    --color: #fff;
    --padding: .375rem .75rem;
    --borderRadius: .25rem;
}

编译出来的结果和我们期待的一样:

现来尝试另一个场景,比如在App.js中引用CSS Modules构建的组件,然后看是否可以覆盖CSS Modules构建的组件:

// App.js
import ButtonModules from './ButtonModules';

<ButtonModules text="Danger" style={buttonThemes.danger} />
<ButtonModules text="Primary" className="primary" />

编译出来的结果如下:

在CSS Modules中使用CSS自定义属性,我们可以很好的通过Inline Style或外链样式文件来覆盖组件中的样式。

如果你对CSS自定义属性在CSS Modules中的相关介绍感兴趣的话,可以继续阅读@julesforrest的《A refactor with CSS variables》一文。该文详细介绍了CSS自定义属性在CSS Modules中怎么给页面级别更换主题。

CSS 自定义属性在Styled-Components中的应用

从《React中编写CSS的姿势》一文中我们可以了解到Styled-Components是CSS-in-JS众多优秀库中的一个。和前面一样,同样拿Button来举例。

// StyledComponentsButton.js
import React from 'react';
import styled from 'styled-components'

function StyledComponentsButton(props) {
    const { text , className , ...rest } = props;

    const Button = styled.div`
        --backgroundColor: #fff;
        --borderColor: #ccc;
        --borderRadius: 5px;
        --fontSize: 16px;
        --color: #333;
        --padding: 5px 10px;

        display: inline-flex;
        justify-content: center;
        align-items: center;
        background: var(--backgroundColor);
        border: 1px solid var(--borderColor);
        color: var(--color);
        font-size: var(--fontSize);
        padding: var(--padding);
        border-radius: var(--borderRadius);
    `
    return (
        <Button className={`${className}`} {...rest}>{ text }</Button>
    );
}

export default StyledComponentsButton;

// App.js
import StyledComponentsButton from './StyledComponentsButton';

<StyledComponentsButton text="Default" />

这个时候可以看到StyledComponentsButton组件中创建的按钮:

在默认按钮上的基础上按照前面的使用方式来构建Primary按钮(引用App.css中的.primary类名):

<StyledComponentsButton text="Primary" className="primary" />

从上图的效果上可以看出Styled-Components生成的样式权重大于引用App.css.primary。至于为什么?我也没有查到原因,权当是该插件的工作机制引起的吧。如果您知道之方面的原因,欢迎在下面的评论中指正。如果实在找不到解决的方案,可以尝试着改变CSS自定义属性作用域的方式来解决。正如前面CSS Modules中的示例一样。

尝试着把StyledComponentsButton中默认按钮声明的自定义属性注释掉:

// StyledComponentsButton.js

const Button = styled.div`
    // --backgroundColor: #fff;
    // --borderColor: #ccc;
    // --borderRadius: 5px;
    // --fontSize: 16px;
    // --color: #333;
    // --padding: 5px 10px;

    display: inline-flex;
    justify-content: center;
    align-items: center;
    background: var(--backgroundColor);
    border: 1px solid var(--borderColor);
    color: var(--color);
    font-size: var(--fontSize);
    padding: var(--padding);
    border-radius: var(--borderRadius);
`

这样一来Primary有样式风格了,但默认按钮又丢失了部分样式,其实我们可以在引用自定义属性的基础上做一些调整。CSS自定义属性中var()函数还有另一个功能,就是通过var()第二个参数,给自定义属性设置一个备用样式:

// StyledComponentsButton.js

const Button = styled.div`
    display: inline-flex;
    justify-content: center;
    align-items: center;
    background: var(--backgroundColor, #fff);
    border: 1px solid var(--borderColor, #ccc);
    color: var(--color, #333);
    font-size: var(--fontSize, 16px);
    padding: var(--padding, 5px 10px);
    border-radius: var(--borderRadius, 5px);
`

这样一来,就达到我们预期想要的效果:

使用Inline Style同样也可以轻意的让按钮样式生效:

<StyledComponentsButton text="Danger" style={buttonThemes.danger} />

另外在任何引用Styled-Components的地方还可以像下面这样使用StyledComponentsButton组件。比如在App.js中像下面这样使用:

// App.js
import styled from 'styled-components'

const SuccessButton = styled(StyledComponentsButton)`
    --color: #fff;
    --backgroundColor: #28a745;
    --borderColor: #28a745;
`
<SuccessButton text="Success" />

效果如下:

上面看到的仅是CSS Modules中Styled-Components中的使用。其实Styled-Components中还有一些强大的功能,比如说通过<ThemeProvider>来支持主题模式,只要是被<ThemeProvider>包起来的组件,不管多深,都可以通过props访问到theme。有关于这方面更多的介绍可以阅读下面的相关教程:

CSS自定义属性实现Tooltips组件的可配置化

在《提示框组件的实现给我带来的思考和探索》末尾处留了一个问题:“CSS自定义属性如何实现Tooltips可配置化”?

实现Tooltips和前面实现Button组件思路是一样的,只不过提供的CSS自定义属性可配置项更多一些。

注意:上面的示例没有使用clip-path来做,而是采用别的方式来实现的

整个Tooltips其实就是两个部分,一个是Tooltips自身,另外一个就是三角形,在该示例中,三角形是通过矩形旋转而来,所以为了让三角形在样式配置上上更为灵活,我采用了两个额外的标签,这样整体的结构如下:

<!-- HTML -->
<div class="tooltips">
    <div class="arrow">
        <div class="arrow-inner"></div>
    </div>
    <div class="tooltips-content">我是一个提示框</div>
</div>

样式上也较为简单,拿默认项配置来举例(三角形朝下):

.tooltips {
    display: flex;
    justify-content: center;
    align-items: center;
    position: relative;
    
    
    // CSS Custom Property for Tooltips
    --padding: 10px;
    --borderRadius: 5px;
    --borderWidth: 0px;
    --color: #333;
    --backgroundColor: #fff;
    --borderColor: #fff;
    --triangleHeight: 10px;
    --triangleColor: var(--backgroundColor);
    --triangleBorderColor: var(--borderColor);
    --triangleBorderWidth: var(--borderWidth);
    --position: 10px;
    
    background-color: var(--backgroundColor);
    color: var(--color);
    padding: var(--padding);
    border-radius: var(--borderRadius);
    border: var(--borderWidth) solid var(--borderColor);
    
    .arrow {
        position: absolute;
        top: 100%;
        overflow: hidden;
        
        width: calc(var(--triangleHeight) * 1.41);
        height: var(--triangleHeight);
        left: var(--position); 
    }
    
    .arrow-inner {
        width: 100%;
        height: 100%;
        transform: rotate(315deg);
        transform-origin: left top;
        
        background-color: var(--triangleColor);
        border: var(--triangleBorderWidth) solid var(--triangleBorderColor);
    }
}

提供的配置项有:

--padding: 10px;                            // Tooltips内距,可以用来控制Box大小,另外可以额外添加width、height属性来指定Tooltip大小
--borderRadius: 5px;                        // Tooltips圆角,默认四个角半么相等
--borderWidth: 0px;                         // Tooltips边框粗细
--color: #333;                              // Tooltips文本颜色
--backgroundColor: #fff;                    // Tooltips背景颜色
--borderColor: #fff;                        // Tooltips 边框颜色
--triangleHeight: 10px;                     // Tooltips三角形高度,宽度是高度的1.41倍,如果朝向是左右,宽高对掉
--triangleColor: var(--backgroundColor);    // Tooltips三角形颜色,和Tooltips背景颜色相同
--triangleBorderColor: var(--borderColor);  // Tooltips三角形边框颜色,和Tooltips边框颜色相同
--triangleBorderWidth: var(--borderWidth);  // Tooltips三角形边框粗细,和Tooltips边框粗细相同
--position: 10px;                           // Tooltips三角形位置,上下方向由left控制,左右方向由top控制 

也就是说,修改上面任何一个参数的值,就可以改变Tooltips的样式风格。在该基础上借助一点点JavaScript代码就可以实现一个可配置化的Tooltips(一个小工具)。

我们再花点时间来看看CSS自定义属性和React结合起来构建一个Tooltips组件:

// Tooltips.js
import React from 'react';
import './Tooltips.css';

function Tooltips(props) {
    const { text , className, direction, ...rest } = props;

    if (direction === 'top') {
        var arrowDirection= {
            bottom: '100%',
            top: 'auto' 
        }

        var arrowDirectionInner = {
            transform: 'rotate(45deg)',
            transformOrigin: 'left bottom'
        } 
    } else if (direction === 'bottom') {
        var arrowDirection= {
            top: '100%',
        }

        var arrowDirectionInner = {
            transform: 'rotate(315deg)',
            transformOrigin: 'left top'
        } 
    } else if (direction === 'left') {
        var arrowDirection= {
            top: 'var(--position, 10px)',
            right: '100%',
            left: 'auto',
            width: 'var(--triangleHeight, 10px)',
            height: 'calc(var(--triangleHeight, 10px) * 1.41)'
        }

        var arrowDirectionInner = {
            transform: 'rotate(315deg)',
            transformOrigin: 'right top'
        } 
    } else if (direction === 'right') {
        var arrowDirection= {
            top: 'var(--position, 10px)',
            left: '100%',
            width: 'var(--triangleHeight, 10px)',
            height: 'calc(var(--triangleHeight, 10px) * 1.41)'
        }

        var arrowDirectionInner = {
            transform: 'rotate(315deg)',
            transformOrigin: 'left bottom'
        } 
    }


    return (
        <div className={`tooltips ${className}`} {...rest} data-direction={direction}>
            <div className="tooltips__arrow" style={arrowDirection}>
                <div className="tooltips__arrow--inner" style={arrowDirectionInner}/>
            </div>
            <div className="tooltips-content">{text}</div>
        </div>
    );
}

export default Tooltips;

// Tooltips.css
.tooltips{
    display: flex;
    justify-content: center;
    align-items: center;
    position: relative;

    /* CSS Custom Property for Tooltips */

    /* --padding: 10px;
    --borderRadius: 5px;
    --borderWidth: 0px;
    --color: #333;
    --backgroundColor: #fff;
    --borderColor: #fff;

    --triangleHeight: 10px;
    --position: 10px; */

    --triangleColor: var(--backgroundColor, #fff);
    --triangleBorderColor: var(--borderColor, #fff);
    --triangleBorderWidth: var(--borderWidth, 0px);

    background-color: var(--backgroundColor, #fff);
    color: var(--color, #333);
    padding: var(--padding, 10px);
    border-radius: var(--borderRadius, 5px);
    border: var(--borderWidth, 0px) solid var(--borderColor, #fff);
}

.tooltips__arrow {
    position: absolute;
    top: 100%;
    overflow: hidden;

    width: calc(var(--triangleHeight, 10px) * 1.41);
    height: var(--triangleHeight, 10px);
    left: var(--position, 10px); 
}

.tooltips__arrow--inner {
    width: 100%;
    height: 100%;
    transform: rotate(315deg);
    transform-origin: left top;

    background-color: var(--triangleColor, #fff);
    border: var(--triangleBorderWidth, 0px) solid var(--triangleBorderColor, #fff);
}

在调用Tooltips组件时,可以根据自己对UI风格的需求来调整相应的CSS自定义属性的值,比如:

// App.js
<Tooltips text="This is Tooltips" className="tooltips__home"/>
<Tooltips direction="top" text="This is Tooltips" className="tooltips__primary" />
<Tooltips direction="left" text="This is Tooltips" className="tooltips__danger" style={buttonThemes.danger}/>
<Tooltips direction="right" text="This is Tooltips" className="tooltips__success" style={buttonThemes.success} />

将看到的效果如下:

示例代码写得不是很好,希望React大神路过能拍正。

小结

CSS自定义属性很强大,在很多地方都可以通过CSS自定义属性。正如上文所演示的,CSS自定义属性给组件设计带来的改变,改善和提高了组件中CSS的编写、维护和扩展的能力。另外也提高了组件使用的灵活性。如果你有这方面使用的经验,欢迎在下面的评论中与我们一起分享。