代码已经关联到github: 链接地址 觉得不错可以顺手点个star,这里会持续分享自己的开发经验(:
我们都知道,JSX
是JS
语法的扩展,增加了对HTML语法的支持,那距离all in js就只差一个CSS语法支持了,目前实现该功能的库比较出名的有60+,感兴趣的可以自己查看:css-in-js。
下面要介绍的就是其中之一的styled-components。
简介
styled-components应该可以说CSS-in-JS最热门的一个库了,到目前为止github的star数已经超过了34k。
通过styled-components,你可以使用ES6的 标签模板字符串 语法在Component中定义一系列CSS属性,当该组件的JS代码被解析执行的时候,styled-components会动态生成一个CSS选择器,并把对应的CSS样式通过style标签的形式插入到head标签里面。
动态生成的CSS选择器会有一小段哈希值来保证全局唯一性来避免样式发生冲突。
Hello World
下面我们简单来了解下其写法:
const Title = styled.h1`
font-size: 1.5em;
text-align: center;
color: palevioletred;
`;
const Wrapper = styled.section`
padding: 4em;
background: papayawhip;
`;
render(<Wrapper>
<Title>Hello World</Title>
</Wrapper>
}
编译后的样子:
其特点是什么?
- 相当与定义了一个空的组件
- 每一段样式必须绑定到一个组件上
- 语法与CSS完全一样
- 模板字符串编写
- …
原理
基于5.3.1
版本代码,下面简单解释其原理。
语法解释
从其写法分析,代码的可以拆解为styled.xx
和后面的模板字符串。
前者可以理解成一个styled-components定义的函数,eg:
const styled = (tag) => {
//...
};
domElements.forEach(domElement => {
styled[domElement] = styled(domElement);
});
那后面的模板字符串,函数是怎么去解析的呢?这就不得不说模板字符串的一个功能带标签的模板字符串,该功能可以用函数解析模板字符串,eg:
function tag(strings,...keys){
console.log('strings:', strings)
console.log('keys:',keys)
}
tag`
color:red;
background:${props => props.bgcolor};
`
//strings: ["\n\tcolor:red;\n\tbackground:", ";\n", raw:["\n\tcolor:red;\n\tbackground:", ";\n"]]
//keys: [props => props.bgcolor]
得到原始字符串和占位符数据,这样就可以组装好css模板,渲染的时候根据传入值渲染即可。
组件创建过程
- 生成唯一id
根据参数和父组件生成,比如上面编译后的sc-jlyJG
。
const styledComponentId = generateId(options.displayName, options.parentComponentId)
- 对模板字符串解析得到样式,在head 中插入一个 style 节点并将样式注入,返回 className,比如之前编译后的
eFyTid
。下面简单描述这一过程:
// 构造className
const generatedClassName = generateName(cssStatic >>> 0);
//构造style节点并获得
const style = document.createElement('style');
style.setAttribute(SC_ATTR, SC_ATTR_ACTIVE);
style.setAttribute(SC_ATTR_VERSION, SC_VERSION);
//...省略上一个style标签位置查找
// Avoid Edge bug where empty style elements don't create sheets
style.appendChild(document.createTextNode(''));
//根据传入的节点获得sheet
this.sheet = style.sheet //getSheet(style)
//最后插入css
this.sheet.insertRule(rule, index);
- 解析组件的其他
props
和attr
得到新的元素参数propsForElement
const computedProps = attrs !== props ? { ...props, ...attrs } : props;
const propsForElement = {};
for (const key in computedProps) {
if (
shouldForwardProp
? shouldForwardProp(key, validAttr, elementToBeCreated)
: isTargetTag
? validAttr(key)
: true
) {
// Don't pass through non HTML tags through to HTML elements
propsForElement[key] = computedProps[key];
}
}
- 最后根据
propsForElement
和className
生成组件
propsForElement.className = Array.prototype
.concat(
styledComponentId,
generatedClassName !== styledComponentId ? generatedClassName : null,
props.className,
attrs.className
)
.filter(Boolean)
.join(' ');
// elementToBeCreated生成过程被省略了,该参数是一个元素标签或者是一个react组件
return createElement(elementToBeCreated, propsForElement);
基础使用指南
基于Props设置样式
styled定义的组件可以接受props
参数,根据不同的参数设置样式。
const Button = styled.button`
background: ${props => props.primary ? "palevioletred" : "white"};
color: ${props => props.primary ? "white" : "palevioletred"};
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid palevioletred;
border-radius: 3px;
`;
render(
<div>
<Button>Normal</Button>
<Button primary>Primary</Button>
</div>
);
继承样式
可以在继承一个已有组件的基础上生成一个新的组件,可以覆盖和新增样式。
const Button = styled.button`
background: white;
color: palevioletred;
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid palevioletred;
border-radius: 3px;
`;
const PrimaryButton = styled(Button)`
background: palevioletred;
color: white;
`
render(
<div>
<Button>Normal</Button>
<PrimaryButton>Primary</Button>
</div>
);
传递HTML属性
我们都知道html元素有很多属性,比如input
的 type、readonly和onChange 等属性。
const Input = styled.input`
padding: 0.5em;
margin: 0.5em;
background: papayawhip;
border: none;
`;
const handleChanged = (e) => {
console.log('input changed:',e.target.value)
}
render(
<Input defaultValue="default val" type="email" onChange={handleChanged}/>
)
组件中维护其他HTML属性
styled-components 同时支持为组件传入 html 元素的其他属性,比如为 input 元素指定一个 type 属性,我们可以使用 attrs
关键字来实现。
使用过程中,我们可以使用静态属性,也可以使用props参数设置:
const Password = styled.input.attrs({
type: 'password',
placeholder:${props=>props.placeholder?props.placeholder:''}
})`
color: palevioletred;
font-size: 1em;
border: 2px solid palevioletred;
border-radius: 3px;
`;
在实际开发中,这个方法还有一个有用处,用来引用第三方类库的 css 样式:
const Input = styled.input.attrs({
className: 'my-input',
})`
color: palevioletred;
font-size: 1em;
border: 2px solid palevioletred;
border-radius: 3px;
`;
与className和css框架搭配使用
可以与组件的className
搭配使用,当然前提className
是不会混淆的命名
const Wrapper = styled.section`
padding: 4em;
background: papayawhip;
& > .my-h1{
font-size: 1.5em;
text-align: center;
color: palevioletred;
}
`;
render(
<Wrapper>
<h1 className={'my-h1'}>My H1</h1>
</Wrapper>
)
同样的也兼容less等css框架使用。
import * as styles from './index.less'
const Wrapper = styled.section`
padding: 4em;
background: papayawhip;
`;
render(
<Wrapper>
<h1 className={styles['my-h1']}>My H1</h1>
</Wrapper>
)
& 关键字
与less
中类似,&
在语法中代表自身选择器,我们上一节就使用了该用法。
const Wrapper = styled.section`
padding: 4em;
background: papayawhip;
& > .my-h1{
font-size: 1.5em;
text-align: center;
color: palevioletred;
}
`;
除了这种用法,他还可以用来增加样式的权重
const Example = styled.li`
color: red;
& {
color:blue;
}
&& {
color: green;
}
`;
动画
styled-components 同样对 css 动画中的 @keyframe 做了很好的支持。
const rotate = styled.keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`;
const styles = styled.css`
animation: ${rotate} 2s linear infinite;
`
const Rotate = styled.div`
${styles};
display: inline-block;
padding: 2rem 1rem;
font-size: 1.2rem;
`;
render(
<Rotate>< 💅🏾 ></Rotate>
);
ref
通过传递ref属性,可以获得组件的元素节点。
const Input = styled.input.attrs({
className: 'my-input',
})`
color: palevioletred;
font-size: 1em;
border: 2px solid palevioletred;
border-radius: 3px;
`;
const inputRef = React.useRef();
render(
<Input ref={inputRef}/>
);
组件嵌套选择器用法
如果两个组件定义的作用域一致,则可以直接嵌套使用选择器。
const Wrapper = styled.section`
padding: 4em;
background: papayawhip;
`;
const Title = styled.h1`
font-size: 1.5em;
text-align: center;
color: palevioletred;
`;
const SubTitle = styled.span`
text-align: center;
color: grey;
${Title}:hover &{
color: red;
}
`
render(
<>
<Wrapper>
<Title>
Hello World
<SubTitle> yes!</SubTitle>
</Title>
</Wrapper>
<SubTitle> no...</SubTitle>
</>
}
对象样式定义
如果样式都是一个对象类型,那也可以之间舍弃模板字符串,直接使用对象定义的方式定义样式。
const Box = styled.div({
background: 'palevioletred',
height: '50px',
width: '50px'
});
const PropsBox = styled.div(props => ({
background: props.background,
height: '50px',
width: '50px'
}));
render(
<div>
<Box />
<PropsBox background="blue" />
</div>
);
其他
优劣
优势
- 标准CSS语法
- 自动注入 CSS
styled-components 根据页面上呈现的组件,自动注入它们的样式,减少多余代码的引入。
- 唯一类名
styled-components 为您的样式生成唯一的类名,不用担心重复或拼写错误。
- 设置动态样式非常简单
根据组件的 props 或全局主题调整组件的样式简单直观,无需手动管理数十个类。
- 维护简单
不必在不同的文件中寻找影响组件的样式。
- 自动添加浏览器前缀
我们只需要编写一次css,sc在渲染的时候根据浏览器自动帮我们增加前缀。
劣势
- 学习成本高。适合已经有一定开发经验的人使用
- 代码可读性差。动态生成的唯一类名阅读困难,不能写z如果是需要开放类组件库适用性差。
- 冗余度高。使用props动态生成样式时,不同的props值生成的样式会一直存在。
- 性能
- 运行时动态生成css
- 运行时包高达
15.4kb