一个关于使用了 unplugin-auto-import 的组件打包优化

背景

一个页面是由多个远程组件组件(如下图)组成,然后页面渲染器加载这些远程组件组成一个页面,最近在做性能优化专项,第一个想到的是减少 远程组件体积

过程

组件开发采用Vite+Vue3+ElementPlus. ElementPlus配置使用的auto import方式 以下都用简单的组件进行示例展示。组件代码.

  1. 使用rollup-plugin-visualizer分析包体积内容, element-plus竟然没有被external掉,这个external的话包体积应该会掉下去很多。

2. 配置 external

json 复制代码
    rollupOptions: {
      external: ['vue', 'axios','element-plus'],
      output: {
        // 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
        globals: {
          vue: 'Vue',
          axios: 'axios',
          'element-plus':'ElementPlus'
        }
      }
    }
  1. 本来已经大功告成的时候,发现打包后还是没有external掉, 后来自己调试了一段时间发现使用了auto import这个配置时,external ElementPlus 会失效。后来试试想着按需引入 的话是不是能external掉。
  2. 发现按需引入 可以有效减小体积并成功external掉ElementPlus,为了提供更好的开发体验,能不能编写一个插件 ,打包时自动将按需引入的Element组件插入到Vue SFC的script部分。
  3. 着手开始写这个插件vite-plugin-extract-encycomp

插件vite-plugin-extract-encycomp

目标 : 写一个插件来自动把template里面的所有 Element组件 自动生成一个引入语句到插入到 Vue script里面。然后插件必须在Vue 编译插件之前。这样Vue插件执行的时候,执行我们转换后的代码就可以了。

vue 复制代码
<template>
  <div class="table-footer" v-loading="loading">
    <el-button class="swwwwww-aa">{{ name }}</el-button>
    <el-checkbox v-model="checked1" label="Option 1"></el-checkbox>
    <el-input v-model="input" clearable> </el-input>
  </div>
</template>
// 当 script 里面出现这三个组件 el-button,el-checkbox,el-input时。我们需要得到一个如下语句
// ' import { ElButton, ElCheckbox, ElInput } from 'element-plus' '

<template>
  <div class="table-footer" v-loading="loading">
    <el-button class="swwwwww-aa">{{ name }}</el-button>
  </div>
</template>
// 当 script 里面出现el-button时。我们需要得到一个
// ' import { ElButton} from 'element-plus' '

动手准备(中间跳过了很多踩坑过程...)

Q1: rollup插件开发这个网上一搜就有很多。

Q2: 怎么能够提取出 SFC template 里面是组件的标签或者指令?

Q3: 怎么知道代码中是否已经引入了 ElementPlus 包的组件?

Q4: 怎么知道SFC 是Vue2或者Vue3?(Vue2组件引入方式不一样)

Q5: 怎么插入代码?

开发(这里主要演示Vue3版本)

A1: 看了文档先创建一个简单的rollup插件

js 复制代码
export default function extractElComponentsPlugin() {
  return {
    name: 'rollup-plugin-extract-el-components',
    async transform(code, id) {
     
    }
  }
}

A2: 使用 @vue/compiler-sfc 先获取 SFC template和script,然后通过compileTemplate方法就能拿到组件和用的指令了。

js 复制代码
import { parse, compileTemplate } from '@vue/compiler-sfc'
// 获取 SFC template和script
export function getSfcContent(code) {
  const sfcParse = parse(code)
  let sfc_parse_script = ''
  if (sfcParse?.descriptor?.scriptSetup?.content) {
    // vue3 setup
    sfc_parse_script = sfcParse?.descriptor?.scriptSetup?.content
  } else {
    // vue2
    sfc_parse_script = sfcParse?.descriptor?.script?.content
  }
  // 获取 SFC 模板内容
  const sfcParseTemplate = sfcParse?.descriptor?.template?.content
  return {
    sfc_parse_script,
    sfcParseTemplate
  }
}

export function getTemplateTag(templateContent) {
  const { ast } = compileTemplate({
    source: templateContent
  })
  const directives = ast.directives.map((item) => `v${item}`)
  return [...ast.components ,...directives]
}

A3: 我用的是 @babel/traverse 解析成ast,然后提取已经引入的组件。

js 复制代码
import traverse from '@babel/traverse'
export function getHasImportElementPlusComp(ast) {
  const imports = []
  // 使用AST遍历工具来查找引用了element-plus的import语句
  traverse(ast, {
    ImportDeclaration(path) {
      const source = path.node.source.value
      if (source === 'element-plus') {
        path.node.specifiers.forEach((specifier) => {
          if (specifier.type === 'ImportSpecifier') {
            imports.push(specifier.imported.name)
          }
        })
      }
    }
  })
  return imports
}

A4: 还是用 @vue/compiler-sfc

javascript 复制代码
import { parse } from '@vue/compiler-sfc'
export function sfcType(code) {
  const sfcParse = parse(code)
  if (sfcParse?.descriptor?.scriptSetup?.content) {
    // vue3 setup
    return 'vue3'
  }
  // vue2
  return 'vue2'
}

A5:这个就比较简单啦,因为我们直接可以插入到 Vue Sfc 的 script开头,所以直接拼接就可以了。然后替换到源码的script部分

javascript 复制代码
  if (sfcType(code) === 'vue3') {
       // 如果有匹配的组件名,插入 import 语句
       const importStatement = getImportStatement(uniqueComponentNames)
       const source = code.replace(
         /(?<=<script\b[^>]*>)[\s\S]*(?=<\/script>)/,
          `${importStatement}${sfc_parse_script}`
         )
        return source
   }

这样一个插件就可以了耶。当然实现过程还是有很多细节的。让我们来测试一下,打包一个组件。 直接从126KB -> 1.54KB。组件使用的越多,可能插件越明显。

有其他的实现方案欢迎大佬们评论。

相关推荐
leoZ23121 分钟前
2026-09-09-springboot-cloud-deploy-pitfalls
java·前端·javascript·vue.js·人工智能·spring boot·后端
芭拉拉小魔仙1 小时前
Vue 2 门诊收费系统中的医保结算流程设计与实践
前端·javascript·vue.js
moonrailgun1 小时前
用 Node.js 复刻 Codex Astra 的终端星光
前端·javascript·算法
默_笙2 小时前
💫 给金鱼建图书馆(下):Milvus 向量数据库,让 AI 拥有长期记忆
前端·javascript
liangshanbo12152 小时前
手写 Function.prototype.call 与 Function.prototype.apply 面试题满分答案
开发语言·javascript·原型模式
CoderYanger3 小时前
前端基础——JavaScript(WebAPI)(下篇)
java·开发语言·前端·javascript·程序人生·面试·职场和发展
JunjunZ5 小时前
Naive UI 虚拟级联选择器适配 Element Plus 风格
前端·javascript·vue.js
Flynt6 小时前
pnpm 12 换上了 Rust 内核,我拿项目实测了一轮构建速度
rust·vite·前端工程化
闲坐含香咀翠6 小时前
百万行数据透视表,我是怎么把 Vue 响应式开销砍到零的
前端·vue.js·性能优化
用户302822530686 小时前
别让 Agent 被 Webhook 叫醒就开工:实现一个幂等启动层
javascript