Vue 3有状态Composable单例模式与跨组件共享

0 阅读20分钟

在 Vue 3 组合式 API(Composition API)的工程实践中,Composable Hook 是逻辑复用、状态封装与代码解耦的核心方案,极大地替代了 Vue 2 中的 Mixins、HOC(高阶组件)等复用方式。但在实际开发中,大量 Composable 包含响应式状态、DOM 操作、全局事件监听、持续循环或第三方重型实例初始化,这类被称为“有状态 Composable”的 Hook,若在多个组件中重复调用,会生成多个相互隔离的实例,进而引发状态不同步、资源重复创建、内存泄漏、功能逻辑割裂等一系列问题。

本文脱离具体业务场景,从通用角度出发,系统阐述有状态 Composable 的实例隔离问题、底层执行机制、判断标准,重点讲解单例模式与依赖注入结合的解决方案,并补充工程化规范、常见误区、适用场景扩展及优化方向,为中大型 Vue 3 项目提供可直接落地、通用性强的最佳实践,帮助开发者规避误用风险,提升代码可维护性、性能与稳定性。

一、引言

Vue 3 推出的组合式 API,以其灵活性、可组合性和可维护性,成为现代 Vue 项目的主流开发范式。Composable Hook 作为组合式 API 的核心载体,允许开发者将分散在组件中的逻辑抽离成独立的可复用函数,实现“逻辑与 UI 分离”,大幅提升代码的复用率和可读性。

但在实际项目开发中,开发者常陷入一个典型误区:将所有 Composable 视为“可随意调用的工具函数”,忽视了 Composable 的“状态特性”。事实上,Composable 分为“有状态”和“无状态”两类,其中有状态 Composable 由于包含响应式状态、副作用或全局资源操作,必须保证全局唯一实例,否则会出现一系列严重的业务问题。

例如,在大屏监控、物联网可视化、音视频播放、地图应用、实时通信等场景中,若多个组件重复调用同一个有状态 Composable,会导致多个重型实例(如渲染器、地图实例、WebSocket 连接)被创建,不仅造成资源浪费,还会出现状态不同步(如开关控制失效、数据显示异常)、事件监听冲突、内存泄漏等问题,严重影响项目稳定性和用户体验。

本文立足通用场景,全面拆解有状态 Composable 的核心问题与解决方案,帮助开发者建立正确的使用认知,规范开发流程,为中大型 Vue 3 项目的长期迭代提供支撑。

二、核心问题:有状态 Composable 的实例隔离与危害

2.1 问题核心表现

有状态 Composable 的核心问题的是:每次调用都会创建一个全新的独立实例,多个实例之间的状态、副作用、资源完全隔离,互不感知。具体表现为:

  • 状态不同步:组件 A 调用 Composable 并修改其响应式状态,组件 B 同样调用该 Composable,无法获取组件 A 修改后的状态,导致界面显示与实际业务逻辑不一致。

  • 资源重复创建:每个实例都会初始化独立的重型资源(如渲染器、地图、WebSocket 连接),造成内存、CPU 资源浪费,严重时会导致页面卡顿、崩溃。

  • 事件监听冲突:多个实例重复绑定全局事件(如 resize、mousemove),导致事件触发多次,出现功能异常(如多次渲染、重复请求)。

  • 内存泄漏:多个实例无法被正确销毁,残留的事件监听、定时器、实例引用会占用内存,长期运行会导致页面性能退化。

  • 功能逻辑割裂:不同组件操作的是不同实例,无法实现跨组件的功能联动(如组件 A 启动某个功能,组件 B 无法暂停该功能)。

2.2 通用场景示例

以下是几个典型的有状态 Composable 误用场景,直观呈现问题:

场景1:全局音视频播放器

封装 `useAudioPlayer()` 用于管理音频播放,内部包含 `isPlaying` 响应式状态、音频实例、播放/暂停方法。若父组件和子组件都调用该 Hook,会创建两个独立的音频实例,父组件播放音频时,子组件的 `isPlaying` 状态仍为 false,且无法控制父组件的音频播放。

场景2:全局实时通信(WebSocket)

封装 `useWebSocket()` 用于建立全局 WebSocket 连接,内部包含连接状态、消息队列、发送/接收方法。若多个组件重复调用,会创建多个 WebSocket 连接,导致消息重复接收、连接冲突,甚至被服务端拒绝连接。

场景3:图表渲染(ECharts/D3)

封装 `useChart()` 用于初始化图表实例,内部包含图表渲染器、数据状态、更新方法。若多个组件重复调用,会创建多个图表实例,占用大量显存和 CPU 资源,导致页面卡顿,且不同组件的图表数据无法同步。

2.3 问题危害总结

有状态 Composable 重复调用的危害,不仅局限于功能异常,还会影响项目的性能、稳定性和可维护性:

  • 性能损耗:重复创建重型实例、重复绑定事件,导致内存占用过高、CPU 使用率飙升,尤其在移动端或低配置设备上,会出现页面卡顿、闪退。

  • 调试困难:多个实例的日志混杂、状态互不同步,出现问题时难以定位根源,增加调试成本。

  • 维护成本高:不同组件的实例逻辑相互独立,后续需求迭代时,需要修改所有调用该 Composable 的组件,代码冗余且易出错。

  • 稳定性风险:残留的实例和事件监听会导致内存泄漏,长期运行可能引发页面崩溃、功能异常等严重问题。

三、根本原因:Composable 的执行机制与状态隔离原理

3.1 Composable 的本质:普通函数,无天然单例

Vue 3 并未对 Composable 做任何“单例化”特殊处理,其本质就是一个返回响应式状态和方法的普通函数。与工具函数不同,Composable 内部通常包含响应式状态、副作用操作,但它的执行机制与普通函数完全一致:

每次调用 Composable,都会从头到尾执行函数体中的所有代码,创建新的响应式对象(ref/reactive)、初始化新的实例、绑定新的事件监听,最终返回一套全新的状态和方法。

示例代码如下:

// 有状态 Composable(每次调用都会创建新实例)
export function useComposable() {
  // 每次调用都会创建新的 ref 对象
  const state = ref({
    isActive: false,
    data: []
  });
  
  // 每次调用都会绑定新的事件监听
  const handleResize = () => {
    // 处理窗口 resize 逻辑
  };
  window.addEventListener('resize', handleResize);
  
  // 每次调用都会返回新的方法集合
  const toggle = () => {
    state.value.isActive = !state.value.isActive;
  };
  
  return {
    state,
    toggle
  };
}

上述代码中,无论在哪个组件中调用 `useComposable()`,都会创建新的 `state` ref 对象、绑定新的 `resize` 事件监听,返回新的 `toggle` 方法。多个组件调用后,会形成多个相互隔离的实例,这就是状态不同步、资源重复创建的根本原因。

3.2 状态隔离的底层逻辑

Vue 3 的响应式系统基于“引用地址”实现响应式追踪,每个 ref/reactive 对象都有唯一的内存地址。当 Composable 被多次调用时,每次创建的响应式对象内存地址不同,组件之间的状态绑定的是不同的引用,因此修改一个组件中的状态,无法影响另一个组件中的状态。

同时,Composable 中的副作用操作(如事件监听、实例初始化),每次调用都会执行一次,不会与其他调用产生关联。例如,多次调用包含 `window.addEventListener` 的 Composable,会绑定多个相同的事件监听,导致事件触发时执行多次回调。

此外,Composable 的生命周期与调用它的组件绑定:当组件销毁时,若 Composable 中没有手动清理副作用(如移除事件监听、销毁实例),会导致内存泄漏;而多个组件调用时,每个实例的生命周期相互独立,清理逻辑无法同步执行。

3.3 与无状态 Composable 的本质区别

为了更清晰地理解有状态 Composable 的执行机制,我们对比无状态 Composable 的特点:

特性有状态 Composable无状态 Composable
内部状态包含 ref/reactive 响应式状态无任何内部状态,仅依赖输入参数
副作用有(事件监听、实例初始化、定时器等)无任何副作用
调用影响每次调用创建新实例,影响全局资源每次调用仅执行逻辑,不影响全局
复用方式必须单例,全局共享可随意调用,多次调用无影响
典型场景音视频、地图、实时通信、渲染器数据格式化、工具函数、逻辑计算

无状态 Composable 本质是“纯函数”,仅负责数据转换或逻辑计算,不依赖任何外部状态,也不产生任何副作用,因此可以在多个组件中随意调用,不会出现任何问题。而有状态 Composable 由于包含状态和副作用,必须严格控制调用次数,保证全局唯一实例。

四、判断标准:如何区分有状态与无状态 Composable?

在实际开发中,正确区分有状态与无状态 Composable,是避免误用的前提。以下是明确的判断标准,满足任意一条即属于“有状态 Composable”,必须采用单例模式;若全部不满足,则属于“无状态 Composable”,可随意调用。

4.1 有状态 Composable 的判断标准

  • 包含响应式状态:内部使用 ref、reactive、computed 等 API 创建响应式对象,且状态不依赖外部输入参数,或状态需要在多个组件中共享。

  • 操作全局 DOM/资源:初始化或操作全局唯一的 DOM 元素(如 canvas、audio、video)、全局样式、全局存储(如 localStorage 操作但需统一管理)。

  • 绑定全局事件监听:使用 `window.addEventListener`、`document.addEventListener` 等绑定全局事件(如 resize、mousemove、keydown、message)。

  • 启动持续循环/定时器:使用 `requestAnimationFrame`、`setInterval`、`setTimeout` 等启动持续执行的逻辑(如动画循环、定时请求)。

  • 初始化第三方重型实例:创建第三方库的重型实例(如 ECharts 图表、地图实例、Three.js 渲染器、WebSocket 连接、音视频播放器)。

  • 建立长连接/持久化资源:建立 WebSocket、EventSource、RTC 等长连接,或创建需要持久化的资源(如数据库连接、缓存实例)。

4.2 常见有状态 Composable 示例

  • `useAudioPlayer()`:管理音频播放,包含播放状态、音频实例。

  • `useWebSocket()`:管理全局 WebSocket 连接,包含连接状态、消息处理。

  • `useChart()`:初始化图表实例,包含图表数据、更新方法。

  • `useMap()`:初始化地图实例,包含地图控制、标记管理。

  • `useTheme()`:管理全局主题,包含主题状态、切换方法。

  • `useScreenFull()`:管理全屏状态,包含全屏切换、状态监听。

  • `useTimer()`:管理全局定时器,包含定时任务、启停控制。

4.3 无状态 Composable 示例

  • `useFormatDate()`:格式化日期,仅接收日期参数,返回格式化结果。

  • `useValidate()`:表单验证,接收表单数据和规则,返回验证结果。

  • `useMathCalculate()`:数学计算(如加减乘除、进制转换),仅依赖输入参数。

  • `useStringUtils()`:字符串处理(如截取、替换、去重),无内部状态。

五、解决方案:单例模式 + 依赖注入(Provide/Inject)

针对有状态 Composable 的实例隔离问题,最通用、最符合 Vue 设计哲学的解决方案是:单例模式 + 依赖注入(Provide/Inject)。核心思路是:保证有状态 Composable 全局仅被调用一次,生成唯一实例,再通过 Vue 3 的 Provide/Inject API 将实例注入全局,所有需要使用该 Composable 的组件,通过 Inject 获取同一个实例,实现状态同步和资源共享。

5.1 核心方案架构

方案整体分为 4 个步骤,形成“唯一入口 + 全局提供 + 下层注入 + 禁止重复调用”的闭环:

  1. 顶层唯一调用:在应用的顶层组件(如 App.vue、布局组件、入口页面组件)中,调用一次有状态 Composable,生成唯一实例。

  2. 全局提供:通过 Vue 3 的 `provide` API,将生成的 Composable 实例注入应用全局,供所有子组件获取。

  3. 下层注入:所有需要使用该 Composable 的子组件、深层组件,通过 Vue 3 的 `inject` API 获取全局唯一实例,避免直接调用 Composable。

  4. 禁止重复调用:通过工程化规范和代码约束,禁止在非顶层组件中直接调用有状态 Composable,避免生成多个实例。

5.2 为什么选择 Provide/Inject?

在众多单例实现方案中,Provide/Inject 是最适合 Vue 3 项目的方案,相比其他方案(如全局变量、状态管理库),具有以下优势:

  • 支持深层组件穿透:无需通过 props 逐层传递实例,深层组件可直接通过 Inject 获取,避免“props 穿透”问题,提升代码可维护性。

  • 类型安全:结合 TypeScript,可通过 `InjectionKey` 实现类型校验,避免注入错误,提升代码健壮性。

  • 按需注入:组件可根据需求选择是否注入,无需强制依赖,灵活性高。

  • 天然支持单例:顶层唯一调用后,所有组件注入的都是同一个实例,确保状态同步和资源唯一。

  • 符合 Vue 官方最佳实践:Vue 官方文档明确推荐,对于跨组件共享的逻辑和状态,优先使用 Provide/Inject + Composable 的组合方式。

  • 不破坏 Composable 封装性:Composable 本身仍保持独立,仅通过 Provide/Inject 实现共享,不修改 Composable 内部逻辑。

5.3 完整落地实现(通用版)

以下是通用化的落地步骤,适用于所有有状态 Composable,无需修改 Composable 内部逻辑,仅需通过 Provide/Inject 实现共享。

步骤1:定义 Composable 及其返回类型(TypeScript)

首先,封装有状态 Composable,并明确其返回类型,确保类型安全。以通用的 `useStatefulComposable` 为例:

// src/hooks/useStatefulComposable.ts
import { ref, onBeforeUnmount } from 'vue';

// 定义 Composable 返回类型
export interface StatefulComposableReturn {
  // 响应式状态
  isActive: Ref<boolean>;
  count: Ref<number>;
  // 方法
  toggle: () => void;
  increment: () => void;
  decrement: () => void;
  // 清理方法
  cleanup: () => void;
}

// 有状态 Composable(全局需单例)
export function useStatefulComposable(): StatefulComposableReturn {
  const isActive = ref(false);
  const count = ref(0);
  
  // 副作用:绑定全局事件
  const handleResize = () => {
    console.log('窗口大小变化');
  };
  window.addEventListener('resize', handleResize);
  
  // 核心方法
  const toggle = () => {
    isActive.value = !isActive.value;
  };
  
  const increment = () => {
    count.value++;
  };
  
  const decrement = () => {
    count.value--;
  };
  
  // 清理副作用方法
  const cleanup = () => {
    window.removeEventListener('resize', handleResize);
    console.log('Composable 实例清理完成');
  };
  
  // 组件卸载时自动清理
  onBeforeUnmount(cleanup);
  
  return {
    isActive,
    count,
    toggle,
    increment,
    decrement,
    cleanup
  };
}

步骤2:创建注入 Key(确保类型安全)

新建注入 Key 文件,使用 Vue 3 的 `InjectionKey` 定义唯一的注入标识,确保注入和获取的实例类型一致,避免类型错误。

// src/hooks/injectionKeys.ts
import type { InjectionKey } from 'vue';
import type { StatefulComposableReturn } from './useStatefulComposable';

// 定义唯一的注入 Key(Symbol 确保唯一性)
export const STATEFUL_COMPOSABLE_KEY: InjectionKey<StatefulComposableReturn>
  = Symbol('statefulComposable');

步骤3:顶层组件(唯一调用 + 全局提供)

在应用的顶层组件(如 App.vue、布局组件)中,唯一调用有状态 Composable,生成实例,并通过 `provide` 将实例注入全局。

// src/App.vue(顶层组件)
<script setup lang="ts">
import { provide } from 'vue';
import useStatefulComposable from './hooks/useStatefulComposable';
import { STATEFUL_COMPOSABLE_KEY } from './hooks/injectionKeys';
import ChildComponent from './components/ChildComponent.vue';

// 【全局唯一调用】有状态 Composable
const statefulInstance = useStatefulComposable();

// 【全局提供】将实例注入全局,供所有子组件获取
provide(STATEFUL_COMPOSABLE_KEY, statefulInstance);
</script>

<template>
  <div>
    <h1>顶层组件</h1>
    <button @click="statefulInstance.toggle">
      切换状态({{ statefulInstance.isActive ? '开启' : '关闭' }})
    </button>
    <ChildComponent />
  </div>
</template>

步骤4:子组件/深层组件(注入使用)

所有需要使用该 Composable 的子组件、深层组件,通过 `inject` 获取全局唯一实例,无需直接调用 Composable,确保所有组件使用的是同一个实例。

// src/components/ChildComponent.vue(子组件)
<script setup lang="ts">
import { inject } from 'vue';
import { STATEFUL_COMPOSABLE_KEY } from '../hooks/injectionKeys';

// 【注入全局唯一实例】
const statefulInstance = inject(STATEFUL_COMPOSABLE_KEY)!;
</script>

<template>
  <div class="child">
    <h2>子组件</h2>
    <p>状态:{{ statefulInstance.isActive ? '开启' : '关闭' }}</p>
    <p>计数:{{ statefulInstance.count }}</p>
    <button @click="statefulInstance.increment">增加计数</button>
    <button @click="statefulInstance.decrement">减少计数</button>
  </div>
</template>

步骤5:组件卸载时统一清理

为避免内存泄漏,在顶层组件卸载时,调用 Composable 的清理方法,销毁实例、移除事件监听等副作用。

// src/App.vue(补充清理逻辑)
<script setup lang="ts">
import { provide, onBeforeUnmount } from 'vue';
import useStatefulComposable from './hooks/useStatefulComposable';
import { STATEFUL_COMPOSABLE_KEY } from './hooks/injectionKeys';
import ChildComponent from './components/ChildComponent.vue';

const statefulInstance = useStatefulComposable();
provide(STATEFUL_COMPOSABLE_KEY, statefulInstance);

// 顶层组件卸载时,统一清理 Composable 实例
onBeforeUnmount(() => {
  statefulInstance.cleanup();
});
</script>

实现效果

通过以上步骤,所有组件注入的都是同一个 Composable 实例,实现:

  • 状态同步:顶层组件修改 `isActive`、`count` 状态,子组件实时更新。

  • 资源唯一:仅创建一个实例,绑定一次事件监听,无资源重复创建。

  • 跨组件联动:子组件调用 `increment` 方法,顶层组件的 `count` 状态同步变化。

  • 统一清理:顶层组件卸载时,一次性清理所有副作用,避免内存泄漏。

5.4 兜底处理(可选,提升健壮性)

为避免注入失败(如忘记在顶层提供实例),可在子组件注入时添加兜底处理,或抛出明确的错误提示,便于调试。

// 子组件注入时添加兜底
const statefulInstance = inject(STATEFUL_COMPOSABLE_KEY, () => {
  throw new Error('请在顶层组件调用 useStatefulComposable 并 provide 实例');
});

六、常见误区澄清与反模式

在实际开发中,开发者常采用一些错误的方式实现有状态 Composable 的共享,这些方式存在严重的隐患,属于反模式,需严格禁止。

误区1:将三方库实例存入 Pinia/Vuex 状态管理库

绝对错误。Pinia、Vuex 等状态管理库的核心作用是存储“可序列化的数据”,而非“类实例、DOM 元素、渲染器、事件句柄”。将三方库实例存入状态管理库,会导致:

  • 无法实现正确的响应式追踪:三方库实例通常包含复杂的方法和属性,无法被 Vue 的响应式系统正确追踪,导致状态更新不及时。

  • 无法正确销毁实例:状态管理库的状态会在页面刷新前一直存在,实例无法被及时销毁,导致内存泄漏。

  • 调试困难:实例存入状态管理库后,难以追踪实例的创建、修改和销毁过程,出现问题时无法快速定位。

  • 性能损耗:状态管理库会对存入的状态进行深度监听,复杂实例会导致性能下降。

正确做法:通过 Provide/Inject 共享三方库实例,状态管理库仅存储实例相关的可序列化数据(如配置、状态标识)。

误区2:使用全局变量实现单例

通过全局变量存储 Composable 实例,看似实现了单例,但存在诸多问题:

// 错误示例:全局变量单例
let instance = null;
export function useComposable() {
  if (instance) return instance;
  instance = {
    // 状态和方法
  };
  return instance;
}

缺点:

  • 热更新失效:在开发环境中,热更新时全局变量不会被重置,导致实例状态混乱,影响开发效率。

  • 多实例场景不支持:若项目需要多个独立实例(如多图表、多音频播放器),全局变量无法满足需求。

  • 跨页面共享混乱:在 SPA 应用中,页面切换时全局变量不会被销毁,导致实例残留,引发功能异常。

  • 销毁困难:无法在组件卸载时主动销毁实例,容易导致内存泄漏。

  • 类型丢失:全局变量无法进行类型校验,容易出现类型错误。

误区3:用 props 逐层传递实例

若组件层级较深,通过 props 将 Composable 实例逐层传递,会导致“props 穿透”问题:

  • 代码冗余:每个中间组件都需要接收 props 并传递给子组件,增加代码量。

  • 维护困难:若实例需要新增方法或属性,所有中间组件的 props 定义都需要修改。

  • 灵活性差:深层组件无法直接获取实例,必须依赖上层组件传递。

正确做法:使用 Provide/Inject 直接穿透深层组件,无需中间组件传递。

误区4:在 Composable 内部实现单例

在 Composable 内部通过闭包实现单例,看似避免了重复调用,但同样存在问题:

// 不推荐:Composable 内部闭包单例
export function useComposable() {
  // 闭包存储实例
  let instance = null;
  if (instance) return instance;
  
  const state = ref(false);
  const toggle = () => {
    state.value = !state.value;
  };
  
  instance = { state, toggle };
  return instance;
}

缺点:

  • 破坏 Composable 的封装性:Composable 本身应是独立的函数,内部实现单例会导致其与全局环境耦合。

  • 无法灵活控制实例生命周期:实例创建后无法主动销毁,只能等待页面刷新。

  • 多实例场景不支持:无法根据需求创建多个独立实例。

  • 调试困难:实例的创建和销毁过程被隐藏在 Composable 内部,难以追踪。

七、适用场景扩展与优化方向

7.1 适用场景扩展

本文提出的“单例模式 + Provide/Inject”方案,不仅适用于前文提到的场景,还可广泛应用于所有有状态 Composable,以下是常见的扩展场景:

  • 实时通信类:WebSocket、EventSource、RTC 等长连接管理,确保全局唯一连接,避免连接冲突。

  • 媒体播放类:音频、视频播放器,确保全局唯一播放器实例,实现跨组件控制(如播放、暂停、音量调节)。

  • 可视化类:ECharts、D3、Three.js、地图等重型可视化实例,避免重复创建,节省资源。

  • 全局状态类:主题管理、语言切换、权限控制等全局状态,确保所有组件状态同步。

  • 工具类实例:全局定时器、缓存实例、请求拦截器等,确保资源唯一,避免冲突。

  • 硬件交互类:摄像头、打印机、扫码枪等硬件设备交互,确保全局唯一交互实例,避免设备冲突。

7.2 方案优化方向

基于基础方案,可根据项目需求进行以下优化,提升方案的灵活性和健壮性:

优化1:实例懒加载

对于初始化成本较高的 Composable(如地图、3D 渲染器),可实现懒加载,仅在需要使用时才初始化实例,减少页面初始加载时间。

// 懒加载示例
export function useLazyComposable() {
  let instance = null;
  
  // 懒加载初始化方法
  const init = () => {
    if (instance) return instance;
    // 初始化逻辑
    instance = {
      // 状态和方法
    };
    return instance;
  };
  
  return {
    init,
    getInstance: () => instance
  };
}

顶层组件可在需要时调用 `init()` 方法初始化实例,避免页面加载时初始化不必要的重型资源。

优化2:多实例隔离(多场景支持)

若项目需要多个独立的有状态实例(如多个独立图表、多个音频播放器),可通过注入 Key 区分不同实例,实现多实例隔离。

// 多实例注入 Key
export const CHART_INJECTION_KEY_1: InjectionKey<ChartReturn> = Symbol('chart1');
export const CHART_INJECTION_KEY_2: InjectionKey<ChartReturn> = Symbol('chart2');

// 顶层组件提供多个实例
const chart1 = useChart();
const chart2 = useChart();
provide(CHART_INJECTION_KEY_1, chart1);
provide(CHART_INJECTION_KEY_2, chart2);

优化3:插件化扩展

将有状态 Composable 封装成 Vue 插件,通过 `app.use()` 全局注册,简化调用和共享流程,提升代码可复用性。

// 插件封装示例
import type { App } from 'vue';
import useWebSocket from './useWebSocket';
import { WEB_SOCKET_INJECTION_KEY } from './injectionKeys';

export default {
  install(app: App) {
    // 初始化实例
    const webSocketInstance = useWebSocket();
    // 全局提供实例
    app.provide(WEB_SOCKET_INJECTION_KEY, webSocketInstance);
  }
};

在入口文件中注册插件:

// main.ts
import { createApp } from 'vue';
import App from './App.vue';
import webSocketPlugin from './hooks/webSocketPlugin';

const app = createApp(App);
app.use(webSocketPlugin);
app.mount('#app');