Auto import APIs on-demand for Vite, Webpack, Rspack, Rollup and esbuild. With TypeScript support. Powered by unplugin.
项目中的 js 模块可以使用 unplugin-auto-import 来自动引入。
比如 vue 的一些 api,ref,reactive 等,可以不用手动导入。但要明白插件只是帮助我们在编译时自动添加 import,而不是代码中可以没有 import 导入。
看看 element-plus 的自动引入配置:
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
export default defineConfig(({ command }) => {
return {
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver({ importStyle: "sass" })],
eslintrc: {
enabled: true
}
}),
Components({
resolvers: [ElementPlusResolver({ importStyle: "sass" })]
})
]
}
}
安装:
pnpm i -D unplugin-auto-import
因为上面是 vite 中使用,因此引入 unplugin-vue-components 的 vite 插件版本unplugin-vue-components/vite
其他常见构建工具引入:webpack、vue cli
// webpack.config.js & vue.config.js
module.exports = {
/* ... */
plugins: [
require('unplugin-auto-import/webpack').default({ /* options */ }),
],
}
AutoImport({
// targets to transform
include: [
/\.[tj]sx?$/, // .ts, .tsx, .js, .jsx
/\.vue$/,
/\.vue\?vue/, // .vue
/\.md$/, // .md
],
// global imports to register
imports: [
// presets
'vue',
'vue-router',
// custom
{
'@vueuse/core': [
// named imports
'useMouse', // import { useMouse } from '@vueuse/core',
// alias
['useFetch', 'useMyFetch'], // import { useFetch as useMyFetch } from '@vueuse/core',
],
'axios': [
// default imports
['default', 'axios'], // import { default as axios } from 'axios',
],
'[package-name]': [
'[import-names]',
// alias
['[from]', '[alias]'],
],
},
// example type import
{
from: 'vue-router',
imports: ['RouteLocationRaw'],
type: true,
},
],
// Enable auto import by filename for default module exports under directories
defaultExportByFilename: false,
// Auto import for module exports under directories
// by default it only scan one level of modules under the directory
dirs: [
// './hooks',
// './composables' // only root modules
// './composables/**', // all nested modules
// ...
],
// Filepath to generate corresponding .d.ts file.
// Defaults to './auto-imports.d.ts' when `typescript` is installed locally.
// Set `false` to disable.
dts: './auto-imports.d.ts',
// Auto import inside Vue template
// see https://github.com/unjs/unimport/pull/15 and https://github.com/unjs/unimport/pull/72
vueTemplate: false,
// Custom resolvers, compatible with `unplugin-vue-components`
// see https://github.com/antfu/unplugin-auto-import/pull/23/
resolvers: [
/* ... */
],
// Inject the imports at the end of other imports
injectAtEnd: true,
// Generate corresponding .eslintrc-auto-import.json file.
// eslint globals Docs - https://eslint.org/docs/user-guide/configuring/language-options#specifying-globals
eslintrc: {
enabled: false, // Default `false`
filepath: './.eslintrc-auto-import.json', // Default `./.eslintrc-auto-import.json`
globalsPropValue: true, // Default `true`, (true | false | 'readonly' | 'readable' | 'writable' | 'writeable')
},
})
上面的配置很完整,但我们一般关注其中几个就可以了。
对于一些常见的库,插件已经内置了它的自动导入规则,如:vue、vue-router、pinia、react 等。
在 imports 数组里添加字符串就代表使用预设:
AutoImport({
imports: ["vue", "vue-router", "pinia"],
}
有些库没有预设,我们也想自动导入该怎么办?比如 axios、loadsh、vueuse。
使用对象语法,自定义导入规则:包名: 数组
AutoImport({
imports: [
"vue",
// 自定义导入规则
{
'包名 ikun': [
// 命名导入,相当于会自动添加 import { sing } from "ikun";
'sing',
// 设置别名导入,自动添加 import { sing as singFromIkun } from "ikun";
['sing', 'singFromIkun'],
],
},
]
})
注意:对于默认导出的库,export default,自定义导入规则要写成别名导入的形式,导入的字段为 default。
比如 axios,我们正常使用是这样导入:
import axios from "axios"
因为 axios 库是 export default 默认导出的,所以我们不会 import { axios } from "axios";
。
对于这种默认导出,AutoImport 自动导入规则要写成别名导入:
AutoImport({
imports: [
"vue",
// 自定义导入规则
{
'包名 ikun': [
// 命名导入,相当于会自动添加 import { sing } from "ikun";
'sing',
// 设置别名导入,自动添加 import { sing as singFromIkun } from "ikun";
['sing', 'singFromIkun'],
],
'axios': [
// default imports
['default', 'axios'], // import { default as axios } from 'axios',
],
},
]
})
说白了,压根没有import axios from "axios";
的写法,它只是import { default as axios} from "axios";
的语法糖。因此在这种编译配置的情况下,需要严格按照规范来定义。
补充一个自动导入 vueuse:
AutoImport({
imports: [
"vue",
{
'@vueuse/core': [
// named imports
'useMouse', // import { useMouse } from '@vueuse/core',
// alias
['useFetch', 'useMyFetch'], // import { useFetch as useMyFetch } from '@vueuse/core',
]
}
]
})
上面都是 js 引入,如果是在 ts 中这样引入,会没有类型,vue-tsc 类型检查过不了,build 构建失败。
导入 ts 类型的规则:
{
from: 'vue-router',
imports: ['RouteLocationRaw'],
type: true,
}
其实上面这种写法才是导入规则的完整写法,包名: 数组
,字符串
这些都是简写,就和 webpack loader 配置一样。
这个对象就是在模拟import { xxx } from "xxx";
,多了一个 type 为 true,就是标明了导入的为类型,等价于:import type { Xxx as Hhh} from "xxx";
这时候就要注意 type 的位置了:
AutoImport({
imports: [
"vue",
// {
// axios: ["default", "axios"]
// },
{
from: "axios",
imports: [["default", "axios"], "AxiosStatic"],
type: true
}
],
})
如上述写法就是错误的,它编译后就是:
import type { default as axios, AxiosStatic} from "axios";
显然 axios 不是类型,正确写法应该是:
import { default as axios, type AxiosStatic} from "axios";
那怎么把 type 和 AxiosStatic 类型绑定到一起,编译出正确的 import 语句呢?
AxiosStatic 继续写成对象形式即可:
AutoImport({
imports: [
// {
// axios: ["default", "axios"]
// },
// {
// from: "axios",
// imports: [["default", "axios"], "AxiosStatic"],
// type: true
// }
{
from: "axios",
imports: [ ["default", "axios"], {
name: "AxiosStatic",
type: true
}],
}
]
})
可以看到 vue 自动导入配置的预设,内部写法就是如上诉一样:
{
"from": "vue",
"imports": [
"EffectScope",
"computed",
"createApp",
"customRef",
"defineAsyncComponent",
"defineComponent",
"effectScope",
"getCurrentInstance",
"getCurrentScope",
"h",
"inject",
"isProxy",
"isReactive",
"isReadonly",
"isRef",
"markRaw",
"nextTick",
"onActivated",
"onBeforeMount",
"onBeforeUnmount",
"onBeforeUpdate",
"onDeactivated",
"onErrorCaptured",
"onMounted",
"onRenderTracked",
"onRenderTriggered",
"onScopeDispose",
"onServerPrefetch",
"onUnmounted",
"onUpdated",
"provide",
"reactive",
"readonly",
"ref",
"resolveComponent",
"shallowReactive",
"shallowReadonly",
"shallowRef",
"toRaw",
"toRef",
"toRefs",
"toValue",
"triggerRef",
"unref",
"useAttrs",
"useCssModule",
"useCssVars",
"useSlots",
"watch",
"watchEffect",
"watchPostEffect",
"watchSyncEffect",
{
"name": "Component",
"type": true
},
{
"name": "ComponentPublicInstance",
"type": true
},
{
"name": "ComputedRef",
"type": true
},
{
"name": "ExtractDefaultPropTypes",
"type": true
},
{
"name": "ExtractPropTypes",
"type": true
},
{
"name": "ExtractPublicPropTypes",
"type": true
},
{
"name": "InjectionKey",
"type": true
},
{
"name": "PropType",
"type": true
},
{
"name": "Ref",
"type": true
},
{
"name": "VNode",
"type": true
},
{
"name": "WritableComputedRef",
"type": true
}
]
}
配置 import 后能自动导入 ts 类型了,但那是在编译时才会导入,在 vscode 编辑器里写代码时,类型可没导入。在 ts 看来,你使用了未定义的变量,会报错。
这其实就和之前按需引入 elmessage 的问题一样:
按需引入 ElMessage,没有样式且类型检查失败
这时就需要对自动导入的内容进行类型声明:
AutoImport({
dts: true // or a custom path
})
开启dts
配置后,就会自动生成auto-imports.d.ts
文件,进行全局类型声明。默认生成在根目录。
另外要确保tsconfig.json
中 include 配置项包含了这个类型声明文件,好让 ts 读取里面的类型。
各种开源组件库按需引入的时候,就默认就打开了这个配置,所以根目录会出现这么个文件。
上面 axios 配置,自动生成的类型声明:
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {}
declare global {
const axios: typeof import('axios')['default']
}
// for type re-export
declare global {
// @ts-ignore
export type { AxiosStatic } from 'axios'
}
ts 通过了禁止使用未定义变量的检查,但 eslint 检查未通过。eslint 的检查中也会认为你使用了未定义的变量。解决办法自然是在 eslint 的配置中声明一下这些是全局变量,可以未导入直接用。
unplugin 官方认为如果使用了 ts,就不必再让 eslint 来检查是否使用未定义的变量了,建议把no-undef
这条规则关掉。
💡 When using TypeScript, we recommend to disable
no-undef
rule directly as TypeScript already check for them and you don’t need to worry about this.
如果开着双重保险,就要让 eslint 识别自动导入的内容为全局变量:
.eslintrc-auto-import.json
AutoImport({
eslintrc: {
enabled: true, // <-- this
},
})
{
"globals": {
"AxiosStatic": true,
"axios": true
}
}
// .eslintrc.js
module.exports = {
extends: [
'./.eslintrc-auto-import.json',
],
}
比如项目中有一个 utils 文件夹,如果想自动引入里面的文件,则可以用 dirs 来配置
AutoImport({
// Auto import for module exports under directories
// by default it only scan one level of modules under the directory
dirs: [
// './hooks',
// './composables' // only root modules
// './composables/**', // all nested modules
// ...
"./src/utils/**"
]
})
https://github.com/unplugin/unplugin-vue-components
unplugin-vue-components 这玩意是用来专门引入 vue SFC 文件的,相当于 unplugin-auot-import 的一个子集。作者都是 antfu。
// vite.config.ts
import Components from 'unplugin-vue-components/vite'
export default defineConfig({
plugins: [
Components({ /* options */ }),
],
})
// webpack.config.js & vue.config.js
module.exports = {
/* ... */
plugins: [
require('unplugin-vue-components/webpack').default({ /* options */ }),
],
}
Components({
// relative paths to the directory to search for components.
dirs: ['src/components'],
// valid file extensions for components.
extensions: ['vue'],
// Glob patterns to match file names to be detected as components.
// When specified, the `dirs` and `extensions` options will be ignored.
globs: ['src/components/*.{vue}'],
// search for subdirectories
deep: true,
// resolvers for custom components
resolvers: [],
// generate `components.d.ts` global declarations,
// also accepts a path for custom filename
// default: `true` if package typescript is installed
dts: false,
// Allow subdirectories as namespace prefix for components.
directoryAsNamespace: false,
// Collapse same prefixes (camel-sensitive) of folders and components
// to prevent duplication inside namespaced component name.
// works when `directoryAsNamespace: true`
collapseSamePrefixes: false,
// Subdirectory paths for ignoring namespace prefixes.
// works when `directoryAsNamespace: true`
globalNamespaces: [],
// auto import for directives
// default: `true` for Vue 3, `false` for Vue 2
// Babel is needed to do the transformation for Vue 2, it's disabled by default for performance concerns.
// To install Babel, run: `npm install -D @babel/parser`
directives: true,
// Transform path before resolving
importPathTransform: v => v,
// Allow for components to override other components with the same name
allowOverrides: false,
// filters for transforming targets
include: [/\.vue$/, /\.vue\?vue/],
exclude: [/[\\/]node_modules[\\/]/, /[\\/]\.git[\\/]/, /[\\/]\.nuxt[\\/]/],
// Vue version of project. It will detect automatically if not specified.
// Acceptable value: 2 | 2.7 | 3
version: 2.7,
// Only provide types of components in library (registered globally)
types: []
})
该插件内置了大多数流行库解析器,可以直接开箱即用。
并且会在根目录生成一个ui库组件以及指令路径components.d.ts
文件
// vite.config.js
import { defineConfig } from 'vite'
import Components from 'unplugin-vue-components/vite'
import {
ElementPlusResolver,
AntDesignVueResolver,
VantResolver,
HeadlessUiResolver,
ElementUiResolver
} from 'unplugin-vue-components/resolvers'
export default defineConfig({
plugins: [
Components({
// ui库解析器,也可以自定义
resolvers: [
ElementPlusResolver(),
AntDesignVueResolver(),
VantResolver(),
HeadlessUiResolver(),
ElementUiResolver()
]
})
]
})
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
declare module 'vue' {
export interface GlobalComponents {
ABreadcrumb: typeof import('ant-design-vue/es')['Breadcrumb']
AButton: typeof import('ant-design-vue/es')['Button']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
SvgIcon: typeof import('./src/components/SvgIcon/index.vue')['default']
SwitchTheme: typeof import('./src/components/SwitchTheme/SwitchTheme.vue')['default']
}
}
默认情况下,自己写的组件放在src/components
路径下是会被自动引入的。
比如上面 components.d.ts 文件中的 SvgIcon 和 SwitchTheme 就是自己写的组件。
当然,也可以进一步配置自动引入的情况:
// vite.config.js
import { defineConfig } from 'vite'
import Components from 'unplugin-vue-components/vite'
export default defineConfig({
plugins: [
Components({
// 指定组件位置,默认是src/components
dirs: ['src/components'],
// ui库解析器
// resolvers: [ElementPlusResolver()],
extensions: ['vue'],
// 配置文件生成位置
dts: 'src/components.d.ts'
})
]
})
如果是自己开发的组件库,为了让它支持自动按需导入,就需要自己编写解析器。
Components({
resolvers: [
// example of importing Vant
(componentName) => {
// where `componentName` is always CapitalCase
if (componentName.startsWith('Van'))
return { name: componentName.slice(3), from: 'vant' }
},
],
})
resolvers 数组里可以传入一个函数,这个函数会在编译时不断执行。
函数接收组件名,并返回一个和 unplugin-auto-import 插件中 imports 配置一样的配置对象,这个对象就是 import 语句的描述对象,最终依据它生成导入语句。
因此所谓的解析器,功能就是根据组件名映射成 import 导入语句。
假设组件库的前缀为 ikun,比如按钮组件: ikun-button
<!-- 在 SFC 中使用了组件 -->
<ikun-button>按钮</ikun-button>
const IkunResolver = componentName => {
console.log(componentName) // IkunButton
// 组件名有很多,通过前缀过滤出本组件库的组件名
if (componentName.startsWith("Ikun")) {
// import 引入规则对象:
// 等价于 import { IkunButton } from "ikun-ui";
return {
name: componentName,
from: "ikun-ui"
}
}
return null
}
Components({
resolvers: [
IkunResolver()
],
})
还有一个细节问题:组件库中组件的样式可能是单独一个文件的,不一定在 .vue 文件中。
比如这样:
ikun-button
|
|—— style
| |—— index.css
|
|—— index.vue
上面的 import 配置对象写法只会引入 SFC 文件,并不会引入样式文件。
解决办法:副作用配置项。引入组件的副作用是会引入另一个文件,我们让这个文件是样式文件。
sideEffects
const IkunResolver = componentName => {
if (componentName.startsWith("Ikun")) {
// 等价于:
// import { IkunButton } from "ikun-ui";
// import "ikun-ui/ikun-button/style/index.css";
return {
name: componentName,
from: "ikun-ui",
sideEffects: `ikun-ui/${componentName}/style/index.css`
}
}
return null
}
// src/core/resolvers/element-plus.ts
function getSideEffectsLegacy(partialName, options) {
const { importStyle } = options;
if (!importStyle)
return;
if (importStyle === "sass") {
return [
"element-plus/packages/theme-chalk/src/base.scss",
`element-plus/packages/theme-chalk/src/${partialName}.scss`
];
} else if (importStyle === true || importStyle === "css") {
return [
"element-plus/lib/theme-chalk/base.css",
`element-plus/lib/theme-chalk/el-${partialName}.css`
];
}
}
function getSideEffects2(dirName, options) {
const { importStyle, ssr, nightly } = options;
const themeFolder = nightly ? "@element-plus/nightly/theme-chalk" : "element-plus/theme-chalk";
const esComponentsFolder = nightly ? "@element-plus/nightly/es/components" : "element-plus/es/components";
if (importStyle === "sass") {
return ssr ? [`${themeFolder}/src/base.scss`, `${themeFolder}/src/${dirName}.scss`] : [`${esComponentsFolder}/base/style/index`, `${esComponentsFolder}/${dirName}/style/index`];
} else if (importStyle === true || importStyle === "css") {
return ssr ? [`${themeFolder}/base.css`, `${themeFolder}/el-${dirName}.css`] : [`${esComponentsFolder}/base/style/css`, `${esComponentsFolder}/${dirName}/style/css`];
}
}
function resolveComponent(name, options) {
if (options.exclude && name.match(options.exclude))
return;
if (!name.match(/^El[A-Z]/))
return;
if (name.match(/^ElIcon.+/)) {
return {
name: name.replace(/^ElIcon/, ""),
from: "@element-plus/icons-vue"
};
}
const partialName = kebabCase(name.slice(2));
const { version, ssr, nightly } = options;
if (compare(version, "1.1.0-beta.1", ">=") || nightly) {
return {
name,
from: `${nightly ? "@element-plus/nightly" : "element-plus"}/${ssr ? "lib" : "es"}`,
sideEffects: getSideEffects2(partialName, options)
};
} else if (compare(version, "1.0.2-beta.28", ">=")) {
return {
from: `element-plus/es/el-${partialName}`,
sideEffects: getSideEffectsLegacy(partialName, options)
};
} else {
return {
from: `element-plus/lib/el-${partialName}`,
sideEffects: getSideEffectsLegacy(partialName, options)
};
}
}
function resolveDirective(name, options) {
if (!options.directives)
return;
const directives2 = {
Loading: { importName: "ElLoadingDirective", styleName: "loading" },
Popover: { importName: "ElPopoverDirective", styleName: "popover" },
InfiniteScroll: { importName: "ElInfiniteScroll", styleName: "infinite-scroll" }
};
const directive = directives2[name];
if (!directive)
return;
const { version, ssr, nightly } = options;
if (compare(version, "1.1.0-beta.1", ">=") || nightly) {
return {
name: directive.importName,
from: `${nightly ? "@element-plus/nightly" : "element-plus"}/${ssr ? "lib" : "es"}`,
sideEffects: getSideEffects2(directive.styleName, options)
};
}
}
var noStylesComponents = ["ElAutoResizer"];
function ElementPlusResolver(options = {}) {
let optionsResolved;
async function resolveOptions() {
if (optionsResolved)
return optionsResolved;
optionsResolved = __spreadValues({
ssr: false,
version: await getPkgVersion("element-plus", "2.2.2"),
importStyle: "css",
directives: true,
exclude: void 0,
noStylesComponents: options.noStylesComponents || [],
nightly: false
}, options);
return optionsResolved;
}
return [
{
type: "component",
resolve: async (name) => {
const options2 = await resolveOptions();
if ([...options2.noStylesComponents, ...noStylesComponents].includes(name))
return resolveComponent(name, __spreadProps(__spreadValues({}, options2), { importStyle: false }));
else
return resolveComponent(name, options2);
}
},
{
type: "directive",
resolve: async (name) => {
return resolveDirective(name, await resolveOptions());
}
}
];
}
// src/core/resolvers/vant.ts
var moduleType = isSSR ? "lib" : "es";
function getSideEffects4(dirName, options) {
const { importStyle = true } = options;
if (!importStyle || isSSR)
return;
if (importStyle === "less")
return `vant/${moduleType}/${dirName}/style/less`;
if (importStyle === "css")
return `vant/${moduleType}/${dirName}/style/index`;
return `vant/${moduleType}/${dirName}/style/index`;
}
function VantResolver(options = {}) {
return {
type: "component",
resolve: (name) => {
if (name.startsWith("Van")) {
const partialName = name.slice(3);
return {
name: partialName,
from: `vant/${moduleType}`,
sideEffects: getSideEffects4(kebabCase(partialName), options)
};
}
}
};
}