前端组件化开发中的CSS
在目前整个前端都使用组件化开发的模式下,CSS样式的编写就成为了一个问题。因为CSS也叫做层叠样式表,意思就是多个css样式作用于同一个HTML元素的时候,浏览器会根据权重的大小来进行覆盖,为元素应用权重最高的那一组css样式,很明显这种特性不适合组件化开发。
组件化开发模式下对于CSS解决方案的要求
- 支持编写局部的css,css具备自己的局部作用域,不会污染其他组件中的元素。
- 支持编写动态的css,也就是元素的某些样式可以根据state/data中的某个属性来动态改变,其实也就是js去控制元素的css样式。当属性的值变化的时候,样式也发生变化。
- 支持所有的css新特性,比如伪类,位元素,动画,过渡,转化等等
- 编写方式简洁易上手,学习成本低,最好符合一贯的css风格特点
React中的CSS缺陷
相比于React,同为前端框架的Vue在css样式编写上要做的比React好,比如:
- Vue通过.vue文件中的style标签来编写属于当前组件的样式,高度样式行为相分离
- scoped属性用于防止当前组件的样式污染其他组件样式
- lang属性用于设置css预处理器如less,sass,stylus
- 通过:style的方法将data中的属性和样式连接起来,实现样式动态变化。
一般实现样式动态变化的方案:
- 动态为一个元素添加clss类名
- 通过style内联样式,将js中属性值和css样式联合起来
React官方一致没有给出在React中统一的风格样式,普通的css,css modules以及css in js,很多种方案带来了上百种不同的库,到目前为止没有统一的方案。
方案一:使用style标签内联样式
React官方推荐我们使用style标签内联样式这种写法来进行组件样式的编写,规定style标签接收一个采用小驼峰命名属性的js对象,而不是css字符串。通过这种方式写的样式会将样式添加到元素的内联样式上。
优点:
基于内联样式书写的样式肯定不会导致样式冲突
可以动态获取state中的状态来完成动态样式
缺点:
采用小驼峰写法,有的css书写没有提示易错
在JSX中写大量的style样式,比较混乱
伪类,伪元素这种样式无法通过内联样式编写
class App extends PureComponent{
constructor(props) {
super(props);
/* 动态改变元素样式 */
this.state = {
textColor:"pink"
}
}
render(){
/* 将样式抽取到一个变量中 */
const h2Style={
fontSize:"18px",
color:"red"
}
return(
<div>
<h2 style={h2Style}>这是一个App组件</h2>
<p style={{fontSize:"18px",color:"red"}}>这是一段文字</p>
<div style={{color:this.state.textColor}}>这是一段动态变化的文字</div>
</div>
)
}
}
方案二:使用普通CSS写法
这种方案和传统的在网页中进行开发时的编写方式是一致的,传统的网页开发编写css的优点它有,对应的缺点它也同样存在。
通常是新建一个和组件一一对应的.css文件,然后给组件最外层的div元素一个className。在.css文件中编写对应的样式文件,然后在组件.js文件中导入该样式文件即可将样式应用到组件中对应的元素标签上。
优点
编写规范简单,不需要用小驼峰这种不熟悉的语法去写
缺点(主要就是样式的层叠覆盖,这种方法写的css都是全局作用域的)
每次都要在最外层增加一个className,避免样式冲突
每次在编写样式都要先写一个.className
就算写了还是有可能会冲突,比如其他组件中有权重更高的选择器
使用直接子代选择器可以避免 但是复杂度就太麻烦了
.app .title{
font-size: 32px;
color: red;
font-weight: bold;
}
import "./index.css"
class App extends PureComponent{
render(){
return(
<div className="app">
<h2 className="title">这是一个App组件</h2>
</div>
)
}
}
方案三:CSS modules
css modules是一种在使用了类似于webpack配置的开发环境下都可以使用的css解决方案,主要用于解决相互独立的组件的样式互相冲突和覆盖的问题。
在Vue项目中,我们需要自己手动在webpack.config.js中进行配置;而React中基于脚手架搭建的项目已经帮助我们内置了css modules的配置。
使用方法(假设为Home组件添加css样式)
- 新建home.module.css/less/scss文件,然后将样式写在该文件中
- 在home.js中通过模块化的方式导入,因为添加了module的css文件会被当作一个大的js对象导入,所以这里需要使用一个标识符去接收,比如homeStyles
- 在jsx中使用className={homeStyles.title}这种方式为元素添加样式,这里的本质是将homeStyles对象中的title属性取出来,对应的属性值就是一个经过编译后的唯一的class类名,然后将这个类名下的样式应用到元素上。
.title{
font-size: 32px;
color: red;
font-weight: bold;
}
.banner {
font-size: 28px;
color:pink;
font-weight: bold;
}
import appStyles from "./index.module.css";
class App extends PureComponent{
render(){
const {title,banner} = appStyles; /* 解构赋值*/
return(
<div>
<h2 className={title}>这是一个title</h2>
<p className={banner}>这是一个banner</p>
</div>
)
}
}
使用原理
打印appStyles对象,如下:
每一个类名都会当作一个属性,属性值为"当前css文件所在文件名_类名_随机唯一值"
类名唯一,所以样式不会冲突,也是唯一的。
如果文件名为index.module.css,那么第一个值是该文件所在的文件夹名称,如"React中的样式方案";
如果文件名不是以index开头的,那么第一个值是该文件本身的名称,如home
{
banner: "React中的样式方案_banner__klcc5"
title: "React中的样式方案_title__26xd3"
banner: "home_banner__klcc5"
}
优点
解决了css中样式冲突的问题,等于让每一个组件中的css样式都有了自己组件作用域
缺点
- 所有JSX中的类名不能使用连接符-,只能使用驼峰写法。如box-title是错误的,而boxTitle是正确的。因为-在js中是不能被识别的。
- 所有的样式都必须采用style.className的形式来编写,只不过这个问题可以用解构赋值来解决
- 不能动态修改元素样式,依然需要使用内联样式的方式。
方案四:CSS in JS【基于第三方库styled-components】
CSS in JS的定义
CSS in JS在React的官方文档上描述为:CSS in JS是一种模式,指的将CSS样式由js生成而不是在外部的样式文件中定义,这个功能不由React提供,需要由第三方库来提供。
在传统的网页开发中提倡结构样式行为相分离,但是React的思想中认为逻辑(js)本身和UI是无法完全分离的,所以才有了JSX语法,一种将逻辑和结构相互结合嵌套的写法。而CSS-in-JS的模式就是将样式CSS代码也写入到js中的方式,并且这种模式的优势在于CSS可以轻松的使用JS中的state状态,正因为此,React才被人们称之为All in JS。
CSS in JS的优势及其第三方库的实现
CSS in JS基于JS提供给CSS的能力,可以实现类似于CSS预处理器的大部分功能,如:
- 样式嵌套【极大程度上避免了样式冲突】
- 伪类和伪元素
- 函数定义
- 动态修改状态【这一点是CSS预处理器无法实现的点】
CSS in JS目前流行的库如下:
- styled-components【社区最流行的库】
- emotion
- glamorous
styled-components库的实现原理————ES6标签模板字符串
ES6标签模板字符串在当做函数调用时的参数的时候,浏览器会按照一种特殊的方式对模板字符串参数进行解析和分隔,如果解析后参数进行打印,那么得到的结果是一个二维数组。
该数组的第一项是分割下来的字符串数组,也是一个数组
该数组的第二项及以后是模板字符串中用${}包裹的变量或者JS表达式
const name = "lilei";
const age = 18;
function test(...args){
console.log(args);
}
test`这是姓名${name},这是年龄${age}`;
/* args数组的打印结果是一个二维数组: */
args = [
0: ["这是姓名",",这是年龄",""],
1: "lilei",
2: 18
]
styled-components库默认导出的对象是什么?
在安装了styled-components库之后,我们在写样式之前需要做两个准备:
- 将库在当前要写的js文件中导入,因为是默认导出,所以我们可以任意起一个标识符styled去接收它导出的对象,打印这个对象:发现这个对象上都是由HTML标签名作为方法名的很多templateFunction方法,而我们写样式也是基于调用这些方法来实现,因为这些方法的返回值都是一个React组件对象,既然是React组件那么就可以在JSX的语法中使用,从而实现样式的注入。
span: ? templateFunction()
div: ? templateFunction()
a: ? templateFunction()
- 习惯使用模板字符串当做函数参数的方式写css代码
调用以上方法时,参数就是模板字符串,而返回值是一个React组件对象,返回的组件对象如下,其中有一项rules就是如何解析模板字符串的规则,而componentId就是为组件元素生成的唯一类名。
styledComponentId: "sc-AxjAm" // 唯一id
target: "div"
componentStyle: {
baseHash: 400283751
componentId: "sc-AxjAm"
isStatic: false
rules: [
0: "\n\twidth:500px;\n\theight:200px;\n\tbackground-color:pink;\n"
]
staticRulesId: ""
}
利用styled-components库在React中实现基本样式编写
-
安装styled-components库
npm install styled-components@5.1.1 --save -
新建同级样式文件styled.js,导入库之后按照模板字符串的语法书写css样式
import styled from "styled-components";
export const HomeWrapper = styled.div`
width:500px;
height:200px;
background-color:pink;
// 结构嵌套
.banner{
font-size:20px;
color:blue;
cursor:pointer;
// 伪元素
&:hover{
color:red;
}
// 伪类元素
&::after{
content:"小尾巴";
}
}
`;
export const H2Wrapper = styled.h2`
font-size:18px;
color:red;
`
- 导入组件的js文件中并进行使用即可
HomeWrapper作用于JSX语法中的时候,此时会生成一个唯一的class类名,这个类名在HomeWrapper组件对象中的styledComponentId属性中进行获取,然后给当前组件的根元素添加这个唯一类名,保证不进行样式冲突。
一般情况下给组件的根元素来一个Wrapper组件包裹就类似于给根元素一个id值一样,后续的子元素都基于嵌套的写法写在里面就可以了;但是由于这里生成的是class类名,所以如果还是不放心怕其他组件的id选择器进行覆盖的话,可以为某些样式再生成一个组件进行替换,确保样式不会覆盖。
import {
HomeWrapper,
H2Wrapper
}from "./styled.js"
class Home extends PureComponent{
render(){
return(
<HomeWrapper>
<H2Wrapper>这是一个title</H2Wrapper>
<p className="banner">这是一个banner</p>
</HomeWrapper>
)
}
}
利用styled-components库在React中实现动态样式编写
主要基于styled-components库中提供的attrs方法以及props属性穿透的特性实现:
- styled.div.attrs(objProps)
styles
该方法接收一个对象类型的参数,对象中的键值对会传入到下面的props中,供样式使用
该方法返回的还是一个函数,所以可以接受模板字符串作为一个参数
attrs方法接收的参数中的对象都可以在写编写样式的时候基于${props=>props.xxx}来进行调用,箭头函数的返回值会作为插值的返回值.
- props的穿透
可以将组件的state属性以及传递给组件的键值对属性都穿透到下面的模板字符串中,方便我们在编写样式的实现动态样式。这一点是任何css预处理器都做不到的。
import styled from "styled-components";
export const StyleInput = styled.input.attrs({
placeholder:"请输入您的姓名",
type:"text",
bgColor:"pink"
})`
font-size:20px;
color:blue;
background-color:${props=>props.bgColor}; /* 使用来自attrs中参数*/
border:${props=>props.bd};/* 使用来自组件state中属性 */
`
import {
StyleInput,
} from ‘./styled.js‘
/* Home组件 */
class Home extends PureComponent{
constructor(props) {
super(props);
this.state = {
borderStyle:"1px solid red"
}
}
render(){
return(
<div>
{/* 将state中的属性borderStyle当做参数穿透到样式中*/}
<StyleInput bd={this.state.borderStyle}/>
</div>
)
}
}
利用styled-components库在React中实现样式继承[样式继承复用]
实现原理:基于styled(FatherCpn)styles
styled方法接收一个经过styled.tag()styles
增强之后的React组件对象作为参数,返回一个新的React组件对象。新的组件对象会继承其父组件的所有样式,如果有自己的样式可以再进行定义。
StylePrimeryButton组件样式继承了StyleButton组件的样式,对于不同的部分再自己进行定义,这一点和实例属性来覆盖父类的原型属性一个道理。
export const StyleButton = styled.button`
width:100px;
height:40px;
color:#6c6c6c;
background-color:#fff;
border:1px solid #eee;
`
export const StylePrimeryButton = styled(StyleButton)`
color:#24a2ff;
background-color:#23272d;
`
class About extends PureComponent{
render(){
return(
<div>
<StyleButton>普通按钮</StyleButton>
<StylePrimeryButton>主要按钮</StylePrimeryButton>
</div>
)
}
}
利用styled-components库在React中实现主题设置[样式共享复用]
从styled-components中导入ThemeProvider这个分享组件,该组件必须传递一个theme属性,属性值就是父组件要共享给每一个子组件的样式,这个样式可以来自于state对象或者props等等。
在编写子组件HomeWrapper的样式的时候,就可以通过${props=>props.theme.xxx}来获取父组件要进行共享的样式,从而达到更高程度的样式复用,减少冗余代码。
export const HomeWrapper = styled.div`
background-color:${props=>props.theme.bgColor};
font-size:${props=>props.theme.lgSize};
`
import {ThemeProvider} from "styled-components";
import {HomeWrapper} from ‘./styled.js‘
class App extends PureComponent{
constructor(){
super();
this.state = {
bgColor:"pink",
lgSize:"40px"
}
}
render(){
return(
<ThemeProvider theme={this.state}>
<Home></Home>
<About></About>
</ThemeProvider>
)
}
}