1.场景

在开发的时候我们常常需要按需导入第三方库,利用工具的tree shake实现打包体积最小化。

往往需要手工在每个页面单独导入和费时。这里可以利用工具如unplugin-auto-import unplugin-vue-components等插件工具简化繁琐的导入。

2.vue

全量导入

在实现按需之前,我们看看粗暴的全量导入,这样才有比较好的对比

创建项目

npm create vite@latest
npm i -D element-plus
npm i -D vite-bundle-analyzer # 用于输出打包大小做对比

配置vite (这里我们先禁用代码混淆压缩)

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

<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>

image.png

运行输出报告

npm run build

这里可以看到默认打包js的大小,这里大小是只使用了ref的大小 image.png 查看输出源码只有ref的定义

image.png

验证vite tree shake

我们再次修改src/components/HelloWorld.vue

<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的库就被引入。

image.png

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

image.png

并且源码输出了很多其他的api代码 不只是ref了 image.png

按需自动引入vue

调整代码 注释掉全部引入vue的代码

<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

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

image.png

现在我们要实现自动引入

为了偷懒少写import,我们使用unplugin-auto-import

npm i unplugin-auto-import -D

配置vite

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 文件,

image.png

/* 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会报错找不到

image.png

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

image.png

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

image.png

大小依然是59kb nice

image.png

3.element-plus

同理我们在element-plus操作一遍。

全局导入

npm i element-plus -D

main.ts 全量引入

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 按钮

<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提示

image.png

vite.config.ts 把混淆压缩打开,注释minify

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)

image.png

手动导入

在还没自动导入前,我们一般手动都是 一个个组件注册并导入,同时导入对应的css文件

新建 src/plugins/element-plus.ts

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 数组定义自己需要引入的组件

image.png

main.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')

image.png

重新构建,总大小只有143k image.png

上面的导入css 其实有对应的自动import工具叫unplugin-element-plus/vite

npm i unplugin-element-plus sass-embedded -D

修改 vite

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

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正常加载

image.png

实际大小比上面大一点点

image.png

自动导入

要实现组件自动安装可以使用 unplugin-vue-components

npm i unplugin-vue-components -D

vite

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: 解决组件引入vue组件引入问题
  • unplugin-vue-components/resolvers 是针对不同ui自带的导入出

重新构建,这时候,除了之前的auto-imports.d.ts ,

还多了个 components.d.ts,他用于自动注册所有组件

auto-imports.d.ts

/* 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

/* 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 image.png

4.实际项目应用

在实际开发中我们还有自己定义的方法,如果也想偷懒也可以借用auto-import

添加工具类 src/util.ts

export function sum(a: number, b: number) {
  return a + b;
}   

HelloWorld.vue 引入sum并输出

<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>

image.png

修改vite

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方法

image.png

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

image.png

好处

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

image.png

image.png 上面代码只剩下 关键的api处理

缺点

由于是全局引入,有可能有命名冲突的风险,所以命名要规范。

5.分析变化的过程

通过vite-plugin-inspect 可以很好的查看整个转货过程

npm i -D vite-plugin-inspect

vite

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'
import Inspect from 'vite-plugin-inspect'

// https://vite.dev/config/
export default defineConfig({
  plugins: [ 
    vue(), 
     AutoImport({
      imports: [
          'vue', 
           {
            'src/util': ['sum'], 
          }
        ],
     resolvers: [ElementPlusResolver()], // 这里用于解决
     }), 
   ,
     Components({
      resolvers: [ElementPlusResolver()],
    }),
    Inspect(),
    // ElementPlusStyle({
    //   // 选项
    //   useSource: true
    // }),
    analyzer()
  ] ,
  build: {
    // minify: false, // 禁用代码压缩混淆
  }
})

npm run dev

运行后多一个诊断的地址 image.png

我们可以看到整个打包的全貌图 image.png

选中中间的hellowworld image.png

第一次是vue的转化 image.png

第二次是unplugin-auto-import的转化 添加了

import { ref } from 'vue';
import { ElMessage } from 'element-plus/es';
import 'element-plus/es/components/base/style/css';
import 'element-plus/es/components/message/style/css';

image.png

第三次是unplugin-vue-components 的转化 添加了,并且压缩了一下import代码

{ ElButton as __unplugin_components_0 } from 'element-plus/es';

image.png

6.源码

github.com/mjsong07/vu…

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]