Chapter 18. Communication
18.0. Introduction
This book has explored a lot of subjects, but the functionality that revolutionized web client development is (arguably) Ajax. With Ajax, we’re no longer dependent on the slow, cumbersome round-trips and associated page load times that plagued early websites. Now we can make an Ajax call, get some data, update the page—sometimes without the user even being aware that the activity is happening.
Ajax is also a relatively uncomplicated functionality, at least compared to other JavaScript functionality I’ve covered in the book. The main steps to an Ajax application are:
Prepare the server-side API call.
Make the call.
Process the result.
Of course, there are interesting challenges that can occur at any time during these three steps, but for a basic application, it really is just that simple.
Ajax is now joined by a new communication kid: the postMessage
. This new functionality originated
with HTML5, though it has since split off to its own specification. It’s
an uncomplicated functionality that allows for easy communication between
a parent and child window, even if the child window is located in another
domain.
Note
There are two other new communication APIs in work: Cross Origin Resource Sharing (CORS) and the Web Sockets API. Both are being developed in the W3C, and both are currently in Working Draft state: CORS at http://www.w3.org/TR/access-control/ and Web Sockets at http://dev.w3.org/html5/websockets/.
CORS is a way of doing cross-domain Ajax calls, and is currently implemented in Firefox 3.5 and up, and Safari 4.x and up. The Web Sockets API is a bidirectional communication mechanism, implemented only in Chrome at this time.
18.1. Accessing the XMLHttpRequest Object
Solution
If you’re not concerned about support for IE6, you can use the following:
var xmlHttp = new XMLHttpRequest();
This works with all of the target browsers for this book, and is the only method I used in examples in the book. However, if you must still support IE6 and you’re not using one of the JavaScript libraries, you’ll need to use the following cross-browser code:
if (window.XMLHttpRequest) { xmlHttp = new XMLHttpRequest(); } else if (window.ActiveXObject) { xmlHttp = new ActiveXObject("Microsoft.XMLHTTP"); }
Discussion
Microsoft invented the XMLHttpRequest
object as an ActiveX object.
However, the XMLHttpRequest
object
that we know and primarily use today, even in newer versions of IE,
evolved independently. There’s now an effort underway to standardize the
object within the W3C.
The XMLHttpRequest
object isn’t
very complicated. Here are the supported client application methods,
which are explored in more depth in the other recipes in this
chapter:
open
Initializes a request. Parameters include the method (GET or POST), the request URL, whether the request is asynchronous, and a possible username and password. By default, all requests are sent asynchronously.
setRequestHeader
send
sendAsBinary
abort
getResponseHeader
Retrieves the header text, or
null
if the response hasn’t been returned yet or there is no header.getAllResponseHeaders
All of our target browsers support the methods just listed.
There’s also an additional, frequently used method, overrideMimeType
, not
included in the list. I didn’t include it because it’s not part of the
XMLHttpRequest
standardization
process, and one browser company doesn’t support it (Microsoft).
The overrideMimeType
method is
normally used to override the MIME type of the server response., As an
example, the following overrides whatever the server’s response is, and
the returned resource is treated as XML:
xmlhttp.overrideMimeType('text/xml');
Lack of support for overrideMimeType
is an inconvenience but not a
showstopper. Either we’ll need to process the data according to MIME
type or ensure that our server applications set the proper content
header for the data. For instance, if we want to ensure that our client
application receives data as XML, we can use the following in a PHP
application:
header("Content-Type: text/xml; charset=utf-8");
There are also a number of properties supported by all browsers:
There are other properties, some proprietary and some not, but these are the ones we’re concerned with in this book.
A major restriction associated with the XMLHttpRequest
object is the
same-origin security restriction. This means that you can’t use XMLHttpRequest
to make a service request to an
API in another domain.
See Also
Recipe 18.7
provides a solution and a discussion related to the same-origin
restriction with XMLHttpRequest
. The
W3C XMLHttpRequest
draft
specification can be found at http://www.w3.org/TR/XMLHttpRequest/.
18.2. Preparing the Data for Transmission
Problem
You want to process form data for an Ajax call rather than send the data via the usual form submit process.
Solution
Access the data from form fields or other page elements:
var state = document.getElementById("state").value;
If the data is user-supplied, such as the data from a text field,
encode the result using the encodeURIComponent
function, so any characters in the text that could impact on the Ajax
call are escaped:
var state = encodeURIComponent(document.getElementById("state").value);
You shouldn’t need to escape data from radio buttons, checkboxes, selections, or any other form element in which your application controls the data, as you can make sure those values are in the proper format.
Discussion
Depending on the type of action, data for an Ajax call can come
from user-supplied text, such as that typed into a text field. When it
is, you’ll need to ensure that the data can be used in the Ajax request
by escaping certain characters, such as an ampersand (&
), plus sign (+
), and equal sign (=
).
The following string:
This is $value3 @value &and ** ++ another
Is encoded as:
This%20is%20%24value3%20%40value%20%26and%20**%20%2B%2B%20another
The spaces are escaped as %20
,
the dollar sign as %24
, the ampersand
as %26
, and the plus sign as %2B
, but the alphanumeric characters and the
reserved asterisk characters are left alone.
Once escaped, the data can be attached as part of an Ajax request.
If the Ajax request is going to be a POST rather than a GET request,
further encoding is needed—the spaces should be encoded as pluses.
Replace the %20
with +
, following the call to encode
URI
Component
.
You can package this functionality for reuse:
function postEncodeURIComponent(str) { str=encodeURIComponent(str); return str.replace(/%20/g,"+"); }
The escaping ensures that the Ajax request will be successfully communicated, but it doesn’t ensure that the Ajax request is safe. All data input by unknown persons should always be scrubbed to prevent SQL injection or cross-site scripting (XSS) attacks. However, this type of security should be implemented in the server-side application, because if people can put together a GET request in JavaScript, they can put together a GET request directly in a browser’s location bar and bypass the script altogether. The only time you need to scrub the input in JavaScript is if you’re planning on embedding a user’s data directly back into the page.
18.3. Determining the Type of Query Call
Solution
For an update, send a POST request. Set the first parameter in the
XMLHttpRequest
open method to POST,
call the setRequestHeader
to set the content-type, and send the request parameters
in the send
method:
xmlhttp.open('POST',url,true); xmlhttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xmlhttp.send(param);
For a query, send a GET request. The first parameter in the
XMLHttpRequest
open method is set to
GET, the parameters are appended on to the URL for the request, and
null
is passed as parameter in the
send
method:
url = url + "?" + param; xmlhttp.open('GET',url,true); xmlhttp.send(null);
Discussion
What the server component of the application is expecting has the most impact on your request type decision. However, accepted practice is that a request for data should be made with a GET, while an update should occur through a POST.
The request type practice used in Ajax is derived from RESTful guidelines (REST translates as REpresentational State Transfer). According to the guidelines, there are four types of HTTP requests:
- GET
Used for retrieving information, with parameters attached to the URL.
- POST
Used for creating new data, and parameters are sent via function parameter.
- DELETE
Used for deleting data records, and parameters are sent via function parameter.
- PUT
Used to send an update, and parameters are sent via function parameter.
There is broad support only for the GET and POST requests at this time, so I’ll focus on these two.
A GET HTTP request is used to retrieve information, and the parameters are attached to the URL in the request. A function that processes a GET request could look like the following, which passes two parameters with the request:
function sendData(evt) { // cancel default form submittal evt = evt || window.event; evt.preventDefault(); evt.returnValue = false; // get input data var one = encodeURIComponent(document.getElementById("one").value); var two = encodeURIComponent(document.getElementById("two").value); var params = "one=" + one + "&two=" + two; // prep request if (!http) { http = new XMLHttpRequest(); } var url = "ajaxserver.php?" + params; http.open("GET", url, true) // callback function http.onreadystatechange=processResult; // make Ajax call with params http.send(null); }
In the code snippet, two parameters are passed with the request.
First, they’re escaped, using encodeURIComponent
. Next, they’re attached to
the URL using RESTful GET notation:
http://somecompany.com?param=value¶m2=value2
The parameters for the XMLHttpRequest
open method are:
- GET
A GET request
- url
The URL for the service
true
Whether the operation is performed asynchronously
The optional third asynchronous parameter should always be set to
true
, or the page blocks until the
server request returns. This is not a cool thing to do to your page
readers.
There are two other optional parameters not shown:
username
and password
. If the
application is protected on the server side, the username and password
can be used for authentication.
A POST request with two parameters would look like the following:
function sendData(evt) { // cancel default form submittal evt = evt || window.event; evt.preventDefault(); evt.returnValue = false; // get input data var one = encodeURIComponent(document.getElementById("one").value). replace(/%20/g,'+'); var two = encodeURIComponent(document.getElementById("two").value). replace(/%20/g,'+'); var params = "one=" + one + "&two=" + two; // prep request if (!http) { http = new XMLHttpRequest(); } var url = "ajaxserver.php"; http.open("POST", url, true) // set up Ajax headers http.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); http.setRequestHeader("Content-length", params.length); http.setRequestHeader("Connection", "close"); // callback function http.onreadystatechange=processResult; // make Ajax call with params http.send(params); }
This code differs from the code for the GET request in several
ways. The first is that after encoding, the spaces in the parameters are
converted from %20
to +
. The second is they’re concatenated into a
parameter-formatted string, which is sent within the send
method.
The first parameter of the open method is set to POST
, but the other two parameters are the
same as those sent with the GET request: the application URL and the
asynchronous flag.
Additional calls are made to the setRequestHeader
method, to set Connection
and Content-length
request headers. You can
use it to send any HTTP request header, but you must provide the
Content-Type
for the POST—in this case, a multipart
form request.
Both of the request approaches set the callback function for the
Ajax object call’s onreadystatechange
event handler.
See Also
Recipe 18.1 covers how
to get the XMLHttpRequest
object, and
Recipe 18.2 covers how to
encode parameters.
18.4. Adding a Callback Function to an Ajax Request
Problem
You want to process the result of an Ajax request, even if the result does an update rather than a query.
Solution
When processing the Ajax request, before calling the XMLHttpRequest
object’s send
method, assign the object’s onreadystatechange
property to the callback function’s name:
xmlhttp.open("GET", url, true); xmlhttp.onreadystatechange=callbackFunction; xmlhttp.send(null);
Discussion
The readyState
attribute for the XMLHttpRequest
object is updated based on the
state of the request. In order to check the state of the request, you
need to assign the onready
state
change
event
handler to a callback function that is called every time the ready state
changes.
You should use onreadystatechange
with every Ajax call, even
one in which you’re doing an update rather than processing a request.
Without the callback function, you have no way of knowing if the update
operation was successful, and if not, what kind of error
occurred.
The readyState
property has the
values shown in Table 18-1 during the Ajax
request.
Value | STATE | Purpose |
0 | UNINITIALIZED |
|
1 | LOADING |
|
2 | LOADED |
|
3 | INTERACTIVE | Downloading response is not completed |
4 | COMPLETED | Request is complete |
You should not set onreadystatechange
if your request is
synchronous, because the code won’t continue until the request returns
anyway—you can check the result of the operation in the very next line
of the code, if you’re so inclined.
See Also
Recipe 18.5 covers how to check the status and results of an Ajax request.
18.5. Checking for an Error Condition
Solution
In addition to checking the readyState
property in the onreadystatechange
event handler, you can also
check the XMLHttpRequest
’s
status:
function processResult() { if (http.readyState == 4 && http.status == 200) { document.getElementById("result").innerHTML=http.responseText; } }
Discussion
The XMLHttpRequest
’s status
property is where the response to the request is
returned. You hope for a value of 200, which means the request is
successful. If the request is something else, such as 403 (forbidden) or
500 (server error), you can access the XMLHttpRequest
’s statusText
property to
get more detailed information:
function processResult() { if (http.readyState == 4 && http.status == 200) { document.getElementById("result").innerHTML=http.responseText; } else { alert(http.statusText); } }
18.6. Processing a Text Result
Solution
If you trust the server application, and the text is formatted to
use immediately in the page, the simplest approach is to assign the text
to the innerHTML
property of the element where it should be placed:
function processResult() { if (http.readyState == 4 && http.status == 200) { document.getElementById("result").innerHTML=http.responseText; } }
Discussion
If you’re writing both the server and client sides of the
application, why make it hard on yourself? Format the response in the
server application in such a way that it can be added into the web page
using the easiest possible approach, and nothing is easier than innerHTML
.
However, if the response text isn’t formatted for immediate
publication, you’ll have to use String
functions, possibly in combination with
regular expressions, to extract the data you need. If HTML formatting
isn’t possible or desirable, and you have any control of the server
application, try to format it either as XML or JSON, both of which can
be more easily supported in the JavaScript environment.
See Also
See Recipe 19.1
for extracting information from an XML response. See Recipe 19.4 and Recipe 19.5 for converting
JSON formatted text into a JavaScript object. See Recipe 12.1 for an
introduction to innerHTML
.
18.7. Making an Ajax Request to Another Domain (Using JSONP)
Problem
You want to query for data using a web service API, such as the Netflix API or Twitter’s API. However, the Ajax same-origin policy prevents cross-domain communication.
Solution
The most commonly used technique to solve the cross-domain problem, and the approach I recommend, is to create a server-side proxy application that is called in the Ajax application. The proxy then makes the call to the other service’s API, returning the result to the client application. This is safe, secure, and very efficient, because we can clean the data before returning it to the Ajax application.
There is another approach: use JSONP (JSON, or JavaScript Object Notation, with Padding) to workaround the security issues. I once used this approach to create a mashup between Google Maps and a Flickr query result:
function addScript( url) { var script = document.createElement('script'); script.type="text/javascript"; script.src = url; document.getElementsByTagName('head')[0].appendChild(script);
The URL looked like the following, including a request to return the data formatted as JSON, and providing a callback function name:
http://api.flickr.com/services/rest/?method=flickr.photos.search&user_id=xxx&api_ke y=xxx&format=json& jsoncallback=processPhotos
When the script
tag is created,
the request to Flickr is made, and since I passed in the request for a
JSON formatted result and provided a callback function name, that’s how
the return was provided. The callback string is similar to the
following:
// assign photos globally, call first to load function processPhotos(obj) { photos = obj.photos.photo; ... }
The callback function would pass the data formatted as a JSON object in the function argument.
Discussion
Ajax works within a protected environment that ensures we don’t end up embedding dangerous text or code into a web page because of a call to an external application (which may or may not be secure).
The downside to this security is that we can’t directly access services to external APIs, such Flickr, Twitter, and Google. Instead, we need to create a server-side proxy application, because server applications don’t face the cross-domain restriction.
The workaround is to use something like JSONP, demonstrated in the
solution. Instead of using XMLHttpRequest
, we convert the request URL
into one that we can attach to a script’s src
attribute, because the script
element does not follow the same-origin
policy. It couldn’t, or we
wouldn’t be able to embed applications such as Google Maps into the
application.
If the service is amenable, it returns the data formatted as JSON, even wrapping it in a callback function. When the script is created, it’s no different than if the function call is made directly in our code, and we’ve passed an object as a parameter. We don’t even have to worry about converting the string to a JavaScript object.
It’s a clever trick, but I don’t recommend it. Even with secure services such as Flickr and Twitter, there is the remote possibility that someone could find a way to inject JavaScript into the data via the client-side application for the service, which can cause havoc in our own applications.
It’s better to be smart then clever. Use a proxy application, scrub the result, and then pass it on to your client application.
See Also
Chapter 19 covers JSON in more detail.
18.8. Populating a Selection List from the Server
Problem
Based on a user’s actions with another form element, you want to populate a selection list with values.
Solution
Capture the change event for the trigger form element:
document.getElementById("nicething").onchange=populateSelect;
In the event handler function, make an Ajax call with the form data:
var url = "nicething.php?nicething=" + value; xmlhttp.open('GET', url, true); xmlhttp.onreadystatechange = getThings; xmlhttp.send(null);
In the Ajax result function, populate the selection list:
if(xmlhttp.readyState == 4 && xmlhttp.status == 200) { var select = document.getElementById("nicestuff"); select.length=0; var nicethings = xmlhttp.responseText.split(","); for (var i = 0; i < nicethings.length; i++) { select.options[select.length] = new Option(nicethings[i],nicethings[i]); } select.style.display="block"; } else if (xmlhttp.readyState == 4 && xmlhttp.status != 200) { document.getElementById('nicestuff').innerHTML = 'Error: Search Failed!'; }
Discussion
One of the more common forms of Ajax is to populate a select
or other form element based on a choice
made by the user. Rather than have to populate a select
element with many options, or build a
set of 10 or 20 radio buttons, you can capture the user’s choice in
another form element, query a server application based on the value, and
then build the other form elements based on the value—all without
leaving the page.
Example 18-1
demonstrates a simple page that captures the change event for radio
buttons within a fieldset
element,
makes an Ajax query with the value of the selected radio button, and
populates a selection list by parsing the returned option list. A comma
separates each of the option items, and new options are created with the
returned text having both an option label and option value. Before
populating the select
element, its
length is set to 0. This is a quick and easy way to truncate the
select
element—removing all existing options, and
starting fresh.
<!DOCTYPE html> <head> <title>On Demand Select</title> <style> #nicestuff { display: none; margin: 10px 0; } #nicething { width: 400px; } </style> <script> var xmlhttp; function populateSelect() { var value; var inputs = this.getElementsByTagName('input'); for (var i = 0; i < inputs.length; i++) { if (inputs[i].checked) { value = inputs[i].value; break; } } // prepare request if (!xmlhttp) { xmlhttp = new XMLHttpRequest(); } var url = "nicething.php?nicething=" + value; xmlhttp.open('GET', url, true); xmlhttp.onreadystatechange = getThings; xmlhttp.send(null); } // process return function getThings() { if(xmlhttp.readyState == 4 && xmlhttp.status == 200) { var select = document.getElementById("nicestuff"); select.length=0; var nicethings = xmlhttp.responseText.split(","); for (var i = 0; i < nicethings.length; i++) { select.options[select.length] = new Option(nicethings[i], nicethings[i]); } select.style.display="block"; } else if (xmlhttp.readyState == 4 && xmlhttp.status != 200) { alert("No items returned for request"); } } window.onload=function() { document.getElementById("submitbutton").style.display="none"; document.getElementById("nicething").onclick=populateSelect; } </script> </head> <body> <form action="backupprogram.php" method="get"> <p>Select one:</p> <fieldset id="nicething"> <input type="radio" name="nicethings" value="bird" /><label for="bird">Birds</label><br /> <input type="radio" name="nicethings" value="flower" /><label for="flower">Flowers</label><br /> <input type="radio" name="nicethings" value="sweets" /><label for="sweets">Sweets</label><br /> <input type="radio" name="nicethings" value="cuddles" /> <label for="cuddles">Cute Critters</label> </fieldset> <input type="submit" id="submitbutton" value="get nice things" /> <select id="nicestuff"></select> </body>
The form does have an assigned action
page, and a submit button that’s hidden
when the script is first run. These are the backup if scripting is
turned off.
18.9. Using a Timer to Automatically Update the Page with Fresh Data
Solution
Use Ajax and a timer to periodically check the file for new values and update the display accordingly.
The Ajax we use is no different than any other Ajax request. We’ll
use a GET, because we’re retrieving data. We put together the request,
attach a function to the onready
state
change
event
handler, and send the request:
var url = "updatedtextfile.txt"; xmlhttp.open("GET", url, true); xmlhttp.onreadystatechange=updateList; xmlhttp.send(null);
The fact that we’re doing a direct request on a static text file might be new, but remember that a GET request is more or less the same as the requests we put into the location bar of our browsers. If something works in the browser, it should successfully return in an Ajax GET request...within reason.
In the code that processes the response, we just place the new
text into a new unordered list item and append it to an existing
ul
element:
// process return function processResponse() { if(xmlhttp.readyState == 4 && xmlhttp.status == 200) { var li = document.createElement("li"); var txt = document.createTextNode(xmlhttp.responseText); li.appendChild(txt); document.getElementById("update").appendChild(li); } else if (xmlhttp.readyState == 4 && xmlhttp.status != 200) { alert(xmlhttp.responseText); } }
The new part is the timer. The timer is controlled with start and stop buttons. When the start button is clicked the first time, the code disables the start button first, initiates the first Ajax call, and then starts a timer. There’s a reason for this, as we’ll see in a second:
// timer function startTimer() { populateList(); timer=setTimeout(timerEvent,15000); }
The reason we want to make the Ajax call first is that the timer
function timerEvent
checks the
readyState
of the XMLHttpRequest
object when it’s invoked. If
the value is not 4, which means the last request was completed, it
doesn’t make another Ajax call. We don’t want to have multiple requests
out at the same time:
function timerEvent() { if (xmlhttp.readyState == 4) { populateList(); } timer=setTimeout(timerEvent, 15000); }
Lastly, we’ll add a cancel timer event. In this case we’re using a global timer variable, but in a production application we want to use either an anonymous function to wrap everything, or create an object literal to maintain both data and methods:
function stopTimer() { clearTimeout(timer); }
Discussion
The key to using timers with Ajax calls is to make sure that the
last call is completed before making the next. By including a check on
the XMLHttpRequest
object’s readyState
property, if the value isn’t 4, we know to skip this Ajax
call and just reset the timer for the next go round. We can also put in
a check for the request status, and cancel the timer event altogether if
we’re concerned about hitting a failing service, over and over
again.
When I ran the application that included the solution code, I
changed the text file by using the Unix echo
command:
$ echo "This is working" > text.txt
And then watched as the text showed up on the page, as shown in Figure 18-1.
If you’re planning on using this form of polling with another service, such as against the Twitter API, be aware that if you’re considered abusive of the service, you may get kicked off. Check to see if there are restrictions for how often you can access a service using the API.
Note
Depending on the browser, you may run into caching issues if you access the text.txt file locally. Providing a full URL should prevent this from occurring.
A few years ago, there was interest in a push rather than pull
type of Ajax communication. Encompassed under the coined term of
Comet, the concept was that the
server would initiate the communication and push the data to the client,
rather than the client pulling the data from the server. Eventually, the
concept led to work in the W3C on a new JavaScript API called WebSockets. Currently only implemented in Chrome,
WebSockets enables bidirectional communication between server and client
by using the send
method on the
WebSocket object for communicating to the server, and then attaching a
function to WebSocket’s onmessage
event handler
to get messages back from the server, as demonstrated in the following
code from the Chromium Blog:
if ("WebSocket" in window) { var ws = new WebSocket("ws://example.com/service"); ws.onopen = function() { // Web Socket is connected. You can send data by send() method. ws.send("message to send"); .... }; ws.onmessage = function (evt) { var received_msg = evt.data; ... }; ws.onclose = function() { // websocket is closed. }; } else { // the browser doesn't support WebSocket. }
Another approach is a concept known as long polling. In long polling, we initiate an Ajax request as we do now, but the server doesn’t respond right away. Instead, it holds the connection open and does not respond until it has the requested data, or until a waiting time is exceeded.
See Also
See Recipe 14.8 for a demonstration of using this same functionality with an ARIA live region to ensure the application is accessible for those using screen readers. The W3C WebSockets API specification is located at http://dev.w3.org/html5/websockets/, and the Chrome introduction of support for WebSockets is at http://blog.chromium.org/2009/12/web-sockets-now-available-in-google.html.
18.10. Communicating Across Windows with PostMessage
Problem
Your application needs to communicate with a widget that’s located in an iFrame. However, you don’t want to have to send the communication through the network.
Solution
Use the new HTML5 postMessage
to enable back-and-forth communication with the iFrame widget, bypassing
network communication altogether.
One or both windows can add an event listener for the new message event. To ensure the event handling works with IE as well as Opera, Firefox, Safari, and Chrome, using object detection:
function manageEvent(eventObj, event, eventHandler) { if (eventObj.addEventListener) { eventObj.addEventListener(event, eventHandler,false); } else if (eventObj.attachEvent) { event = "on" + event; eventObj.attachEvent(event, eventHandler); } }
The sender window that has the iFrame prepares and posts the message to the widget window:
function sendMessage() { try { var farAwayWindow = document.getElementById("widgetId").contentWindow; farAwayWindow.postMessage( "dragonfly6.thumbnail.jpg,Dragonfly on flower", 'http://burningbird.net'); } catch (e) { alert(e); } };
Two parameters are required for the HTML5 implementation of
postMessage
: the first is the message
string; the second is the target window’s origin.
If the iFrame window’s source is something like
http://somecompany.com/test/testwindow.html
,
then the target origin would be
http://somecompany.com
. You could also use
“*
” for the target origin. This is a wildcard, which
means the value will match to any origin.
In the receiving window, an event listener picks up the message
and responds accordingly. In this example, the string is split on the
comma (,
); the first part of the
string is assigned the image element’s src
property, and the second is assigned the
image element’s alt
property:
function receive(e) { var img = document.getElementById("image"); img.src = e.data.split(",")[0]; img.alt = e.data.split(",")[1]; e.source.postMessage("Received " + e.data, "*"); }
In the code, the widget window responds with a postMessage
of its own, but uses the wildcard
origin. The widget window also doesn’t check to see what domain sent the
message. But when it responds, the host window checks origin
:
function receive(e) { if (e.origin == "http://burningbird.net") ... does something with response message }
Discussion
The postMessage
functionality
is based on listener/sender functionality. Example 18-2 contains an example sender page. It contains an
iFrame in which a requested image is displayed when the cross-document
communication finishes. When you click on the web page, a message is
posted to the listener to parse the message, find the name for a
photograph, and add the photograph to an img
element in the page.
<!DOCTYPE html> <head> <title>Sender</title> <script> function manageEvent(eventObj, event, eventHandler) { if (eventObj.addEventListener) { eventObj.addEventListener(event, eventHandler,false); } else if (eventObj.attachEvent) { event = "on" + event; eventObj.attachEvent(event, eventHandler); } } window.onload=function() { manageEvent(document.getElementById("button1"),"click",sendMessage); manageEvent(window,"message",receive); } // make sure to change URL to your location function sendMessage() { try { var farAwayWindow = document.getElementById("widgetId").contentWindow; farAwayWindow.postMessage( "dragonfly6.thumbnail.jpg,Dragonfly on flower", 'http://jscb.burningbird.net'); } catch (e) { alert(e); } }; // change URL to your location function receive(e) { if (e.origin == "http://jscb.burningbird.net") alert(e.data); } </script> </head> <body> <div><button id="button1">Load the photo</button></div> <iframe src="example18-3.html" id="widgetId"></iframe> </body>
Example 18-3 contains the listener page.
<!DOCTYPE html> <head> <title>Listener</title> <script> function manageEvent(eventObj, event, eventHandler) { if (eventObj.addEventListener) { eventObj.addEventListener(event, eventHandler,false); } else if (eventObj.attachEvent) { event = "on" + event; eventObj.attachEvent(event, eventHandler); } } window.onload=function() { manageEvent(window,"message",receive); } function receive(e) { var img = document.getElementById("image"); img.src = e.data.split(",")[0]; img.alt = e.data.split(",")[1]; e.source.postMessage("Received " + e.data, "http://burningbird.net"); } </script> </head> <body> <img src="" id="image" alt="" /> </body>
Figure 18-2 shows the page after the successful communication.
The new postMessage
will soon
be joined by a message channel capability, and both are attuned to use
with widgets. This capability enables a higher level of interactivity
between the hosting window and the widget that wasn’t possible before,
either because of network activity or because of same-origin security
restrictions.
The loosening of security restrictions is also a risk associated
with postMessage
. We can help
mitigate the risk by following some simple rules:
Use the
targetOrigin
rather than the wildcard (*
) when posting a message.Use the
origin
value in the return message object to ensure the sender is the expected agent.Check and double-check message data received. Needless to say, don’t pass it to
eval
, and avoid plunking the data directly into a page usinginnerHTML
.
Companies providing widgets can also add a layer of security. For instance, when a person downloads a widget, he also registers the domain where the widget is used. This is the same functionality that Google uses when you use the Google Maps API: you register the domain where the map API is used, and if the domain differs, the API doesn’t work.
The widget company can use this to check the message’s origin, and
also use this information when sending any postMessage
response. In addition, the widget
company can use whatever precaution necessary with the sent data and
respond in kind.
Get JavaScript Cookbook now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.