前言
相信很多vue开发的同学都应该听说过antfu这号人物,也使用过他开发的系列工具,比如我个人常用的vueuse vitesse unocss等,对于一些懒得引入api的开发者来说(是的,就是我),unplugin-vue-components unplugin-auto-import更是偷懒必备神器,下面我就从使用和原理两方面来浅析unplugin-auto-import插件。
介绍
unplugin-auto-import插件,为 Vite、Webpack、Rollup 和 esbuild 按需自动导入 API,支持TypeScript
使用方法
这章节我们先来简单使用下unplugin-auto-import,对于已经使用过该插件的同学可以先跳过这章节看后面的原理。
准备工程
我们先创建一个vite+vue+typescript工程
pnpm create vite
D:\project>pnpm create vite
Progress: resolved 1, reused 1, downloaded 0, added 1, done
+ √ Project name: ... unimport-fun
+ √ Select a framework: » Vue
+ √ Select a variant: » TypeScript
Scaffolding project in D:\project\unimport-fun...
Done. Now run:cd unimport-funpnpm installpnpm run dev
D:\project>
pnpm install后运行工程
下面有个count按钮,对应的是src/components/HelloWorld.vue,我们查看这个组件,以下是精简后的主要代码:
<script setup lang="ts"> import { ref } from 'vue'
const count = ref(0) </script>
<template><div class="card"><button type="button" @click="count++">count is {{ count }}</button></div>
</template>
就是一个ref响应式数据count,点击按钮+1,这对于学过vue3的应该都不难,觉得理解上有困难的同学可以先去学习下vue3的基础。
引入插件
执行命令安装插件
pnpm add unplugin-auto-import
在vite.config.ts配置插件
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
+ import AutoImport from 'unplugin-auto-import/vite'
// https://vitejs.dev/config/
export default defineConfig({plugins: [vue(),
+ AutoImport()]
})
unplugin-auto-import插件内置了一些预设,预设的作用是不用我们自己去配置,就能使用主流框架的api,比如我希望自动导入vue3的api,是这样使用的:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
// https://vitejs.dev/config/
export default defineConfig({plugins: [vue(),AutoImport({
+ imports: ['vue']})]
})
这时候运行工程,会发现工程中多了个auto-imports.d.ts文件,内容是:
// Generated by 'unplugin-auto-import'
export {}
declare global {const EffectScope: typeof import('vue')['EffectScope']const computed: typeof import('vue')['computed']const createApp: typeof import('vue')['createApp']const customRef: typeof import('vue')['customRef']const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']const defineComponent: typeof import('vue')['defineComponent']const effectScope: typeof import('vue')['effectScope']const getCurrentInstance: typeof import('vue')['getCurrentInstance']const getCurrentScope: typeof import('vue')['getCurrentScope']const h: typeof import('vue')['h']const inject: typeof import('vue')['inject']const isProxy: typeof import('vue')['isProxy']const isReactive: typeof import('vue')['isReactive']const isReadonly: typeof import('vue')['isReadonly']const isRef: typeof import('vue')['isRef']const markRaw: typeof import('vue')['markRaw']const nextTick: typeof import('vue')['nextTick']const onActivated: typeof import('vue')['onActivated']const onBeforeMount: typeof import('vue')['onBeforeMount']const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']const onDeactivated: typeof import('vue')['onDeactivated']const onErrorCaptured: typeof import('vue')['onErrorCaptured']const onMounted: typeof import('vue')['onMounted']const onRenderTracked: typeof import('vue')['onRenderTracked']const onRenderTriggered: typeof import('vue')['onRenderTriggered']const onScopeDispose: typeof import('vue')['onScopeDispose']const onServerPrefetch: typeof import('vue')['onServerPrefetch']const onUnmounted: typeof import('vue')['onUnmounted']const onUpdated: typeof import('vue')['onUpdated']const provide: typeof import('vue')['provide']const reactive: typeof import('vue')['reactive']const readonly: typeof import('vue')['readonly']const ref: typeof import('vue')['ref']const resolveComponent: typeof import('vue')['resolveComponent']const resolveDirective: typeof import('vue')['resolveDirective']const shallowReactive: typeof import('vue')['shallowReactive']const shallowReadonly: typeof import('vue')['shallowReadonly']const shallowRef: typeof import('vue')['shallowRef']const toRaw: typeof import('vue')['toRaw']const toRef: typeof import('vue')['toRef']const toRefs: typeof import('vue')['toRefs']const triggerRef: typeof import('vue')['triggerRef']const unref: typeof import('vue')['unref']const useAttrs: typeof import('vue')['useAttrs']const useCssModule: typeof import('vue')['useCssModule']const useCssVars: typeof import('vue')['useCssVars']const useSlots: typeof import('vue')['useSlots']const watch: typeof import('vue')['watch']const watchEffect: typeof import('vue')['watchEffect']const watchPostEffect: typeof import('vue')['watchPostEffect']const watchSyncEffect: typeof import('vue')['watchSyncEffect']
}
熟悉的人应该会发现这就是vue的所有api,这时候我们把HelloWorld.vue中引入ref的语句去掉:
<script setup lang="ts">
+ // import { ref } from 'vue'
defineProps<{ msg: string }>()
const count = ref(0)
</script>
可以看到,虽然去掉ref的import,但其实还是能正常使用ref的功能
我们这次用computed试试:
<script setup lang="ts">
// import { ref } from 'vue'
defineProps<{ msg: string }>()
const count = ref(0)
+ const doubleCount = computed(() => count.value * 2)
</script>
<template>
+ <h2>{{ doubleCount }}</h2><div class="card"><button type="button" @click="count++">count is {{ count }}</button></div>
</template>
可以看到,ref和computed都能生效
类型问题
如果是用的vscode,会发现ref和computed处会有类型报错:
作为强迫症肯定不能置之不管,其实我们只需要将刚才说的自动生成的auto-import.d.ts,改一下生成的位置,到src目录下即可,我们修改vite.config.ts:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
// https://vitejs.dev/config/
export default defineConfig({plugins: [vue(),AutoImport({imports: ['vue'],
+dts: 'src/auto-imports.d.ts'})]
})
可以看到,重新运行后在src目录下会生成auto-imports.d.ts,并且HelloWorld.vue组件不会报类型错误了:
这时候把根目录下的auto-imports.d.ts删掉即可。
到这里基本使用讲完啦,如果还需要更灵活的操作,可以通过查阅官方文档
原理分析
我们先把unplugin-auto-import的源码克隆到本地:
git clone https://github.com/antfu/unplugin-auto-import.git
浅看下工程目录:
我们是vite工程,所以引入的语句是import AutoImport from 'unplugin-auto-import/vite',可以看到入口文件就是这个vite.ts文件,核心处理在unplugin目录下
可以看到,最终导出一个createUnplugin方法执行的结果,这里可以做个额外拓展,这里使用的unplugin插件,是unplugin-auto-import插件能被webpack,vite,rollup,esbuild使用的关键处理,我们先不深究,我考虑后续写新文来介绍这插件(鸽 鸽 鸽
在unplugin.ts文件中操作不多,主要是围绕ctx变量做个操作,我们先不看这个变量的内容,我们先理解这是一个对象,里面包含了很多方法:
let ctx = createContext(options)
export function createContext(options: Options = {}, root = process.cwd()) {// ...省略return {root,dirs,filter,scanDirs,writeConfigFiles,writeConfigFilesThrottled,transform,generateDTS,generateESLint,}
}
我们先来解析下createUnplugin中返回的对象的各个属性的含义:
| 属性 | 含义 |
|---|---|
| name | 插件的名字 |
| enforce | 控制插件的执行时机,这里的post是指在插件流的靠后执行 |
| transformInclude | 表示哪些文件需要进行转换,用正则表达式控制,在这里是执行ctx.filter(id)方法,其中这个id是指文件名 |
| transform | 执行代码转换处理,可以根据你的设定来令到代码发生改变,比如我希望代码中的var全部变成let,就可以在这里进行处理(简单举个例子,没实现过) |
| buildStart | 在执行build操作的时候执行,在这里是执行ctx.scanDirs()方法 |
| buildEnd | 在build操作执行完成后,生成打包文件前执行,在这里是执行ctx.writeConfigFiles()方法 |
| vite | 针对vite工程的特定钩子,也就是说在这里能使用vite特有的钩子函数,在使用vite工程时会一起执行 |
实例详解
我们以文章中的demo工程作为例子讲解,文本尽量做到通俗易懂。
在vite.config.ts中,我们使用插件的姿势是:
import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
export default defineConfig({plugins: [AutoImport({imports: ['vue'],dts: 'src/auto-imports.d.ts'})]
})
所以插件中的options是:
// userOptions
const options = {imports: ['vue'],dts: 'src/auto-imports.d.ts'
}
...
// plugin usage
export default createUnplugin<Options>((options) => {let ctx = createContext(options)
})
我们重点看ctx的处理过程,希望看到这里的同学可以打开源码跟着我一起看下面的内容,不方便打开或者不想打开的也没关系,在最后我会把ctx生成的处理代码贴到文章最后
ctx实例细节分析
第一部分,处理预设
export function flattenImports(map: Options['imports'], overriding = false): Import[] {const flat: Record<string, Import> = {}/** * map = ['vue'] */toArray(map).forEach((definition) => {if (typeof definition === 'string') {if (!presets[definition])throw new Error(`[auto-import] preset ${definition} not found`)const preset = presets[definition]definition = typeof preset === 'function' ? preset() : preset}/** * definition = { *vue: [ *'ref', *'computed' *] *} */for (const mod of Object.keys(definition)) {// mod = vuefor (const id of definition[mod]) {/** * id=ref * id=computed */const meta = {from: mod,} as Importlet name: stringif (Array.isArray(id)) {name = id[1]meta.name = id[0]meta.as = id[1]}else {name = idmeta.name = idmeta.as = id}/** * meta = { *from: vue, *name: ref, *as: ref * } */// 用flat的原因是这里,避免重复引入if (flat[name] && !overriding)throw new Error(`[auto-import] identifier ${name} already defined with ${flat[name].from}`)flat[name] = meta/** * flat = { *ref: { *from: vue, *name: ref, *as: ref *} * } */}}})/** * 循环完之后 * flat = { *ref: { *from: vue, *name: ref, *as: ref *}, *computed: { *from: vue, *name: computed, *as: computed *} * } */return Object.values(flat)
}
export function createContext(options: Options = {}, root = process.cwd()) {const imports = flattenImports(options.imports, options.presetOverriding)// ...
}
imports变量就是我们配置的预设,执行完flattenImprts处理后值为:
imports = [
{ from: vue, name: ref, as: ref
},
{ from: vue, name: computed, as: computed
}
]
第二部分,生成类型文件.d.ts的路径
export function createContext(options: Options = {}, root = process.cwd()) {/** * isPackageExists => _require.resolve(name, options) => require.resolve('typescript') * 检查工程中是否有typescript */const {dts: preferDTS = isPackageExists('typescript'),} = options// .../** * dts文件生成路径,如果不传,默认生成在根目录下 * 所以如果想要生成在src下,直接传入'./src'即可,因为这里帮你resolve处理了 */const dts = preferDTS === false? false: preferDTS === true? resolve(root, 'auto-imports.d.ts'): resolve(root, preferDTS)// ...// 生成.d.ts处理function generateDTS(file: string) {const dir = dirname(file)return unimport.generateTypeDeclarations({resolvePath: (i) => {if (i.from.startsWith('.') || isAbsolute(i.from)) {const related = slash(relative(dir, i.from).replace(/\.ts(x)?$/, ''))return !related.startsWith('.')? `./${related}`: related}return i.from},})
}
这里会先检查工程中是否有用typescript,如果有就会生成.d.ts类型文件,这样就能避免产生一些类型报错,比如xxx is not defined
第三部分,生成.eslintrc配置处理
export function createContext(options: Options = {}, root = process.cwd()) {// ...const eslintrc: ESLintrc = options.eslintrc || {}eslintrc.enabled = eslintrc.enabled === undefined ? false : eslintrc.enabledeslintrc.filepath = eslintrc.filepath || './.eslintrc-auto-import.json'eslintrc.globalsPropValue = eslintrc.globalsPropValue === undefined ? true : eslintrc.globalsPropValue/** * eslintrc = { *enabled: false, *filepath: './eslintrc-auto-import.json', *globalsPropValue: true * } */ // ... // 生成.eslintrc处理async function generateESLint() {return generateESLintConfigs(await unimport.getImports(), eslintrc)}
}
这里配置eslint的相关配置,默认是不会生成
第四部分,核心功能,创建unimport实例
export function createContext(options: Options = {}, root = process.cwd()) {// .../** * 核心功能,创建unimport实例,将上面预设好的imports传入 * 在生成的d.ts文件最后,加上// Generated by 'unplugin-auto-import'\n */const unimport = createUnimport({imports: imports as Import[],presets: [],addons: [...(options.vueTemplate ? [vueTemplateAddon()] : []),resolversAddon(resolvers),{declaration(dts) {if (!dts.endsWith('\n'))dts += '\n'return `// Generated by 'unplugin-auto-import'\n${dts}`},},],})
}
unimport是这个插件的核心内容,传入我们设定的预设imports,初始化unimport实例
原本自动注入功能是直接写在unplugin-auto-import里的,后续又将这部分功能抽取出一个更底层的插件unimport,也是这系列文章的核心,更具体的解析我会在下一篇文章中讲解,现在只需要理解成这是一个能自动注入的插件即可
第五部分,过滤器,用于识别文件是否自动注入目标文件
export function createContext(options: Options = {}, root = process.cwd()) {// ... /** * 创建正则过滤文件,如果不传,默认排查node_modules和.git目录 * 默认检查js jsx ts tsx vue svelte文件是否需要自动import * 最终生成filter是一个(id) => boolean函数,id为文件名,符合配置的返回true */const filter = createFilter(options.include || [/\.[jt]sx?$/, /\.vue$/, /\.vue\?vue/, /\.svelte$/],options.exclude || [/[\\/]node_modules[\\/]/, /[\\/]\.git[\\/]/],)// ...
}
filter的作用就是过滤需要自动注入的文件类型,默认是包含js jsx ts tsx vue svelte类型文件,可以通过include属性来控制注入文件类型,比如我只希望vue组件进行注入,那么就可以写成:
export default defineConfig({plugins: [AutoImport({include: [/\.vue$/]})]
})
第六部分,生成配置文件方法节流处理
export function createContext(options: Options = {}, root = process.cwd()) {
// 节流处理dts和eslintrc文件的生成const writeConfigFilesThrottled = throttle(500, writeConfigFiles, { noLeading: false })let lastDTS: string | undefinedlet lastESLint: string | undefinedasync function writeConfigFiles() {const promises: any[] = []if (dts) {promises.push(generateDTS(dts).then((content) => {if (content !== lastDTS) {lastDTS = contentreturn fs.writeFile(dts, content, 'utf-8')}}),)}if (eslintrc.enabled && eslintrc.filepath) {promises.push(generateESLint().then((content) => {if (content !== lastESLint) {lastESLint = contentreturn fs.writeFile(eslintrc.filepath!, content, 'utf-8')}}),)}return Promise.all(promises)}
}
这部分需要结合第二和第三部分一起看,就是在生成配置文件的时候加上节流处理,能降低文件更新频率
第七部分,开始注入代码
export function createContext(options: Options = {}, root = process.cwd()) {// ...// 开始执行代码注入async function transform(code: string, id: string) {const s = new MagicString(code)// unimport实例中,已经包含我们配置的imports,这里injectImports就是将配置的imports注入到代码中// s就是每个命中文件的源码,比如HelloWorld.vueawait unimport.injectImports(s, id)if (!s.hasChanged())return// 注入后节流生成配置文件writeConfigFilesThrottled()return {code: s.toString(),map: s.generateMap({ source: id, includeContent: true }),}// ...
}
这个方法中,入参code就是命中filter规则的文件,比如demo中的HelloWorld.vue文件
重点处理是执行配置好了的unimport实例的injectImports方法将api注入到s中,s就是经过MagicString处理过的HelloWorld.vue的源码,也是string,但操作api会更友好,感兴趣的可以点击这里看MagicString
经过这一步处理,虽然你的文件中没有手动引入api,但其实已经存在于代码中了,所以你就能使用自动引入的api了
回到插件本身
经过上面的分析,相信对ctx这个实例有一定的了解,是自动引入api的核心实例,插件的全部操作基本上都是围绕该实例进行的处理,我们回到插件内容,来看各个时期执行的操作
第一部分,enforce
export default createUnplugin<Options>((options) => {let ctx = createContext(options)return {enforce: 'post'}
}
enforce控制插件在插件流的执行位置,post表示该插件在插件流靠后部分执行,如果只有这个插件是post,那么就是最后执行的插件
因为是post在靠后执行,所以此时的命中代码已经被处理成js代码,此时将配置的api插入到代码的头部即可,这就是unimport的主要处理过程,会放在下一篇进行分析
第二部分,transformInclude
export default createUnplugin<Options>((options) => {let ctx = createContext(options)return {transformInclude(id) {return ctx.filter(id)},}
}
这里会调用ctx.filter方法,结合上面ctx分析第五部分,ctx.filter返回的是一个入参为id,返回值为boolean的函数,如果id符合条件,则返回true,否则false
transformInclude的入参id是文件名,比如main.ts App.vue HelloWorld.vue等,如果钩子返回true,则表示该文件需要进行transform钩子处理,如果我们AutoImport插件不传入include属性,默认会处理.vue和.ts文件,所以这三个文件都会注入api
第三部分,transform
export default createUnplugin<Options>((options) => {let ctx = createContext(options)return {async transform(code, id) {return ctx.transform(code, id)},}
}
这里会调用ctx.transform方法,结合上面ctx分析第七部分,这里会调用unimport插件来完成api注入
到这步为止,我们也就能使用上unplugin-auto-import插件,并且生成d.ts类型文件了
总结
unplugin-auto-import为 Vite、Webpack、Rollup 和 esbuild 按需自动导入 api,我们可以使用该插件并且配置上内置的预设,达到不用手动引入vue或者react的api,也能正常运行工程并使用api。
unplugin-auto-import插件的自动注入api功能是使用了unimport插件,关于unimport插件的具体实现我们留到下一篇讲解。
最后
整理了75个JS高频面试题,并给出了答案和解析,基本上可以保证你能应付面试官关于JS的提问。




有需要的小伙伴,可以点击下方卡片领取,无偿分享
本文详细介绍了unjs插件的使用方法和原理,包括如何配置Vite、Webpack、Rollup和esbuild工程以按需自动导入API。通过实例展示了如何解决类型报错问题,并对插件的内部工作流程进行了分析,包括ctx实例的细节和核心功能的实现。文章适合想要简化API引入的前端开发者阅读。
1785

被折叠的 条评论
为什么被折叠?



