🧩 环境信息
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并外部化处理依赖 ,例如vue或react。
官方示例:
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,构建期解决 | 路径需宿主一致 | 自定义部署结构、多版本环境 |
🚀 实践建议
-
统一宿主与子项目的 Vue 实例来源 。
→ 不要在子组件中重复打包
vue。 -
打包时外部化 Vue:
jsexternal: ['vue'] -
根据浏览器兼容性选择方案:
-
支持现代浏览器 → ✅ ImportMap;
-
需要兼容旧环境 → ✅ Rollup
paths。
-