unimport系列——揭秘自动引入api那些事
前言
相信很多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-fun
pnpm install
pnpm 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 = vue
for (const id of definition[mod]) {
/**
* id=ref
* id=computed
*/
const meta = {
from: mod,
} as Import
let name: string
if (Array.isArray(id)) {
name = id[1]
meta.name = id[0]
meta.as = id[1]
}
else {
name = id
meta.name = id
meta.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.enabled
eslintrc.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 | undefined
let lastESLint: string | undefined
async function writeConfigFiles() {
const promises: any[] = []
if (dts) {
promises.push(
generateDTS(dts).then((content) => {
if (content !== lastDTS) {
lastDTS = content
return fs.writeFile(dts, content, 'utf-8')
}
}),
)
}
if (eslintrc.enabled && eslintrc.filepath) {
promises.push(
generateESLint().then((content) => {
if (content !== lastESLint) {
lastESLint = content
return 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.vue
await 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
插件的具体实现我们留到下一篇讲解。
转载自:https://juejin.cn/post/7172876375580213262