前端組件化開發中的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>
)
}
}