Vue 3 多实例 + 缓存复用:理念及实践

Vue 3 多实例 + 缓存复用:理念、实践与挑战

在一些复杂的 Web 应用场景中,我们希望在同一页面或多个入口动态创建多个 Vue 实例,它们界面功能完全相同、逻辑相同,但内部数据与状态互不干扰;同时,用户切换回来后实例要能复用,不重建、不丢失状态。本文从需求拆解、设计思路、技术细节、难点与性能优化讲起,给出可运行的代码模板与注意事项,助你在项目中构建健壮的多实例架构。

目录

  • 背景与动机
  • 设计目标与核心需求
  • 实例工厂 + 缓存池 模式
  • 注入初始数据与状态隔离
  • 切换 / 显示 / 隐藏 / 卸载 策略
  • 常见难点与坑
  • 性能 / 内存 优化
  • 对比设计方案
  • 案例演示:多聊天窗口实例
  • 总结与未来方向

1. 背景与动机

1.1 为什么传统单实例模式不够?

在常规 SPA 架构下,一个 Vue 实例管理整个页面的路由、状态、组件树,是主流、也是最清晰的方式。但在以下几类场景中,单实例往往无法满足:

  • 插件 / SDK 嵌入:你希望你的应用片段可以被其他页面插入多个位置,每个位置运行独立模块;
  • 多个浮窗 / 工具面板:每个浮窗其实是一个完整的小应用,希望状态隔离;
  • 多个子页面 / 多入口实例:同一页面可能存在多个 "子应用" 实例并行存在;
  • 切换回来状态要保留:当用户从 A 切换到 B,再回 A,希望 A 的状态维持,不要重置。

如果你用单实例 + 组件切换,要么共享状态,要么每次切换清空,非常影响体验。

1.2 需求拆解:我们到底要什么?

把需求拆成几个关键点:

  • 界面 / 功能 一致:每个实例使用相同组件结构、逻辑代码;
  • 状态隔离:一个实例的修改不影响其他实例;
  • 缓存复用:切回来时还能继续原来状态,不要重 mount/unmount;
  • 按需创建:不是所有 tab / 页面都提前创建,只有访问时才启动;
  • 可控销毁:避免无限实例消耗资源,设计回收机制;
  • 简洁接口:对业务使用方暴露的 API 简单易用,不要让使用方关心内部细节。

2. 设计目标与核心机制

整体架构可以设计成以下模块,各模块职责清晰,协同工作以实现多实例的高效管理与缓存复用。

模块 职责
实例工厂 接收挂载 id + 初始化数据,返回或创建 Vue 实例
容器管理 在 DOM 上创建 / 隐藏 / 显示对应容器节点
状态注入 给实例注入其独立的业务上下文数据
缓存池 保存已创建实例的引用与其状态快照
卸载 / 销毁 过期或不再使用时卸载实例,释放资源
切换逻辑 在显示 / 隐藏之间切换,而不是反复卸载 / 重挂

在这个框架下,最核心是 createAppInstance(targetId, initData) 函数 + instancePool 缓存策略。通过实例工厂函数统一创建和获取实例,借助缓存池实现实例的复用,避免重复创建带来的性能损耗,同时保证实例状态的稳定。

3. 实例工厂 + 缓存池 模式

3.1 缓存池结构

我们使用一个 Map<string, InstanceRecord> 来缓存每个实例,这种数据结构能够快速进行键值对的查找、插入和删除操作,非常适合用于实例的缓存管理。

typescript 复制代码
interface InstanceRecord {
  app: ReturnType<typeof createApp> // Vue 应用实例
  cache: any                        // 业务数据快照,用于保存实例相关的业务数据
  mounted: boolean                  // 标记实例是否已挂载
  lastUsedAt: number                // 最后使用时间,用于判断实例是否过期
}

const instancePool = new Map<string, InstanceRecord>()

其中,key 是挂载容器的 targetId,通过它可以唯一标识一个实例;cache 用于存储业务层传入的初始数据或数据快照,确保实例复用时有数据可恢复;lastUsedAt 则用于后续的过期销毁判断,当实例长时间未使用时,可根据该时间进行清理,释放资源。

3.2 工厂函数 createAppInstance

工厂函数是创建和获取实例的核心入口,它首先检查缓存池中是否已存在该实例,如果存在则直接返回,不存在则创建新实例并加入缓存池。下面是可直接使用或改造的模板代码:

typescript 复制代码
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

export function createAppInstance(targetId: string, initData: any = {}) {
  // 检查缓存池,存在则更新最后使用时间并返回
  if (instancePool.has(targetId)) {
    const rec = instancePool.get(targetId)!
    rec.lastUsedAt = Date.now()
    return rec
  }

  // 确保挂载容器存在,不存在则创建并添加到文档中
  let el = document.getElementById(targetId)
  if (!el) {
    el = document.createElement('div')
    el.id = targetId
    document.body.appendChild(el)
  }

  // 创建 Vue 应用实例,配置 Pinia 状态管理
  const app = createApp(App)
}

代码解释要点:

  • 缓存优先:如果之前已创建该实例,直接从缓存池获取并更新最后使用时间,避免重复创建;
  • 容器保障:确保挂载容器存在,为实例提供稳定的挂载目标;
  • 状态隔离:使用 app.provide 注入初始化数据时进行深拷贝,防止不同实例间因数据引用导致的状态污染;
  • 缓存记录:创建实例后,将其相关信息(应用实例、数据快照、挂载状态、最后使用时间)记录到缓存池,便于后续管理。

4. 注入初始数据与状态隔离策略

4.1 初始化数据注入

在 createAppInstance 函数中,我们通过 app.provide('initData', ...) 向实例注入初始化业务数据。这种方式可以在实例内部的任意组件中通过 inject 方法获取数据,且每个实例获取到的都是独立的一份数据。

注入代码回顾:

typescript 复制代码
app.provide('initData', JSON.parse(JSON.stringify(initData)))

组件中获取数据:

在组件(如 App.vue 或子组件)中,可以通过 inject 方法轻松拿到注入的初始化数据,代码如下:

html 复制代码
<script setup>
import { inject } from 'vue'
// 获取注入的初始化数据,默认值为空对象
const initData = inject('initData', {})
console.log('当前实例初始化数据:', initData)
</script>

由于注入数据时进行了深拷贝,每个实例内部拿到的都是自己独有的数据,修改数据不会影响其他实例,保证了数据层面的状态隔离。

4.2 独立状态管理(Pinia)

在多实例场景下,状态管理的隔离至关重要。如果多个实例共用一个 store 或 Pinia 实例,会导致状态共享,无法实现状态隔离。因此,关键策略是:每个 Vue 实例对应一个独立的 Pinia 实例。

实现方式:

在工厂函数 createAppInstance 中,为每个新创建的 Vue 实例单独创建 Pinia 实例并挂载,代码如下:

这样一来,当在不同实例的组件中使用 useMyStore() 获取状态时,得到的是对应 Pinia 实例下的状态,不同实例的状态完全独立,互不干扰。例如,在组件中使用 Pinia 状态:

通过这种方式,实现了状态管理层面的彻底隔离,确保每个实例的状态独立可控。

5. 切换 / 显示 / 隐藏 / 卸载 策略

在多实例场景中,实例的切换、显示、隐藏和卸载是高频操作。合理的操作策略不仅能保证用户体验(状态不丢失),还能优化性能(减少资源消耗)。

5.1 切换展示(隐藏 / 显示,而不是卸载)

用户在不同实例之间切换时,传统的卸载旧实例、挂载新实例的方式会导致状态丢失,且频繁的 DOM 操作和实例重建会消耗大量性能。因此,我们采用隐藏 / 显示容器的方式实现实例切换,实例本身不进行卸载,从而保留状态并提升性能。

切换逻辑代码:

typescript 复制代码
/**
 * 显示指定实例的容器
 * @param targetId 实例挂载容器的 id
 */
export function showApp(targetId: string) {
  const el = document.getElementById(targetId)
  if (el) el.style.display = 'block'
  // 可选:更新实例最后使用时间
  const rec = instancePool.get(targetId)
  if (rec) rec.lastUsedAt = Date.now()
}

/**
 * 隐藏指定实例的容器
 * @param targetId 实例挂载容器的 id
 */
export function hideApp(targetId: string) {
  const el = document.getElementById(targetId)
  if (el) el.style.display = 'none'
}

优势:

  • 状态保留:实例未被卸载,内部的响应式状态、定时器、事件监听等均保持正常,用户切换回来时可继续之前的操作;
  • 性能优化:避免了实例卸载和重建过程中的 DOM 销毁与创建、组件生命周期重新执行等开销,提升切换速度。

5.2 卸载 / 销毁实例

虽然隐藏 / 显示策略能很好地实现实例复用,但如果实例长时间不用,一直保留在内存中会造成资源浪费。因此,需要设计实例卸载 / 销毁机制,对过期或不再使用的实例进行清理。

销毁逻辑代码:

过期销毁机制设计:

为了自动清理长时间未使用的实例,我们可以设计一个定时检查机制,遍历缓存池中的实例,根据 lastUsedAt 判断实例是否过期(例如,超过 30 分钟未使用),若过期则执行销毁操作。

通过这种主动清理机制,可以有效防止实例无限累积导致的内存泄漏和性能下降问题。

6. 常见难点与坑

在实现 Vue 3 多实例 + 缓存复用的过程中,会遇到一些常见的难点和问题,需要提前规避和解决。

6.1 console.log (app) 太庞大,调试困难

Vue 实例包含大量内部属性(如响应式代理、VNode 树、依赖收集相关数据等),直接打印整个实例会输出海量信息,导致控制台卡顿甚至崩溃,难以定位关键问题。

解决方案:

调试时只打印关键信息,避免打印完整实例。例如,打印实例对应的 targetId、缓存数据、挂载容器等核心信息:

typescript 复制代码
// 推荐:只打印关键信息
const rec = instancePool.get(targetId)
if (rec) {
  console.log('实例调试信息:', {
    targetId,
    cache: rec.cache,
    container: rec.app._container,
    lastUsedAt: new Date(rec.lastUsedAt).toLocaleString()
  })
}

// 不推荐:直接打印完整实例
// console.log(rec.app) // 会输出大量冗余信息

6.2 watch /computed 重复触发

如果在模块级别定义共享的 ref 或 reactive 数据,或者多个实例共用同一个未隔离的状态源(如未单独创建 Pinia 实例),会导致不同实例中的 watch 或 computed 对同一数据进行监听,当数据变化时,所有实例的监听逻辑都会触发,造成不必要的性能消耗和逻辑混乱。

解决方案:

  • 状态私有化:所有状态(包括 ref、reactive、Pinia Store)都在实例内部创建,不在模块层共享;
  • Pinia 独立:确保每个 Vue 实例对应一个独立的 Pinia 实例(如 4.2 节所述);
  • 避免模块级共享数据:模块中只定义工具函数、类型接口等,不定义可修改的响应式数据。

反例(错误做法):

typescript 复制代码
// 模块级共享的响应式数据,会导致多实例监听冲突
export const sharedRef = ref(0)
html 复制代码
<script setup>
import { sharedRef, watch } from 'vue'
// 多个实例都会监听 sharedRef,数据变化时所有实例的 watch 都会触发
watch(sharedRef, (newVal) => {
  console.log('sharedRef 变化:', newVal)
})
</script>

正例(正确做法):

html 复制代码
<script setup>
import { ref, watch } from 'vue'
// 组件内部创建响应式数据,每个实例独立
const privateRef = ref(0) 
watch(privateRef, (newVal) => {
  console.log('当前实例 privateRef 变化:', newVal)
})
</script>

6.3 生命周期 & 异步 / 订阅清理不及时

组件中如果使用定时器(setTimeout、setInterval)、WebSocket 连接、全局事件监听(window.addEventListener)、订阅流(如 RxJS)等资源,若未在组件的 onUnmounted 钩子中及时清理,即使实例容器被隐藏,这些资源仍会在后台运行,导致内存泄漏、不必要的网络请求或逻辑错误。

解决方案:

在组件的 onUnmounted 钩子中,彻底清理所有占用的资源。例如:

html 复制代码
<script setup>
import { onUnmounted } from 'vue'

// 1. 定时器清理
const timer = setInterval(() => {
  console.log('定时器执行')
}, 1000)

// 2. 全局事件监听清理
function handleResize() {
  console.log('窗口大小变化')
}
window.addEventListener('resize', handleResize)

// 3. WebSocket 连接清理
const ws = new WebSocket('wss://example.com')
ws.onopen = () => {
  ws.send('连接建立')
}

// 在组件卸载时清理所有资源
onUnmounted(() => {
  // 清理定时器
  clearInterval(timer)
  // 移除全局事件监听
  window.removeEventListener('resize', handleResize)
  // 关闭 WebSocket 连接
  if (ws.readyState === WebSocket.OPEN) {
    ws.close(1000, '组件卸载')
  }
  // 若使用 RxJS 等订阅流,需取消订阅
  // subscription.unsubscribe()
})
</script>

关键原则:所有 "跨生命周期" 的资源(即创建后不会自动随组件卸载而释放的资源),都必须在 onUnmounted 中手动清理,避免内存泄漏。

6.4 容器 id 冲突 / DOM 被意外删除

多实例依赖唯一的 targetId 挂载容器,若出现以下情况,会导致实例挂载失败或状态异常:

  • id 重复:不同实例使用相同的 targetId,后创建的实例会覆盖先创建的实例;
  • DOM 被删除:业务代码意外删除了实例的挂载容器(如 document.getElementById(targetId).remove()),但未调用 destroyApp,导致实例引用残留。

解决方案:

  • id 生成规范:使用 "前缀 + 唯一标识" 生成 targetId,例如 chat-window-userId−{userId}-userId−{timestamp},避免手动指定重复 id;
  • DOM 操作封装:禁止业务代码直接操作实例挂载容器,所有容器的创建 / 删除通过 createContainer/destroyContainer 工具函数进行:
typescript 复制代码
// 工具函数:创建并返回唯一容器
export function createContainer(prefix: string): string {
  const targetId = `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`
  const el = document.createElement('div')
  el.id = targetId
  document.body.appendChild(el)
  return targetId
}

// 工具函数:安全删除容器
export function destroyContainer(targetId: string) {
  const el = document.getElementById(targetId)
  if (el && el.parentNode) {
    el.parentNode.removeChild(el)
  }
}
  • 实例状态校验:在 showApp/hideApp 等函数中增加容器存在性校验,避免操作不存在的 DOM:
typescript 复制代码
export function showApp(targetId: string) {
  const el = document.getElementById(targetId)
  if (!el) {
    throw new Error(`实例容器 ${targetId} 已被删除,请检查 DOM 操作`)
  }
  el.style.display = 'block'
}

6.5 CSS / 样式污染

多个实例共用相同的组件结构,若样式未做隔离,会出现 "一个实例的样式影响另一个实例" 的问题(例如,两个聊天窗口的标题样式互相覆盖)。

解决方案:

  • 基础隔离:优先使用 Vue 内置的
html 复制代码
<!-- ChatWindow.vue:使用 scoped 样式 -->
<style scoped>
.chat-title {
  font-size: 16px;
  color: #333;
}
</style>
html 复制代码
<!-- 或使用 CSS Modules -->
<style module>
.title {
  font-size: 16px;
  color: #333;
}
</style>
<template>
  <h2 :class="$style.title">聊天窗口</h2>
</template>
  • 进阶隔离:若需更强的样式隔离(如实例间有完全不同的主题),可结合 Shadow DOM 封装实例容器:
typescript 复制代码
// 改造 createAppInstance:使用 Shadow DOM 隔离样式
export function createAppInstance(targetId: string, initData: any = {}) {
  // ... 省略缓存检查逻辑 ...

  let el = document.getElementById(targetId)
  if (!el) {
    el = document.createElement('div')
    el.id = targetId
    // 创建 Shadow DOM 并附加到容器
    const shadowRoot = el.attachShadow({ mode: 'open' })
    // 在 Shadow DOM 中创建挂载点
    const mountPoint = document.createElement('div')
    mountPoint.id = `mount-${targetId}`
    shadowRoot.appendChild(mountPoint)
    document.body.appendChild(el)
  }

  // 注意:此时挂载目标需改为 Shadow DOM 中的挂载点
  const shadowRoot = el.shadowRoot
  if (!shadowRoot) {
    throw new Error(`容器 ${targetId} 未初始化 Shadow DOM`)
  }
  const mountPoint = shadowRoot.getElementById(`mount-${targetId}`)
  if (!mountPoint) {
    throw new Error(`Shadow DOM 挂载点不存在`)
  }

  // 挂载到 Shadow DOM 内的节点
  const app = createApp(App)
  app.mount(mountPoint)
  // ... 省略后续逻辑 ...
}
  • 命名规范:若无法使用 Shadow DOM,可通过 BEM 命名规范 为每个实例的样式添加唯一前缀(如基于 targetId 生成前缀):
typescript 复制代码
// 实例中生成唯一样式前缀
const stylePrefix = `chat-window-${targetId}`
html 复制代码
<template>
  <div :class="stylePrefix">
    <h2 :class="`${stylePrefix}__title`">聊天窗口</h2>
  </div>
</template>
<style>
.chat-window-${targetId}__title {
  font-size: 16px;
  color: #333;
}
</style>

7. 性能 / 内存 优化

多实例架构若不做优化,会因实例数量累积、资源占用过高导致页面卡顿。以下是针对性的优化策略:

7.1 限制实例总数,避免无限创建

通过 "最大实例数阈值" 控制缓存池大小,当实例数量超过阈值时,销毁 "最久未使用(LRU)" 的实例:

typescript 复制代码
// 配置:最大实例数
const MAX_INSTANCE_COUNT = 5

export function createAppInstance(targetId: string, initData: any = {}) {
  // 1. 检查缓存,存在则直接返回
  if (instancePool.has(targetId)) {
    // ... 省略缓存逻辑 ...
  }

  // 2. 若实例数超过阈值,销毁最久未使用的实例
  if (instancePool.size >= MAX_INSTANCE_COUNT) {
    // 按 lastUsedAt 排序,取最久未使用的实例
    const sortedInstances = Array.from(instancePool.entries()).sort(
      ([, a], [, b]) => a.lastUsedAt - b.lastUsedAt
    )
    const [oldTargetId] = sortedInstances[0]
    destroyApp(oldTargetId)
    console.log(`实例数超过阈值,销毁最久未使用实例:${oldTargetId}`)
  }

  // 3. 创建新实例
  // ... 省略实例创建逻辑 ...
}

7.2 懒加载非核心组件与逻辑

实例初始化时,仅加载当前必需的组件(如聊天窗口的输入框、消息列表),非核心组件(如历史消息搜索、设置面板)通过 "按需加载" 延迟加载:

html 复制代码
<!-- ChatWindow.vue:懒加载非核心组件 -->
<template>
  <div class="chat-window">
    <MessageList /> <!-- 核心组件:立即加载 -->
    <ChatInput />   <!-- 核心组件:立即加载 -->
    <template v-if="showSearchPanel">
      <!-- 非核心组件:懒加载 -->
      <Suspense>
        <template #default>
          <SearchPanel />
        </template>
        <template #fallback>
          <div>加载中...</div>
        </template>
      </Suspense>
    </template>
  </div>
</template>

<script setup>
import MessageList from './MessageList.vue'
import ChatInput from './ChatInput.vue'
// 懒加载非核心组件
const SearchPanel = defineAsyncComponent(() => import('./SearchPanel.vue'))
const showSearchPanel = ref(false)
</script>

7.3 仅缓存轻量业务数据,避免序列化复杂对象

instancePool 的 cache 字段仅存储 "必要的业务快照数据"(如聊天窗口的用户 ID、未读消息数),不缓存复杂对象(如完整消息列表、DOM 引用),减少内存占用:

typescript 复制代码
// 错误:缓存复杂对象(消息列表)
const rec: InstanceRecord = {
  app,
  cache: {
    userId: initData.userId,
    messages: initData.messages // 复杂数组,占用内存大
  },
  // ... 其他字段 ...
}

// 正确:仅缓存轻量快照
const rec: InstanceRecord = {
  app,
  cache: {
    userId: initData.userId,
    unreadCount: initData.messages.filter(m => !m.read).length // 仅缓存统计结果
  },
  // ... 其他字段 ...
}

若需恢复复杂数据(如消息列表),可在实例复用时分发 API 请求重新获取,而非缓存完整数据。

7.4 异步销毁,避免阻塞主线程

实例销毁时(尤其是包含大量 DOM 节点的实例),直接执行 unmount 和 DOM 删除可能阻塞主线程,导致页面卡顿。可通过 requestIdleCallback 或 setTimeout 异步执行销毁逻辑:

typescript 复制代码
export function destroyApp(targetId: string) {
  const rec = instancePool.get(targetId)
  if (!rec) return

  // 异步执行销毁,避免阻塞主线程
  requestIdleCallback(() => {
    // 1. 卸载 Vue 应用
    rec.app.unmount()
    // 2. 移除缓存记录
    instancePool.delete(targetId)
    // 3. 删除 DOM 容器
    const el = document.getElementById(targetId)
    if (el && el.parentNode) {
      el.parentNode.removeChild(el)
    }
    console.log(`实例 ${targetId} 已异步销毁`)
  }, { timeout: 1000 }) // 1 秒内若未空闲,强制执行
}

8. 对比设计方案

在 "多实例" 相关场景中,常见的替代方案有 "Tab + 组件切换" 和 "iframe",以下是三者的对比分析:

方案 优点 缺点 适用场景
多实例(本文方案) 1. 状态完全隔离; 2. 复用灵活,切换无状态丢失; 3. 通信成本低(可通过全局事件 / 状态管理通信) 1. 实例管理逻辑复杂; 2. 内存占用高于组件切换; 3. 需手动处理样式隔离 1. 插件 / SDK 嵌入; 2. 多浮窗 / 工具面板; 3. 小子应用并行运行
Tab + 组件切换 1. 实现简单,无需额外管理实例; 2. 内存占用低(仅一个实例); 3. 样式天然隔离 1. 状态隔离困难(需手动重置 / 保存状态); 2. 切换时需重新渲染,可能有卡顿; 3. 无法并行运行多个组件 1. 管理后台 Tab 页; 2. 数据仪表盘; 3. 无状态保留需求的切换场景
iframe 1. 完全隔离(DOM、CSS、JavaScript 环境); 2. 无需担心样式 / 脚本冲突; 3. 可嵌入第三方应用 1. 通信成本高(仅支持 postMessage); 2. 内存占用极高; 3. 性能损耗大(页面重绘 / 回流独立) 1. 第三方应用嵌入; 2. 需完全隔离的沙盒环境; 3. 插件平台(如浏览器插件)

结论:若需 "状态隔离 + 复用保留" 且不希望过高通信 / 性能成本,优先选择本文的 "多实例" 方案;若仅需简单切换且无状态保留需求,选择 "Tab + 组件切换";若需完全隔离第三方内容,选择 "iframe"。

9. 案例演示:多聊天窗口实例

基于前文的设计思路,我们实现一个 "多聊天窗口" 案例,支持打开多个独立聊天窗口、切换保留状态、关闭销毁实例。

9.1 核心工具函数(chatInstanceFactory.ts)

typescript 复制代码
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ChatWindow from './ChatWindow.vue'

// 实例记录接口
interface InstanceRecord {
  app: ReturnType<typeof createApp>
  cache: { userId: string; title: string }
  mounted: boolean
  lastUsedAt: number
}

// 缓存池
const instancePool = new Map<string, InstanceRecord>()
// 配置:最大实例数
const MAX_INSTANCE_COUNT = 3

// 创建/获取聊天窗口实例
export function openChatWindow(initData: { userId: string; title: string }): string {
  // 生成唯一 targetId(基于用户 ID + 时间戳)
  const targetId = `chat-window-${initData.userId}-${Date.now().toString().slice(-4)}`

  // 1. 检查缓存,存在则显示并返回
  if (instancePool.has(targetId)) {
    const rec = instancePool.get(targetId)!
    rec.lastUsedAt = Date.now()
    showChatWindow(targetId)
    return targetId
  }

  // 2. 实例数超过阈值,销毁最久未使用实例
  if (instancePool.size >= MAX_INSTANCE_COUNT) {
    const sortedInstances = Array.from(instancePool.entries()).sort(
      ([, a], [, b]) => a.lastUsedAt - b.lastUsedAt
    )
    const [oldTargetId] = sortedInstances[0]
    closeChatWindow(oldTargetId)
  }

  // 3. 创建容器(含 Shadow DOM 样式隔离)
  const container = document.createElement('div')
  container.id = targetId
  container.style.position = 'fixed'
  container.style.bottom = '20px'
  container.style.right = `${(instancePool.size * 320) + 20}px` // 窗口横向排列
  container.style.width = '300px'
  container.style.height = '400px'
  container.style.border = '1px solid #eee'
  container.style.borderRadius = '8px'
  container.style.overflow = 'hidden'

  // 初始化 Shadow DOM
  const shadowRoot = container.attachShadow({ mode: 'open' })
  const mountPoint = document.createElement('div')
  shadowRoot.appendChild(mountPoint)
  document.body.appendChild(container)

  // 4. 创建 Vue 实例
  const app = createApp(ChatWindow)
  const pinia = createPinia()
  app.use(pinia)
  // 注入初始化数据
  app.provide('chatInitData', { ...initData, targetId })
  // 挂载到 Shadow DOM 内的节点
  app.mount(mountPoint)

  // 5. 加入缓存池
  const rec: InstanceRecord = {
    app,
    cache: { userId: initData.userId, title: initData.title },
    mounted: true,
    lastUsedAt: Date.now()
  }
  instancePool.set(targetId, rec)

  return targetId
}

// 显示聊天窗口
export function showChatWindow(targetId: string) {
  const container = document.getElementById(targetId)
  if (container) container.style.display = 'block'
}

// 隐藏聊天窗口
export function hideChatWindow(targetId: string) {
  const container = document.getElementById(targetId)
  if (container) container.style.display = 'none'
}
相关推荐
一大树3 小时前
Vue3优化指南:少写代码,多提性能
vue.js
在下木子生3 小时前
SpringBoot基于工厂模式的多类型缓存设计
java·spring boot·缓存
HuangYongbiao3 小时前
Rspack 原理:webpack,我为什么不要你
前端
yinuo3 小时前
前端项目开发阶段崩溃?试试这招“Node 内存扩容术”,立马复活!
前端
Lu Yao_3 小时前
Redis 缓存
数据库·redis·缓存
前端鳄鱼崽3 小时前
【react-native-inspector】全网唯一开源 react-native 点击组件跳转到编辑器
前端·react native·react.js
用户98402276679183 小时前
【React.js】渐变环形进度条
前端·react.js·svg
90后的晨仔3 小时前
Webpack完全指南:从零到一彻底掌握前端构建工具
前端·vue.js
Holin_浩霖3 小时前
JavaScript 语言革命:ES6+ 现代编程范式深度解析与工程实践
前端