Vite 库模式输出 ESM 格式时的依赖处理方案

🧩 环境信息

json 复制代码
{
  "vite": "5.4.15",
  "vue": "2.7.16"
}

📖 背景说明

当前项目是一个前端框架型宿主项目,主要负责:

  • 管理功能菜单与用户模块;

  • 通过 iframe 嵌入多个子前端项目

  • 在"工作台"页面中动态加载来自其他前端项目的小组件(Widget)

为了便于这些小组件的动态加载与复用,我们使用了 Vite 的库模式(Library Mode) 来进行打包。


⚙️ 初始配置

最早的 widget.vite.config.js 如下:

js 复制代码
return {
  build: {
    lib: {
      entry: {
        customReport: 'widgets/custom_report/index.js'
      },
      fileName: (_, entryName) => `${entryName}.js`,
      formats: ['es']
    },
    rollupOptions: {
      output: {
        chunkFileNames: 'js/[name]-[hash].js',
        assetFileNames: '[name].[ext]',
        manualChunks: id => {
          if (id.includes('node_modules')) {
            for (let chunk of chunks) {
              if (id.includes(`node_modules/${chunk}`)) return chunk
            }
          }
        }
      }
    }
  }
}

起初并没有将 vue 从打包中排除,因为看似一切正常,直到后来出现了严重问题👇


💥 问题分析:Vue 多实例导致渲染死循环

在某个小组件中使用函数式组件渲染 VNode 数组时,页面发生死循环渲染

vue 复制代码
<template>
  <VNode :data="bottomInst" />
</template>

<script>
export default {
  components: {
    VNode: {
      functional: true,
      render: (h, ctx) =>
        isFunction(ctx.props.data) ? ctx.props.data() : ctx.props.data
    }
  }
}
</script>
  • 页面会不断触发 render(),导致浏览器卡死。

  • 后续测试发现是因为宿主项目与子组件中使用的 Vue 实例不一致

换句话说,小组件自己又打包进了一份 vue,与宿主项目的 Vue 实例"冲突"了。


🧠 回到根源:Vue 外部化问题

Vite 官方文档明确建议:

当你的库要被宿主项目引用时,应使用 build.lib外部化处理依赖 ,例如 vuereact

官方示例:

js 复制代码
export default defineConfig({
  build: {
    lib: {
      entry: {
        'my-lib': resolve(__dirname, 'lib/main.js'),
        secondary: resolve(__dirname, 'lib/secondary.js')
      },
      name: 'MyLib'
    },
    rollupOptions: {
      external: ['vue'],
      output: {
        globals: {
          vue: 'Vue'
        }
      }
    }
  }
})

⚠️ 实际遇到的问题

当配置为多入口时:

  • 默认打包格式会变为 ['es', 'cjs']

  • 如果强制设置 formats: ['umd'],Rollup 会报错;

  • 而在 ESM 模式下globals 配置不会生效(即不会转成全局变量访问)。

因此,当外部化 vue 后,代码仍然保留:

js 复制代码
import vue from 'vue'

浏览器在执行时会报错:

javascript 复制代码
Uncaught TypeError: Failed to resolve module specifier "vue".

因为浏览器根本不知道 "vue" 这个导入路径应该从哪里加载。


✅ 正确的解决方案

方法一:使用 HTML Import Map(推荐,前提是宿主环境支持)

如果不需要兼容 Chrome 89 以下版本,可以在宿主项目的 HTML 中声明:

html 复制代码
<script type="importmap">
{
  "imports": {
    "vue": "https://example.com/vue.js"
  }
}
</script>

这样浏览器在解析到:

js 复制代码
import Vue from 'vue'

时,会自动从 /vue.js 加载,而不是去找 node_modules

✅ 优点:

  • 符合原生 ES Module 机制;

  • 适合现代浏览器;

  • 简洁直观。

❌ 缺点:

  • Chrome 89 以下(含 IE)不支持 importmap

  • 需要在宿主 HTML 中维护映射。


方法二:使用 Rollup 的 output.paths 配置

如果你希望在构建阶段就将路径转换成一个可访问的 URL,可使用:

js 复制代码
return {
  build: {
    lib: {
      entry: {
        customReport: 'widgets/custom_report/index.js'
      },
      fileName: (_, entryName) => `${entryName}.js`,
      formats: ['es']
    },
    rollupOptions: {
      external: ['vue'],
      output: {
        paths: {
          vue: '/dist/assets/vue.js'
        }
      }
    }
  }
}

打包后:

js 复制代码
// ✅ 自动替换成可访问路径
import vue from '/dist/assets/vue.js'

宿主项目只需确保 /dist/assets/vue.js 存在(即宿主与子项目共享同一份 Vue 文件)。

✅ 优点:

  • 不依赖 importmap;

  • 可控制精确路径;

  • 浏览器 100% 可识别。

❌ 缺点:

  • 宿主项目需要在相同路径下提供该文件;

  • 一旦路径变更,需要同步更新。


🧾 总结对比

方案 关键配置 优点 缺点 适用场景
方法一 ImportMap <script type="importmap"> 原生 ESM 支持、配置简单 浏览器兼容性要求高 现代浏览器环境
方法二 Rollup paths rollupOptions.output.paths 无需 importmap,构建期解决 路径需宿主一致 自定义部署结构、多版本环境

🚀 实践建议

  1. 统一宿主与子项目的 Vue 实例来源

    → 不要在子组件中重复打包 vue

  2. 打包时外部化 Vue

    js 复制代码
    external: ['vue']
  3. 根据浏览器兼容性选择方案

    • 支持现代浏览器 → ✅ ImportMap;

    • 需要兼容旧环境 → ✅ Rollup paths

相关推荐
开发者小天2 小时前
React中使用useParams
前端·javascript·react.js
lichong9512 小时前
Android studio release 包打包配置 build.gradle
android·前端·ide·flutter·android studio·大前端·大前端++
nvvas2 小时前
npm : 无法加载文件 D:\nvm\nodejs\npm.ps1,因为在此系统上禁止运行脚本问题解决
前端·npm·node.js
拉不动的猪3 小时前
浏览器之内置四大多线程API
前端·javascript·浏览器
林太白3 小时前
5大排序算法&2大搜索&4大算法思想
前端
摇滚侠3 小时前
浏览器的打印功能,如果通过HTML5,控制样式
前端·html·html5
喵喵侠w3 小时前
uni-app微信小程序相机组件二次拍照白屏问题的排查与解决
前端·数码相机·微信小程序·小程序·uni-app
超大只番薯3 小时前
在Next.js中实现页面级别KeepAlive
前端
快递鸟3 小时前
第三方物流接口优选:快递鸟物流 API,打破单一快递对接壁垒
前端