1、websocket
用戶量量大,數據量大,而且要求實時更新數據的時候,需要使用websocket。
tradingview正好就是這樣的應用場景。
2、tradingview和websocket結合
- getBars方法。tradingview圖表插件響應用戶操作,根據用戶界面渲染需要的數據時間段,調用getBars方法,傳遞參數function(symbolInfo, resolution, rangeStartDate, rangeEndDate, onLoadedCallback),我們需要構造出預期的參數返回給onLoadedCallback方法,即onLoadedCallback(data)。
- onLoadedCallback方法。該方法接收的data有兩種,一種是list數組。另一種是newBars對象。list是歷史數據,rangeStartDate - rangeEndDate時間段的數據,根據時間顆粒resolution來劃分。newBars是增量數據,同樣是rangeStartDate - rangeEndDate時間段的數據,但是這個數據必定是在當前的resolution內。當我們把數據返回給onLoadedCallback,圖表會渲染這些數據。
3、數據獲取和填充
- 頁面初始化,實例化圖表,連接websocket。
- 從websocket得到的數據存入緩存cacheData。
- 因爲getBars是圖表自己調用的,所以當我們存入數據到cacheData的時候,在getBars方法中調用onLoadedCallback就可以。
4、實例
圖表調用的getBars方法:
TVjsApi.prototype.getBars = function(symbolInfo, resolution, rangeStartDate, rangeEndDate, onLoadedCallback) {
//console.log(' >> :', this.formatt(rangeStartDate*1000), this.formatt(rangeEndDate*1000))
var ticker = this.symbol + "-" + resolution;
var tickerload = ticker + "load";
var tickerstate = ticker + "state";
if(!this.cacheData[ticker] && !this.cacheData[tickerstate]){
//如果緩存沒有數據,而且未發出請求,記錄當前節點開始時間
this.cacheData[tickerload] = rangeStartDate;
//發起請求,從websocket獲取當前時間段的數據
this.initMessage(symbolInfo, resolution, rangeStartDate, rangeEndDate, onLoadedCallback);
//設置狀態爲true
this.cacheData[tickerstate] = !0;
return false;
}
if(!this.cacheData[tickerload] || this.cacheData[tickerload] > rangeStartDate){
//如果緩存有數據,但是沒有當前時間段的數據,更新當前節點時間
this.cacheData[tickerload] = rangeStartDate;
//發起請求,從websocket獲取當前時間段的數據
this.initMessage(symbolInfo, resolution, rangeStartDate, rangeEndDate, onLoadedCallback);
//設置狀態爲true
this.cacheData[tickerstate] = !0;
return false;
}
if(this.cacheData[tickerstate]){
//正在從websocket獲取數據,禁止一切操作
return false;
}
//ticker = this.symbol + "-" + this.interval;
//如果緩存有數據,且長度不爲0,構造數據
if (this.cacheData[ticker] && this.cacheData[ticker].length) {
this.isLoading = false
var newBars = []
this.cacheData[ticker].forEach(item => {
if (item.time >= rangeStartDate * 1000 && item.time <= rangeEndDate * 1000) {
newBars.push(item)
}
})
onLoadedCallback(newBars)
} else {
//如果沒有數據,等待10ms,再執行一次
var self = this
this.getBarTimer = setTimeout(function() {
self.getBars(symbolInfo, resolution, rangeStartDate, rangeEndDate, onLoadedCallback)
}, 10)
}
}
獲取歷史數據list的init方法:
TVjsApi.prototype.initMessage = function(symbolInfo, resolution, rangeStartDate, rangeEndDate, onLoadedCallback){
//console.log('發起請求,從websocket獲取當前時間段的數據');
var that = this;
//保留當前回調
that.cacheData['onLoadedCallback'] = onLoadedCallback;
//獲取需要請求的數據數目
var limit = that.initLimit(resolution, rangeStartDate, rangeEndDate);
//商品名
var symbol = that.symbol;
//如果當前時間顆粒已經改變,停止上一個時間顆粒的訂閱,修改時間節點值
if(that.interval !== resolution){
that.unSubscribe(that.interval)
that.interval = resolution;
}
//獲取當前時間段的數據,在onMessage中執行回調onLoadedCallback
if (that.interval < 60) {
that.socket.send({
cmd: 'req',
args: ["candle.M"+resolution+"."+symbol, limit, rangeEndDate],
id: 'trade.M'+ resolution +'.'+ symbol.toLowerCase()
})
} else if (that.interval >= 60) {
that.socket.send({
cmd: 'req',
args: ["candle.H"+(resolution/60)+"."+symbol, limit, rangeEndDate],
id: 'trade.H'+ (resolution / 60) +'.'+ symbol.toLowerCase()
})
} else {
that.socket.send({
cmd: 'req',
args: ["candle.D1."+symbol, limit, rangeEndDate],
id: 'trade.D1.'+ symbol.toLowerCase()
})
}
}
響應websocket的Message方法:
TVjsApi.prototype.onMessage = function(data) {
var thats = this;
// console.log("這是後臺返回的數據"+count+":"+JSON.stringify(data) )
if (data.data && data.data.length) {
//websocket返回的值,數組代表時間段歷史數據,不是增量
var list = []
var ticker = thats.symbol + "-" + thats.interval;
var tickerstate = ticker + "state";
//var that = thats;
//遍歷數組,構造緩存數據
data.data.forEach(function(element) {
list.push({
time: element.id*1000,
open: element.open,
high: element.high,
low: element.low,
close: element.close,
volume: element.quote_vol
})
}, thats)
//如果沒有緩存數據,則直接填充,發起訂閱
if(!thats.cacheData[ticker]){
/*thats.cacheData[ticker] = thats.cacheData[ticker].concat(list);
thats.cacheData['onLoadedCallback'](list);
}else{*/
thats.cacheData[ticker] = list;
thats.subscribe()
}
//新數據即當前時間段需要的數據,直接餵給圖表插件
if(thats.cacheData['onLoadedCallback']){
thats.cacheData['onLoadedCallback'](list);
}
//請求完成,設置狀態爲false
thats.cacheData[tickerstate] = !1;
//記錄當前緩存時間,即數組最後一位的時間
thats.lastTime = thats.cacheData[ticker][thats.cacheData[ticker].length - 1].time
}
if (data.type && data.type.indexOf(thats.symbol.toLowerCase()) !== -1) {
// console.log(' >> sub:', data.type)
// data帶有type,即返回的是訂閱數據,
//緩存的key
var ticker = thats.symbol + "-" + thats.interval;
//構造增量更新數據
var barsData = {
time: data.id * 1000,
open: data.open,
high: data.high,
low: data.low,
close: data.close,
volume: data.quote_vol
}
/*if (barsData.time >= thats.lastTime && thats.cacheData[ticker] && thats.cacheData[ticker].length) {
thats.cacheData[ticker][thats.cacheData[ticker].length - 1] = barsData
}*/
//如果增量更新數據的時間大於緩存時間,而且緩存有數據,數據長度大於0
if (barsData.time > thats.lastTime && thats.cacheData[ticker] && thats.cacheData[ticker].length) {
//增量更新的數據直接加入緩存數組
thats.cacheData[ticker].push(barsData)
//修改緩存時間
thats.lastTime = barsData.time
}else if(barsData.time == thats.lastTime && thats.cacheData[ticker].length){
//如果增量更新的時間等於緩存時間,即在當前時間顆粒內產生了新數據,更新當前數據
thats.cacheData[ticker][thats.cacheData[ticker].length - 1] = barsData
}
// 通知圖表插件,可以開始增量更新的渲染了
thats.datafeeds.barsUpdater.updateData()
}
}
代碼邏輯如下:
- getBars方法由圖表插件調用,帶有當前需要渲染的時間參數、時間顆粒和callback。
- 如果緩存中有當前時間段的數據,構造newBars,調用onLoadedCallback(newBars)。
- 如果緩存中沒有數據,判斷當前需要的數據類型,如果是list歷史記錄,那麼我們需要通過websocket來獲取,執行initMessage。
- 判斷當前緩存中的時間顆粒resolution是否已經改變,如果已經改變,需要清除緩存數據,更新緩存cacheData = list,返回圖表onLoadedCallback(list),另外需要訂閱當前時間顆粒的增量數據,取消上一次訂閱。
- 如果時間顆粒resolution沒有改變,websocket得到的歷史數據不需要存入緩存,直接調用onLoadedCallback(list)就可以,因爲圖表插件的渲染是根本得到的數據時間段來執行的,即:圖表插件會緩存已經渲染過的數據。
- 如果當前請求的數據爲增量數據,等待10ms,因爲增量數據來源於websocket,我們已經訂閱,需要等待websocket返回,相當於pending。
以上邏輯基本含括了用戶的兩個重要操作:切換時間顆粒、查看歷史記錄。
可運行代碼在GitHub上已經更新,可預覽。