TypeScript × @ant-design/icons 动态加载与类型提示
众所周知
从 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;
秋名山车神, 就是你了! 但是机智如你, 很快发现了 emmmmm...
好像 Icon
还有其他属性呢, 于是你只好又浪费了生命中宝贵的一分钟这样尝试获取
// !!! IconProps 与 AntdIconProps 都是不存在的, 所以下面这行会报导入错误
import { IconProps, AntdIconProps } from '@ant-design/icons';
Icon
的 通用 props
定义, 然后发现, 果然并没有 export 出来啊!!! (╯‵□′)╯︵┻━┻
事实上, 不仅是 @ant-design/icons
没有导出 props
的 type
, 很多 react 组件库也都不会导出 组件的 props
的 type
;
啊这...总不能去改库吧 看不见我: 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
属性怎么消失了???
阿这...因为刚才 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;
源码点我 完结撒花 🎉🎉🎉
当然针对这个需求, 还有别的方案
- tree shaking 是不是也可以用 babel 插件来完成呢? (是的!
时间有点赶, 很多地方写的比较仓促, 欢迎留言讨论或者等我填坑 (逃
typescript 学习资料
我们也在招聘!!
转载自:https://juejin.cn/post/6922086052027072520