什么是 styled-components
styled-components 是一种 css in js 方案
优势
- 方便设置动态样式
- styled-components 生成的 className 是唯一的,不用担心 className 冲突
- 使用方便,不需要配置 webpack、开箱即用
- SSR 类框架处理 CSS Modules 变量相当棘手,所以使用 styled-components 作方案
简易实现
下面我们来实现一个 v-styled。以下实现和源码有出入,做了很多简化
简单用法
styled-components 一般使用方法如下:
const Div = styled.div`
border: 1px solid red;
font-size: ${(props) => (props.fs ? props.fs : '20px')};
color: red;
border-radius: ${(props) => (props.radius ? props.radius : '')};
`;
function App() {
return (
<Div fs='30px' radius='10px'>
Hello world
</Div>
);
}
复制代码
由上面可以看出,styled-components 使用带标签的模板字符串。使用 styled.div 相当于 styled('div') ,styled('div')返回一个标签模板字符串函数
首先,我们声明一个函数 v
function v(tag) {}
复制代码
为了让我们的 v-styled 也支持多个 html 标签, 需进行如下转化:
let domElement = ['a', 'span', ...];
domElements.forEach((domElement) => {
v[domElement] = v(domElement);
});
复制代码
v('div') 执行完, 返回一个标签模板字符串函数,修改一下函数 v 的实现
function v(tag) {
return (strings: any[], ...args: any) => {};
}
复制代码
第一步:处理 css 内容
带标签的模板字符串,其第一个参数是 strings 数组,其余的参数与 ${expression}的表达式相关
首先,我们声明一个 div 组件
// 声明一个 Div 组件
const Div = v.div`
border: 1px solid red;
font-size: ${(props) => (props.fs ? props.fs : '20px')};
color: red;
border-radius: ${(props) => (props.radius ? props.radius : '')};
`;
const APP = ()=>{
// 使用
return <Div fs="12px" radius="4px">
}
复制代码
因此假如我们不做任何处理, 函数 v 输出如下:
function v(tag) {
return (strings: any[], ...args: any) => {
/**
第一个参数 strings 为:
["
border: 1px solid red;
font-size: ", ";
color: red;
border-radius: ", ";
"]
剩下的参数 args 为: [ƒ (), ƒ ()]
**/
console.log(strings, args);
};
}
复制代码
得到了模板字符串传递的参数后,我们需要对它进行处理,即把传入的表达式执行,然后放回其原来的位置,希望生成如下的 css rules:
{
border: 1px solid red;
font-size: 12px;
color: red;
border-radius: 4px;
}
复制代码
所以为了把${(props) => (props.fs ? props.fs : '20px')}
, ${(props) => (props.radius ? props.radius : '')}
这两个表达式分别放在 font-size 和 border-radius 属性后面,我们需要将其第一个参数 strings 和其余参数混合起来,写一个交换函数:
const interleave = (strings, interpolations) => {
const result = [strings[0]];
for (let i = 0, len = interpolations.length; i < len; i += 1) {
result.push(interpolations[i], strings[i + 1]);
}
return result;
};
复制代码
那么现在我们就可以把参数做混合,混合后的结果为:
function v(tag) {
return (strings: any[], ...args: any) => {
const cssRules = interleave(strings, args);
/**
参数输出为: ["
border: 1px solid red;
font-size: ", ƒ (), ";
color: red;
border-radius: ", ƒ (), ";
"]
**/
console.log(cssRules);
};
}
复制代码
剩下做的事情就简单了,我们把传入其中的表达式执行一下输出结果,然后把这个数组拼接成字符串生成对应的 css rules。为了后面使用方便,我们把混合后的结果称为 cssRules
第二步:生成 react element
因为我们需要支持多个 dom tag , 使用 React.createElement 创建 domElement 元素
function styledComponentImpl() {
return React.createElement('div', { className: 'test' });
}
复制代码
我们调用 styledComponentImpl() 就能创建一个 className 为 test 的 div 标签
回到我们的函数 v 上来 , 接下来我们需要把 cssRules ,tag , 对应的 className 这个信息传递 styledComponentImpl ,怎么办呢 ? 很简单,传参。通过 WrappedStyledComponent 这个对象把 tag, rules 等信息传递给 styledComponentImpl。
function v(tag: string) {
return (strings: any[], ...args: any) => {
const cssRules = interleave(strings, args);
let WrappedStyledComponent: any;
// 声明一个函数组件
const forwardRef = (props: any, ref: any) =>
styledComponentImpl(WrappedStyledComponent, props, ref);
WrappedStyledComponent = React.forwardRef(forwardRef);
WrappedStyledComponent.tag = tag;
WrappedStyledComponent.rules = rules;
// 简单生成了一个不重复的 className
WrappedStyledComponent.className = `v-${uuidv4()}`;
return WrappedStyledComponent;
};
}
复制代码
获取到 cssRules ,className ,tag 之后就可以进行计算样式,插入样式,最后返回响应的 react element 。styledComponentImpl 实现如下:
// 创建 react element
function styledComponentImpl(
forwardedComponent: IStyledComponent,
props: Object,
forwardedRef: Ref<any>,
) {
const { children, ...restProps } = props;
// 获取到 cssRules, className,tag
const { cssRules, className ,tag} = forwardedComponent;
// 在这里我们把 props 传入到模板字符串的表达式中执行
const css = cssRules
.map((r: any) => (typeof r === "function" ? r(restProps) : r))
.join("");
// 生成样式,把样式插入到 head 中
const injectedCSS = `.${className} { ${css} }`;
insertCss(injectedCSS);
// 返回带这个 className 的 react element
return React.createElement(tag, { className }, children);
}
复制代码
第三步:往 head 中插入样式
通过获取到 styleSheet 实例,然后调用其 insertRule 方法插入样式,例子如下:
// 往 head 中插入 css
function insertCss(css: string, index = 0) {
const { styleSheets } = document;
styleSheets[0].insertRule(css, index);
}
复制代码
talk is cheap , show you the code
笔者参照 styled-components 源码简单实现了一下,实现地址
styled-components 的问题
props 经常变化会生成大量冗余样式
每一次新的 props 传入都会生成一个全新的 className ,会产生大量冗余样式。因此,如果样式组件的 props 变化频繁,建议直接使用 inline style。 复现 demo
其他
styled-components 通过 monorepo 管理代码 ,在 sandbox 目录下 yarn start
启动例子后,我们进到 styled-components 源目录下,进行修改后,sandbox 会监听文件修改,将会重新 rebuild 的 styled-components 代码。本地调试体验极佳