2017年6月16日 星期五

[JS] 用 NodeJS 和 Socket.IO 製作實時更新的自家股價指標查詢網頁

繼續Node.JS 的學習,這篇是要記錄另一個嘗試,在後台的Server不斷更新股價資料和自設的指標,做一個網頁可以讓自己在屋企以外也可以查詢這些後台的資訊。

之前的星期看完了Coursera的那個Node 的課程,內容是充實的,繼之前的內容後,還有HTTP 的session , https, authorise token。 不過未有用到的想法,看完也像過眼雲煙。能夠上心的反而只有自己試的Socket.IO。會這樣做是因為看到一篇Socket.IO 的文章,可以讓客戶端的瀏覽器和伺服器端作實時雙向溝通,不必客戶端去不斷刷新。


Socket.IO 由兩部份組成:
- 整合在Node.JS HTTP Server的伺服端: "socket.io"
- 瀏覽器端掛載的 Client Library: "socket.io-client"

在處理在客戶端的瀏覽器網頁,先加入引用Socket.io 和 jQuery 的JS庫:
<script src="/socket.io/socket.io.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>

之後呼叫 var socket = io.connect() 設定連線位置及初始化。 之後的socket.on('event', eventHandler); 會監聽server 傳來名為'stockSnap'的事件,當中的eventHandler會處理連接收後的工作,這裡把它寫成匿名函式,就是把伺服器傳來的資料'data'用jQuery方式更新到HTML Table中的相應 #id 的 <div>。
<script>
  var socket = io.connect();
  socket.on('stockSnap', function(data) {
    $('#stock').text(data.symbol);
    $('#name').text(data.name);
    $('#price').text(data.lastTradePriceOnly);
    $('#lastTradeDate').text(data.lastTradeDate);
  });
</script>


伺服器和以往一樣開設HTTP Server,每5分鐘提取yahoo-finance 的snapshot data,當然也要預先安裝Socket.IO的套件。
在Socket.IO的部份,var serv_io = io.listen(server) 會開設一個Server: "serv_io" 依附到原本的HTTP Server 上,之後用serv_io.on('connect', callback)監聽外來到的連線。連線過程中用socket.emit('stockSnap',[arg])發出名為'stockSnap'的事件,當然[arg]其中的資料就是Yahoo-finance拿來的股票資料,或再經運算出來的自定指標。

var server = http.createServer()
server.listen(8080);
var serv_io = io.listen(server); 

serv_io.on('connect', function(socket) {
socket.emit('stockSnap', {
    'symbol': snapshot['symbol'], 
    'name': snapshot['name'], 
    'lastTradePriceOnly': snapshot['lastTradePriceOnly'], 
    'lastTradeDate': snapshot['lastTradeDate'] 
});

所以,基本使用方面就是這些指令:
server.listen(httpServer)                // 基於Node.JS原有的http Server上開設一個Server
server.on('connect', callback)           // 伺服器端監聽外來查詢的連線
server.on('disconnect', callback)        // 伺服器端監聽現有連線的中斷

socket.connect('http://127.0.0.1:8080/') // 網頁端打開一個Socket連接到localhost:8080
socket.close()                           // 手動在網頁端關閉一個Socket

socket.emit(eventName[, ...args][, ack]) //發送一個事件
socket.on(eventName, callback)           //監聽一個事件,執行callback

如果在網頁加上一個文字的輸入欄,按「確定」後觸發socket.emit()將股票編號傳回,更新伺服器程式中的股票名單SYMBOLS,就可以做到更互動的查詢。這個已加入到下面的完整代碼中。


//----------Code part.1 -----------//
socket-stock.html
<html>
  <head>
    <style> 
        div {display:inline} 
        table, th, td {
            border: 1px solid black;
        }
    </style> 
    <script src="/socket.io/socket.io.js"></script>
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
  </head>
  <body>
    <script>
      var socket = io.connect();
      socket.on('connect', function(data) {
          socket.on('stockSnap', function(data) {
            $('#stock').text(data.symbol);
            $('#name').text(data.name);
            $('#price').text(data.lastTradePriceOnly);
            $('#lastTradeDate').text(data.lastTradeDate);
            $('#lastTradeTime').text(data.lastTradeTime);
            $('#customIndicator').text(data.customIndicator);
            $('#sysTimestamp').text(data.sysTimestamp);
          });
          $('#form').submit(function(){
            socket.emit('stockReq', $('#stockCode').val() );
            return false;
          });
      });
    </script>

     <table style="width:100%">
      <tr>
        <th>Symbol</th>
        <th>Name</th>
        <th>Last Trade Price</th>
        <th>Last Trade Date</th>
        <th>Last Trade Time</th>
        <th>Custom Indicator</th>
        <th>System Timestamp</th>
      </tr>
      <tr>
        <td><div id="stock"></div></td>
        <td><div id="name"></div></td>
        <td><div id="price"></div></td>
        <td><div id="lastTradeDate"></div></td>
        <td><div id="lastTradeTime"></div></td>
        <td><div id="customIndicator"></div></td>
        <td><div id="sysTimestamp"></div></td>
      </tr>
    </table> 

   <form id = "form" action = "">
        Input: <input type="text" id="stockCode"><br>
        <input type="submit" value="Submit">
   </form>

   </body>
</html>



//----------Code part.2 -----------//
app-stock.js
var http = require("http");
var url = require('url');
var fs = require('fs');
//var util = require('util');
//require('colors');
var _ = require('lodash');
var io = require('socket.io'); // 加入 Socket.IO
var yahooFinance = require('yahoo-finance');

var FIELDS01 = _.flatten([
    ['n'], //name
    ['l1','d1','d2','t1'], //Last Trade, Last Trade Date, Trade Date, Last Trade Time
    ['p','c1','w','j6','k5'], 
    ['y','r','p6']
    ]);
var SYMBOLS = ['0823.HK'];
var INDICATORS = ['Positive', 'Neutral', 'Negative'];
var result;

var server = http.createServer(function(request, response) {
  console.log('Connection');
  var path = url.parse(request.url).pathname;
  switch (path) {
    case '/':
      response.writeHead(200, {'Content-Type': 'text/html'});
      response.write('Hello, World.');
      response.end();
      break;
    case '/socket-stock.html':
      fs.readFile(__dirname + path, function(error, data) {
        if (error){
          response.writeHead(404);
          response.write("opps this doesn't exist - 404");
        } else {
          response.writeHead(200, {"Content-Type": "text/html"});
          response.write(data, "utf8");
        }
        response.end();
      });
      break;
    default:
      response.writeHead(404);
      response.write("Opps, this doesn't exist - 404");
      response.end();
      break;
  }
});
server.listen(8080);
console.log('Page shown in http://127.0.0.1:8080/socket-stock.html');

var serv_io = io.listen(server); 
serv_io.on('connect', function(socket) {

  // 定時接收yahoo-finance的資料,並發出到瀏覽器
  setInterval(function() {
    yahooFinance.snapshot({
      fields: FIELDS01,
      symbols: SYMBOLS
    }, function (err, result) {
      if (err) { throw err; }
      _.forEach(result, function (snapshot, symbol) {
        var d = new Date();
        var dString = d.toLocaleTimeString();
        var customIndicator = INDICATORS[Math.floor(Math.random() * INDICATORS.length)];
        
        console.log(util.format('=== %s ===', (dString + " - " +snapshot['symbol']).cyan ));
        console.log(JSON.stringify(snapshot, null, 2));
        socket.emit('stockSnap', {
            'symbol': snapshot['symbol'].toString(), 
            'name': snapshot['name'].toString(), 
            'lastTradePriceOnly': snapshot['lastTradePriceOnly'].toString(), 
            'lastTradeDate': snapshot['lastTradeDate'].toLocaleDateString("en-GB"),
            'lastTradeTime': snapshot['lastTradeTime'].toString(),
            'customIndicator': customIndicator,
            'sysTimestamp': dString  
        });
      });
    });
  }, 10*1000);

  // 接收來自於瀏覽器的資料
  socket.on('stockReq', function(data) {
    console.log('message: ' + data);
    SYMBOLS[0] = data;
  });
  
});



沒有留言:

張貼留言