原文地址:https://feinterview.poetries.top/blog/nextjs-tradingview-integration
# 导语
TradingView 是全球最专业的金融图表可视化库之一,提供了功能强大的 K 线图、指标系统和技术分析工具。在金融行情类 Web 应用中,接入 TradingView 是提升用户体验的首选方案。
本文将基于实际项目代码,系统讲解如何在 Next.js 项目中接入 TradingView Charts,包括环境配置、Datafeed 数据馈送实现、自定义指标开发、主题样式定制、以及关键的性能优化策略。
# 一、项目准备与环境配置
# 1.1 获取 TradingView 图表库
TradingView 图表库需要从官方获取授权后下载。获取后将文件放置在项目的 public/static/charting_library 目录下:
public/
└── static/
└── charting_library/
├── charting_library.standalone.js
└── bundles/
├── *.js
└── *.css
@前端进阶之旅: 代码已经复制到剪贴板
# 1.2 组件目录结构
src/components/Tradingview/
├── index.tsx # 主组件
├── datafeed.ts # 数据馈送实现
├── widgetOpts.tsx # 图表配置选项
├── widgetMethods.ts # 图表方法工具
├── theme.ts # 主题配置
├── constant.ts # 常量定义
└── customIndicators/ # 自定义指标
├── ma.ts
├── macd.ts
├── kdj.ts
└── customerRSI.ts
@前端进阶之旅: 代码已经复制到剪贴板
# 二、核心组件实现
# 2.1 主组件:TradingView 图表容器
# 2.2 Datafeed 数据馈送实现
Datafeed 是 TradingView 与后端数据交互的核心接口,需要实现以下方法:
// src/components/Tradingview/datafeed.ts
class DataFeedBase {
configuration: DatafeedConfiguration
constructor(props: Partial<ChartingLibraryWidgetOptions>) {
this.configuration = {
supports_time: true,
supports_timescale_marks: true,
supports_marks: true,
// 支持的分辨率
supported_resolutions: ['1', '5', '15', '30', '60', '240', '1D', '1W', '1M'],
intraday_multipliers: ['1', '5', '15', '30', '60', '240', '1D', '1W', '1M']
} as DatafeedConfiguration
this.setActiveSymbolInfo = props.setActiveSymbolInfo
this.removeActiveSymbol = props.removeActiveSymbol
this.getDataFeedBarCallback = props.getDataFeedBarCallback
this.isZh = props.locale === 'zh_TW'
}
// 图表初始化时调用,设置支持的配置
onReady(callback) {
setTimeout(() => {
callback(this.configuration)
}, 0)
}
// 解析品种信息
async resolveSymbol(symbolName, onSymbolResolvedCallback, onResolveErrorCallback, extension) {
const resolution = String(STORAGE_GET_TRADINGVIEW_RESOLUTION() || '')
const ENV = getEnv()
const urlPrefix = ENV.isApp ? getInjectParams().baseUrl : ''
let symbolInfo
if (!ENV.isApp) {
// HTTP 请求获取品种信息
const res = await request(`${urlPrefix}/api/trade-core/coreApi/symbols/symbol/detail?symbol=${symbolName}`)
symbolInfo = res?.data || {}
} else {
// APP 内获取 RN 传递的数据
symbolInfo = {
...(ENV?.injectParams?.symbolInfo || {}),
...(stores.global.symbolInfo || {})
}
}
const currentSymbol = {
...symbolInfo,
precision: symbolInfo?.symbolDecimal || 2,
description: symbolInfo?.remark || '',
exchange: '',
session: '24x7',
name: symbolInfo.symbol,
dataSourceCode: symbolInfo.dataSourceCode
}
const commonSymbolInfo = {
has_intraday: true,
has_daily: true,
has_weekly_and_monthly: true,
intraday_multipliers: this.configuration.intraday_multipliers,
supported_resolutions: this.configuration.supported_resolutions,
data_status: 'streaming',
format: 'price',
minmov: 1,
pricescale: Math.pow(10, currentSymbol.precision),
ticker: currentSymbol?.name
} as LibrarySymbolInfo
const currentSymbolInfo = {
...commonSymbolInfo,
...currentSymbol,
description: this.isZh ? currentSymbol.description : currentSymbol?.name,
exchange: this.isZh ? currentSymbol?.exchange : '',
session: '0000-0000|0000-0000:1234567;1',
timezone: ['D', 'W', 'M', 'Y'].some((item) => resolution.endsWith(item)) ? 'Etc/UTC' : 'Asia/Shanghai'
} as LibrarySymbolInfo
setTimeout(() => {
onSymbolResolvedCallback(currentSymbolInfo)
}, 0)
}
// 搜索品种
searchSymbols(userInput, exchange, symbolType, onResultReadyCallback) {
const keyword = userInput || ''
const resultArr = symbolInfoArr
.filter((item) => item.name.includes(keyword))
.map((item) => ({
symbol: item.name,
name: item.name,
full_name: `${item.name}`,
description: this.isZh ? item.description : item.name,
exchange: this.isZh ? item.exchange : '',
type: item.type,
ticker: item.name
}))
setTimeout(() => {
onResultReadyCallback(resultArr)
}, 0)
}
// 获取 K 线历史数据(核心方法)
getBars(symbolInfo, resolution, periodParams, onHistoryCallback, onErrorCallback) {
const { from, to, firstDataRequest, countBack } = periodParams
this.setActiveSymbolInfo({ symbolInfo, resolution })
this.getDataFeedBarCallback({
symbolInfo,
resolution,
from,
to,
countBack,
onHistoryCallback,
onErrorCallback,
firstDataRequest
})
}
// 订阅实时数据更新
subscribeBars(symbolInfo, resolution, onRealtimeCallback, subscriberUID, onResetCacheNeededCallback) {
this.setActiveSymbolInfo({
symbolInfo,
resolution,
onRealtimeCallback,
subscriberUID,
onResetCacheNeededCallback
})
mitt.on('symbol_change', () => {
onResetCacheNeededCallback()
})
}
// 取消订阅
unsubscribeBars(subscriberUID) {
this.removeActiveSymbol(subscriberUID)
}
}
export default DataFeedBase
@前端进阶之旅: 代码已经复制到剪贴板
# 2.3 图表配置选项
# 三、K线数据与WebSocket实时更新
# 3.1 WebSocket Store 实现
# 四、自定义指标开发
# 4.1 自定义 MA 指标示例
# 五、主题与样式定制
# 5.1 主题配置
# 5.2 K线颜色与涨跌色设置
# 六、性能优化策略
# 6.1 数据加载优化
# 6.2 WebSocket 连接优化
# 6.3 图表渲染优化
# 6.4 内存管理与清理
useEffect(() => {
return () => {
// 组件卸载时清理
tvWidget.remove() // 销毁图表实例
mitt.off('symbol_change') // 取消事件订阅
this.stopHeartbeat() // 停止心跳
this.socket?.close() // 关闭 WebSocket
}
}, [])
@前端进阶之旅: 代码已经复制到剪贴板
# 七、常见问题与解决方案
# 7.1 主题切换不生效
# 7.2 数据请求重复
# 7.3 移动端适配
# 八、完整调用示例
URL 参数说明: