使用unplugin-auto-import+antd+vite+react插件自动导入antdesign组件

2,422 阅读2分钟

前言

在使用vite+react+antd5+项目开发中,发现unplugin-auto-import插件,但是该插件默认不支持antd5的自动引入。故想着自己写一个解析器处理该需求。

vite.config.ts

import { defineConfig } from 'vite';
import AutoImport from 'unplugin-auto-import/vite';
import { AntDesignResolver } from './build/resolvers/antd';

export default defineConfig(() => {
  return {
    plugins: [
      AutoImport({
        imports: ['react'],
        dts: true,
        resolvers: [
            // 使用我自己编写的解析器,处理antd的组件
          AntDesignResolver({
            resolveIcons: true,
          }),
        ],
      }),
    ],
  };
});

解析器核心代码解析


//  `./build/resolvers/antd.ts`

// ...此处省略N行代码
const prefix = "A"; // 我定义的antd组件的默认前缀,因为antd可能会和其他组件冲突

export function AntDesignResolver(
  options: AntDesignResolverOptions = {}
): ComponentResolver {
  return {
    type: 'component',
    resolve: (name: string) => {
     /*
     ** 如果选项中resolveIcons为真,代表需要自动解析icons
     ** 引用模块名称(name)中含有 Outlined|Filled|TwoTone,代表为antd默认的icon规则
     */
      if (options.resolveIcons && name.match(/(Outlined|Filled|TwoTone)$/)) {
        return {
          name,
          from: '@ant-design/icons',
        };
      }

     /*
     ** 如果引用模块名称(name)是我定义的antd组件名称
     ** 且不在黑名单之内,将自动引入我定义的规则
     */
      if (isAntd(name) && !options?.exclude?.includes(name)) {
      
        // 将我定义的前缀去除
        // AButtom => Button
        const importName = name.slice(prefix.length);
        const { cjs = false, packageName = 'antd' } = options;
        
        // 以Button为例,这里则会自动引入import Button from 'antd/es/index.ts'
        const path = `${packageName}/${cjs ? 'lib' : 'es'}`;
        return {
          name: importName,
          from: path,
          sideEffects: getSideEffects(importName, options),
        };
      }
    },
  };
}

测试及使用

// App.tsx

 const App =() => {
  useEffect(() => {
    console.log(123)
  }, [])
  return <div>

    <ASpace wrap>
      <AButton type="primary">Primary Button</AButton>
      <HomeOutlined rev={undefined} />
    </ASpace>
  </div>
}

export default App

打开本地vite web服务,显示正常

image.png

我们可以看到auto-imports.d.ts文件自动引入了类型,说明unplugin-auto-import内部机制生效了

image.png

./build/resolvers/antd.ts 完整代码如下,无依赖版本,可直接引用

export function kebabCase(key: string) {
  const result = key.replace(/([A-Z])/g, ' $1').trim();
  return result.split(' ').join('-').toLowerCase();
}

export type Awaitable<T> = T | PromiseLike<T>;

export interface ImportInfo {
  as?: string;
  name?: string;
  from: string;
}

export type SideEffectsInfo =
  | (ImportInfo | string)[]
  | ImportInfo
  | string
  | undefined;

export interface ComponentInfo extends ImportInfo {
  sideEffects?: SideEffectsInfo;
}

export type ComponentResolveResult = Awaitable<
  string | ComponentInfo | null | undefined | void
>;

export type ComponentResolverFunction = (
  name: string
) => ComponentResolveResult;
export interface ComponentResolverObject {
  type: 'component' | 'directive';
  resolve: ComponentResolverFunction;
}
export type ComponentResolver =
  | ComponentResolverFunction
  | ComponentResolverObject;

interface IMatcher {
  pattern: RegExp;
  styleDir: string;
}

const matchComponents: IMatcher[] = [
  {
    pattern: /^Avatar/,
    styleDir: 'avatar',
  },
  {
    pattern: /^AutoComplete/,
    styleDir: 'auto-complete',
  },
  {
    pattern: /^Anchor/,
    styleDir: 'anchor',
  },

  {
    pattern: /^Badge/,
    styleDir: 'badge',
  },
  {
    pattern: /^Breadcrumb/,
    styleDir: 'breadcrumb',
  },
  {
    pattern: /^Button/,
    styleDir: 'button',
  },
  {
    pattern: /^Checkbox/,
    styleDir: 'checkbox',
  },
  {
    pattern: /^Card/,
    styleDir: 'card',
  },
  {
    pattern: /^Collapse/,
    styleDir: 'collapse',
  },
  {
    pattern: /^Descriptions/,
    styleDir: 'descriptions',
  },
  {
    pattern: /^RangePicker|^WeekPicker|^MonthPicker/,
    styleDir: 'date-picker',
  },
  {
    pattern: /^Dropdown/,
    styleDir: 'dropdown',
  },

  {
    pattern: /^Form/,
    styleDir: 'form',
  },
  {
    pattern: /^InputNumber/,
    styleDir: 'input-number',
  },

  {
    pattern: /^Input|^Textarea/,
    styleDir: 'input',
  },
  {
    pattern: /^Statistic/,
    styleDir: 'statistic',
  },
  {
    pattern: /^CheckableTag/,
    styleDir: 'tag',
  },
  {
    pattern: /^TimeRangePicker/,
    styleDir: 'time-picker',
  },
  {
    pattern: /^Layout/,
    styleDir: 'layout',
  },
  {
    pattern: /^Menu|^SubMenu/,
    styleDir: 'menu',
  },

  {
    pattern: /^Table/,
    styleDir: 'table',
  },
  {
    pattern: /^TimePicker|^TimeRangePicker/,
    styleDir: 'time-picker',
  },
  {
    pattern: /^Radio/,
    styleDir: 'radio',
  },

  {
    pattern: /^Image/,
    styleDir: 'image',
  },

  {
    pattern: /^List/,
    styleDir: 'list',
  },

  {
    pattern: /^Tab/,
    styleDir: 'tabs',
  },
  {
    pattern: /^Mentions/,
    styleDir: 'mentions',
  },

  {
    pattern: /^Step/,
    styleDir: 'steps',
  },
  {
    pattern: /^Skeleton/,
    styleDir: 'skeleton',
  },

  {
    pattern: /^Select/,
    styleDir: 'select',
  },
  {
    pattern: /^TreeSelect/,
    styleDir: 'tree-select',
  },
  {
    pattern: /^Tree|^DirectoryTree/,
    styleDir: 'tree',
  },
  {
    pattern: /^Typography/,
    styleDir: 'typography',
  },
  {
    pattern: /^Timeline/,
    styleDir: 'timeline',
  },
  {
    pattern: /^Upload/,
    styleDir: 'upload',
  },
];

export interface AntDesignResolverOptions {
  /**
   * exclude components that do not require automatic import
   *
   * @default []
   */
  exclude?: string[];
  /**
   * import style along with components
   *
   * @default 'css'
   */
  importStyle?: boolean | 'css' | 'less';
  /**
   * resolve `antd' icons
   *
   * requires package `@ant-design/icons-vue`
   *
   * @default false
   */
  resolveIcons?: boolean;

  /**
   * @deprecated use `importStyle: 'css'` instead
   */
  importCss?: boolean;
  /**
   * @deprecated use `importStyle: 'less'` instead
   */
  importLess?: boolean;

  /**
   * use commonjs build default false
   */
  cjs?: boolean;

  /**
   * rename package
   *
   * @default 'antd'
   */
  packageName?: string;
}

function getStyleDir(compName: string): string {
  let styleDir;
  const total = matchComponents.length;
  for (let i = 0; i < total; i++) {
    const matcher = matchComponents[i];
    if (compName.match(matcher.pattern)) {
      styleDir = matcher.styleDir;
      break;
    }
  }
  if (!styleDir) styleDir = kebabCase(compName);

  return styleDir;
}

function getSideEffects(
  compName: string,
  options: AntDesignResolverOptions
): SideEffectsInfo {
  const { importStyle = true, importLess = false } = options;

  if (!importStyle) return;
  const lib = options.cjs ? 'lib' : 'es';
  const packageName = options?.packageName || 'antd';

  if (importStyle === 'less' || importLess) {
    const styleDir = getStyleDir(compName);
    return `${packageName}/${lib}/${styleDir}/style`;
  } else {
    const styleDir = getStyleDir(compName);
    return `${packageName}/${lib}/${styleDir}/style`;
  }
}
const primitiveNames = [
  'Affix',
  'Anchor',
  'AnchorLink',
  'AutoComplete',
  'AutoCompleteOptGroup',
  'AutoCompleteOption',
  'Alert',
  'Avatar',
  'AvatarGroup',
  'BackTop',
  'Badge',
  'BadgeRibbon',
  'Breadcrumb',
  'BreadcrumbItem',
  'BreadcrumbSeparator',
  'Button',
  'ButtonGroup',
  'Calendar',
  'Card',
  'CardGrid',
  'CardMeta',
  'Collapse',
  'CollapsePanel',
  'Carousel',
  'Cascader',
  'Checkbox',
  'CheckboxGroup',
  'Col',
  'Comment',
  'ConfigProvider',
  'DatePicker',
  'MonthPicker',
  'WeekPicker',
  'RangePicker',
  'QuarterPicker',
  'Descriptions',
  'DescriptionsItem',
  'Divider',
  'Dropdown',
  'DropdownButton',
  'Drawer',
  'Empty',
  'Form',
  'FormItem',
  'FormItemRest',
  'Grid',
  'Input',
  'InputGroup',
  'InputPassword',
  'InputSearch',
  'Textarea',
  'Image',
  'ImagePreviewGroup',
  'InputNumber',
  'Layout',
  'LayoutHeader',
  'LayoutSider',
  'LayoutFooter',
  'LayoutContent',
  'List',
  'ListItem',
  'ListItemMeta',
  'Menu',
  'MenuDivider',
  'MenuItem',
  'MenuItemGroup',
  'SubMenu',
  'Mentions',
  'MentionsOption',
  'Modal',
  'Statistic',
  'StatisticCountdown',
  'PageHeader',
  'Pagination',
  'Popconfirm',
  'Popover',
  'Progress',
  'Radio',
  'RadioButton',
  'RadioGroup',
  'Rate',
  'Result',
  'Row',
  'Select',
  'SelectOptGroup',
  'SelectOption',
  'Skeleton',
  'SkeletonButton',
  'SkeletonAvatar',
  'SkeletonInput',
  'SkeletonImage',
  'Slider',
  'Space',
  'Spin',
  'Steps',
  'Step',
  'Switch',
  'Table',
  'TableColumn',
  'TableColumnGroup',
  'TableSummary',
  'TableSummaryRow',
  'TableSummaryCell',
  'Transfer',
  'Tree',
  'TreeNode',
  'DirectoryTree',
  'TreeSelect',
  'TreeSelectNode',
  'Tabs',
  'TabPane',
  'Tag',
  'CheckableTag',
  'TimePicker',
  'TimeRangePicker',
  'Timeline',
  'TimelineItem',
  'Tooltip',
  'Typography',
  'TypographyLink',
  'TypographyParagraph',
  'TypographyText',
  'TypographyTitle',
  'Upload',
  'UploadDragger',
  'LocaleProvider',
];
const prefix = 'A';

let antdNames: Set<string>;

function genAntdNames(primitiveNames: string[]): void {
  antdNames = new Set(primitiveNames.map((name) => `${prefix}${name}`));
}
genAntdNames(primitiveNames);

function isAntd(compName: string): boolean {
  return antdNames.has(compName);
}


export function AntDesignResolver(
  options: AntDesignResolverOptions = {}
): ComponentResolver {
  return {
    type: 'component',
    resolve: (name: string) => {
      if (options.resolveIcons && name.match(/(Outlined|Filled|TwoTone)$/)) {
        return {
          name,
          from: '@ant-design/icons',
        };
      }

      if (isAntd(name) && !options?.exclude?.includes(name)) {
        const importName = name.slice(prefix.length);
        const { cjs = false, packageName = 'antd' } = options;
        const path = `${packageName}/${cjs ? 'lib' : 'es'}`;
        return {
          name: importName,
          from: path,
          sideEffects: getSideEffects(importName, options),
        };
      }
    },
  };
}

总结

  1. @done 学习了简易解析器的原理与实践
  2. @todo antd的组件每个版本都是动态的,随着不同用户的不同版本,我们需要动态的拉取支持的组件
  3. @todo 目前只支持vite版本,不支持webpack、rollup等构建工具
  4. @todo 组件前缀写死为'A',不支持动态扩展,有的用户觉得'AButton'不好看就无法修改