1.场景
在开发的时候我们常常需要按需导入第三方库,利用工具的tree shake实现打包体积最小化。
往往需要手工在每个页面单独导入和费时。这里可以利用工具如unplugin-auto-import
unplugin-vue-components
等插件工具简化繁琐的导入。
2.vue
全量导入
在实现按需之前,我们看看粗暴的全量导入,这样才有比较好的对比
创建项目
sh
npm create vite@latest
npm i -D element-plus
npm i -D vite-bundle-analyzer # 用于输出打包大小做对比
配置vite (这里我们先禁用代码混淆压缩)
js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { analyzer } from 'vite-bundle-analyzer'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
analyzer()
] ,
build: {
minify: false, // 禁用代码压缩混淆,方便查看
}
})
修改src/components/HelloWorld.vue
这里我们只引入的ref
js
<script setup lang="ts">
import { ref } from 'vue'
defineProps<{ msg: string }>()
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
</div>
</template>

运行输出报告
js
npm run build
这里可以看到默认打包js的大小,这里大小是只使用了ref的大小 查看输出源码只有ref的定义

验证vite tree shake
我们再次修改src/components/HelloWorld.vue
js
<script setup lang="ts">
import { ref } from 'vue'
import * as Vue from 'vue'
const allkey = Object.keys(Vue)
defineProps<{ msg: string }>()
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
{{allkey}}
</div>
</template>
我们通过引入这个vue 并遍历打印到页面,这样vue的库就被引入。

再次build,这次200多k ,证明tree shake有效果

并且源码输出了很多其他的api代码 不只是ref了
按需自动引入vue
调整代码 注释掉全部引入vue的代码
js
<script setup lang="ts">
import { ref } from 'vue'
// import * as Vue from 'vue'
// const allkey = Object.keys(Vue)
defineProps<{ msg: string }>()
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<!-- {{allkey}} -->
</div>
</template>
vite.config.ts 把混淆压缩打开,注释minify
js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { analyzer } from 'vite-bundle-analyzer'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
analyzer()
] ,
build: {
// minify: false, // 禁用代码压缩混淆
}
})
再次构建
正常使用ref 打包tree shake的压缩的大小59kb

现在我们要实现自动引入
为了偷懒少写import,我们使用unplugin-auto-import
库
js
npm i unplugin-auto-import -D
配置vite
js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { analyzer } from 'vite-bundle-analyzer'
import AutoImport from "unplugin-auto-import/vite";
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
AutoImport({
imports: [
'vue'
],
}),
analyzer()
] ,
build: {
// minify: false, // 禁用代码压缩混淆
}
})
再次构建,在代码根目录会多一个auto-imports.d.ts 文件,

js
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
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 getCurrentWatcher: typeof import('vue').getCurrentWatcher
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 isShallow: typeof import('vue').isShallow
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 onWatcherCleanup: typeof import('vue').onWatcherCleanup
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 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 toValue: typeof import('vue').toValue
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 useId: typeof import('vue').useId
const useModel: typeof import('vue').useModel
const useSlots: typeof import('vue').useSlots
const useTemplateRef: typeof import('vue').useTemplateRef
const watch: typeof import('vue').watch
const watchEffect: typeof import('vue').watchEffect
const watchPostEffect: typeof import('vue').watchPostEffect
const watchSyncEffect: typeof import('vue').watchSyncEffect
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}
这里会根据所有代码上下文,在我们实际代码自动加入import,并且自动导出了 auto-imports.d.ts需要的全局变量类型,这样我们在vscode就知道这个全局定义不会报错。
正常在没有导入ref的时候,使用ref,ts会报错找不到

回到 HelloWorld.vue 文件,可以看到错误消失了。

再次构建正常,大小不变,但是已经可以不再繁琐的导入ref了

大小依然是59kb nice

3.element-plus
同理我们在element-plus操作一遍。
全局导入
sh
npm i element-plus -D
main.ts 全量引入
js
import { createApp } from 'vue'
// import './style.css'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
src/components/HelloWorld.vue 加入el-button 按钮
js
<script setup lang="ts">
// import { ref } from 'vue'
// import * as Vue from 'vue'
// const allkey = Object.keys(Vue)
import { ElMessage } from 'element-plus'
ElMessage.error(`xxxx`)
defineProps<{ msg: string }>()
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<el-button type="primary">Element Plus Button</el-button>
<!-- {{allkey}} -->
</div>
</template>
我们加入el-button组件,和 ElMessage提示

vite.config.ts 把混淆压缩打开,注释minify
js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { analyzer } from 'vite-bundle-analyzer'
import AutoImport from "unplugin-auto-import/vite";
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
AutoImport({
imports: [
'vue',
],
}),
analyzer()
] ,
build: {
// minify: false, // 禁用代码压缩混淆
}
})
由于是全量,打包后大小1.24m(注意这里是包含的vue和element-plus)

手动导入
在还没自动导入前,我们一般手动都是 一个个组件注册并导入,同时导入对应的css文件
新建 src/plugins/element-plus.ts
js
import { ElButton, ElInput, ElMessage, ElTable, ElTableColumn,ElMessageBox ,ElForm, ElUpload } from 'element-plus'
// 组件列表
const components = [ElButton, ElMessage ]
// 插件安装方法
const install = (app :any) => {
components.forEach((component) => {
app.component(component.name, component)
})
}
export default {
install,
ElMessage
}
通过 components 数组定义自己需要引入的组件

main.ts
ts
import { createApp } from 'vue'
// import './style.css'
// import ElementPlus from 'element-plus'
import elementPlusPlugins from './plugins/element-plus'
// import 'element-plus/dist/index.css'
import 'element-plus/es/components/base/style/css'// 基础样式
// import 'element-plus/es/components/badge/style/css'
import 'element-plus/es/components/button/style/css'// 对应按钮样式
import 'element-plus/es/components/message/style/css'// 对应消息样式
import App from './App.vue'
const app = createApp(App)
// app.use(ElementPlus)
app.use(elementPlusPlugins)
app.mount('#app')

重新构建,总大小只有143k
上面的导入css 其实有对应的自动import工具叫unplugin-element-plus/vite
sh
npm i unplugin-element-plus sass-embedded -D
修改 vite
js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { analyzer } from 'vite-bundle-analyzer'
import AutoImport from "unplugin-auto-import/vite";
import ElementPlusStyle from 'unplugin-element-plus/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
AutoImport({
imports: [
'vue',
],
}),
ElementPlusStyle({
// 选项
useSource: true
}),
analyzer()
] ,
build: {
// minify: false, // 禁用代码压缩混淆
}
})
注释所有 css
ts
import { createApp } from 'vue'
// import './style.css'
// import ElementPlus from 'element-plus'
import elementPlusPlugins from './plugins/element-plus'
// import 'element-plus/dist/index.css'
// import 'element-plus/es/components/base/style/css'
// import 'element-plus/es/components/badge/style/css'
// import 'element-plus/es/components/button/style/css'
// import 'element-plus/es/components/message/style/css'
import App from './App.vue'
const app = createApp(App)
// app.use(ElementPlus)
app.use(elementPlusPlugins)
app.mount('#app')
重新运行 ,css正常加载

实际大小比上面大一点点

自动导入
要实现组件自动安装可以使用 unplugin-vue-components
sh
npm i unplugin-vue-components -D
vite
js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { analyzer } from 'vite-bundle-analyzer'
import AutoImport from "unplugin-auto-import/vite";
import ElementPlusStyle from 'unplugin-element-plus/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
AutoImport({
imports: [
'vue',
],
resolvers: [ElementPlusResolver()], // 这里用于解决
}),
,
Components({
resolvers: [ElementPlusResolver()],
}),
ElementPlusStyle({
// 选项
useSource: true
}),
analyzer()
] ,
build: {
// minify: false, // 禁用代码压缩混淆
}
})
注意:
- unplugin-auto-import : 解决的事js ts的import 问题
- unplugin-vue-components: 解决组件引入和样式问题
- unplugin-vue-components/resolvers 是针对不同ui自带的导入出
重新构建,这时候,除了之前的auto-imports.d.ts ,
还多了个 components.d.ts,他用于自动注册所有组件
auto-imports.d.ts
js
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue').EffectScope
const ElMessage: typeof import('element-plus/es').ElMessage // 这里多了 HelloWorld.vue里面使用的ElMessage ,这样ts就不会报错
const ElMessageBox: typeof import('element-plus/es').ElMessageBox
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 getCurrentWatcher: typeof import('vue').getCurrentWatcher
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 isShallow: typeof import('vue').isShallow
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 onWatcherCleanup: typeof import('vue').onWatcherCleanup
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 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 toValue: typeof import('vue').toValue
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 useId: typeof import('vue').useId
const useModel: typeof import('vue').useModel
const useSlots: typeof import('vue').useSlots
const useTemplateRef: typeof import('vue').useTemplateRef
const watch: typeof import('vue').watch
const watchEffect: typeof import('vue').watchEffect
const watchPostEffect: typeof import('vue').watchPostEffect
const watchSyncEffect: typeof import('vue').watchSyncEffect
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}
components.d.ts
js
/* eslint-disable */
// @ts-nocheck
// biome-ignore lint: disable
// oxlint-disable
// ------
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
ElButton: typeof import('element-plus/es')['ElButton']
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
}
}
重新打包依然是 143kb
4.实际项目应用
在实际开发中我们还有自己定义的方法,如果也想偷懒也可以借用auto-import
添加工具类 src/util.ts
js
export function sum(a: number, b: number) {
return a + b;
}
HelloWorld.vue 引入sum并输出
js
<script setup lang="ts">
// import { ref } from 'vue'
// import * as Vue from 'vue'
// const allkey = Object.keys(Vue)
// import { ElMessage } from 'element-plus'
import {sum} from '../util'
ElMessage.error(`xxxx`)
defineProps<{ msg: string }>()
const count = ref(0)
</script>
<template>
<h1>{{ msg }}</h1>
<div class="card">
<button type="button" @click="count++">count is {{ count }}</button>
<el-button type="primary">Element Plus Button</el-button>
<p>{{sum(1,2)}}</p>
<!-- {{allkey}} -->
</div>
</template>

修改vite
js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { analyzer } from 'vite-bundle-analyzer'
import AutoImport from "unplugin-auto-import/vite";
// import ElementPlusStyle from 'unplugin-element-plus/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
AutoImport({
imports: [
'vue',
{
'src/util': ['sum'], // 这里加入引用
}
],
resolvers: [ElementPlusResolver()], // 这里用于解决
}),
,
Components({
resolvers: [ElementPlusResolver()],
}),
// ElementPlusStyle({
// // 选项
// useSource: true
// }),
analyzer()
] ,
build: {
// minify: false, // 禁用代码压缩混淆
}
})
再次构建 auto-imports.d.ts 多出了这个sum方法

这样在HelloWorld.vue 就可以省略引入,

好处
在实际应用中不仅节省的导入的时间,尤其大量页面都是类似的业务逻辑,臃肿的代码就不复纯在在,你只需要关心最差异化的处理,如api的调用差异

上面代码只剩下 关键的api处理
缺点
由于是全局引入,有可能有命名冲突的风险,所以命名要规范。