用Ticker API写一个行情面板:一次完整的实现过程
在"行情展示"这个场景里,REST Ticker + 定时刷新通常已经能满足需求;这篇我用一个可运行的 Demo,把这件事做出来验证一遍。
在上一篇专栏中,我把行情API的使用拆成了三个阶段:启动阶段 / 展示阶段 / 实时阶段。这篇文章只聚焦第二个阶段,用一个可运行的Demo把它落地。
一、先把页面结构和真实数据跑通
这个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状态变得不可信(到底是在刷新,还是卡住了?)
本质上是请求重叠导致的时序问题:上一轮没回来,下一轮又开始了。
我必须引入一个"刷新中"状态来控制节奏:
text
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"。
一开始我以为自选列表需要后端支持。但实际上,这是一个纯前端状态问题:
text
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状态、请求延迟和上次更新时间。
逻辑上并不复杂,大致就是把数据请求和渲染包在一层异常处理里:
text
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里看到完整代码:
👉 https://github.com/tickdb/tickdb-demo-ticker-panel
运行步骤:
- 打开
config.js,填入你的API Key - 直接用浏览器打开
index.html即可
常见问题:
- 看到"请配置API Key"提示,说明
config.js未正确配置 - 数据显示
-,说明该市场该字段确实没有数据(这是正常的) - 请求失败,检查API Key是否有效、网络是否正常