摘要

本文基于 HarmonyOS 原生开发框架,结合 WebSocket 长连接技术,完成一款具备实时股票行情推送、标的动态订阅/取消、数据本地解析、界面实时刷新的股票行情应用。文章从整体架构设计、环境准备、核心模块拆分、WebSocket 通信封装、业务逻辑实现到完整代码落地进行全流程讲解,兼顾工程化规范与实战落地性,同时针对金融行情场景下的长连接保活、动态订阅、异常容错等问题给出解决方案。适合鸿蒙应用开发者、金融类客户端研发人员参考学习。

一、整体架构设计

1.1 技术选型

  • 应用框架:HarmonyOS ArkTS + ArkUI(声明式UI),适配鸿蒙标准开发范式,跨设备兼容;
  • 通信方案:WebSocket 长连接,对接股票行情 API,实现低延迟实时数据推送;
  • 数据管理:内存集合维护订阅标的状态,轻量状态管理,保障动态订阅逻辑一致性;
  • 线程模型:鸿蒙异步任务分发,网络通信独立子线程执行,避免主线程阻塞、界面卡顿;
  • 容错机制:连接心跳保活、断连自动重连、数据空值校验、异常捕获,提升应用稳定性。

1.2 整体分层架构

采用分层解耦的工程化架构,自上而下分为四层,职责清晰、便于后期迭代维护:

  1. UI 表现层:ArkUI 页面组件,负责行情列表渲染、用户交互(添加/移除自选股票)、界面刷新;
  2. 业务逻辑层:封装标的管理、指令组装、数据解析、状态同步等核心业务;
  3. 网络通信层:统一封装 WebSocket 客户端,提供连接、断连、发指令、消息监听通用能力;
  4. 基础工具层:公共工具类,包含 JSON 序列化、异常日志、心跳检测、线程调度。

1.3 核心业务流程

  1. 应用启动 → 初始化 WebSocket 客户端并建立长连接;
  2. 页面加载 → 初始化默认股票标的,发送订阅指令;
  3. 用户操作 → 新增/删除自选股,本地更新订阅列表 + 同步向服务端下发订阅/取消指令;
  4. 服务端推送行情数据 → 客户端解析数据 → 回调通知 UI 层刷新列表;
  5. 网络异常/连接断开 → 自动执行重连逻辑,恢复订阅状态,保障数据连续性。

二、开发环境与前置准备

2.1 开发环境

  • 开发工具:DevEco Studio 最新稳定版
  • 编译 SDK:HarmonyOS SDK 8+
  • 开发语言:ArkTS
  • 运行设备:鸿蒙手机/模拟器(API 8 及以上)

2.2 接口前置说明

本文对接标准股票 WebSocket 行情接口,遵循通用金融行情协议:

  • 服务端地址:wss://quote.alltick.co/quote-stock-b-ws-api?token=YOUR_TOKEN
  • 协议帧标识:cmd_id: 22004
  • 操作指令:subscribe(订阅)、unsubscribe(取消订阅)
  • 标的字段:code 存储股票代码,支持单只/批量传递

说明:请将代码中 YOUR_TOKEN 替换为实际有效的接口密钥。

三、核心模块实现

按照工程化拆分原则,将项目划分为网络封装模块、标的管理模块、页面UI模块、入口应用模块,逐个实现核心功能。

3.1 工具常量类(Constants.ets)

统一管理接口地址、指令、标识位,集中配置便于后期维护修改。

/**
 * 全局常量配置
 */
export class Constants {
  // 股票WebSocket服务端地址
  public static readonly WS_STOCK_URL: string = "wss://quote.alltick.co/quote-stock-b-ws-api?token=YOUR_TOKEN";
  // 协议指令ID
  public static readonly CMD_ID: number = 22004;
  // 订阅指令
  public static readonly ACTION_SUBSCRIBE: string = "subscribe";
  // 取消订阅指令
  public static readonly ACTION_UNSUBSCRIBE: string = "unsubscribe";
  // 心跳间隔 单位:毫秒
  public static readonly PING_INTERVAL: number = 10000;
}

3.2 WebSocket 网络封装模块(StockWebSocket.ets)

独立封装 WebSocket 客户端,实现连接建立、消息监听、指令发送、心跳保活、断连重连、资源释放,完全解耦业务层。

import webSocket from '@ohos.net.webSocket';
import { Constants } from './Constants';
import hilog from '@ohos.hilog';

/**
 * 股票行情WebSocket客户端封装
 */
export class StockWebSocket {
  private ws: webSocket.WebSocket | null = null;
  // 心跳定时器
  private pingTimer: number | null = null;
  // 重连定时器
  private reconnectTimer: number | null = null;
  // 重连次数限制
  private readonly MAX_RECONNECT_COUNT: number = 5;
  private reconnectCount: number = 0;

  // 消息回调、连接状态回调
  public onMessageCallback: (data: string) => void | null = null;
  public onConnectCallback: () => void | null = null;
  public onDisConnectCallback: () => void | null = null;

  /**
   * 建立WebSocket连接
   */
  public connect(): void {
    if (this.ws) {
      return;
    }
    let wsInstance = webSocket.createWebSocket();
    this.ws = wsInstance;

    // 连接成功回调
    wsInstance.on('open', () => {
      hilog.info(0x0001, 'StockWS', 'WebSocket 连接成功');
      this.reconnectCount = 0;
      this.startPing();
      this.onConnectCallback?.();
    });

    // 接收服务端消息
    wsInstance.on('message', (data: string | ArrayBuffer) => {
      if (typeof data === 'string') {
        this.onMessageCallback?.(data);
      }
    });

    // 连接关闭
    wsInstance.on('close', (code: number, reason: string) => {
      hilog.warn(0x0001, 'StockWS', `连接关闭: ${code}, ${reason}`);
      this.stopPing();
      this.onDisConnectCallback?.();
      this.reconnect();
    });

    // 网络异常
    wsInstance.on('error', (err: Error) => {
      hilog.error(0x0001, 'StockWS', `WebSocket异常: ${JSON.stringify(err)}`);
    });

    // 发起连接
    wsInstance.connect(Constants.WS_STOCK_URL);
  }

  /**
   * 发送消息指令
   * @param msg 待发送JSON字符串
   */
  public sendMessage(msg: string): void {
    if (!this.ws) {
      hilog.warn(0x0001, 'StockWS', 'WebSocket未连接,无法发送指令');
      return;
    }
    this.ws.send(msg);
  }

  /**
   * 主动关闭连接
   */
  public close(): void {
    this.stopPing();
    this.stopReconnect();
    if (this.ws) {
      this.ws.close();
      this.ws = null;
    }
  }

  /**
   * 启动心跳保活
   */
  private startPing(): void {
    this.stopPing();
    this.pingTimer = setInterval(() => {
      // 此处可根据接口要求补充心跳报文
    }, Constants.PING_INTERVAL);
  }

  /**
   * 停止心跳
   */
  private stopPing(): void {
    if (this.pingTimer) {
      clearInterval(this.pingTimer);
      this.pingTimer = null;
    }
  }

  /**
   * 自动重连逻辑
   */
  private reconnect(): void {
    if (this.reconnectCount >= this.MAX_RECONNECT_COUNT) {
      hilog.error(0x0001, 'StockWS', '重连次数超限,停止重连');
      return;
    }
    this.stopReconnect();
    this.reconnectTimer = setTimeout(() => {
      this.reconnectCount++;
      this.connect();
    }, 3000);
  }

  /**
   * 停止重连定时器
   */
  private stopReconnect(): void {
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
      this.reconnectTimer = null;
    }
  }
}

3.3 标的管理与业务逻辑模块(StockManager.ets)

核心业务层,维护本地订阅标的集合、组装订阅/取消指令、解析行情数据,衔接网络层与UI层,保证本地状态与服务端一致。

import { StockWebSocket } from './StockWebSocket';
import { Constants } from './Constants';
import hilog from '@ohos.hilog';

/**
 * 股票标的管理、业务逻辑封装
 */
export class StockManager {
  private static instance: StockManager;
  // 单例WebSocket实例
  private wsClient: StockWebSocket = new StockWebSocket();
  // 本地订阅标的集合,防止重复订阅
  private subscribeSet: Set<string> = new Set<string>();

  // 行情数据回调,通知UI刷新
  public onStockDataCallback: (stockCode: string, data: object) => void | null = null;

  private constructor() {
    this.initWebSocket();
  }

  /**
   * 单例获取
   */
  public static getInstance(): StockManager {
    if (!StockManager.instance) {
      StockManager.instance = new StockManager();
    }
    return StockManager.instance;
  }

  /**
   * 初始化WebSocket及回调
   */
  private initWebSocket(): void {
    this.wsClient.onMessageCallback = (msg: string) => {
      this.parseStockData(msg);
    };
    this.wsClient.connect();
  }

  /**
   * 初始化默认订阅标的
   * @param codeList 默认股票代码列表
   */
  public initDefaultSubscribe(codeList: string[]): void {
    this.addSubscribe(codeList);
  }

  /**
   * 动态新增订阅标的
   * @param codeList 股票代码数组
   */
  public addSubscribe(codeList: string[]): void {
    const validList: string[] = codeList.filter(item => !this.subscribeSet.has(item));
    if (validList.length === 0) {
      return;
    }
    // 更新本地状态
    validList.forEach(code => this.subscribeSet.add(code));
    // 组装订阅指令
    const sendMsg = JSON.stringify({
      cmd_id: Constants.CMD_ID,
      action: Constants.ACTION_SUBSCRIBE,
      code: validList
    });
    this.wsClient.sendMessage(sendMsg);
  }

  /**
   * 动态取消订阅标的
   * @param codeList 股票代码数组
   */
  public removeSubscribe(codeList: string[]): void {
    const validList: string[] = codeList.filter(item => this.subscribeSet.has(item));
    if (validList.length === 0) {
      return;
    }
    // 更新本地状态
    validList.forEach(code => this.subscribeSet.delete(code));
    // 组装取消订阅指令
    const sendMsg = JSON.stringify({
      cmd_id: Constants.CMD_ID,
      action: Constants.ACTION_UNSUBSCRIBE,
      code: validList
    });
    this.wsClient.sendMessage(sendMsg);
  }

  /**
   * 解析服务端推送的行情数据
   * @param msg 原始字符串数据
   */
  private parseStockData(msg: string): void {
    try {
      const data = JSON.parse(msg);
      const stockCode: string = data.code || "";
      if (!stockCode) {
        return;
      }
      // 回调通知UI更新
      this.onStockDataCallback?.(stockCode, data);
    } catch (e) {
      hilog.error(0x0001, 'StockManager', `数据解析异常: ${JSON.stringify(e)}`);
    }
  }

  /**
   * 释放资源
   */
  public release(): void {
    this.wsClient.close();
    this.subscribeSet.clear();
  }
}

3.4 页面 UI 实现模块(Index.ets)

基于 ArkUI 声明式语法实现主页面,包含股票列表展示、添加/删除自选股交互、实时数据刷新,完成用户交互与视图渲染。

import { StockManager } from './StockManager';

// 股票数据实体
interface StockInfo {
  code: string;
  price: string;
  change: string;
}

@Entry
@Component
struct Index {
  // 标的管理实例
  stockManager: StockManager = StockManager.getInstance();
  // 页面数据源
  @State stockList: Array<StockInfo> = [];
  // 输入框股票代码
  @State inputCode: string = "";

  aboutToAppear() {
    // 初始化默认标的
    const defaultCodes: string[] = ["stock_a", "stock_b"];
    this.stockManager.initDefaultSubscribe(defaultCodes);
    // 注册数据回调,刷新UI
    this.stockManager.onStockDataCallback = (code: string, data: object) => {
      this.updateStockView(code, data);
    }
  }

  aboutToDisappear() {
    // 页面销毁释放资源
    this.stockManager.release();
  }

  /**
   * 更新单只股票视图数据
   */
  updateStockView(code: string, data: object): void {
    let target = this.stockList.find(item => item.code === code);
    if (target) {
      target.price = data["price"] ?? "0.00";
      target.change = data["change"] ?? "0.00";
    } else {
      this.stockList.push({
        code: code,
        price: data["price"] ?? "0.00",
        change: data["change"] ?? "0.00"
      })
    }
  }

  build() {
    Column() {
      // 标题
      Text("HarmonyOS 实时股票行情")
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 15 })

      // 输入框 + 操作按钮
      Row() {
        TextInput({ text: this.inputCode, placeholder: "请输入股票代码" })
          .layoutWeight(1)
          .onChange((value: string) => {
            this.inputCode = value;
          })

        Button("添加")
          .margin({ left: 10 })
          .onClick(() => {
            if (this.inputCode.trim()) {
              this.stockManager.addSubscribe([this.inputCode.trim()]);
              this.inputCode = "";
            }
          })
      }.width("100%").margin({ bottom: 15 })

      // 股票行情列表
      List({ space: 8 }) {
        ForEach(this.stockList, (item: StockInfo) => {
          ListItem() {
            Row() {
              Text(item.code)
                .layoutWeight(1)
                .fontSize(16)

              Text(`价格: ${item.price}`)
                .margin({ left: 10 })
                .fontSize(16)

              Text(`涨跌: ${item.change}`)
                .margin({ left: 10 })
                .fontSize(16)

              Button("移除")
                .margin({ left: 10 })
                .fontSize(14)
                .onClick(() => {
                  this.stockManager.removeSubscribe([item.code]);
                  // 移除本地列表
                  const index = this.stockList.findIndex(s => s.code === item.code);
                  if (index > -1) {
                    this.stockList.splice(index, 1);
                  }
                })
            }.width("100%").padding(10)
          }
        })
      }
      .layoutWeight(1)
      .width("100%")
    }
    .width("100%")
    .height("100%")
    .padding(15)
  }
}

四、工程化规范与项目目录结构

4.1 标准目录结构

遵循鸿蒙应用工程化规范,目录职责划分清晰,便于团队协作与后期维护:

entry
├── ets
│   ├── components      # 公共UI组件(可扩展复用组件)
│   ├── pages           # 页面文件
│   │   └── Index.ets   # 行情主页面
│   ├── utils           # 工具类、网络、常量、业务管理类
│   │   ├── Constants.ets
│   │   ├── StockWebSocket.ets
│   │   └── StockManager.ets
├── resources           # 图片、字符串、样式等资源文件
└── module.json5        # 应用配置文件

4.2 工程化要点说明

  1. 分层解耦:网络、业务、UI 完全拆分,单一职责原则,修改某一模块不影响其他逻辑;
  2. 单例管理:行情管理器使用单例,全局共用一套 WebSocket 连接,避免多连接资源浪费;
  3. 状态防护:本地集合管控订阅状态,严格规避重复订阅、无效取消订阅;
  4. 异常容错:内置心跳保活、自动重连、JSON解析异常捕获、空值判断,提升线上稳定性;
  5. 资源释放:页面生命周期回调中统一释放网络连接、定时器,防止内存泄漏。

五、功能测试与问题优化

5.1 核心功能测试点

  1. 应用启动自动建立 WebSocket 连接,默认标的正常接收行情数据;
  2. 输入股票代码点击添加,动态完成订阅并展示实时数据;
  3. 点击移除按钮,取消对应标的订阅,列表同步删除条目;
  4. 网络断开后自动重连,恢复原有订阅状态,数据正常推送。

5.2 常见问题与优化方案

  1. 界面卡顿
    原因:网络回调在主线程频繁刷新UI。
    优化:鸿蒙异步分发任务,数据解析与预处理放在子线程,仅最终视图更新回调至主线程。

  2. 重复订阅
    优化:依托本地 Set 集合做前置校验,重复标的不再向服务端发送指令。

  3. 连接长期空闲断开
    优化:启用定时心跳报文,维持长连接活性,适配服务端空闲断开策略。

  4. 重连后数据丢失
    优化:重连成功后,自动读取本地订阅列表,批量重新下发订阅指令。

六、总结与扩展方向

6.1 文章总结

本文基于 HarmonyOS 完整实现了实时股票行情 App,采用「UI层-业务层-网络层-工具层」分层架构,通过 WebSocket 长连接实现实时数据推送与标的动态订阅/取消。代码遵循鸿蒙工程化开发规范,具备完善的异常处理、心跳保活、自动重连、资源释放能力,可直接作为金融类鸿蒙应用的基础框架使用。

整套方案既满足金融行情低延迟、数据连续的业务要求,也兼顾了移动端应用的稳定性与可维护性。

6.2 后续扩展方向

  1. 增加本地数据缓存,应用后台保活后恢复历史行情;
  2. 实现多页面状态同步,跨页面共享订阅标的;
  3. 增加行情走势图、K线图等可视化组件;
  4. 对接本地数据库,持久化存储用户自选股列表;
  5. 增加网络状态监听,动态提示用户网络异常。

补充说明

  1. 运行前务必替换代码中 YOUR_TOKEN 为合法的接口访问令牌;
  2. 该项目基于 HarmonyOS ArkTS 开发,兼容手机、平板、鸿蒙折叠屏等多端设备;
  3. 所有网络逻辑独立封装,可快速替换为其他行情 WebSocket 接口。
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐