unplugin-auto-import 自动导入插件配置详解
前言
使用 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);
优势
- 减少样板代码:不需要手动导入常用的 API
- 提高开发效率:专注于业务逻辑,而不是导入语句
- 类型安全:自动生成 TypeScript 类型声明
- 统一管理:集中配置,便于维护
- 按需导入:只导入实际使用的 API
注意事项
- 首次使用需要重启开发服务器:配置更改后需要重启才能生效
- 类型声明文件:确保生成的
auto-imports.d.ts文件被 TypeScript 识别 - IDE 支持:某些 IDE 可能需要重启才能识别自动导入的类型
- 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→buttonButtonGroup→button-groupImagePreview→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→toastshowDialog→dialogshowImagePreview→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 模块
}),
],
}),
],
};
工作原理:
组件解析:当使用
<VanButton>时,会转换为:import { Button } from 'vant/es'; import 'vant/es/button/style/index';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',
}),
],
}),
],
});
关键设计点分析
- 组件命名约定:使用
Van前缀识别组件(如VanButton) - API 映射机制:通过
getAPIMap建立方法名到模块的映射 - 样式自动导入:通过
sideEffects自动导入组件样式 - 模块类型支持:支持 ESM 和 CJS 两种模块格式
- 排除机制:支持通过
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,实现真正的按需加载。

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