手把手教你三步实现简易 styled-components

什么是 styled-components

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 代码。本地调试体验极佳

猜你喜欢

转载自juejin.im/post/7040229858189770782