一个优化90%体积的组件打包思考

1,612 阅读3分钟

体积差异

image.png 在我组件内只用了三个Element组件的情况下,成功将组件体积从126\color{red}{126}KB 优化至 1.54\color{red}{1.54}KB。

背景

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

image.png

过程

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

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

image.png 2. 配置 external

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

插件vite-plugin-extract-encycomp

目标: 写一个插件来自动把template里面的所有 Element组件 自动生成一个模块导入语句插入到 Vue script里面。然后插件必须在Vue编译插件之前。这样Vue插件执行的时候,执行我们转换后的代码就可以了。(如下图,组件内用了ElButton,ElCheckBox,ElInput。插件需要生成 import {ElButton, ElCheckBox, ElInput} from 'element-plus'这个语句然后插入到Vue script里)

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

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

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

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,然后提取script中已经引入的element-plus组件。

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

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

  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。组件使用的越多,插件作用越明显。

image.png image.png

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