一、 组件化css需要的特点
- 可以编写局部css:css具备自己的具备作用域,不会随意污染其他组件内的原生;
- 可以编写动态的css:可以获取当前组件的一些状态,根据状态的变化生成不同的css样式;
- 支持所有的css特性:伪类、动画、媒体查询等;
- 编写起来简洁方便、最好符合一贯的css风格特点;
- 等等...
二、 React中的CSS
事实上,css一直是React的痛点,也是被很多开发者吐槽、诟病的一个点
在这一点上,Vue做的要确实要好于React:
- Vue通过在.vue文件中编写
<style><style>标签来编写自己的样式 - 通过是否添加 scoped 属性来决定编写的样式是全局有效还是局部有效
- 通过
lang属性来设置你喜欢的less、sass等预处理器 - 通过内联样式风格的方式来根据最新状态设置和改变css;
- 等等...
Vue在CSS上虽然不能称之为完美,但是已经足够简洁、自然、方便了,至少统一的样式风格不会出现多个开发人员、多个项目 采用不一样的样式风格。
相比而言,React官方并没有给出在React中统一的样式风格:
- 由此,从普通的css,到css modules,再到css in js,有几十种不同的解决方案,上百个不同的库;
- 大家一致在寻找最好的或者说最适合自己的CSS方案,但是到目前为止也没有统一的方案;
2.1 内联样式
内联样式是官方推荐的一种css样式的写法:
- style 接受一个采用小驼峰命名属性的 JavaScript 对象,,而不是 CSS 字符串;
- 并且可以引用state中的状态来设置相关的样式;
import React, { PureComponent } from 'react'
export default class App extends PureComponent {
constructor(props) {
super(props)
this.state = {
pStyle: {
color: 'purple',
marginTop: '10px'
}
}
}
render() {
return (
<div>
{/*
这里可以传递一个对象来表示内联样式
注意: 其key值只可以接受小驼峰
其value值必须为字符串
*/}
<h3 style={{ fontSize: '30px', color: 'red' }} >
这是h3标签
</h3>
{/*
使用内联样式的写法就可以去state中读取变量作为p标签的内联样式
*/}
<p style={ this.state.pStyle }>
这是一个p标签
</p>
</div>
)
}
}
内联样式的优点:
- 内联样式, 样式之间不会有冲突
- 可以动态获取当前state中的状态
内联样式的缺点:
- 写法上都需要使用驼峰标识
- 某些样式没有提示
- 大量的样式, 代码混乱
- 某些样式无法编写(比如伪类/伪元素)
所以官方依然是希望内联合适和普通的css来结合编写
2.2 原生css
其中
Home是组件,其下index.js是入口文件,style.js是样式文件
App.js
import React, { PureComponent, Fragment } from 'react'
// 在引入的时候,直接引入文件夹即可
// 因为webpack在进行打包的时候,会默认将文件夹下的index.js作为默认的入口文件
import Home from './Home'
export default class App extends PureComponent {
render() {
return (
<Fragment>
{/*
和vue不同的是,react的组件是只要引入以后就可以直接使用
不需要使用componets来进行声明
*/}
<Home />
</Fragment>
)
}
}
index.js
import React, { PureComponent, Fragment } from 'react'
// 样式文件需要先引入以后,才可以继续使用
import './style.css'
class Home extends PureComponent {
render() {
return (
<Fragment>
<h3 className="title">这是Home组件</h3>
<article className="content">这是Home组件中的内容</article>
</Fragment>
)
}
}
export default Home
style.css
.title {
color: red
}
.content {
color: skyblue;
}
存在的问题
-
样式会被覆盖
import React, { PureComponent, Fragment } from 'react' import Home from './Home' import Profile from './Profile' export default class App extends PureComponent { render() { return ( <Fragment> <Home /> <Profile /> </Fragment> ) } }此时 在
Home组件的后边加上一个新的组件Profile,在
profile组件中的style.css的内容如下.title { color: pink; } .content { color: green; }此时,界面的效果如下
可以看到
profile中的样式会Home中的样式给覆盖-
尝试解决方案:
#Profile .title { color: pink; } #Profile > .content { color: green; }给每一个样式前面加上唯一标识,或者使用直接子代选择器找到唯一的那个元素设置对应的样式
但是 这样就会导致样式的书写变得极其复杂,
而且这样编写很有可能会导致因为样式的权重问题导致样式的覆盖(例如
#Profile .title的权重比.Profile .title高),不利于维护
-
总结:
普通的css我们通常会编写到一个单独的文件,之后再进行引入
这样的编写方式和普通的网页开发中编写方式是一致的:
- 如果我们按照普通的网页标准去编写,那么也不会有太大的问题;
- 但是组件化开发中我们总是希望组件是一个独立的模块,即便是样式也只是在自己内部生效,不会相互影响;
- 但是普通的css都属于全局的css,样式之间会相互影响;
2.3 css module
-
css modules并不是React特有的解决方案,而是所有使用了类似于webpack配置的环境下都可以使用的。
- 但是,如果在其他项目中使用个,那么我们需要自己来进行配置,比如配置webpack.config.js中的modules: true等
-
React的脚手架已经内置了css modules的配置:
- .css/.less/.scss 等样式文件都修改成 .module.css/.module.less/.module.scss 等;
- 之后就可以引用并且进行使用了;
import React, { PureComponent, Fragment } from 'react'
// 使用这种方式的话,其是将样式文件进行引入,
// 使webpack在进行打包编译的时候,可以自动将这个文件进行编译
// 但是因为我们并没有定义变量去指向这个引入的模块,
// 所以我们是无法使用它的
// import './style.css'
// 将原本的css文件改名为module.css 书写方式依旧和之前的css的书写方式一致
import style from './style.module.css'
class Home extends PureComponent {
render() {
return (
<Fragment>
<h3 className={ style.title }>这是Home组件</h3>
<article className={ style.content }>这是Home组件中的内容</article>
</Fragment>
)
}
}
export default Home
可以看到在编译后,其样式为
style_样式名_唯一标识,这样所有的样式就是唯一的,不会重复的了
但是这种方案也有自己的缺陷:
- 引用的类名,不能使用连接符(.home-title),在JavaScript中是不识别的;
- 所有的className都必须使用{style.className} 的形式来编写;
- 不方便动态来修改某些样式,依然需要使用内联样式的方式;
{/*
如果需要在css in module中使用state中的状态
那么就必须将className和style进行组合使用
这一点和vue是很像的
*/}
<h3 className={ style.title } style={{ color: this.state.color }}>这是Home组件</h3>
2.4 styled-components
CSS in JS
实际上,官方文档也有提到过CSS in JS这种方案:
- “CSS-in-JS” 是指一种模式,其中 CSS 由 JavaScript 生成而不是在外部文件中定义;
- 注意此功能并不是 React 的一部分,而是由第三方库提供。 React 对样式如何定义并没有明确态度;
在传统的前端开发中,我们通常会将结构(HTML)、样式(CSS)、逻辑(JavaScript)进行分离。
- 但是在前面的学习中,我们就提到过,React的思想中认为逻辑本身和UI是无法分离的,所以才会有了JSX的语法。
- 样式呢?样式也是属于UI的一部分;
- 事实上CSS-in-JS的模式就是一种将样式(CSS)也写入到JavaScript中的方式,并且可以方便的使用JavaScript的状态;
- 所以React有被人称之为
All in JS;
优点
- CSS-in-JS通过JavaScript来为CSS赋予一些能力,包括类似于CSS预处理器一样的样式嵌套、函数定义、逻辑复用、动态修 改状态等等;
- 依然CSS预处理器也具备某些能力,但是获取动态状态依然是一个不好处理的点;
- 所以,目前可以说CSS-in-JS是React编写CSS最为受欢迎的一种解决方案;
常见的css in js的库
- styled-components
- emotion
- glamorous
目前可以说styled-components依然是社区最流行的CSS-in-JS库,所以我们以styled-components进行练习
模板字符串
模板字符串还有另外一种用法:标签模板字符串(Tagged Template Literals)
正常情况下,我们都是通过 函数名() 方式来进行调用的,其实函数还有另外 一种调用方式:
function printStr(...args) {
console.log(args)
}
printStr`Tagged Template Literals`
const name = 'Klaus'
const age = 23
function printStr(...args) {
console.log(args)
}
printStr`My name is ${name}, age is ${age}`
-
模板字符串被拆分了;
-
第一个元素是数组,是被模块字符串拆分的字符串组合;
-
后面的元素是一个个模块字符串传入的内容
在styled component中,就是通过这种方式来解析模块字符串,最终生成 我们想要的样式的
styled-components的本质是通过函数的调 用,最终创建出一个组件:
- 这个组件会被自动添加上一个不重复的 class;
- styled-components会给该class添加相 关的样式
import React, { PureComponent } from 'react'
import styled from 'styled-components'
// 这里会生成一个div标签包裹的组件
// 在这个组件中会自动添加上唯一的标识类名
// 在styled组件中设置样式的时候,每一行的样式结尾的分号是不可以省略的
const HomeWrapper = styled.div`
color: red;
font-size: 20px;
`
class Home extends PureComponent {
constructor(props) {
super(props)
this.state = {
color: 'skyblue'
}
}
render() {
return (
<HomeWrapper>
{/*
使用这个生成的组件进包裹后
其下的子组件就可以继承并使用其对应的样式
*/}
<h3>这是Home组件</h3>
<article>这是Home组件中的内容</article>
</HomeWrapper>
)
}
}
export default Home
在styled-components中可以书写样式
注意:其本质是调用js函数,生成组件,所以如果引出到一个单独的文件中的话,其是js文件,不是css文件
const HomeWrapper = styled.div`
color: red;
font-size: 20px;
h3 {
color: skyblue;
&:hover {
color: green;
}
}
article {
&::after {
content: 'after'
}
}
`
styled-components的几大特性
- props穿透
import React, { PureComponent, Fragment } from 'react'
import styled from 'styled-components'
const XFInput = styled.input`
background-color: lightgray;
`
export default class Home extends PureComponent {
render() {
return (
<Fragment>
{/*
因为XFInput最后会被转换为带有样式的input标签
所以这个type虽然设置在XFInput上,但是最后依旧会被添加在最后的input标签上
这种就被叫做 props穿透
*/}
<XFInput type="password" />
</Fragment>
)
}
}
- attrs的基本使用
import React, { PureComponent, Fragment } from 'react'
import styled from 'styled-components'
/*
attrs是一个函数,其参数是一个对象
其返回值依旧是一个函数,所以可以继续使用标签模板字符串来进行调用
也就是链式调用
在写样式的时候, 其值可以传递一个箭头函数
有一个参数props,就是attrs中的配置对象
*/
const XFInput = styled.input.attrs({
color: 'lightgray'
})`
background-color: ${ props => props.color };
`
export default class Home extends PureComponent {
render() {
return (
<Fragment>
<XFInput type="password" />
</Fragment>
)
}
}
-
使用state作为props传递
import React, { PureComponent, Fragment } from 'react' import styled from 'styled-components' /* 这里的props的值来源有2个 1. attrs函数中的配置对象 2. 组件上传递过来的props tips: 如果组件上传递过来的props和attrs中的配置对象上的属性名重合 attrs中的对应属性的属性值会将传入的props中的属性值给覆盖 */ const XFInput = styled.input.attrs({ bgColor: 'lightgray' })` background-color: ${ props => props.bgColor }; color: ${ props => props.color } ` export default class Home extends PureComponent { constructor(props) { super(props) this.state = { color: 'red' } } render() { return ( <Fragment> {/* 将state中样式值传递给 scoped-compoents 写在这里的样式属性一般都是动态的,会发生改变的 写在attrs中的属性一般是固定的,不会发生改变的 */} <XFInput type="password" color={ this.state.color } /> </Fragment> ) } }
结合上述特性,就可以将样式动态的设置给react,并进行渲染
高阶使用
- 继承
import React, { PureComponent, Fragment } from 'react'
import styled from 'styled-components'
const WXFButton = styled.button`
padding: 8px 30px;
border-radius: 5px;
`
/*
在styled函数中传入一个参数,表示这个styled组件中的样式值
继承自传入的哪一个styled组件中的相关样式
tips: 继承之后就不需要在写button了
*/
const WXFPrimaryButton = styled(WXFButton)`
color: #fff;
background-color: skyblue;
`
export default class Home extends PureComponent {
render() {
return (
<Fragment>
<WXFPrimaryButton>
primary-button
</WXFPrimaryButton>
</Fragment>
)
}
}
- theme的使用(设置需要全局使用或需要共享的全局样式属性值)
app.js
import React, { PureComponent } from 'react'
import { ThemeProvider } from 'styled-components'
import Home from './Home'
import Profile from './Profile'
export default class App extends PureComponent {
render() {
return (
<ThemeProvider theme={{ color: 'skyblue', fontSize: '30px' }}>
<Home />
<Profile />
</ThemeProvider>
)
}
}
Home.js
import React, { PureComponent, Fragment } from 'react'
import styled from 'styled-components'
const XFSpan = styled.span`
color: ${ props => props.theme.color };
font-size: ${ props => props.theme.fontSize };
`
export default class Home extends PureComponent {
render() {
return (
<Fragment>
<XFSpan>
这是使用了theme的styled组件
</XFSpan>
</Fragment>
)
}
}
2.5 多类名共存
vue中添加class是一件非常简单的事情:
对象语法
数组语法
数组和对象共存
React在JSX给了我们开发者足够多的灵活性,你可以像编写JavaScript代码一样,通过一些逻辑来决定是否添加某些class:
React原生的样式写法
<h1 className="foo bar active">这是H1标题</h1>
{/* foo后边必须要有一个空格 表示多个类之间的区分 */}
{/*
后面的三目运算符必须加上()以表示他们是一个整体,
如果没有加上,那么在编译的时候foo会被丢失
最后编译出来的样式为'active' 是不包含'foo'的
*/}
<h1 className={'foo ' + (isActive ? 'active' : '')}>这是H1标题</h1>
{/*
这里的逻辑运算符外也必须包上括号,
如果没有包含的话,编译出来的样式依旧是没有foo的
*/}
<h1 className={'foo ' + (isActive && 'active')}>这是H1标题</h1>
<h1 className={ ['foo', (isActive ? 'active' : '')].join(' ') }>这是H1标题</h1>
使用第三方样式库(classnames)来帮助我们书写React样式
<h1 className={className('foo', 'bar', 'active')}>这是H1标题</h1>
<h1 className={className({'active': isActive }, 'foo')}>这是H1标题</h1>
{/*
所有的值会被转换为false的值,都不会被渲染
例如: null, undefined, 0
*/}
<h1 className={className({'active': isActive }, 'foo', null, undefined)}>这是H1标题</h1>
{/* 但是这里的10 会被转换为字符串10 作为一个有效的类名存在 */}
<h1 className={className({'active': isActive }, 'foo', 10)}>这是H1标题</h1>
<h1 className={className(['foo', 'active'])}>这是H1标题</h1>
<h1 className={className(['foo', {'active': isActive }])}>这是H1标题</h1>
上一篇高阶组件和组件补充