likes
comments
collection
share

TypeScript × @ant-design/icons 动态加载与类型提示

作者站长头像
站长
· 阅读数 439

众所周知

antd@4.x 中的一个重大升级是图标调整, 将原来 <Icon /> 组件提纯到了另一个包, 原因如下

在 antd@3.9.0 中,我们引入了 svg 图标(为何使用 svg 图标?)。使用了字符串命名的图标 API 无法做到按需加载,因而全量引入了 svg 图标文件,这大大增加了打包产物的尺寸。在 4.0 中,我们调整了图标的使用 API 从而支持 tree shaking,减少 antd 默认包体积约 150 KB(Gzipped)。🎉🎉🎉 原文

打包体积减少效果立竿见影, AWESOME TREE SHAKING!

没有银弹 / No Silver Bullet

这么帅气的优化当然也是需要付出代价的, 一丢丢不那么愉快的使用体验, 比如这样

  // tree-shaking supported
- import { Icon } from 'antd';
+ import { SmileOutlined } from '@ant-design/icons';

  const Demo = () => (
    <div>
-     <Icon type="smile" />
+     <SmileOutlined />
      <Button icon={<SmileOutlined />} />
    </div>
  );

Before: 只需要导入一个Icon组件, 并声明一下 type 表明是哪个组件就行

After: 需要单独引入具体的 IconName 这样的组件, 在 typescript 环境下, import 的只能提示可是比 JSX 的提示要差劲的多了, 更别说还要从 import 部分 cv 好几个 IconName 过来, 并增加 </> 标签这种即蠢又笨的操作...

就这就这?

老司机已经按捺不住内心的嘲讽, 花1分钟写出来第一份答案

// src/Icon.tsx
import React, { FC } from "react";
import * as AllIcons from "@ant-design/icons";

const Icon: FC<{ name: keyof typeof AllIcons }> = ({ name }) => {
  const Comp = AllIcons[name] as any; // 这里写成 any 的原因有机会再开一篇单独来解释
  return <Comp />;
};

export default Icon;

TypeScript × @ant-design/icons 动态加载与类型提示

秋名山车神, 就是你了! 但是机智如你, 很快发现了 emmmmm... 好像 Icon 还有其他属性呢, 于是你只好又浪费了生命中宝贵的一分钟这样尝试获取

// !!! IconProps 与 AntdIconProps 都是不存在的, 所以下面这行会报导入错误
import { IconProps, AntdIconProps } from '@ant-design/icons';

Icon 的 通用 props 定义, 然后发现, 果然并没有 export 出来啊!!! (╯‵□′)╯︵┻━┻

事实上, 不仅是 @ant-design/icons 没有导出 propstype, 很多 react 组件库也都不会导出 组件的 propstype;

啊这...总不能去改库吧 看不见我: patch-package 幸好 我们 typescript 足够机智, 我们只需要这样这样, 然后再这样这样 typescript infer

要有光

// 获取组件类型
type PickProps<T> = T extends (props: infer P1) => any
  ? P1
  : T extends React.ComponentClass<infer P2>
  ? P2
  : unknown;

就能借用此工具来获取 React 组件的属性类型了, 这样我们就可以稍微改造一下

// src/Icon.tsx
import React, { FC } from "react";
import AndtIcon from "@ant-design/icons";
import * as AllIcons from "@ant-design/icons";

type PickProps<T> = T extends (props: infer P1) => any
  ? P1
  : T extends React.ComponentClass<infer P2>
  ? P2
  : unknown;

type AllKeys = keyof typeof AllIcons;
//  获取大写开头的导出们, 认为是组件
type PickCapitalizeAsComp<K extends AllKeys> = K extends Capitalize<K>
  ? K
  : never;
// ------------------------------------------------^ typescript 4.1+ --------
type IconNames = PickCapitalizeAsComp<AllKeys>;
// 没有 4.1 的可以手动排除 小写开头的方法们
// type IconNames = Exclude<
//   AllKeys,
//   "createFromIconfontCN" | "default" | "getTwoToneColor" | "setTwoToneColor"
// >;

// 这里将不再能用 FC 来包裹, 原因的话 也可以再开一篇来讲了
const Icon: FC<{ name: IconNames } & PickProps<typeof AndtIcon>> = ({
  name,
  ...props
}) => {
  const Comp = AllIcons[name] as React.ClassType<any, any, any>;
  return <Comp {...props} />;
};

export default Icon;

Awesome! 但是等等, 说好双色图标呢??? twoToneColor 属性怎么消失了???

TypeScript × @ant-design/icons 动态加载与类型提示

阿这...因为刚才 import 进来的只是 import AndtIcon from "@ant-design/icons"; 啊!!!, 而默认的导出 AntdIcon 是没有添加这个属性的, 可以点击到详细的类型定义中查看

// src/Icon.tsx
import AntdIcon from "@ant-design/icons";
----------------------^点这里查看类型定义----------
// node_modules/@ant-design/icons/index.d.ts
export { default } from './components/Icon';
---------^点这里查看类型定义----------
// node_modules/@ant-design/icons/libs/components/icon.d.ts
// 可以搜索一下 twoToneColor 可以发现并没有 Pick 出来

这个问题其实可以拓展一下

指定的组件集合中, 我们该如何根据指定的组件名称来获取这个组件对应的属性呢? (TODO: 有机会再开一篇讲讲应用

毕竟 Icon 就有还有好几种类型呢, 而他们的props 是不太一样的

Awesome Typescript!

跟刚才一样我们依然可以先这样这样, 然后再这样这样 typescript 泛型

import React from "react";
import * as AllIcons from "@ant-design/icons";

type PickProps<T> = T extends (props: infer P1) => any
  ? P1
  : T extends React.ComponentClass<infer P2>
  ? P2
  : unknown;

type AllKeys = keyof typeof AllIcons;
//  获取大写开头的导出们, 认为是组件
type PickCapitalizeAsComp<K extends AllKeys> = K extends Capitalize<K>
  ? K
  : never;
// ------------------------------------------------^ typescript 4.1+ --------
type IconNames = PickCapitalizeAsComp<AllKeys>;
// 没有 4.1 的可以手动排除 小写开头的方法们
// type IconNames = Exclude<
//   AllKeys,
//   "createFromIconfontCN" | "default" | "getTwoToneColor" | "setTwoToneColor"
// >;

export type PickIconPropsOf<K extends IconNames> = PickProps<
  typeof AllIcons[K]
>;

// 这里将不再能用 FC 来包裹, 原因的话 也可以再开一篇来讲了
const Icon = <T extends IconNames, P extends Object = PickIconPropsOf<T>>({
  name,
  ...props
}: { name: T } & P) => {
  const Comp = AllIcons[name] as React.ClassType<any, any, any>;
  return <Comp {...props} />;
};

export default Icon;

狸猫换太子

一波操作猛如虎, 一看打包250kb; 一波花里胡哨的操作下来, 好像我们把最重要的 Tree Shaking 给丢了啊!!! 毕竟我们现在是import * as AllIcons from '@ant-design/icons';

所以有请 import() dynamic import 的小知识点; 利用动态加载的特效来逃过 Tree Shaking 的追杀

finally!!

import React, { useState, useEffect } from "react";
import { LoadingOutlined } from "@ant-design/icons";
// TODO: 这一行应该会导致全量导入, 但其实我们这里只是使用了类型, 所以实际使用去单独提取到了一个单独的 d.ts 文件中
import * as AllIcons from "@ant-design/icons";

type PickProps<T> = T extends (props: infer P1) => any
  ? P1
  : T extends React.ComponentClass<infer P2>
  ? P2
  : unknown;

type AllKeys = keyof typeof AllIcons;
//  获取大写开头的导出们, 认为是组件
type PickCapitalizeAsComp<K extends AllKeys> = K extends Capitalize<K>
  ? K
  : never;
// ------------------------------------------------^ typescript 4.1+ --------
type IconNames = PickCapitalizeAsComp<AllKeys>;
// 没有 4.1 的可以手动排除 小写开头的方法们
// type IconNames = Exclude<
//   AllKeys,
//   "createFromIconfontCN" | "default" | "getTwoToneColor" | "setTwoToneColor"
// >;

export type PickIconPropsOf<K extends IconNames> = PickProps<
  typeof AllIcons[K]
>;

// 这里将不再能用 FC 来包裹, 原因的话 也可以再开一篇来讲了
const Icon = <T extends IconNames, P extends Object = PickIconPropsOf<T>>({
  name,
  ...props
}: { name: T } & Omit<P, 'name'>) => {
  const [Comp, setComp] = useState<React.ClassType<any, any, any>>(
    LoadingOutlined
  );
  useEffect(() => {
    import(`@ant-design/icons/${name}.js`).then((mod) => {
      setComp(mod.default);
    });
  }, [name]);
  return <Comp {...props} />;
};

export default Icon;

源码点我 完结撒花 🎉🎉🎉


当然针对这个需求, 还有别的方案

  1. tree shaking 是不是也可以用 babel 插件来完成呢? (是的!

时间有点赶, 很多地方写的比较仓促, 欢迎留言讨论或者等我填坑 (逃

typescript 学习资料

我们也在招聘!!

转载自:https://juejin.cn/post/6922086052027072520
评论
请登录