用Ticker API写一个行情面板:一次完整的实现过程
一、先把页面结构和真实数据跑通这个Demo要做什么这次我做的是一个 Ticker 行情面板把外汇、贵金属、美股、A 股、加密货币放进同一张表里满足“看一眼行情”的需求。数据来源用的是TickDB的/v1/market/ticker目标很简单能稳定跑起来、能长期用起来。这不是一个盯盘页用户不需要盯着价格跳动做决策只是看一眼当前行情。先定页面结构一开始我确实想直接调接口。打开文档看到/v1/market/ticker第一反应是写个fetch打印JSON看看数据长什么样。但我停下来了。因为我还没想清楚这个页面到底要长什么样。如果这是一个盯盘页那页面结构会完全不同需要大字号价格、跳动动画、实时连接状态。如果这是一个列表页那就是另一回事。我当时的判断是这是一个行情展示页面用户只是看一眼行情。所以我先把页面结构定下来Header标题 说明控制区刷新按钮、刷新间隔选择、管理自选表格Symbol、最新价、涨跌、24h高低、量、时间状态栏API状态、延迟、上次更新时间这个页面没有秒级跳动、动画效果、深度盘口、K线图但它已经能回答最常见的问题现在价格是多少今天涨还是跌波动范围大不大不同市场能不能放在一张表里看UI结构定下来之后后面的实现就有了明确的边界。后面所有的工作都是在这张结构里往里填东西。接入真实数据页面结构定型后我把 fetchTicker() 接到 /v1/market/ticker 上先跑通一次“从请求到渲染”的闭环。真正麻烦的是同一张表里不同市场返回的字段并不一致。有的没有成交量有的缺少涨跌额/涨跌幅有的只有买卖价相关字段。市场类型有成交量有涨跌数据有买卖价加密货币✅✅❌股票✅❌❌外汇/贵金属❌❌✅如果不做字段容错页面会直接报错或显示NaN。所以我必须做字段容错有值就显示没有值就显示-。这样无论行情接口返回什么数据表格都能正常显示。右上角的延迟也是这个阶段加的。很多Demo截图看起来很漂亮但不知道它是不是真的在跑。加一个延迟数字100ms、150ms这个Demo就不再是演示品。到这里为止这个面板已经可以稳定地用真实市场数据跑起来了。二、让行情面板稳定刷新并真正可用自动刷新不能无脑setInterval面板能跑了但如果真的使用起来马上会遇到一个问题没人愿意一直点刷新按钮。一开始我以为自动刷新很简单setInterval调一下fetchTicker()就行了。但实际跑起来发现如果上一次请求还没回来下一次刷新已经开始了请求重叠后数据顺序可能错乱UI状态变得不可信到底是在刷新还是卡住了本质上是请求重叠导致的时序问题上一轮没回来下一轮又开始了。我必须引入一个刷新中状态来控制节奏state { isFetching: false, nextRefreshAt: null } function refreshTicker() { if (state.isFetching) return state.isFetching true fetchTicker() .finally(() { state.isFetching false state.nextRefreshAt now interval }) }关键是刷新行为本身必须是显式、可控的状态而不是隐形的副作用。工具栏这一行还做了一件事显示下次刷新: 5s每秒倒计时。我当时的想法是当用户能看到还有3秒刷新他会知道系统没有卡住、刷新是有节奏的、如果数据没变不是系统坏了而是市场本身没动。Demo里默认是5秒刷新但也可以切换3秒或10秒。我当时选5秒的原因是3秒收益不明显但请求量翻倍10秒用户会觉得有点慢5秒是在感知延迟和系统成本之间找到的平衡点。自选列表是前端状态面板能稳定刷新了但又会遇到一个问题没人想看所有Symbol。用户真正想要的是我关心的那几个Symbol。一开始我以为自选列表需要后端支持。但实际上这是一个纯前端状态问题state { watchlist: loadFromStorage() } function updateWatchlist(list) { state.watchlist list saveToStorage(list) } function refresh() { fetchTicker(state.watchlist) }把watchlist提升为明确的状态后行情刷新逻辑反而变得更简单每次刷新只请求watchlist里的Symbol。我还把自选列表存到localStorage。如果每次刷新页面都要重新选Symbol用户会直接放弃。这个决策看起来简单但它背后有一个判断自选列表是用户状态不是行情状态。它不需要后端支持不需要账号系统只需要浏览器本地存储就够了。到这里为止这个面板具备了最小可用性刷新稳定、关注列表可保存、打开就能继续用。三、补齐可用性与工程完整性搜索和筛选只是视图层问题面板能用了但当我加了几十个产品的时候就会遇到一个问题找不到想看的那个。我引入了基础筛选和搜索市场筛选只看外汇、只看美股行情、只看加密货币行情、搜索框输入关键词实时过滤。我把搜索/筛选限定在视图层它只改变表格展示的行不改变请求的 symbols 列表。这样请求层和渲染层解耦避免为了 UI 交互去打乱刷新节奏。这样筛选逻辑和行情逻辑就能彻底解耦互不干扰。异常状态的处理做到这里其实行情面板的“正常路径”已经跑通了。但我很快意识到一件事如果这个 Demo 真的要给别人用异常路径不能空着。最直接的问题就是一旦接口出错现在的页面只会“什么都不显示”。这在自己调试时还能接受但对使用者来说很难判断到底发生了什么。于是我补了一套最基本的错误状态处理API Key未配置、请求失败以及接口返回错误码的情况。同时把底部状态栏的信息也补全了统一展示API状态、请求延迟和上次更新时间。逻辑上并不复杂大致就是把数据请求和渲染包在一层异常处理里try { data fetchTicker() render(data) } catch (err) { showErrorState(err) }错误码这块我参考了TickDB的错误文档做了友好提示1001是API Key无效或已过期2002是交易品种不存在3001是请求频率超限。另外我在底部状态栏加了一个「导出 CSV」的按钮。当时的想法很简单如果用户能把当前行情数据直接导出来自己再做分析或处理这个 Demo 就不只是“看一眼效果”而是已经具备了最小可用的价值。四、复盘行情展示型面板的技术边界这个Demo用的是REST Ticker 定时刷新这是我在这个场景下的选择。这个面板解决的是什么场景用户的行为是看一眼行情不是盯着价格跳动做决策。在这个场景下5秒刷新已经足够用户关心的Symbol通常不超过10个当刷新节奏是可感知、可解释的用户对实时性的焦虑会明显下降。回头看这个面板之所以能成立不是因为选了什么“高级技术”而是每一步都围绕同一个目标让刷新节奏可控、让状态可解释、让用户能长期用。在“看一眼行情”的场景里REST Ticker 定时刷新就是顺势而为。WebSocket什么时候才是正确选项我的判断很简单当用户的行为从看变成盯的时候。具体来说如果用户只是看一眼价格定时刷新够用如果用户需要盯着价格变化做决策才需要WebSocket推送。这不是技术选型问题而是场景判断问题。很多行情系统一上来就想做实时第一反应是上WebSocket、做秒级刷新、加动画。但实际跑起来会发现用户根本不需要秒级更新连接管理、断线重连、消息积压反而成了负担前端性能问题DOM频繁更新比接口延迟更严重。工程上的专业恰恰是知道什么时候用什么技术。附录如何运行这个Demo这个Demo是纯HTML 原生JavaScript无需构建工具。这个Demo的代码我已经整理成一个完整仓库包括页面结构、数据请求、刷新逻辑和异常处理。如果你想直接跑一下、或者对某一步的实现细节更感兴趣可以在GitHub里看到完整代码