广告位 · 页头(后台-模块管理-新建模块后替换此区域)

如何在 Vue 3 中工程化集成 Timeplus 实时流分析平台?

在 Vue 3 中接入 Timeplus 实时流分析能力,难点往往不在图表展示,而在于把连接创建、SQL 查询、WebSocket 订阅与组件生命周期沉淀为可复用的工程化模块。直接在每个组件里调用 HTTP 接口虽然起步快,但连接复用、卸载清理、重复订阅和错误提示很容易失控。

如何在 Vue 3 中工程化集成 Timeplus 实时流分析平台?

客户端封装与环境配置

Timeplus 提供 REST API 和 WebSocket 订阅两种交互方式,前端不应当把 URL、请求头、错误处理散落在各个组件中。工程化的第一步是建立独立的客户端模块,例如 src/api/timeplus.ts。该模块负责拼接查询地址、发送 SQL 请求、解析响应以及建立 WebSocket 连接。这样一来,组件只需要关心业务逻辑,不直接接触底层协议。

Vite 项目可以通过环境变量区分开发和生产配置。在 .env.development 中设置 VITE_TIMEPLUS_BASE_URL=http://127.0.0.1:8000,在 .env.production 中设置正式服务地址。客户端读取 import.meta.env.VITE_TIMEPLUS_BASE_URL,避免在代码里硬编码环境相关值。WebSocket 地址可以由 HTTP 地址自动转换,当基础地址以 https 开头时,替换后得到 wss,符合安全连接要求。

// src/api/timeplus.ts
export interface QueryResult {
  rows: Record<string, unknown>[];
  elapsed?: number;
}
export class TimeplusClient {
  private baseUrl: string;
  private wsBaseUrl: string;
  constructor(baseUrl: string) {
    this.baseUrl = baseUrl.replace(/\/$/, '');
    this.wsBaseUrl = this.baseUrl.replace(/^http/, 'ws');
  }
  async runQuery(sql: string): Promise<QueryResult> {
    const response = await fetch(`${this.baseUrl}/api/query`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ sql })
    });
    if (!response.ok) {
      const message = await response.text();
      throw new Error(`Timeplus 查询失败:${message}`);
    }
    return response.json();
  }
  subscribe(sql: string, onMessage: (payload: unknown) => void): WebSocket {
    const url = `${this.wsBaseUrl}/api/subscribe?query=${encodeURIComponent(sql)}`;
    const socket = new WebSocket(url);
    socket.onmessage = (event) => {
      onMessage(JSON.parse(event.data));
    };
    return socket;
  }
}

客户端类同时暴露 runQuery 和 subscribe 两个方法,分别对应一次性查询和持续订阅。后续如果 Timeplus 接口调整,只需要修改这一处,不会影响页面组件。这种封装方式也便于在单元测试中使用模拟客户端替换真实网络请求。

组合式函数统一管理查询生命周期

在 Vue 3 的 <script setup> 语法中,组合式函数是复用有状态逻辑的最佳载体。查询场景中最常见的问题是:用户快速切换时间范围或过滤条件时,旧请求可能在新请求之后返回,导致页面展示过期数据。通过请求序号与 watch 监听,可以可靠地丢弃过期响应。

useTimeplusQuery 接收客户端实例和响应式 SQL 参数,返回 data、loading、error 三个状态。当 SQL 变化时,函数自动重新执行查询,并通过递增 requestId 判断当前返回是否仍然有效。组件卸载时同样递增序号,使已发出但未完成的请求不再写入状态。

import { ref, watch, onBeforeUnmount, type Ref } from 'vue';
import type { TimeplusClient, QueryResult } from '../api/timeplus';
export function useTimeplusQuery(client: TimeplusClient, sql: Ref<string>) {
  const data = ref<QueryResult['rows']>([]);
  const loading = ref(false);
  const error = ref<string | null>(null);
  let requestId = 0;
  async function execute(currentSql: string) {
    const current = ++requestId;
    loading.value = true;
    error.value = null;
    try {
      const result = await client.runQuery(currentSql);
      if (current === requestId) {
        data.value = result.rows;
      }
    } catch (err) {
      if (current === requestId) {
        error.value = err instanceof Error ? err.message : '未知错误';
      }
    } finally {
      if (current === requestId) {
        loading.value = false;
      }
    }
  }
  watch(sql, execute, { immediate: true });
  onBeforeUnmount(() => {
    requestId++;
  });
  return { data, loading, error };
}

这种竞态处理方式不需要使用 AbortController,但如果查询接口支持取消,可以进一步配合 AbortController 减少无效网络流量。对于大多数分析页面,序号判断已经足够简单可靠。组件中只需要调用 const { data, loading, error } = useTimeplusQuery(client, sqlRef),模板直接绑定即可。

实时流订阅与浅层响应式优化

Timeplus 的实时订阅通过 WebSocket 持续推送变更数据,适合实时大屏和监控场景。订阅管理比一次性查询更复杂,需要处理连接建立、消息累积、断线重连和组件卸载时的资源释放。如果直接在组件内部管理 WebSocket,很容易出现关闭旧连接不及时、多次重连叠加等问题。

在 useTimeplusStream 中,使用 shallowRef 保存行数组。Vue 3 的 shallowRef 不会深度代理数组内部对象,当流数据量较大时可以显著降低响应式转换开销。每次收到新消息时,通过展开运算符创建新数组,确保视图能够触发更新。若需要长期保留大量数据,可以设置最大长度,超出后丢弃最旧数据。

import { ref, shallowRef, onBeforeUnmount } from 'vue';
import type { TimeplusClient } from '../api/timeplus';
export function useTimeplusStream(client: TimeplusClient, sql: string) {
  const rows = shallowRef<Record<string, unknown>[]>([]);
  const status = ref<'idle' | 'connecting' | 'connected' | 'error'>('idle');
  let socket: WebSocket | null = null;
  let retryCount = 0;
  let timer: number | undefined;
  function connect() {
    status.value = 'connecting';
    socket = client.subscribe(sql, (payload) => {
      rows.value = [...rows.value, payload as Record<string, unknown>];
    });
    socket.onopen = () => {
      status.value = 'connected';
      retryCount = 0;
    };
    socket.onerror = () => {
      status.value = 'error';
    };
    socket.onclose = () => {
      if (retryCount >= 5) {
        status.value = 'error';
        return;
      }
      retryCount += 1;
      timer = window.setTimeout(connect, 1000 * 2 ** retryCount);
    };
  }
  function close() {
    if (timer) window.clearTimeout(timer);
    socket?.close();
    socket = null;
    status.value = 'idle';
  }
  onBeforeUnmount(close);
  return { rows, status, connect, close };
}

重连策略采用指数退避,第一次断开后等待 2 秒,第二次等待 4 秒,最多尝试 5 次。这个数值可以根据业务容忍度调整。调用方可以在进入页面时执行 connect(),离开页面时由组合式函数自动关闭连接并清理定时器,不会留下隐藏的 WebSocket 占用资源。

Vite 代理、错误处理与生产构建

开发环境下,浏览器直接访问 Timeplus 服务可能遇到跨域问题,尤其是 WebSocket 升级请求。Vite 的 server.proxy 可以统一代理 /api 路径,并启用 ws: true 转发 WebSocket 连接。这样前端代码只需要写相对路径,同源策略也不会限制 WebSocket 握手。

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
  plugins: [vue()],
  server: {
    proxy: {
      '/api': {
        target: 'http://127.0.0.1:8000',
        changeOrigin: true,
        ws: true
      }
    }
  }
});

生产环境通常由 Nginx 或网关统一处理代理和证书,Vite 代理仅用于本地开发。错误处理也需要集中管理,例如在网络异常或 SQL 语法错误时展示统一的提示组件,而不是让每个页面各自实现 alert 或 toast。可以在客户端层抛出规范化错误对象,在组合式函数中统一赋值给 error 状态。

如果项目规模继续扩大,还可以将查询结果缓存到 Pinia,让多个组件共享同一份数据,减少重复请求。性能优化方面,除 shallowRef 之外,还应控制 SQL 的 LIMIT 和时间范围,避免一次性拉取过多数据。图表组件按需更新,结合 requestAnimationFrame 节流处理高频推送,可以让实时流分析页面在复杂交互下依然保持流畅。

广告位 · 文章底部(后台-模块管理替换此区域)