vue3中createApp多个实例共享状态
1.背景
在 Vue 3 开发中,通常一个应用只需要调用一次 createApp() 创建一个根应用实例。但在某些特定场景下,确实需要创建多个 Vue 应用实例(即多次调用 createApp)。这些场景主要包括:
2.场景
1.动态生成html
说明:
比如在使用google地图的时候,点击弹框使用传入一个html弹框内容详情内容。
上面就是谷歌点击时候提供的弹框内容,使用InfoWindow.open触发弹框,。infoWindow.setContent插入自己要显示的详情内容。
老办法是直接jquery 插各种dom操作。但现在都组件化了如果能复用现有的架构和样式是最理想的。
方案1 createApp
这时候就可以利用createApp创建vue来渲染详情,这样就可以复用系统已经开发好的样式的结构。(缺点重新实例了一遍有一定开销)
示例:
import StoreInfoWindow from './components/StoreInfoWindow.vue'
let infoWindow: google.maps.InfoWindow
const markerShowDetail = async (marker: google.maps.marker.AdvancedMarkerElement) => {
try {
// 调用接口查询详情数据
const res: any = await getStoreInfo(marker)
if (res.data && res.data.row) {
// 详情页面显示
const storeDetail = res.data.row
const content = document.createElement('div')
infoWindow.setContent(content)
infoWindow.open(map, marker)
const app = createApp(StoreInfoWindow, { store: xxx })
app.use(ElementPlus)
app.mount(content)
}
} catch (error) {
// loading.value = false
console.error('Error fetching store info:', error)
}
}
方案2 隐藏div
当然也可以不使用createApp,直接在现有sfc页面里 插入一个隐藏的div,内容把内容渲染到隐藏div,调用infoWindow.setContent传入dom
import StoreInfoWindow from './components/StoreInfoWindow.vue'
<div class="hideDiv">
<StoreInfoWindow ref="storeInfoRef" :store="storeDetail" ></StoreInfoWindow>
</div>
const storeDetail = ref<MapStore>()
const storeInfoRef = ref()
let infoWindow: google.maps.InfoWindow
const markerShowDetail = async (marker: google.maps.marker.AdvancedMarkerElement) => {
try {
// 调用接口查询详情数据
const res: any = await getStoreInfo(marker)
if (res.data && res.data.row) {
// 详情页面显示
storeDetail.value = res.data.row
if (storeInfoRef.value) {
nextTick(() => {
infoWindow.setContent(storeInfoRef.value.$el)
infoWindow.open(map, marker)
})
}
}
} catch (error) {
console.error('Error fetching store info:', error)
}
}
由于一个dom节点 不能同时挂在多个不同节点下,所以上面的infoWindow.setContent(storeInfoRef.value.$el) 设置后,hideDiv的下面的内容会被移走。所以关闭时候需要还原回来。防止节点引用丢失。
关闭后,补偿方法
const infoWindowClose = () => {
infoWindow.close()
const hideDiv = document.querySelector('.hideDiv')
if (hideDiv) {
if (!hideDiv.contains(storeInfoRef.value.$el)) {
hideDiv.appendChild(storeInfoRef.value.$el)
}
}
}
2.微前端架构(Micro Frontends)
在微前端架构中,一个页面可能由多个独立的子应用组成,每个子应用可能是由不同的团队开发、使用不同的框架或不同版本的 Vue。为了隔离作用域和避免冲突,每个子应用应拥有自己的 Vue 实例。
1// 子应用 A
2const appA = createApp(AppA);
3appA.mount('#micro-app-a');
4
5// 子应用 B
6const appB = createApp(AppB);
7appB.mount('#micro-app-b');
每个子应用可以独立注册插件、全局组件、指令等,互不影响。
3.在同一个页面嵌入多个独立的 Vue 应用
说明:
比如一个传统多页网站(非 SPA)中,某些页面包含多个功能模块(如导航栏、侧边购物车、评论区),它们彼此逻辑独立,不需要共享状态,也不需要通信。
示例:
<!-- index.html -->
<div id="header-widget"></div>
<div id="cart-widget"></div>
<div id="comment-section"></div>
// main.js
import { createApp } from 'vue';
import HeaderWidget from './HeaderWidget.vue';
import CartWidget from './CartWidget.vue';
import CommentSection from './CommentSection.vue';
createApp(HeaderWidget).mount('#header-widget');
createApp(CartWidget).mount('#cart-widget');
createApp(CommentSection).mount('#comment-section');
每个 widget 是一个独立的 Vue 应用,可单独开发、测试、部署。
4.插件或第三方库需要隔离的 Vue 实例
说明:
当你开发一个 Vue 插件(如 UI 组件库中的弹窗、通知等),而该插件内部需要渲染 Vue 组件时,为避免污染主应用的全局配置(如全局指令、混入、provide/inject 等),应创建独立的 Vue 实例。
示例(封装一个全局 Toast 组件):
// toast.js
import { createVNode, render } from 'vue';
import ToastComponent from './Toast.vue';
export function showToast(message) {
const container = document.createElement('div');
document.body.appendChild(container);
const vm = createVNode(ToastComponent, { message });
const app = createApp({}); // 创建干净实例
app.mount(container);
render(vm, container);
}
这样 Toast 不会继承主应用的全局配置,更安全可靠。
5.单元测试或多实例沙箱环境
说明:
在编写测试用例时,为避免测试之间互相干扰,每个测试用例应使用独立的 Vue 应用实例。
示例(Vitest / Jest):
test('Component A works', () => {
const app = createApp(ComponentA);
const div = document.createElement('div');
app.mount(div);
// ...断言
app.unmount();
});
test('Component B works', () => {
const app = createApp(ComponentB); // 全新实例,无污染
// ...
});
3.createApp 构造方式
我们复习一下 创建的方式
1.传入 SFC(单文件组件)【最常用】
传入 .vue 文件作为根组件
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
带 root props 的方式
createApp(App, { title: 'Hello' }).mount('#app')
SFC 内:
<script setup>
defineProps({
title: String
})
</script>
2.传入 Options API 对象(构造对象组件)
不用 SFC,直接传一个对象:
直接传组件对象
createApp({
data() {
return { msg: 'Hello' }
},
template: `<div>{{ msg }}</div>`
}).mount('#app')
带 root props
createApp({
props: ['title'],
template: `<h1>{{ title }}</h1>`
}, {
title: 'Hello Props'
}).mount('#app')
3.传入 Render Function(函数式创建根组件)
使用 h() 渲染函数
import { createApp, h } from 'vue'
createApp({
render() {
return h('div', 'Hello from render')
}
}).mount('#app')
带 root props 的 render 写法
createApp({
props: ['msg'],
render(props) {
return h('div', props.msg)
}
}, {
msg: 'Hello props'
})
.mount('#app')
4.传入 Template 字符串(inline 模板)
适用于快速 demo:
根组件直接写 template 字符串
createApp({
template: `<p>Hello Template</p>`
}).mount('#app')
root props + template
createApp({
props: ['text'],
template: `<p>{{ text }}</p>`
}, {
text: 'Hello!'
}).mount('#app')
4.数据共享问题
由于两个app 是独立的沙盒,但是我们又需要同步部分数据状态
1.全局变量(简单场景,不推荐大型项目)
通过浏览器全局对象(window)存储共享数据,利用 Vue 的响应式 API(ref/reactive)保证数据变更能触发视图更新。
<script>
const { createApp, ref } = Vue;
// 1. 定义全局共享的响应式数据
window.sharedState = ref({
username: 'Vue开发者',
count: 0
});
// 2. 应用实例1:使用全局共享数据
createApp({
setup() {
const shared = window.sharedState;
const increment = () => shared.value.count++;
return { shared, increment };
},
template: `
<div>
<h3>应用1 - 计数:{{ shared.count }}</h3>
<button @click="increment">+1</button>
</div>
`
}).mount('#app1');
// 3. 应用实例2:共享同一份数据
createApp({
setup() {
const shared = window.sharedState;
const changeName = () => shared.value.username = '新名称';
return { shared, changeName };
},
template: `
<div>
<h3>应用2 - 用户名:{{ shared.username }}</h3>
<h3>应用2 - 同步计数:{{ shared.count }}</h3>
<button @click="changeName">修改用户名</button>
</div>
`
}).mount('#app2');
</script>
2.事件总线
通过第三方事件库(如 mitt)实现跨实例的 “发布 - 订阅” 通信,适用于需要触发行为 / 传递临时数据的场景(而非持久化共享状态)。
步骤:
- 安装
mitt(工程化项目):npm install mitt; - 创建全局事件总线实例;
- 不同应用实例通过
emit发布事件,on监听事件传递数据。
<!-- CDN 方式示例 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="https://unpkg.com/mitt/dist/mitt.umd.js"></script>
<script>
const { createApp, ref } = Vue;
// 1. 创建全局事件总线
window.eventBus = mitt();
// 应用1:发布事件(传递数据)
createApp({
setup() {
const count = ref(0);
const sendCount = () => {
count.value++;
// 发布事件,携带数据
window.eventBus.emit('count-change', count.value);
};
return { count, sendCount };
},
template: `<button @click="sendCount">应用1发送计数</button>`
}).mount('#app1');
// 应用2:监听事件(接收数据)
createApp({
setup() {
const receiveCount = ref(0);
// 监听事件,接收数据
window.eventBus.on('count-change', (val) => {
receiveCount.value = val;
});
return { receiveCount };
},
template: `<div>应用2接收的计数:{{ receiveCount }}</div>`
}).mount('#app2');
</script>
3.Pinia/Vuex
Pinia(Vue 3 官方推荐)/ Vuex 是专门的状态管理库,可创建全局共享的状态仓库,多个应用实例通过访问同一仓库实现数据共享(最规范的方案)。
创建全局 Pinia
// src/store/index.js
import { createPinia, defineStore } from 'pinia';
// 1. 创建全局 Pinia 实例(唯一)
export const pinia = createPinia();
// 2. 定义共享仓库
export const useSharedStore = defineStore('shared', {
state: () => ({
count: 0,
message: 'Pinia 共享数据'
}),
actions: {
increment() {
this.count++;
},
updateMessage(newMsg) {
this.message = newMsg;
}
}
});
多个应用实例挂载同一 Pinia 并使用仓库
// src/app1.js(应用实例1)
import { createApp } from 'vue';
import { pinia, useSharedStore } from './store';
import App1 from './App1.vue';
const app1 = createApp(App1);
// 挂载全局 Pinia 实例
app1.use(pinia);
// 组件内使用仓库
// App1.vue 中:
// setup() { const store = useSharedStore(); store.increment(); }
app1.mount('#app1');
// src/app2.js(应用实例2)
import { createApp } from 'vue';
import { pinia, useSharedStore } from './store';
import App2 from './App2.vue';
const app2 = createApp(App2);
// 挂载同一个 Pinia 实例
app2.use(pinia);
// App2.vue 中可直接访问同一份仓库数据
app2.mount('#app2');
组件内使用示例(App1.vue):
<template>
<div>
<h3>应用1 - {{ store.message }}</h3>
<p>计数:{{ store.count }}</p>
<button @click="store.increment">+1</button>
</div>
</template>
<script setup>
import { useSharedStore } from './store';
const store = useSharedStore();
</script>
组件内使用示例(App2.vue):
<template>
<div>
<h3>应用2 - {{ store.message }}</h3>
<p>同步计数:{{ store.count }}</p>
<button @click="store.updateMessage('应用2修改了消息')">修改消息</button>
</div>
</template>
<script setup>
import { useSharedStore } from './store';
const store = useSharedStore();
</script>
4.共享响应式对象
1. 非sfc方式
直接创建一个独立的响应式对象(ref/reactive),作为多个应用实例的 “数据源”,本质是将响应式数据抽离到实例外部。
<script>
const { createApp, ref } = Vue;
// 1. 抽离共享的响应式数据(独立于应用实例)
const sharedData = ref({
count: 0,
text: '共享响应式数据'
});
// 应用1:使用共享数据
createApp({
setup() {
const increment = () => sharedData.value.count++;
return { sharedData, increment };
},
template: `<div>应用1:{{ sharedData.count }} <button @click="increment">+1</button></div>`
}).mount('#app1');
// 应用2:使用同一份共享数据
createApp({
setup() {
const changeText = () => sharedData.value.text = '应用2修改';
return { sharedData, changeText };
},
template: `<div>应用2:{{ sharedData.text }} / {{ sharedData.count }} <button @click="changeText">改文本</button></div>`
}).mount('#app2');
</script>
2.sfc的方式
![]()
<template>
<div class="container">
<h1>Vue 3 共享Ref示例</h1>
<!-- 主应用组件 -->
<div class="main-app">
<h2>主应用</h2>
<p>共享计数: {{ sharedCount }}</p>
<p>标题: {{ title }}</p>
<button @click="incrementCount">增加计数</button>
<button @click="changeTitle">修改标题</button>
</div>
<!-- 动态创建的组件容器 -->
<div id="dynamic-component"></div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { createApp } from 'vue'
// 子组件定义
const ChildComponent = {
template: `
<div class="child-component">
<h3>动态创建的子组件</h3>
<p>共享计数: {{ count }}</p>
<p>标题: {{ title }}</p>
<button @click="decrementCount">减少计数</button>
<button @click="resetTitle">重置标题</button>
</div>
`,
props: {
count: {
// 这里要传入ref类型
type: Object,
required: true,
},
title: {
// 这里要传入ref类型
type: Object,
required: true,
},
onDecrement: {
type: Function,
required: true,
},
onResetTitle: {
type: Function,
required: true,
},
},
methods: {
decrementCount() {
this.onDecrement()
},
resetTitle() {
this.onResetTitle()
},
},
}
// 创建共享的ref
const sharedCount = ref(0)
const title = ref('Hello')
let dynamicApp = null
const incrementCount = () => {
sharedCount.value++
}
const decrementCount = () => {
if (sharedCount.value > 0) {
sharedCount.value--
}
}
const changeTitle = () => {
title.value = `标题已修改 ${new Date().toLocaleTimeString()}`
}
const resetTitle = () => {
title.value = 'Hello'
}
// 动态应用的根组件
const DynamicRoot = {
template: '<ChildComponent :count="count" :title="title" :on-decrement="onDecrement" :on-reset-title="onResetTitle" />',
components: {
ChildComponent,
},
props: {
count: Number,
title: String,
onDecrement: Function,
onResetTitle: Function,
},
}
onMounted(() => {
// 使用createApp(App, props)的写法创建动态应用
dynamicApp = createApp(DynamicRoot, {
count: sharedCount,
title: title,
onDecrement: decrementCount,
onResetTitle: resetTitle,
})
// 挂载到DOM
dynamicApp.mount('#dynamic-component')
})
onUnmounted(() => {
// 清理动态创建的应用
if (dynamicApp) {
dynamicApp.unmount()
}
})
</script>
<style scoped>
.container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
.main-app,
.child-component {
border: 2px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
background-color: #f9f9f9;
}
.child-component {
border-color: #007bff;
background-color: #f0f8ff;
}
button {
background-color: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin: 5px;
}
button:hover {
background-color: #0056b3;
}
h1,
h2,
h3 {
color: #333;
}
</style>
注意 子组件需要使用ref类型作为参数,因为是根节点