让 styled-components 也拥有层级结构

1,141 阅读4分钟

前言

最近在使用 styled-components 时,遇到个让我很难受的事情

目前效果

多个节点虽然在使用上有层级关系的,但是 styled-components 其实是没有相关支持的,只能一个个节点单独声明

import styled from "styled-components";

const Layout = styled.div``;
const LayoutHeader = styled.div``;
const LayoutContent = styled.div``;
const LayoutContentLeft = styled.div``;
const LayoutContentRight = styled.div``;
const LayoutFooter = styled.div``;

function App() {
  return (
    <Layout>
      <LayoutHeader>头部</LayoutHeader>
      <LayoutContent>
        主体
        <LayoutContentLeft>主体的左边</LayoutContentLeft>
        <LayoutContentRight>主体的右边</LayoutContentRight>
      </LayoutContent>
      <LayoutFooter>尾部</LayoutFooter>
    </Layout>
  );
}

我期望的

我期望组件是有层级结构的,如下:

function App() {
  return (
    <Layout>
      <Layout.Header>头部</Layout.Header>
      <Layout.Content>
        主体
        <Layout.Content.Left>主体的左边</Layout.Content.Left>
        <Layout.Content.Right>主体的右边</Layout.Content.Right>
      </Layout.Content>
      <Layout.Footer>尾部</Layout.Footer>
    </Layout>
  );
}

是有层级结构的:

  • Layout
    • Layout.Header
    • Layout.Content
      • Layout.Content.Left
      • Layout.Content.Right
    • Layout.Footer

难点分析

把一个平铺结构改成树状结构不是难事,写个 function 处理一下不就好了

难的是如何让转化之后的结果实现 ts 类型推导,也就是根据你输入的内容,提示当前层级有哪些子层级

没错,这片文章我是来教你如何写 typescript 的

提示.png

完整代码

我们将要实现一个 styledTier 方法来完成这件事

先来看下完整代码

App.tsx

import { styledTier } from "./utils.ts";

/**
 * 每一级可以单传一个组件也可以传一个对象
 * 如果存在下一层则只能传对象
 **/
const Layout = styledTier({
  _self: styled.div``,
  Header: styled.div``, // 单传一个组件
  Content: {
    // 传一个对象
    _self: styled.div``, // "_self" 属性表示本层组件
    Left: styled.div``,
    Right: styled.div``,
  },
  Footer: styled.div``,
});

export default function App() {
  return (
    <Layout>
      <Layout.Header>头部</Layout.Header>
      <Layout.Content>
        主体
        <Layout.Content.Left>主体的左边</Layout.Content.Left>
        <Layout.Content.Right>主体的右边</Layout.Content.Right>
      </Layout.Content>
      <Layout.Footer>尾部</Layout.Footer>
    </Layout>
  );
}

utils.ts

import { StyledComponent } from "styled-components";

interface IHierarchyInput {
  [T: string]: StyledComponent<any, any> | IHierarchyInput;
  _self: StyledComponent<any, any>;
}
type IHierarchyOutput<Input extends IHierarchyInput = any> = Input["_self"] & {
  [Prop in keyof Omit<Input, "_self">]: Input[Prop] extends IHierarchyInput
    ? IHierarchyOutput<Input[Prop]>
    : Input[Prop];
};
export const styledTier = function <O extends IHierarchyInput>(
  input: O
): IHierarchyOutput<O> {
  const { _self: component, ...children } = input;
  const current: any = component;

  for (const [pKey, pItem] of Object.entries(children)) {
    if (pItem._self) {
      current[pKey] = styledTier(pItem);
    } else {
      current[pKey] = pItem;
    }
  }

  return current;
};

详细讲解

入参

IHierarchyInput 是用来约束入参的

import { StyledComponent } from "styled-components";
interface IHierarchyInput {
  [T: string]: StyledComponent<any, any> | IHierarchyInput;
  _self: StyledComponent<any, any>;
}

有三个关键点:

  • 属性的定义(_self 属性是有特殊的类型定义的)
  • 联合类型(其他属性可以直接传组件,也可以传一个对象)
  • 类型递归嵌套

属性的定义

构造一个简单的对象:

// 属性名为 string,属性值为 number
interface MyObj {
  [T: string]: number;
}

// 属性名为 number,属性值为 number(其实这样没有意义,因为对象在 js 中属性名都会转成 string)
interface MyObj {
  [T: number]: number;
}

// 或者还可以这样(这样只在 ts 中有意义,而在 js 中是没有意义的)
interface MyObj {
  [T: string]: number;
  [T: number]: number;
}

但是如果我要对其中某个属性做特定的约束呢,譬如下面的属性 beforeafter

interface MyTest {
  before: string;
  [T: string]: number | string | Array<any>;
  after: Array<any>;
}

有两点需要注意的:

  • 位置没有先后之分,所以上面 beforeafter 实现效果是一致的
  • [T: string] 对应的类型必须包含 beforeafter 的类型

联合类型

type StrOrNum = string | number;
const str: StrOrNum = "abc";
const num: StrOrNum = 123;

类型递归嵌套

类型的定义也可以使用递归嵌套的

type MyArr = Array<number | MyArr>;
const arr: MyArr = [1, 2, 3, [4, 5, 6, [7, 8, 9]]];

函数范型

如何才能让函数在调用时根据传入的参数类型来决定其他参数或者返回值类型

这就要用到范型了,基本用法长这样:

function fn<T>(){}

举个简单的例子:

// 定义范型 T,分别用于两个参数的不同位置,用以确定这两个位置最终类型会是一致
function definition<T>(input: T, callback: (result: T) => void) {
  callback(input);
}

definition(2, function (result) {
  console.log(result); // 提示为 number
});

definition("i'm sb", function (result) {
  console.log(result); // 提示为 string
});

返回值

IHierarchyOutput 是用来约束返回值的

type IHierarchyOutput<Input extends IHierarchyInput = any> = Input["_self"] & {
  [Prop in keyof Omit<Input, "_self">]: Input[Prop] extends IHierarchyInput
    ? IHierarchyOutput<Input[Prop]>
    : Input[Prop];
};

首先,有以下几个知识点:

  • 范型
  • 约束范型的类型(通过 extends 来约束范型只能是某些类型)
  • 属性的定义(通过 inkeyof 遍历别的类型来构造一个对象)
  • 类型判断(通过 extends 还有三目运算符来实现类型的判断和转化)
  • 递归嵌套结构

范型

这里不过多陈述了,都是基础,举个简单的例子吧:

// 定义一个有范型的类型
type MyTuple<A, B> = [A, B];
const t1: MyTuple<string, number> = ["abc", 123];
const t2: MyTuple<number, number> = [456, 123];

约束范型的类型

extends 关键字字面意思是“继承”,同时他也可以用来约束范型必须是什么类型,譬如

// A 必须是 string 或者是 number,而 B 必须是 number
type MyTuple<A extends string | number, B extends number> = [A, B];
const t1: MyTuple<number, number> = [456, 123];
const t2: MyTuple<boolean, number> = [false, 123]; // 报错
const t2: MyTuple<number, string> = [456, "abc"]; // 报错

属性的定义

关于 inkeyof 的详细介绍可以看我这篇文章,Typescript 关键字

类型判断

关于 extends 的详细介绍可以看我这篇文章,Typescript 关键字

递归嵌套结构

type IHierarchyOutput<Input extends IHierarchyInput = any> = Input["_self"] & {
  // 排除 _self
  [Prop in keyof Omit<Input, "_self">]: Input[Prop] extends IHierarchyInput // 判断当前属性值是否还是对象结构
    ? IHierarchyOutput<Input[Prop]> // 如果是,则把当前属性值作为范型重新传入 IHierarchyOutput 中,开启下一轮循环
    : Input[Prop];
};