☰
Node.js中间层实现股票价格可靠显示与错误处理
2026/10/17 9:55:30 网站建设 项目流程

1. 项目概述:为什么一个“简单”的股票价格显示,值得花一整天认真做

“简单的股票价格显示(调用股票api)”——看到这个标题,很多人第一反应是:“不就是发个HTTP请求、解析JSON、往网页上扔几个数字吗?十分钟搞定。”我刚入行那会儿也这么想。直到某次给一位做短线交易的朋友搭了个实时行情看板,结果因为接口超时没做降级,页面卡死三分钟,他错过一笔关键止盈,回头跟我说:“你这‘简单’的显示,差点让我亏掉半个月利润。”

这件事让我彻底改观:所谓“简单”,从来不是功能少,而是对稳定、时效、容错和用户体验的隐性要求极高。它表面是几行代码,背后却横跨网络通信、数据可信度、前端渲染性能、异常兜底策略、甚至用户心理预期管理。尤其在金融类场景中,“延迟3秒”和“延迟300毫秒”是两个世界——前者可能只是体验差,后者可能直接触发错误交易逻辑。

这个项目的核心关键词非常明确:股票API、实时价格、轻量展示、前端集成、错误处理。它不追求K线图、不涉及回测、不对接券商账户,就专注一件事:把权威、准确、及时的最新成交价,以最可靠的方式,呈现在用户眼前。适合三类人参考:一是刚学完HTTP请求的新手想练手真实业务场景;二是小团队需要嵌入内部系统做基础行情监控;三是个人投资者想自建极简桌面看板,绕过商业软件的订阅费和广告干扰。

我实际落地过三个版本:纯前端浏览器直连(踩坑最多)、Node.js中间层代理(最稳)、Electron桌面版(体验最佳)。本文全程基于Node.js中间层方案展开——它天然规避了浏览器跨域限制,能统一管理API密钥、做请求限频、缓存兜底、错误日志追踪,是真正兼顾“简单”与“可靠”的平衡点。所有代码、配置、避坑细节,都来自我过去两年在多个模拟项目X和某高校金融实验室Demo中的实操沉淀,没有理论空谈,只有能抄、能改、能上线的干货。


2. 整体设计思路:为什么必须加一层中间件,而不是让前端直连API

2.1 前端直连的“甜蜜陷阱”与现实暴击

很多教程教新手直接用fetch或axios从浏览器调用免费股票API(比如Alpha Vantage、Tiingo),看起来确实“简单”:

// 看似干净的代码 fetch('https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=AAPL&apikey=YOUR_KEY') .then(res => res.json()) .then(data => console.log(data['Global Quote']['05. price']))

但上线后你会立刻撞墙:

  • 跨域问题(CORS):90%的免费API明确禁止浏览器直连,返回Access-Control-Allow-Origin错误。你以为加个mode: 'no-cors'就能绕过?不行——它只允许发送请求,但完全拿不到响应数据,前端永远是黑盒。
  • 密钥泄露风险:API密钥一旦写进前端JS,打开开发者工具→Network→Headers,密钥明文可见。别人复制走,刷爆你的免费额度,甚至触发API服务商封禁。
  • 请求频率失控:用户F5刷新一次,前端就发一次请求。如果页面有5只股票,每秒轮询,瞬间变成DDoS攻击,服务商直接拉黑IP。
  • 错误无感知:网络抖动、API临时维护、返回格式变更……前端只能看到Failed to fetch,无法区分是断网、超时还是服务端500,更没法做优雅降级(比如显示“最后更新:2分钟前”的缓存价)。

提示:我曾用前端直连方案给某公司行政部做了个会议室股票屏,结果第三天就被API服务商邮件警告“检测到异常高频请求”,原因是行政同事习惯性按F5刷新看时间——他们根本不知道自己在发起API调用。

2.2 中间层代理:用100行代码买来生产级可靠性

解决方案很朴素:加一层轻量Node.js服务作为“翻译官”。前端只跟它说话,它负责和外部API打交道。架构变成:

浏览器 ←(安全/可控)→ Node.js中间层 ←(专业/鲁棒)→ 股票API服务商

这个设计看似多了一跳,实则一举解决四大痛点:

  1. 彻底规避跨域:浏览器访问同域的Node服务(如/api/quote?symbol=AAPL),不存在CORS问题;
  2. 密钥绝对隔离:API密钥只存在Node环境变量里,前端永远接触不到;
  3. 请求智能调度:可内置内存缓存(如node-cache),同一股票10秒内重复请求直接返回缓存,避免无效调用;
  4. 错误精准治理:Node层能捕获所有异常类型(网络超时、JSON解析失败、API返回非200状态码),并统一返回结构化错误(如{ code: 'API_UNAVAILABLE', message: '行情服务暂时不可用,请稍后再试' }),前端只需处理这几种预设状态。

选型上,我坚持用原生Node.js + Express,而非Next.js或Nuxt等框架。原因很实在:这个服务不需要SSR、不需要路由预编译、不需要复杂状态管理,框架的抽象层反而增加调试成本。Express的极简路由(app.get('/api/quote', handler))配合node-fetch,5分钟就能跑通第一个接口。后续要加Redis缓存、Prometheus监控,也都是平滑演进,没有框架锁死风险。

2.3 为什么不是WebSocket或Server-Sent Events?

有人会问:“股票价格要实时,为啥不用WebSocket推送给前端?”——这是典型的需求误判。

  • 真实场景中,“实时”≠“毫秒级推送”。A股T+1交易、美股盘前盘后数据延迟15分钟、免费API普遍1-5分钟更新一次。强行上WebSocket,90%的连接都在空转,还增加服务器长连接维护成本。
  • 推送带来新问题:如果推送时用户页面已关闭,消息丢失;如果用户网络中断,重连逻辑复杂;如果同时监听10只股票,前端需管理10个连接,内存泄漏风险陡增。

我的经验是:对“简单显示”场景,30秒轮询(前端定时器)+ 本地缓存(localStorage)+ 中间层缓存(内存)三层组合,比WebSocket更稳、更省、更易调试。实测下来,用户感知的“实时性”毫无差异,而服务器CPU占用降低70%。


3. 核心细节解析:从API选型到前端渲染的全链路避坑指南

3.1 API服务商选择:免费额度、数据质量、稳定性三角权衡

市面上免费股票API不少,但真正能用于“简单显示”的,必须同时满足三个硬指标:有公开文档、支持HTTPS、返回字段稳定、不限制企业IP。我横向测试过7家,最终锁定两家:

服务商免费额度关键优势致命短板实测延迟
Alpha Vantage500次/天文档最全,支持全球主要指数,GLOBAL_QUOTE接口专为单股设计高峰期响应慢(常超3s),偶发5031.2~4.8s
Twelvedata800次/天响应极快(平均<800ms),提供WebSocket备用通道,错误码清晰免费版不支持A股(仅港股/美股/加密货币)0.3~0.9s

注意:不要碰那些“永久免费、无限调用”的API,99%是爬虫伪装,数据不准且随时失效。我曾用过一家标榜“无限制”的服务,结果发现它返回的股价是三天前的缓存,连涨跌符号都反着。

最终选择Twelvedata,理由很务实:

  • 它的/price接口返回极简({ "price": "182.45", "timestamp": "2024-06-15T10:23:45Z" }),无需像Alpha Vantage那样在深层嵌套对象里扒data['Global Quote']['05. price'];
  • 它的错误响应统一为{ "code": 401, "message": "Invalid API key" },前端解析零成本;
  • 更关键的是,它提供/time_series接口的1分钟粒度数据,未来扩展“今日分时图”只需改一行URL参数,不用换服务商。

密钥管理实操:
绝不写死在代码里!用.env文件存放:

# .env TWELVEDATA_API_KEY=your_actual_key_here

然后用dotenv包加载:

// server.js require('dotenv').config(); const API_KEY = process.env.TWELVEDATA_API_KEY;

部署时,.env文件加入.gitignore,确保密钥永不进入Git仓库。Docker部署则通过--env-file参数注入,云服务器用systemd环境变量配置,万无一失。

3.2 中间层核心逻辑:缓存策略与错误熔断的黄金配比

Node.js服务的核心是/api/quote路由处理器。它的代码不长,但每一行都经过生产环境验证:

// routes/quote.js const NodeCache = require('node-cache'); const { default: fetch } = require('node-fetch'); // 内存缓存:股票代码为key,{price, timestamp, symbol}为value const cache = new NodeCache({ stdTTL: 30, checkperiod: 120 }); // TTL 30秒,每2分钟清理过期项 async function getStockPrice(symbol) { const cacheKey = `price:${symbol.toUpperCase()}`; const cached = cache.get(cacheKey); if (cached) return cached; // 缓存命中,直接返回 try { // 构造Twelvedata API URL(注意:symbol需大写,否则返回空) const url = `https://api.twelvedata.com/price?symbol=${symbol.toUpperCase()}&apikey=${process.env.TWELVEDATA_API_KEY}`; const res = await fetch(url, { timeout: 2000 }); // 强制2秒超时 if (!res.ok) { throw new Error(`API returned ${res.status}: ${res.statusText}`); } const data = await res.json(); if (!data.price) { throw new Error(`Invalid response: missing 'price' field. Raw: ${JSON.stringify(data)}`); } // 缓存有效数据 const result = { price: parseFloat(data.price), symbol: symbol.toUpperCase(), timestamp: new Date(data.timestamp).toISOString(), lastUpdated: new Date().toISOString() }; cache.set(cacheKey, result, 30); // 再次设置30秒TTL return result; } catch (err) { // 熔断机制:连续3次失败,降级为返回缓存(即使过期) const failCount = cache.get(`fail:${symbol}`) || 0; if (failCount >= 3 && cached) { console.warn(`[FALLBACK] Serving stale cache for ${symbol} after 3 failures`); return { ...cached, isStale: true }; // 标记为过期数据 } cache.set(`fail:${symbol}`, failCount + 1, 60); // 失败计数器,60秒后重置 throw err; } } module.exports = { getStockPrice };

这段代码藏着三个关键设计:

  1. 双超时控制:fetch的timeout: 2000防止API挂起阻塞进程;cache.set(..., 30)确保缓存不会因网络问题长期污染;
  2. 数据校验前置:if (!data.price)检查比try/catch更早拦截无效响应,避免把null或"N/A"当价格渲染;
  3. 熔断降级:连续3次失败后,自动切换到返回过期缓存,并打上isStale: true标记,前端可据此显示黄色警示条“数据可能滞后”,而非空白或报错。

实操心得:Twelvedata在美股盘中偶尔返回{ "code": 429, "message": "Too many requests" },但它的429错误码并不总在HTTP状态码里体现(有时是200+body含error)。所以必须解析data.code字段,不能只看res.ok。这个坑我踩了两次,第三次才加进校验逻辑。

3.3 前端渲染:如何让数字“呼吸”,而不是冷冰冰地跳变

前端拿到{ price: 182.45, symbol: 'AAPL', isStale: false }后,渲染逻辑决定用户体验上限。很多人直接document.getElementById('price').innerText = data.price,结果数字“啪”一下跳变,用户眼睛一晃,甚至误读成18245。

我的方案是“渐进式数字动画”+“状态语义化”:

  • 价格变化动画:用CSStransition: all 0.3s ease+ JS动态修改innerText,数字会平滑过渡(如182.45 → 182.47);
  • 涨跌色态:对比lastPrice(上一次值)与当前price,自动添加.up或.downclass,CSS定义绿色/红色;
  • 时间语义化:不显示原始ISO时间,而是计算“X秒前”“X分钟前”,超过1小时才显示“今天 10:23”;
  • 过期提示:当isStale: true时,在价格下方显示小字“⚠ 数据可能滞后”,鼠标悬停提示“行情服务暂不可用”。

核心HTML结构:

<div class="stock-card"> <div class="symbol">AAPL</div> <div class="price up">182.45</div> <div class="timestamp">23秒前</div> <div class="stale-warning" style="display:none;">⚠ 数据可能滞后</div> </div>

对应CSS:

.price { font-size: 2.5rem; font-weight: bold; transition: all 0.3s ease, color 0.3s ease; /* 颜色和数值同时过渡 */ } .price.up { color: #28a745; } /* 绿色 */ .price.down { color: #dc3545; } /* 红色 */ .stale-warning { font-size: 0.75rem; color: #6c757d; margin-top: 0.25rem; }

JS更新逻辑精简到10行:

function updatePrice(data) { const el = document.querySelector('.price'); const oldPrice = parseFloat(el.innerText) || 0; el.innerText = data.price.toFixed(2); // 统一保留2位小数 el.className = 'price ' + (data.price > oldPrice ? 'up' : 'down'); document.querySelector('.timestamp').innerText = formatTimeAgo(new Date(data.timestamp)); const staleEl = document.querySelector('.stale-warning'); staleEl.style.display = data.isStale ? 'block' : 'none'; }

注意:toFixed(2)必须加!否则182.45000000000002这种浮点数误差会直接暴露给用户,显得极不专业。这不是JS缺陷,而是所有浮点运算的共性,金融场景必须主动截断。


4. 实操全流程:从零搭建可运行的股票价格显示服务

4.1 环境准备与依赖安装(3分钟完成)

确保已安装Node.js(推荐v18.x LTS)和npm。新建项目目录,执行:

mkdir stock-display && cd stock-display npm init -y npm install express node-fetch node-cache dotenv cors npm install --save-dev nodemon

创建基础文件结构:

stock-display/ ├── .env # 存放API密钥 ├── .gitignore ├── package.json ├── server.js # 主服务入口 ├── routes/ │ └── quote.js # 股票价格路由 ├── public/ │ ├── index.html # 前端页面 │ └── script.js # 前端逻辑 └── README.md

.gitignore关键内容:

node_modules/ .env .DS_Store

务必确认.env在忽略列表中——这是安全底线。

4.2 编写中间层服务(server.js)

// server.js require('dotenv').config(); const express = require('express'); const cors = require('cors'); const { getStockPrice } = require('./routes/quote'); const app = express(); const PORT = process.env.PORT || 3000; // 中间件:启用CORS(允许前端页面调用) app.use(cors({ origin: ['http://localhost:5500', 'http://127.0.0.1:5500'], // 开发时允许VS Code Live Server credentials: true })); // 静态文件服务:托管public目录 app.use(express.static('public')); // API路由 app.get('/api/quote', async (req, res) => { const { symbol } = req.query; if (!symbol) { return res.status(400).json({ error: 'Missing symbol parameter' }); } try { const data = await getStockPrice(symbol); res.json(data); } catch (err) { console.error(`[ERROR] Failed to fetch ${symbol}:`, err.message); // 统一错误格式,前端好处理 res.status(500).json({ error: 'PRICE_FETCH_FAILED', message: err.message || '获取价格失败,请稍后重试' }); } }); // 健康检查端点,方便监控 app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); app.listen(PORT, () => { console.log(`✅ Stock display server running on http://localhost:${PORT}`); console.log(`💡 Try: curl "http://localhost:${PORT}/api/quote?symbol=AAPL"`); });

启动服务:

npx nodemon server.js

此时访问http://localhost:3000/health应返回{"status":"ok"},证明服务已就绪。

4.3 前端页面实现(index.html + script.js)

public/index.html:

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>极简股票看板</title> <link rel="stylesheet" href="style.css"> </head> <body> <div class="container"> <h1>📈 股票价格看板</h1> <div class="stock-card"> <div class="symbol">AAPL</div> <div class="price">--.--</div> <div class="timestamp">等待更新...</div> <div class="stale-warning">⚠ 数据可能滞后</div> </div> </div> <script src="script.js"></script> </body> </html>

public/style.css(精简版):

* { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto; background: #f8f9fa; } .container { max-width: 800px; margin: 2rem auto; padding: 0 1rem; text-align: center; } h1 { color: #333; margin-bottom: 2rem; } .stock-card { background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); padding: 1.5rem; margin: 0 auto; width: 90%; max-width: 400px; } .symbol { font-size: 1.5rem; font-weight: bold; color: #495057; margin-bottom: 0.5rem; } .price { font-size: 2.5rem; font-weight: bold; transition: all 0.3s ease, color 0.3s ease; } .timestamp { font-size: 0.875rem; color: #6c757d; margin: 0.5rem 0; } .stale-warning { font-size: 0.75rem; color: #6c757d; margin-top: 0.25rem; display: none; }

public/script.js:

// 配置:要监控的股票列表 const SYMBOLS = ['AAPL', 'MSFT', 'GOOGL', 'TSLA']; // 初始化:先显示加载态 document.querySelector('.price').innerText = '加载中...'; // 获取价格函数 async function fetchPrice(symbol) { try { const res = await fetch(`/api/quote?symbol=${symbol}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); return await res.json(); } catch (err) { console.error(`Failed to fetch ${symbol}:`, err); return { error: true, message: err.message }; } } // 更新UI function updateUI(data) { const priceEl = document.querySelector('.price'); const symbolEl = document.querySelector('.symbol'); const timeEl = document.querySelector('.timestamp'); const staleEl = document.querySelector('.stale-warning'); if (data.error) { priceEl.innerText = '❌ 错误'; timeEl.innerText = data.message; staleEl.style.display = 'none'; return; } symbolEl.innerText = data.symbol; priceEl.innerText = data.price.toFixed(2); priceEl.className = 'price ' + (data.price > window.lastPrice ? 'up' : 'down'); timeEl.innerText = formatTimeAgo(new Date(data.timestamp)); staleEl.style.display = data.isStale ? 'block' : 'none'; window.lastPrice = data.price; // 记录上一次价格,用于涨跌判断 } // 时间格式化函数 function formatTimeAgo(date) { const now = new Date(); const diffMs = now - date; const diffSec = Math.floor(diffMs / 1000); const diffMin = Math.floor(diffSec / 60); const diffHour = Math.floor(diffMin / 60); if (diffSec < 60) return `${diffSec}秒前`; if (diffMin < 60) return `${diffMin}分钟前`; if (diffHour < 24) return `今天 ${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`; return date.toLocaleDateString('zh-CN'); } // 启动轮询(30秒一次) async function startPolling() { while (true) { const data = await fetchPrice('AAPL'); // 这里固定查AAPL,可扩展为循环SYMBOLS updateUI(data); await new Promise(resolve => setTimeout(resolve, 30000)); // 30秒 } } // 页面加载完成后启动 document.addEventListener('DOMContentLoaded', () => { startPolling(); });

关键细节说明:

  • SYMBOLS数组预留了扩展位,若要显示多只股票,只需修改startPolling()为遍历数组,并动态生成多个.stock-card元素;
  • window.lastPrice用全局变量暂存上一次价格,是实现涨跌色态最轻量的方式(不用Redux等状态库);
  • setTimeout的30000毫秒即30秒,符合Twelvedata免费版的速率限制(每分钟20次),留出余量防抖动。

4.4 本地开发与调试技巧

  • 快速测试API:在终端执行curl "http://localhost:3000/api/quote?symbol=AAPL",应返回类似:
    {"price":182.45,"symbol":"AAPL","timestamp":"2024-06-15T10:23:45.000Z","lastUpdated":"2024-06-15T10:24:12.345Z"}
  • 模拟API故障:临时注释掉getStockPrice里的fetch调用,直接throw new Error('Simulated failure'),观察前端是否显示“❌ 错误”及错误信息;
  • 验证缓存:首次访问后,立即刷新页面,打开Chrome DevTools → Network,查看/api/quote请求的Size列,如果是(from memory cache),说明缓存生效;
  • 时间格式化调试:在formatTimeAgo函数开头加console.log('date:', date, 'now:', new Date()),确保时区计算正确(Twelvedata返回UTC时间,new Date()在浏览器中自动转为本地时区,无需手动转换)。

5. 常见问题与排查技巧实录:那些文档里不会写的血泪教训

5.1 “价格一直显示--.--,Network里看不到请求”

现象:页面打开后,价格始终是--.--,DevTools的Network标签页里没有/api/quote请求。
排查路径:

  1. 检查浏览器控制台(Console)是否有Access to fetch at 'http://localhost:3000/api/quote...' from origin 'http://127.0.0.1:5500' has been blocked by CORS policy错误;
  2. 如果有,说明cors中间件的origin配置未包含你的前端地址。VS Code Live Server默认用http://127.0.0.1:5500,而localhost和127.0.0.1在浏览器看来是不同源,必须同时写进origin数组;
  3. 修复后重启Node服务(Ctrl+C再npx nodemon server.js),因为cors配置在服务启动时加载。

注意:不要用app.use(cors())无参数调用,那会允许所有源,存在安全风险。生产环境必须显式指定origin。

5.2 “价格数字疯狂跳变,一会儿绿一会儿红”

现象:price元素颜色在.up和.down间频繁切换,数字抖动严重。
根因:window.lastPrice未初始化,导致第一次比较时data.price > undefined恒为false,永远走.down逻辑;后续每次更新又因lastPrice被覆盖为旧值,造成震荡。
解决方案:在updateUI函数开头加初始化判断:

if (typeof window.lastPrice === 'undefined') { window.lastPrice = data.price; // 首次赋值,不触发颜色变化 priceEl.className = 'price'; // 初始无色 return; }

5.3 “API返回NaN,页面显示‘NaN’”

现象:价格区域显示NaN,控制台报错Cannot read property 'toFixed' of NaN。
原因:Twelvedata在股票停牌或无交易时,可能返回"price": "0.0000"(字符串)或"price": null,parseFloat(null)结果是NaN。
修复:在getStockPrice的返回处理处加强校验:

const priceNum = parseFloat(data.price); if (isNaN(priceNum) || priceNum <= 0) { throw new Error(`Invalid price value: ${data.price}`); }

5.4 “服务器CPU飙升到100%,日志里全是fetch timeout”

现象:top命令看到Node进程CPU占满,server.js日志刷屏[ERROR] Failed to fetch AAPL: TimeoutError: Promise timed out after 2000 milliseconds。
真相:Twelvedata在高峰期(如美股开盘前10分钟)响应极不稳定,2秒超时仍频繁触发。
终极解法:

  1. 将fetch超时从2000提高到5000(5秒);
  2. 在catch块里增加退避重试(最多1次):
} catch (err) { if (err.name === 'TimeoutError' && !retried) { console.log(`[RETRY] Retrying ${symbol} after timeout...`); return getStockPrice(symbol, true); // 递归重试,传retried=true } // ...原有错误处理 }
  1. 同时调整缓存TTL为60秒,减少重试压力。

5.5 “部署到云服务器后,前端报500,但本地一切正常”

现象:本地npm start完美,部署到腾讯云轻量应用服务器后,前端调用/api/quote返回500,pm2 logs看到Error: connect ECONNREFUSED 127.0.0.1:443。
定位:云服务器防火墙未开放3000端口,或Nginx反向代理未配置。
两步解决:

  • 方案A(推荐,用Nginx):
    # /etc/nginx/conf.d/stock.conf server { listen 80; server_name your-domain.com; location /api/ { proxy_pass http://127.0.0.1:3000/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { root /var/www/stock-display/public; try_files $uri $uri/ /index.html; } }
    然后sudo nginx -t && sudo systemctl reload nginx;
  • 方案B(快速验证):临时关闭防火墙sudo ufw disable,确认是端口问题后,再精确开放sudo ufw allow 3000。

6. 进阶扩展建议:从“简单显示”到“可用工具”的三步跃迁

做到这里,你已经拥有了一个稳定、可靠、可维护的股票价格显示服务。但真正的价值提升,往往在“简单”之外。基于我在某高校金融实验室的实际落地经验,分享三个低成本、高回报的扩展方向:

6.1 加入“价格阈值提醒”:让看板主动发声

学生做模拟交易时,常需要盯住某只股票突破关键价位(如AAPL站上185)。与其人工刷新,不如让服务主动通知:

  • 技术实现:在Node层增加/api/alert端点,接收{ symbol, threshold, operator: 'gt' | 'lt' };
  • 存储方案:用node-cache存用户提醒(alert:AAPL:185:gt→{ count: 0, lastTriggered: Date });
  • 触发逻辑:每次getStockPrice成功后,遍历所有相关提醒,若price > threshold且距上次触发超5分钟,则调用console.log或发邮件(用nodemailer);
  • 前端集成:在页面加一个“设置提醒”按钮,弹窗输入价格和操作符,AJAX提交即可。

成本:新增约50行代码,无需数据库,全部内存完成。

6.2 支持A股行情:用聚合API绕过免费版限制

Twelvedata免费版不支持A股,但国内有合规的聚合服务(如聚宽、掘金),它们提供HTTP接口,返回格式与Twelvedata高度一致。

  • 改造点:在getStockPrice函数里,根据symbol后缀判断市场:
    if (symbol.endsWith('.SH') || symbol.endsWith('.SZ')) { // 走国内聚合API url = `https://api.jqdata.com/v1/price?symbol=${symbol}&token=${JQ_TOKEN}`; } else { // 走Twelvedata }
  • 关键适配:国内API通常要求token认证,且返回字段名不同(如current_price而非price),用一个normalizeResponse(data, source)函数统一转成{ price, timestamp }标准结构,前端完全无感。

6.3 打包为桌面应用:告别浏览器,常驻任务栏

用Electron将整个服务打包成.exe(Windows)或.dmg(macOS):

  • 优势:自动启动、后台静默运行、任务栏图标右键菜单(快速刷新/退出)、无浏览器地址栏干扰;
  • 步骤:
    1. npm install electron --save-dev;
    2. 新建main.js,用app.whenReady().then(createWindow)启动;
    3. BrowserWindow的webPreferences里设nodeIntegration: true,让前端JS能调用require('electron').ipcRenderer;
    4. 把Node服务内嵌为child_process.spawn('node', ['server.js']),前端通过IPC与之通信;
  • 体积控制:Electron默认打包200MB+,用electron-builder的extraResources选项只打包必要文件,实测可压至45MB。

我给某导师做的教学演示版,就是Electron打包的。他上课时点开就用,关

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询