Skip to content

自动引入插件

对于组件的按需引入,你可以直接使用我们准备好的自动引入插件 yc-design-vue-resolver,当然如果不方便下载,想要本地化使用,在也会提供其源码供各位按需取用。

插件源码

typescript
interface ComponentInfo {
  name: string;
  from: string;
  as: string;
  sideEffects?: string | string[];
}

interface ComponentResolver {
  type: 'component';
  resolve: (name: string) => ComponentInfo | undefined | null;
}

interface YcDesignVueResolverOptions {
  exclude?: string | RegExp | (string | RegExp)[];
  sideEffect?: boolean;
}

// 子组件映射表:将子组件映射到其父组件/目录
const subCompt: Record<string, string> = {
  AnchorLink: 'Anchor',
  AvatarGroup: 'Avatar',
  ButtonGroup: 'Button',
  BreadcrumbItem: 'Breadcrumb',
  CardMeta: 'Card',
  CardGrid: 'Card',
  CarouselItem: 'Carousel',
  CheckboxGroup: 'Checkbox',
  CollapseItem: 'Collapse',
  Countdown: 'Statistic',
  DescriptionsItem: 'Descriptions',
  DropdownButton: 'Dropdown',
  Doption: 'Dropdown',
  Dgroup: 'Dropdown',
  Dsubmenu: 'Dropdown',
  GridCol: 'Grid',
  GridItem: 'Grid',
  GridRow: 'Grid',
  ImagePreview: 'Image',
  ImagePreviewAction: 'Image',
  ImagePreviewGroup: 'Image',
  LayoutContent: 'Layout',
  LayoutFooter: 'Layout',
  LayoutHeader: 'Layout',
  LayoutSider: 'Layout',
  ListItem: 'List',
  ListItemMeta: 'List',
  MenuItem: 'Menu',
  MenuItemGroup: 'Menu',
  Option: 'Select',
  Optgroup: 'Select',
  RadioGroup: 'Radio',
  SkeletonLine: 'Skeleton',
  SkeletonShape: 'Skeleton',
  Step: 'Steps',
  SubMenu: 'Menu',
  TabPane: 'Tabs',
  TimelineItem: 'Timeline',
  TypographyText: 'Typography',
  TypographyTitle: 'Typography',
  TypographyParagraph: 'Typography',
};

// 组件依赖关系表:记录一个组件依赖哪些其他组件
const componentDependencies: Record<string, string[]> = {
  VerificationCode: ['Input'],
  TypographyBase: ['Input', 'Tooltip'],
  Transfer: ['Checkbox', 'Button', 'Scrollbar', 'Input', 'Empty'],
  Tooltip: ['Trigger'],
  TimePicker: ['Trigger', 'Button', 'Scrollbar'],
  Timeline: ['Spin'],
  Tag: ['Spin'],
  Switch: ['Spin'],
  Slider: ['InputNumber', 'Tooltip'],
  Select: [
    'Spin',
    'Scrollbar',
    'Input',
    'InputTag',
    'Trigger',
    'Empty',
    'Checkbox',
  ],
  Popover: ['Trigger'],
  Popconfirm: ['Button', 'Trigger'],
  Pagination: ['InputNumber', 'Select'],
  PageHeader: ['Divider'],
  OverflowList: ['Tag'],
  Modal: ['Button'],
  Menu: ['Dropdown', 'Tooltip'],
  Mention: ['AutoComplete'],
  List: ['Spin', 'Scrollbar', 'Pagination', 'Empty'],
  Link: ['Spin'],
  Layout: ['ResizeBox'],
  InputTag: ['Tag'],
  InputNumber: ['Button', 'Input'],
  ImagePreviewAction: ['Tooltip'],
  Image: ['Spin'],
  Dropdown: [
    'Scrollbar',
    'Trigger',
    'Button',
    'ButtonGroup',
    'Trigger',
    'Scrollbar',
  ],
  Drawer: ['Button'],
  Comment: ['Avatar'],
  ColorPicker: ['Trigger', 'Select', 'Input', 'InputNumber'],
  Cascader: [
    'Scrollbar',
    'Spin',
    'Checkbox',
    'Input',
    'InputTag',
    'Trigger',
    'Empty',
  ],
  Card: ['Spin'],
  Calendar: ['Button', 'Radio'],
  Button: ['Spin'],
  Breadcrumb: ['Dropdown'],
  BackTop: ['Button'],
  Avatar: ['Popover'],
  AutoComplete: ['Select'],
};

//检查一个组件名是否应该被排除
function isExclude(
  name: string,
  exclude?: string | RegExp | (string | RegExp)[]
): boolean {
  if (!exclude) return false;
  if (typeof exclude === 'string') return name === exclude;
  if (exclude instanceof RegExp) return exclude.test(name);
  if (Array.isArray(exclude)) {
    for (const item of exclude) {
      if (isExclude(name, item)) return true;
    }
  }
  return false;
}

// 使用 Map 作为缓存,存储已经计算过的组件依赖关系
const cache = new Map<string, Set<string>>();
// 递归查找一个组件的所有最终依赖(包括它自己)。
function findFinalDependencies(
  componentName: string,
  visited: Set<string> = new Set()
): Set<string> {
  // 性能优化:如果这个组件的结果已经被计算过,直接从缓存中返回。
  if (cache.has(componentName)) {
    return cache.get(componentName)!;
  }
  // 循环检测:如果当前组件已经在递归路径中,返回一个空集合以打破循环。
  if (visited.has(componentName)) {
    return new Set();
  }
  // 将当前组件添加到结果集和访问路径中。
  const allDeps = new Set<string>([componentName]);
  visited.add(componentName);
  // 获取当前组件的直接依赖。
  const directDependencies = componentDependencies[componentName];
  if (directDependencies) {
    // 遍历所有直接依赖。
    for (const dep of directDependencies) {
      // 递归调用此函数来查找所有子依赖,并将结果合并到当前集合中。
      const subDependencies = findFinalDependencies(dep, visited);
      subDependencies.forEach((finalDep) => allDeps.add(finalDep));
    }
  }
  // 回溯:处理完其分支后,将当前组件从访问路径中移除。
  visited.delete(componentName);
  // 将计算结果存入缓存以备将来使用。
  cache.set(componentName, allDeps);
  return allDeps;
}

// 解析器主函数
export const YcDesignVueResolver = (
  options: YcDesignVueResolverOptions = {
    exclude: [],
  }
): ComponentResolver => {
  return {
    type: 'component',
    resolve: (name: string) => {
      // 检查组件名是否有效(以 'Yc' 开头且后跟一个大写字母)或是否被排除了
      if (!name.match(/^Yc[A-Z]/) || isExclude(name, options.exclude)) {
        return undefined;
      }
      // 解析组件名和可能的父组件名
      const componentName = name.slice(2);
      const parentName = subCompt[componentName];
      const importDir = parentName || componentName;
      const importName = parentName ? componentName : 'default';
      // 构建组件信息对象
      const config: ComponentInfo = {
        name: importName,
        as: componentName, // 别名
        from: `yc-design-vue/es/${importDir}`, // 导入路径
      };
      // 创建一个 Set 来收集所有需要引入的样式文件(副作用)
      const sideEffects = new Set<string>();
      // 首先,添加全局共享样式
      sideEffects.add('yc-design-vue/es/shared.css');
      // 添加组件自身的样式
      sideEffects.add(`yc-design-vue/es/${importDir}/index.css`);
      // 获取当前组件的所有传递性依赖(包括它自己)
      const transitiveDependencies = findFinalDependencies(componentName);
      // 为所有这些依赖添加样式
      transitiveDependencies.forEach((depName) => {
        // 对于每个依赖,我们需要找到它的导入目录
        const depParentName = subCompt[depName];
        const depImportDir = depParentName || depName;
        sideEffects.add(`yc-design-vue/es/${depImportDir}/index.css`);
      });
      // 将 Set 转换为数组并赋值给配置对象
      config.sideEffects = Array.from(sideEffects);
      return config;
    },
  };
};