HTML5 通訊技術

by vivid 21. 十一月 2012 05:40

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:N121113002
出刊日期:2012/11/21

利用HTML5新增的postMessage API,可以讓不同文件,或跨網站溝通,並且允許使用跨網站指令碼( Cross-site Scripting)。用戶端和伺服端若要進行即時通訊,在HTML5之中可以採用Server-Sent Event,透過HTTP 讓伺服端傳送通知訊息到用戶端來接收。本篇文章將介紹postMessage API與Server-Sent Event的設計。

 

了解postMessage API

postMessage API可以讓不同的網頁文件進行資料交換,並接聽message事件來取得訊息。要使用到postMessage傳送訊息之前,最好先測試一下瀏覽器是否支援postMessage的功能,我們無法得知使用者會使用哪一個版本的瀏覽器來執行,因此建議利用JavaScript在程式中進行判斷,並適當提示使用者。只要使用一小段JavaScript程式碼就可以判斷瀏覽器是否支援postMessage API,參考以下HTML範例程式碼在DOMContentLoaded事件中利用window.postMessage語法來判斷支援度,若瀏覽器支援postMessage API,window就會包含postMessage屬性。

<!DOCTYPE html>
<html xmlns = "http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv = "Content-Type" content = "text/html; charset=utf-8" />
  <title> </title>
  <script type = "text/javascript">
    function contentLoaded () {
      if ( typeof ( window.postMessage ) !== "undefined" ) {
        document.getElementById( "result" ).innerHTML = "您的瀏覽器支援postMessage功能<br>";
      }
      else {
        document.getElementById( "result" ).innerHTML = "您的瀏覽器不支援postMessage功能<br>";
        return ;
      }
    }
    window.addEventListener( "DOMContentLoaded", contentLoaded, false );
  </script>

</head>
<body>
  <div id = "result"> </div>
</body>
</html>

 

 

在不同文件間交換訊息

postMessage API在不同的文件交換訊息。舉例來說,若想要跨文件來交換訊息,其中有一個HTML內容畫面如圖1,網頁中內嵌一個iframe指向另外一個網頁,我們想讓HTML與iFrame能夠互通資料:

clip_image001

圖 1:讓HTML與iFrame能夠互通資料。

HTML網頁完整程式碼如下所示:

<!DOCTYPE html>

<html xmlns = "http://www.w3.org/1999/xhtml">

<head>

<meta http-equiv = "Content-Type" content = "text/html; charset=utf-8" />

<title> </title>

<script type = "text/javascript">

var txt;

function contentLoaded () {

if ( typeof ( window.postMessage ) !== "undefined" ) {

document.getElementById( "result" ).innerHTML = "您的瀏覽器支援postMessage功能<br>";

}

else {

document.getElementById( "result" ).innerHTML = "您的瀏覽器不支援postMessage功能<br>";

return;

}

document.getElementById( "Button1" ).addEventListener( "click", Button1_Click, false );

}

function Button1_Click () {

txt = document.getElementById( 'Text1' ).value;

document.getElementById( 'MyiFrame' ).contentWindow.postMessage( txt, "http://localhost:7650/MyiFrame.html" );

document.getElementById( "result" ).innerHTML += "送出訊息 : " + txt + "<br>";

}

window.addEventListener( "DOMContentLoaded", contentLoaded, false );

</script>

<style type = "text/css">

#Parent {

display: -moz-box;

display: -ms-box;

display: -webkit-box;

width: 100%;

position: absolute;

bottom: 0;

left:0;

}

#Child1 {

-moz-box-flex: 1;

-ms-box-flex: 1;

-webkit-box-flex: 1;

width: 100px;

background-color: #ffd800;

}

#Child2 {

-moz-box-flex: 3;

-ms-box-flex: 3;

-webkit-box-flex: 3;

width: 200px;

background-color: #00ff21;

}

</style>

</head>

<body>

<div id = "result"> </div>

<div id = "Parent">

<div id = "Child1">

<input id = "Text1" type = "text" />

<input id = "Button1" type = "button" value = "Send Message:" />

</div>

<div id = "Child2">

<iframe id = "MyiFrame" src="http://localhost:7650/MyiFrame.html"

style = "overflow: hidden; height: 100%; width: 100%; border: 0px" />

</div>

</div>

</body>

</html>

 

網頁的上方使用一個id為result的 div標籤用來顯示訊息,網頁左下方包含一個文字方塊和按鈕,右下方包含一個id為MyiFrame 的iFrame,指向相同網站中的MyiFrame.html。

當使用者在網頁左下方文字方塊中輸入資料按下了「Send Message:」按鈕後,將執行Button1_Click方法,在此方法中我們利用了contentWindow屬性,取回代表iFrame的window物件,接著叫用了postMessage方法,傳送一個MessageEvent物件到iFrame視窗。傳入postMessage方法的第一個參數代表要傳送的資料,第二個參數為targetOrigin,表示允許收到此訊息的目地網頁。targetOrigin若設為「/」代表限定在相同origin,若設為「*」則表示不限定origin。基於安全性的考量,建議儘量不要設定成「*」。

MyIFrame.html程式如下所示,假設目前網站應用程式的url為本機(localhost),使用7650埠號執行:

<!DOCTYPE html>

<html xmlns = "http://www.w3.org/1999/xhtml">

<head>

<meta http-equiv = "Content-Type" content = "text/html; charset=utf-8" />

<title> </title>

<script type = "text/javascript">

function myMessageHandler( event ) {

if ( event.origin == "http://localhost:7650" ) {

document.getElementById( "result" ).innerHTML = event.data;

// event.source.postMessage( "Response from iFrame: " + event.data , event.origin );

}

}

function contentLoaded() {

window.addEventListener( "message", myMessageHandler );

}

window.addEventListener( "DOMContentLoaded", contentLoaded, false );

</script>

</head>

<body>

<h1> iFrame </h1>

<div id = "result"> </div>

</body>

</html>

MyIFrame.html程式碼在DOMContentLoaded事件中註冊了message事件,只要接收到訊息時,便會觸發此事件。當收到網頁送至的訊息時,為了安全性考量,先檢查event.origin,看看是否是相同網站網頁傳送過來的訊息,然後從此事件處理常式參數的event.data屬性可以取得送至的訊息。這範例的執行結果參考下圖,當你在畫面上文字方塊輸入「Hi」,按下「Send Message:」按鈕,iFrame將收到訊息,網頁上方,以及iFrame都會顯示出相同的訊息,請參考圖2所示:

clip_image002

圖 2:使用postMessage交換訊息。

若想要從iFrame回傳收到的訊息到原來的HTML頁面,只要將MyIFrame.html以下此行註解移除即可。此行程式利用event.source取得來源的window物件,再叫用postMessage方法傳回訊息。

event.source.postMessage( "Response from iFrame: " + event.data, event.origin );

 

接收iFrame回送的訊息

內嵌iframe的HTML網頁若想要收iFrame傳送過來的訊息,可以依樣畫葫蘆,註冊message事件,完整程式碼如下所示,在DOMContentLoaded事件處理程式中,利用window.addEventListener註冊了message事件,在事件發生時,交由myMessageHandler函數來處理,若origin相同,則將訊息從event.Data取出,顯示在畫面上。

<!DOCTYPE html>

<html xmlns = "http://www.w3.org/1999/xhtml">

<head>

<meta http-equiv = "Content-Type" content = "text/html; charset=utf-8" />

<title> </title>

<script type = "text/javascript">

var txt;

function contentLoaded () {

if ( typeof ( window.postMessage ) !== "undefined" ) {

document.getElementById( "result" ).innerHTML = "您的瀏覽器支援postMessage功能<br>";

}

else {

document.getElementById( "result" ).innerHTML = "您的瀏覽器不支援postMessage功能<br>";

return;

}

window.addEventListener( "message", myMessageHandler );

document.getElementById( "Button1" ).addEventListener( "click", Button1_Click, false );

}

function myMessageHandler( event ) {

if ( event.origin === "http://localhost:7650" ) {

txt = event.data;

document.getElementById( "result" ).innerHTML += txt + "<br>";

}

}

function Button1_Click() {

txt = document.getElementById( 'Text1' ).value;

document.getElementById( 'MyiFrame' ).contentWindow.postMessage( txt, "http://localhost:7650/MyiFrame.html" );

document.getElementById( "result" ).innerHTML += "送出訊息 : " + txt + "<br>";

}

window.addEventListener( "DOMContentLoaded", contentLoaded, false );

</script>

<style type = "text/css">

#Parent {

display: -moz-box;

display: -ms-box;

display: -webkit-box;

width: 100%;

position: absolute;

bottom: 0;

left:0;

}

#Child1 {

-moz-box-flex: 1;

-ms-box-flex: 1;

-webkit-box-flex: 1;

width: 100px;

background-color: #ffd800;

}

#Child2 {

-moz-box-flex: 3;

-ms-box-flex: 3;

-webkit-box-flex: 3;

width: 200px;

background-color: #00ff21;

}

</style>

</head>

<body>

<div id = "result"> </div>

<div id = "Parent">

<div id = "Child1">

<input id = "Text1" type = "text" />

<input id = "Button1" type = "button" value = "Send Message:" />

</div>

<div id = "Child2">

<iframe id = "MyiFrame" src = "http://localhost:7650/MyiFrame.html"

style = "overflow: hidden; height: 100%; width: 100%; border: 0px" />

</div>

</div>

</body>

</html>

 

範例程式執行的結果請參考圖3所示,當你在畫面上文字方塊輸入「Hi」,按下「Send Message:」按鈕,iFrame將收到訊息,並將收到的訊息顯示在畫面上,隨即iFrame回應一個訊息,顯示在網頁上方:

clip_image003

圖 3:使用postMessage交換訊息。

 

跨網站交換資料

我們也可以利用postMessage API在不同的網站之間交換資料,舉例來說,將MyiFrame.html放到一台名稱為purple機器上的IIS伺服器test虛擬目錄下執行,其url為:

參考以下程式列表,修改HTML網頁內容,指定iFrame的src為「http://purple/test/MyiFrame.html」,叫用postMessage時,第二個targetOrigin參數設定為「"http://purple/test/MyiFrame.html"」,然後接收iFrame訊息的myMessageHandler改成判斷event.origin是否等於「http://purple」,這樣程式一樣可以運作,而且跨網站運行:

<!DOCTYPE html>

<html xmlns = "http://www.w3.org/1999/xhtml">

<head>

<meta http-equiv = "Content-Type" content = "text/html; charset=utf-8" />

<title> Main </title>

<script type = "text/javascript">

var txt;

function contentLoaded () {

if ( typeof ( window.postMessage ) !== "undefined" ) {

document.getElementById( "result" ).innerHTML = "您的瀏覽器支援postMessage功能<br>";

}

else {

document.getElementById( "result" ).innerHTML = "您的瀏覽器不支援postMessage功能<br>";

return;

}

window.addEventListener( "message", myMessageHandler );

document.getElementById( "Button1" ).addEventListener( "click", Button1_Click, false );

}

function myMessageHandler( event ) {

if ( event.origin === "http://purple" ) {

txt = event.data;

document.getElementById( "result" ).innerHTML += txt + "<br>";

}

}

function Button1_Click() {

txt = document.getElementById( 'Text1' ).value;

document.getElementById( 'MyiFrame' ).contentWindow.postMessage( txt, "http://purple/test/MyiFrame.html" );

document.getElementById( "result" ).innerHTML += "送出訊息 : " + txt + "<br>";

}

window.addEventListener( "DOMContentLoaded", contentLoaded, false );

</script>

<style type = "text/css">

#Parent {

display: -moz-box;

display: -ms-box;

display: -webkit-box;

width: 100%;

position: absolute;

bottom: 0;

left:0;

}

#Child1 {

-moz-box-flex: 1;

-ms-box-flex: 1;

-webkit-box-flex: 1;

width: 100px;

background-color: #ffd800;

}

#Child2 {

-moz-box-flex: 3;

-ms-box-flex: 3;

-webkit-box-flex: 3;

width: 200px;

background-color: #00ff21;

}

</style>

</head>

<body>

<div id = "result"> </div>

<div id = "Parent">

<div id = "Child1">

<input id = "Text1" type = "text" />

<input id = "Button1" type = "button" value = "Send Message:" />

</div>

<div id = "Child2">

<iframe id = "MyiFrame" src = "http://purple/test/MyiFrame.html"

style = "overflow: hidden; height: 100%; width: 100%; border: 0px" />

</div>

</div>

</body>

</html>

 

Server-Sent Event

網頁應用程式通常都是由使用者從瀏覽器輸入想連結的網站url來開始一個http請求,伺服端收到請求之後,才會回應執行的結果。而HTML5新增一個機制,Server-Sent Event,能由伺服端透過HTTP推撥通知到用戶端的瀏覽器,這機制非常適合想要主動通知用戶端的程式,例如及時更新拍賣貨物的最新價格,或股票最新價格、最新及時新聞等等。Server-Sent Event支援單向訊息通知,從伺服端傳送訊息到用戶端。Server-Sent Event可以讓用戶端與伺服端建立一個連線 ,伺服端可以隨時傳送新訊息給用戶端,不用再重新建立兩者之間的連線。

 

判斷是否支援Server-Sent Event

要使用到Server-Sent Event之前,先測試一下瀏覽器是否支援此功能,對於Chrome、Firefox、Safari與Opera瀏覽器來說大多不是問題,但傷腦筋的是,ie10瀏覽器卻不支援Server-Sent Event。以下範例程式碼,使用JavaScript判斷瀏覽器是否支援Server-Sent Event,在DOMContentLoaded事件中利用「typeof ( EventSource ) !== "undefined"」語法來判斷是否支援:

<!DOCTYPE html>
<html xmlns = "http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv = "Content-Type" content = "text/html; charset=utf-8" />
  <title> </title>
  <script type = "text/javascript">
    function contentLoaded () {
      if ( typeof ( EventSource ) !== "undefined" ) {
        document.getElementById( "result" ).innerHTML = "您的瀏覽器支援Server-Sent Event功能<br>";
      }
      else {
        document.getElementById( "result" ).innerHTML = "您的瀏覽器不支援Server-Sent Event功能<br>";
        return;
      }
    }
    window.addEventListener( "DOMContentLoaded", contentLoaded, false );
  </script>
</head>
<body>
  <div id = "result"> </div>
</body>
</html>

 

EventSource物件

要使用Server Sent Event,我們可以利用EventSource物件與伺服端的程式建立連線,參考以下HTML範例程式:

<!DOCTYPE html>
<html xmlns = "http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv = "Content-Type" content = "text/html; charset=utf-8" />
  <title> </title>
  <script type = "text/javascript">
    function contentLoaded() {
      if ( typeof ( EventSource ) !== "undefined" ) {
        document.getElementById( "result" ).innerHTML = "您的瀏覽器支援Server-Sent Event功能<br>";
      }
      else {
        document.getElementById( "result" ).innerHTML = "您的瀏覽器不支援Server-Sent Event功能<br>";
        return;
      }
      var source = new EventSource( "ESServercode.aspx" );
      source.onopen = function ( event ) {
        document.getElementById( 'result' ).innerHTML += '開啟連線!<br>';
      };
    }
    window.addEventListener( "DOMContentLoaded", contentLoaded, false );
  </script>
</head>
<body>
  <div id = "result"> </div>
</body>
</html>

 

範例中EventSource物件的建構函式傳入一個URL參數來指明要連結到伺服端的ASP.NET網頁,ESServercode.aspx。接著我們註冊open事件,這個事件會在用戶端瀏覽器連結到伺服端的ESServercode.aspx程式時觸發。

接著我們來談談伺服端的程式碼該如何和用戶端的EventSource溝通。首先伺服端的程式碼,可以使用ASP.NET、ASP、PHP…等等網站開發技術來設計,在這個範例中,我們選擇的是ASP.NET,在伺服端的程式碼內,需要將MIME Type設定為「text/event-stream」,伺服端的ESServercode.aspx程式如下:

<%@ Page Language = "C#" %>
<%
  Response.ContentType = "text/event-stream";
%> 

執行網頁程式,建立連線之後,每隔一段時間,伺服器會定期傳送訊息到用戶端,請參考圖4所示:

clip_image004

圖 4:利用EventSource物件與伺服端的程式建立連線。

 

註冊message事件

用戶端HTML網頁的寫法也不難,每回伺服端推播訊息到用戶端時就會觸發EventSource 物件的message事件,只要註冊此事件,從事件物件的data屬性就可以取得伺服端傳送過來的訊息,參考以下HTML範例程式碼:

<!DOCTYPE html>
<html xmlns = "http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv = "Content-Type" content = "text/html; charset=utf-8" />
  <title> </title>
  <script type = "text/javascript">
    function contentLoaded () {
      if ( typeof ( EventSource ) !== "undefined" ) {
        document.getElementById( "result" ).innerHTML = "您的瀏覽器支援Server-Sent Event功能<br>";
      }
      else {
        document.getElementById( "result" ).innerHTML = "您的瀏覽器不支援Server-Sent Event功能<br>";
        return;
      }
      var source = new EventSource( "ESServercode.aspx" );
      source.onopen = function ( event ) {
        document.getElementById( 'result' ).innerHTML += '開啟連線!<br>';
      };
      source.onmessage = function ( event ) {
        document.getElementById( "result" ).innerHTML += event.data + "<br />";
      };
    }
    window.addEventListener( "DOMContentLoaded", contentLoaded, false );
  </script>
</head>
<body>
  <div id = "result"> </div>
</body>
</html>

 

改寫伺服端的ESServercode.aspx程式碼如下:

<%@ Page Language = "C#" %>
<%
  Response.ContentType = "text/event-stream";
  Response.Buffer = false;
  Response.Expires = -1;
  Response.Write ("data: " + DateTime.Now.ToLongTimeString ());
  Response.Write ("\n\n");
  Response.Flush ();
  System.Threading.Thread.Sleep ( 5000 );
  Response.End (); 
%> 

伺服端的程式碼先送出MIME Type:「text/event-stream」,接著將Response.Buffer設為「false」關閉Buffer,讓訊息馬上送出。將Response .Expires設定為「-1」表示瀏覽器的Cache會馬上失效。

伺服端要傳送訊息時 ,訊息有特殊格式,每一個訊息都要以「data:」字串開頭,再加上想要傳送的訊息,訊息結尾要加一個換行符號,例如以下範例中的ASP.NET (C#)程式一樣:

Response.Write( "data: 下午 02:27:39\n" );

若送出兩個換行符號,則代表訊息結束:

Response.Write ( "\n\n" );

Response.Flush可將緩衝中的訊息馬上送出,接著我們利用System.Threading.Thread.Sleep方法暫停5000毫秒。最後利用Response.End方法結束執行。

HTML範例程式執行結果,請參考下圖所示。

clip_image005

圖 5:定期接收訊息。

 

訊息格式

我們再回頭來討論一下訊息格式,除了上述的每一個訊息都要以「data:」字串開頭,串接想要傳送的訊息與訊息結尾要加一個換行符號規則之外,訊息也可以多行,例如以下程式片段,也可以一次送出多個訊息:

Response.Write("data: 下午 02:27:39\n");
Response.Write ("data: mary\n");

你也可以傳送JSON格式的資料,例如以下程式片段,送出JSON格式字串:「{"empidid":"1", "name":"mary"}」。

Response.Write ("data: {\"empid\":\"1\", \"name\":\"mary\"}");

而用戶端只需要在message事件處理常式中,從event.data取出JSON格式字串,再利用JSON.parse方法把字串還原成物件,就可以利用屬性的語法取出empid與name的值:

source.onmessage = function ( event ) {
        var d = JSON.parse( event.data );
        document.getElementById( "result" ).innerHTML += "empid=" + d.empid + " ,name=" + d.name + "<br />";
      };

除了傳送訊息到用戶端之外,伺服器也可以傳送訊息代號(id)與重試的時間間隔(retry),例如以下程式片段,送出id為「100」;retry為「5000」,接著再送出JSON格式的資料:

Response.Write ("id:100\n");
Response.Write ("retry:5000\n");
Response.Write ("data: {\"empid\":\"1\", \"name\":\"mary\"}");

若用戶端因故與伺服端斷線,用戶端瀏覽器會在 5000毫秒後重試連線,並傳送100代號到伺服端,以利伺服端識別。

 

停止接收訊息

用戶端瀏覽器若不想要繼續接收伺服端的訊息,可以叫用EventSource物件的close方法結束連線,例如在上述網頁之中,加入一個按鈕:

<input id = "Button1" type = "button" value = "停止接收訊息" />

然後在HTML網頁中利用以下JavaScript註冊click事件,叫用EventSource物件的close方法停止接收訊息:

window.addEventListener( "click", function () {
      source.close();
      document.getElementById( "result" ).innerHTML += "已停止接收訊息";
    }, false );

 

錯誤處理

若實作Server- Sent Event網頁執行發生錯誤,則會觸發EventSource的error事件,我們可以攔截這個事件,範例如下所示,當事件發生時,先判斷EventSource目前的狀態是否為「CLOSED」,若為是,則顯示「關閉連線!」的訊息:

<!DOCTYPE html>
<html xmlns = "http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv = "Content-Type" content = "text/html; charset=utf-8" />
  <title> </title>
  <script type="text/javascript">
    function contentLoaded() {
      if ( typeof ( EventSource ) !== "undefined" ) {
        document.getElementById( "result" ).innerHTML = "您的瀏覽器支援Server-Sent Event功能<br>";
      }
      else {
        document.getElementById( "result" ).innerHTML = "您的瀏覽器不支援Server-Sent Event功能<br>";
        return;
      }
      var source = new EventSource( "ESServercode.aspx" );
      source.onopen = function ( event ) {
        document.getElementById( 'result' ).innerHTML += '開啟連線!<br>';
      };
      source.onmessage = function ( event ) {
          document.getElementById( "result" ).innerHTML += event.data + "<br />";
        };
        source.onerror = function ( event ) {
          if ( event.eventPhase == EventSource.CLOSED ) {
            document.getElementById( 'result' ).innerHTML += '關閉連線!<br>';
          }
        }
      }
    window.addEventListener( "DOMContentLoaded", contentLoaded, false );
  </script>
</head>
<body>
  <div id = "result"> </div>
</body>
</html>

最後修改一下伺服端的ESServercode.aspx程式碼,將Response.ContentType這行程式註解,模擬錯誤,再執行網頁測試,一執行網頁就會顯示錯誤訊息:

<%@ Page Language="C#" %>
<%
  //Response.ContentType = "text/event-stream";
  Response.Buffer = false;
  Response.Expires = -1;
  Response.Write ("data: " + DateTime.Now.ToLongTimeString ());
  Response.Write ("\n\n");
  Response.Flush ();
  System.Threading.Thread.Sleep (5000);
  Response.End (); 
%> 


總結

若用戶端瀏覽器執行在行動裝置上,使用Server-Sent Event可以節省行動裝置的電源。

Server-Sent Event支援從伺服端傳送訊息到用戶端的單向訊息通知功能,若想要使用雙向的訊息溝通,可以選擇使用Web Socket API。

 

參考資料

  • Server-sent events

http://www.whatwg.org/specs/web-apps/current-work/multipage/comms.html#server-sent-events

  • Cross-document messaging

http://www.whatwg.org/specs/web-apps/current-work/multipage/web-messaging.html#web-messaging

Tags:

.NET Magazine國際中文電子雜誌 | HTML | 許薰尹Vivid Hsu

新增評論




  Country flag
biuquote
  • 評論
  • 線上預覽
Loading






NET Magazine國際中文電子雜誌

NET Magazine國際中文電子版雜誌,由恆逸資訊創立於2000,自發刊日起迄今已發行超過500篇.NET相關技術文章,擁有超過40000名註冊讀者群。NET Magazine國際中文電子版雜誌希望藉於電子雜誌與NET Developer達到共同學習與技術新知分享,歡迎每一位對.NET 技術有興趣的朋友們多多支持本雜誌,讓作者群們可以有持續性的動力繼續爬文。<請加入免費訂閱>

月分類Month List