◆

unplugin-auto-import 自动导入插件配置详解

2023-12-31 16:000

前言

使用 vite 开发 vue 时,使用了 unplugin-vue-components 进行了按需加载组件库,然后在使用组件库的同时又实用了组件库里的方法, 在没有进行项目打包分析之前,认为组件库已经按需加载了,但最后项目开发完成,进行打包分析时,发现使用的组件库没有进行按需加载。

问题原因

下面以 vant2 组件库为例,首先 vant2 组件库官方没有提供按需加载的解析器,对没有提供解析器的组件库进行按需加载需要实现自定义解析器

function kebabCase(str) {
  if (!str) return '';
  str = str.replace(/([A-Z])/g, ' $1').toLowerCase();
  str = str.replace(/[\s_]+/g, '-');
  str = str.replace(/^-+|-+$/g, '');
  str = str.replace(/-+/g, '-');
  return str;
}

{
  plugins: [
    Components({
      resolvers: [
        [
          (name: string) => {
            if (name.startsWith('Van')) {
              const partialName = kebabCase(name.slice(3));
              return {
                from: `vant/lib/${partialName}`,
                sideEffects: `vant/lib/${partialName}/style`,
              };
            }
          },
        ],
      ],
      dts: resolve(root, './types/components.d.ts'),
    }),
  ];
}

如上代码便是 vant2 组件库的自定义解析器,当我们使用 vant2 的组件时,他就会自动给我们生成如下代码,也就形成了按需加载

import Button from 'vant/lib/button';
import 'vant/lib/button/style';

至到这一步其实按需加载已经完美实现了,但是当我们在代码中又使用了 vant 的方法时,我们的按需加载失效了,比如 Toast 方法

import { Toast } from 'vant';

这是因为我们在代码里通过解构的方式,导出方法后,便又进行了全量加载,也就是说只要使用了 unplugin-vue-components 进行按需加载,我们的代码中就不能显示的使用 vant, 这时候就进入正题了,我们需要用 unplugin-auto-import 自动导入 vant 里的方法。

import AutoImport from 'unplugin-auto-import/vite';

{
  plugins: [
    AutoImport({
      imports: [
        {
          'vant/lib/toast': [
            ['default', 'Toast'], // 将默认导出重命名为 Toast
          ],
        },
      ],
    }),
  ];
}

如上配置,我们就实现了 vant 中 Toast 方法的自动引入,他会在 auto-imports.d.ts 生成如下代码

declare global {
  const Toast: (typeof import('vant/lib/toast'))['default'];
}

我们在使用 Toast 时就可以不用引入直接使用了,其实配置很简单,但是这个插件的功能不仅仅如此,下面继续介绍下插件的其他用法

unplugin-auto-import 自动导入插件

  • unplugin-auto-import 是一个强大的 Vite 插件,可以自动导入常用的 API,无需手动 import,让代码更加简洁。
  • unplugin-auto-import 是一个自动导入工具,它可以在构建时自动检测代码中使用的 API,并自动添加相应的 import 语句。这样你就不需要手动导入 ref、computed、watch 等常用的 Vue API 了。

安装

pnpm add -D unplugin-auto-import

使用效果对比

Vue 示例

不使用自动导入(without):

import { computed, ref } from 'vue';

const count = ref(0);
const doubled = computed(() => count.value * 2);

使用自动导入(with):

const count = ref(0);
const doubled = computed(() => count.value * 2);

React 示例

不使用自动导入(without):

import { useState } from 'react'

export function Counter() {
  const [count, setCount] = useState(0)
  return <div>{ count }</div>
}

使用自动导入(with):

export function Counter() {
  const [count, setCount] = useState(0)
  return <div>{ count }</div>
}

安装

npm i -D unplugin-auto-import
# 或
pnpm add -D unplugin-auto-import
# 或
yarn add -D unplugin-auto-import

支持的构建工具

  • ✅ Vite
  • ✅ Rollup
  • ✅ Rolldown
  • ✅ Webpack
  • ✅ Rspack
  • ✅ Nuxt
  • ✅ Vue CLI
  • ✅ Quasar
  • ✅ esbuild
  • ✅ Astro

配置

基础配置

import AutoImport from 'unplugin-auto-import/vite';

export default {
  plugins: [
    AutoImport({
      // 配置选项
    }),
  ],
};

完整配置选项

AutoImport({
  // 要转换的目标文件
  include: [
    /\.[tj]sx?$/, // .ts, .tsx, .js, .jsx
    /\.vue$/,
    /\.vue\?vue/, // .vue
    /\.vue\.[tj]sx?\?vue/, // .vue (vue-loader with experimentalInlineMatchResource enabled)
    /\.md$/, // .md
  ],

  // 要注册的全局导入
  imports: [
    // 预设
    'vue',
    'vue-router',

    // 自定义导入
    {
      '@vueuse/core': [
        // 命名导入
        'useMouse', // import { useMouse } from '@vueuse/core',
        // 别名
        ['useFetch', 'useMyFetch'], // import { useFetch as useMyFetch } from '@vueuse/core',
      ],
      'axios': [
        // 默认导入
        ['default', 'axios'], // import { default as axios } from 'axios',
      ],
      '[package-name]': [
        '[import-names]',
        // 别名
        ['[from]', '[alias]'],
      ],
    },

    // 类型导入示例
    {
      from: 'vue-router',
      imports: ['RouteLocationRaw'],
      type: true,
    },
  ],

  // 要过滤掉的导入字符串或正则表达式数组
  ignore: ['useMouse', 'useFetch'],

  // 通过文件名启用默认模块导出的自动导入(在目录下)
  defaultExportByFilename: false,

  // 扫描目录以自动导入的选项
  dirsScanOptions: {
    filePatterns: ['*.ts'], // 匹配文件的 Glob 模式
    fileFilter: file => file.endsWith('.ts'), // 过滤文件
    types: true, // 启用目录下类型的自动导入
  },

  // 目录下模块导出的自动导入
  // 默认只扫描目录下一级的模块
  dirs: [
    './hooks',
    './composables', // 仅根模块
    './composables/**', // 所有嵌套模块
    // ...
    {
      glob: './hooks',
      types: true, // 启用类型导入
    },
    {
      glob: './composables',
      types: false, // 如果顶级 dirsScanOptions.types 导入已启用,仅禁用此目录
    },
    // ...
  ],

  // 生成对应的 .d.ts 文件的文件路径
  // 当本地安装了 `typescript` 时,默认为 './auto-imports.d.ts'
  // 设置为 `false` 以禁用
  dts: './auto-imports.d.ts',

  // 生成 .d.ts 文件的模式
  // 'overwrite': 用新的类型定义覆盖整个现有的 .d.ts 文件
  // 'append': 仅将新的类型定义追加到现有的 .d.ts 文件,意味着将保留现有的类型定义
  // 默认为 'append'
  dtsMode: 'append',

  // 在生成的 .d.ts 文件中保留原始文件扩展名
  // 设置为 `true` 以保留 .ts 和 .tsx 文件的扩展名
  // 默认为 false
  dtsPreserveExts: false,

  // 在声明文件生成期间要忽略的导入字符串或正则表达式数组
  // 当你需要为函数提供自定义签名时,这可能会很有用
  ignoreDts: ['ignoredFunction', /^ignore_/],

  // Vue 模板内的自动导入
  // 参见 https://github.com/unjs/unimport/pull/15 和 https://github.com/unjs/unimport/pull/72
  vueTemplate: false,

  // Vue 模板内指令的自动导入
  // 参见 https://github.com/unjs/unimport/pull/374
  vueDirectives: undefined,

  // 自定义解析器,与 `unplugin-vue-components` 兼容
  // 参见 https://github.com/antfu/unplugin-auto-import/pull/23/
  resolvers: [
    /* ... */
  ],

  // 在 Vite 的 `optimizeDeps` 选项中包含自动导入的包
  // 建议启用
  viteOptimizeDeps: true,

  // 在其他导入的末尾注入导入
  injectAtEnd: true,

  // 生成对应的 .eslintrc-auto-import.json 文件
  // eslint globals 文档 - https://eslint.org/docs/user-guide/configuring/language-options#specifying-globals
  eslintrc: {
    enabled: false, // 默认 `false`
    // 提供以 `.mjs` 或 `.cjs` 结尾的路径以生成相应格式的文件
    filepath: './.eslintrc-auto-import.json', // 默认 `./.eslintrc-auto-import.json`
    globalsPropValue: true, // 默认 `true`, (true | false | 'readonly' | 'readable' | 'writable' | 'writeable')
  },

  // 生成对应的 .biomelintrc-auto-import.json 文件
  // biomejs extends 文档 - https://biomejs.dev/guides/how-biome-works/#the-extends-option
  biomelintrc: {
    enabled: false, // 默认 `false`
    filepath: './.biomelintrc-auto-import.json', // 默认 `./.biomelintrc-auto-import.json`
  },

  // 将 unimport 项保存到 JSON 文件中,供其他工具使用
  dumpUnimportItems: './auto-imports.json', // 默认 `false`
});

配置选项说明

include

要转换的目标文件,支持正则表达式数组。

include: [
  /\.[tj]sx?$/, // .ts, .tsx, .js, .jsx
  /\.vue$/,
  /\.md$/,
];

imports

要注册的全局导入,可以是字符串(预设)或对象(自定义导入)。

预设导入:

imports: ['vue', 'vue-router', 'pinia'];

自定义导入:

imports: [
  {
    '@vueuse/core': [
      'useMouse',
      'useFetch',
      ['useFetch', 'useMyFetch'], // 别名
    ],
    'axios': [
      ['default', 'axios'], // 默认导入
    ],
  },
];

类型导入:

imports: [
  {
    from: 'vue-router',
    imports: ['RouteLocationRaw'],
    type: true, // 仅导入类型
  },
];

dirs

自动扫描目录下的模块导出。

dirs: [
  './hooks', // 仅根模块
  './composables/**', // 所有嵌套模块
  {
    glob: './utils',
    types: true, // 启用类型导入
  },
];

dts

生成 TypeScript 类型声明文件的路径。

dts: './auto-imports.d.ts'; // 默认值
dts: false; // 禁用类型生成

dtsMode

生成类型声明文件的模式。

  • 'append' - 追加模式(默认),保留现有类型定义
  • 'overwrite' - 覆盖模式,完全覆盖现有文件

eslintrc

生成 ESLint 配置文件,用于告诉 ESLint 哪些全局变量是可用的。

eslintrc: {
  enabled: true,
  filepath: './.eslintrc-auto-import.json',
  globalsPropValue: true,
}

resolvers

自定义解析器,与 unplugin-vue-components 兼容。

resolvers: [
  // 自定义解析器
];

使用示例

Vue 3 示例

<script setup lang="ts">
// ✅ 无需手动导入
const count = ref(0);
const doubled = computed(() => count.value * 2);

watch(count, newVal => {
  console.log('count changed:', newVal);
});

onMounted(() => {
  console.log('组件已挂载');
});
</script>

使用 VueUse

// ✅ 无需手动导入
const { x, y } = useMouse();
const debouncedFn = useDebounceFn(() => {
  // ...
}, 300);

使用自定义目录导出

// 假设 ./utils/helper.ts 导出了以下函数
export const formatDate = (date: Date) => {
  /* ... */
};
export const formatCurrency = (amount: number) => {
  /* ... */
};

// 在组件中直接使用,无需导入
const formatted = formatDate(new Date());
const price = formatCurrency(100);

优势

  1. 减少样板代码:不需要手动导入常用的 API
  2. 提高开发效率:专注于业务逻辑,而不是导入语句
  3. 类型安全:自动生成 TypeScript 类型声明
  4. 统一管理:集中配置,便于维护
  5. 按需导入:只导入实际使用的 API

注意事项

  1. 首次使用需要重启开发服务器:配置更改后需要重启才能生效
  2. 类型声明文件:确保生成的 auto-imports.d.ts 文件被 TypeScript 识别
  3. IDE 支持:某些 IDE 可能需要重启才能识别自动导入的类型
  4. ESLint 配置:如果启用了 eslintrc,需要在 ESLint 配置中引用生成的文件

参考链接

基于 Vant4 的自定义 Resolver 分析

源码如下

export interface VantResolverOptions {
  /**
   * Whether to automatically import the corresponding styles of the components.
   *
   * @default true
   */
  importStyle?:
    | boolean
    | 'css'
    /** Compatible with Vant 2.x / 3.x */
    | 'less';

  /**
   * Set the referenced module type.
   *
   * @default 'esm'
   */
  module?: 'esm' | 'cjs';

  /**
   * @deprecated Please use `module` option instead.
   */
  ssr?: boolean;

  /**
   * exclude components or API that do not require automatic import
   *
   * @default []
   */
  exclude?: string[];
}

export type VantImportsOptions = Pick<VantResolverOptions, 'module' | 'ssr'>;

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

function getModuleType(options: VantResolverOptions): string {
  const { ssr, module = 'esm' } = options;

  // compatible with the deprecated `ssr` option
  if (ssr !== undefined) {
    return ssr ? 'lib' : 'es';
  }

  return module === 'cjs' ? 'lib' : 'es';
}

function getSideEffects(dirName: string, options: VantResolverOptions) {
  const { importStyle = true } = options;

  if (!importStyle) {
    return;
  }

  const moduleType = getModuleType(options);

  if (importStyle === 'less') return `vant/${moduleType}/${dirName}/style/less`;

  return `vant/${moduleType}/${dirName}/style/index`;
}

function getAPIMap() {
  const apiMap = new Map<string, string>();

  const api = {
    dialog: [
      'showDialog',
      'closeDialog',
      'showConfirmDialog',
      'setDialogDefaultOptions',
      'resetDialogDefaultOptions',
    ],
    imagePreview: ['showImagePreview'],
    notify: [
      'showNotify',
      'closeNotify',
      'setNotifyDefaultOptions',
      'resetNotifyDefaultOptions',
    ],
    toast: [
      'showToast',
      'closeToast',
      'showFailToast',
      'showLoadingToast',
      'showSuccessToast',
      'allowMultipleToast',
      'setToastDefaultOptions',
      'resetToastDefaultOptions',
    ],
  };

  Object.entries(api).forEach(([importName, apiList]) => {
    apiList.forEach(api => {
      apiMap.set(api, importName);
    });
  });

  return apiMap;
}

export function VantResolver(options: VantResolverOptions = {}) {
  const moduleType = getModuleType(options);
  const apiMap = getAPIMap();

  return {
    type: 'component' as const,

    resolve: (name: string) => {
      if (name.startsWith('Van')) {
        const partialName = name.slice(3);
        if (!options.exclude?.includes(partialName)) {
          return {
            name: partialName,
            from: `vant/${moduleType}`,
            sideEffects: getSideEffects(kebabCase(partialName), options),
          };
        }
      }

      // import API
      if (apiMap.has(name) && !options.exclude?.includes(name)) {
        const partialName = apiMap.get(name)!;
        return {
          name,
          from: `vant/${moduleType}`,
          sideEffects: getSideEffects(kebabCase(partialName), options),
        };
      }
    },
  };
}

export function VantImports(options: VantImportsOptions = {}) {
  const moduleType = getModuleType(options);

  return {
    [`vant/${moduleType}`]: [...getAPIMap().keys()],
  };
}

核心接口定义

export interface VantResolverOptions {
  /**
   * 是否自动导入组件对应的样式
   * @default true
   */
  importStyle?: boolean | 'css' | 'less';

  /**
   * 设置引用的模块类型
   * @default 'esm'
   */
  module?: 'esm' | 'cjs';

  /**
   * @deprecated 请使用 `module` 选项代替
   */
  ssr?: boolean;

  /**
   * 排除不需要自动导入的组件或 API
   * @default []
   */
  exclude?: string[];
}

工具函数

1. kebabCase - 转换组件名

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

作用:将驼峰命名转换为短横线命名,用于生成样式文件路径。

示例:

  • Button → button
  • ButtonGroup → button-group
  • ImagePreview → image-preview

2. getModuleType - 获取模块类型

function getModuleType(options: VantResolverOptions): string {
  const { ssr, module = 'esm' } = options;

  // 兼容已废弃的 `ssr` 选项
  if (ssr !== undefined) {
    return ssr ? 'lib' : 'es';
  }

  return module === 'cjs' ? 'lib' : 'es';
}

作用:根据配置确定使用 ESM 还是 CJS 模块。

返回值:

  • 'es' - ESM 模块(默认)
  • 'lib' - CJS 模块

3. getSideEffects - 获取样式文件路径

function getSideEffects(dirName: string, options: VantResolverOptions) {
  const { importStyle = true } = options;

  if (!importStyle) {
    return;
  }

  const moduleType = getModuleType(options);

  if (importStyle === 'less') {
    return `vant/${moduleType}/${dirName}/style/less`;
  }

  return `vant/${moduleType}/${dirName}/style/index`;
}

作用:生成组件样式文件的导入路径。

示例:

  • button → vant/es/button/style/index(CSS)
  • button → vant/es/button/style/less(Less)

4. getAPIMap - 创建 API 映射表

function getAPIMap() {
  const apiMap = new Map<string, string>();

  const api = {
    dialog: [
      'showDialog',
      'closeDialog',
      'showConfirmDialog',
      'setDialogDefaultOptions',
      'resetDialogDefaultOptions',
    ],
    imagePreview: ['showImagePreview'],
    notify: [
      'showNotify',
      'closeNotify',
      'setNotifyDefaultOptions',
      'resetNotifyDefaultOptions',
    ],
    toast: [
      'showToast',
      'closeToast',
      'showFailToast',
      'showLoadingToast',
      'showSuccessToast',
      'allowMultipleToast',
      'setToastDefaultOptions',
      'resetToastDefaultOptions',
    ],
  };

  Object.entries(api).forEach(([importName, apiList]) => {
    apiList.forEach(api => {
      apiMap.set(api, importName);
    });
  });

  return apiMap;
}

作用:创建 API 方法名到导入模块名的映射关系。

映射关系:

  • showToast → toast
  • showDialog → dialog
  • showImagePreview → imagePreview

VantResolver - 组件 Resolver

export function VantResolver(options: VantResolverOptions = {}) {
  const moduleType = getModuleType(options);
  const apiMap = getAPIMap();

  return {
    type: 'component' as const,
    resolve: (name: string) => {
      // 处理组件(以 Van 开头)
      if (name.startsWith('Van')) {
        const partialName = name.slice(3);

        if (!options.exclude?.includes(partialName)) {
          return {
            name: partialName,
            from: `vant/${moduleType}`,
            sideEffects: getSideEffects(kebabCase(partialName), options),
          };
        }
      }

      // 处理 API 方法
      if (apiMap.has(name) && !options.exclude?.includes(name)) {
        const partialName = apiMap.get(name)!;
        return {
          name,
          from: `vant/${moduleType}`,
          sideEffects: getSideEffects(kebabCase(partialName), options),
        };
      }
    },
  };
}

使用方式:

import { VantResolver } from 'unplugin-vue-components/resolvers';
import Components from 'unplugin-vue-components/vite';

export default {
  plugins: [
    Components({
      resolvers: [
        VantResolver({
          importStyle: true, // 自动导入样式
          module: 'esm', // 使用 ESM 模块
        }),
      ],
    }),
  ],
};

工作原理:

  1. 组件解析:当使用 <VanButton> 时,会转换为:

    import { Button } from 'vant/es';
    import 'vant/es/button/style/index';
    
  2. API 解析:当使用 showToast 时,会转换为:

    import { showToast } from 'vant/es/toast';
    import 'vant/es/toast/style/index';
    

VantImports - 自动导入配置

export function VantImports(options: VantImportsOptions = {}) {
  const moduleType = getModuleType(options);

  return {
    [`vant/${moduleType}`]: [...getAPIMap().keys()],
  };
}

使用方式:

import AutoImport from 'unplugin-auto-import/vite';
import { VantImports } from 'unplugin-vue-components/resolvers';

export default {
  plugins: [
    AutoImport({
      imports: [
        VantImports({
          module: 'esm',
        }),
      ],
    }),
  ],
};

效果:自动导入所有 Vant API 方法,无需手动 import。

// ✅ 无需手动导入
showToast('提示信息');
showDialog({ message: '确认操作' });
showImagePreview(['url1', 'url2']);

完整配置示例

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import Components from 'unplugin-vue-components/vite';
import AutoImport from 'unplugin-auto-import/vite';
import { VantResolver, VantImports } from 'unplugin-vue-components/resolvers';

export default defineConfig({
  plugins: [
    vue(),
    Components({
      resolvers: [
        VantResolver({
          importStyle: true,
          module: 'esm',
          exclude: ['Toast'], // 排除 Toast 组件
        }),
      ],
    }),
    AutoImport({
      imports: [
        'vue',
        VantImports({
          module: 'esm',
        }),
      ],
    }),
  ],
});

关键设计点分析

  1. 组件命名约定:使用 Van 前缀识别组件(如 VanButton)
  2. API 映射机制:通过 getAPIMap 建立方法名到模块的映射
  3. 样式自动导入:通过 sideEffects 自动导入组件样式
  4. 模块类型支持:支持 ESM 和 CJS 两种模块格式
  5. 排除机制:支持通过 exclude 排除不需要的组件或 API

为其他组件库创建 Resolver

基于 Vant4 resolver 的实现,可以为其他组件库创建类似的 resolver:

export function CustomResolver(options = {}) {
  return {
    type: 'component' as const,
    resolve: (name: string) => {
      // 1. 识别组件命名规则
      if (name.startsWith('Custom')) {
        const componentName = name.slice(6);
        return {
          name: componentName,
          from: `custom-lib/es`,
          sideEffects: `custom-lib/es/${kebabCase(componentName)}/style`,
        };
      }

      // 2. 处理 API 方法
      const apiMap = new Map([
        ['showMessage', 'message'],
        ['showConfirm', 'confirm'],
      ]);

      if (apiMap.has(name)) {
        return {
          name,
          from: `custom-lib/es/${apiMap.get(name)}`,
        };
      }
    },
  };
}

总结

Vant4 resolver 的实现展示了如何:

  • 通过命名约定识别组件
  • 建立 API 方法到模块的映射关系
  • 自动导入组件样式
  • 支持多种模块格式
  • 提供灵活的配置选项

理解这些原理后,可以为任何组件库创建自定义的 resolver,实现真正的按需加载。

💬留言板0

还没有留言,来发表第一条吧~

PAGE LOAD / LIVEunplugin-auto-import 自动导入插件配置详解加载中