64
The technique of using a
<script>
element as an Ajax transport has come to be known
as JSONP: it works when the response body of the HTTP request is JSON-encoded.
The “P” stands for “padding” or “prefix”—this will be explained in a moment.
4
Suppose you’ve written a service that handles GET requests and returns JSON-encoded
data. Same-origin documents can use it with XMLHttpRequest and
JSON.parse()
with
code like that in Example 18-3. If you enable CORS on your server, cross-origin docu-
ments in new browsers can also use your service with XMLHttpRequest. Cross-origin
documents in older browsers that do not support CORS can only access your service
with a
<script>
element, however. Your JSON response body is (by definition) valid
JavaScript code, and the browser will execute it when it arrives. Executing JSON-
encoded data decodes it, but the result is still just data, and it doesn’t do anything.
This is where the P part of JSONP comes in. When invoked through a
<script>
element,
your service must “pad” its response by surrounding it with parentheses and prefixing
it with the name of a JavaScript function. Instead of just sending JSON data like this:
[1, 2, {"buckle": "my shoe"}]
It sends a padded-JSON response like this:
handleResponse(
[1, 2, {"buckle": "my shoe"}]
)
As the body of a
<script>
element, this padded response does something valuable: it
evaluates the JSON-encoded data (which is nothing more than one big JavaScript ex-
pression, after all) and then passes it to the function
handleResponse()
, which, we as-
sume, the containing document has defined to do something useful with the data.
In order for this to work, we have to have some way to tell the service that it is being
invoked from a
<script>
element and must send a JSONP response instead of a plain
JSON response. This can be done by adding a query parameter to the URL: append-
ing
?json
(or
&json
), for example.
In practice, services that support JSONP do not dictate a function name like
“handleResponse” that all clients must implement. Instead, they use the value of a
query parameter to allow the client to specify a function name, and then use that func-
tion name as the padding in the response. Example 18-14 uses a query parameter named
“jsonp” to specify the name of the callback function. Many services that support JSONP
recognize this parameter name. Another common name is “callback”, and you might
have to modify the code shown here to make it work with the particular requirements
of the service you need to use.
Example 18-14 defines a function
getJSONP()
that makes a JSONP request. This ex-
ample is a little tricky, and there are some things you should note about it. First, notice
how it creates a new
<script>
element, sets its URL, and inserts it into the document.
It is this insertion that triggers the HTTP request. Second, notice that the example
4.Bob Ippolito coined the term “JSONP” in 2005.
514 | Chapter 18: Scripted HTTP
50
creates a new internal callback function for each request, storing the function as a
property of
getJSONP()
itself. Finally, note that callback performs some necessary
cleanup: it removes the script element and deletes itself.
Example 18-14. Making a JSONP request with a script element
// Make a JSONP request to the specified URL and pass the parsed response
// data to the specified callback. Add a query parameter named "jsonp" to
// the URL to specify the name of the callback function for the request.
function getJSONP(url, callback) {
// Create a unique callback name just for this request
var cbnum = "cb" + getJSONP.counter++; // Increment counter each time
var cbname = "getJSONP." + cbnum; // As a property of this function
// Add the callback name to the url query string using form-encoding
// We use the parameter name "jsonp". Some JSONP-enabled services
// may require a different parameter name, such as "callback".
if (url.indexOf("?") === -1) // URL doesn't already have a query section
url += "?jsonp=" + cbname; // add parameter as the query section
else // Otherwise,
url += "&jsonp=" + cbname; // add it as a new parameter.
// Create the script element that will send this request
var script = document.createElement("script");
// Define the callback function that will be invoked by the script
getJSONP[cbnum] = function(response) {
try {
callback(response); // Handle the response data
}
finally { // Even if callback or response threw an error
delete getJSONP[cbnum]; // Delete this function
script.parentNode.removeChild(script); // Remove script
}
};
// Now trigger the HTTP request
script.src = url; // Set script url
document.body.appendChild(script); // Add it to the document
}
getJSONP.counter = 0; // A counter we use to create unique callback names
18.3 Comet with Server-Sent Events
The Server-Sent Events draft standard defines an EventSource object that makes Comet
applications trivial to write. Simply pass a URL to the
EventSource()
constructor and
then listen for message events on the returned object:
var ticker = new EventSource("stockprices.php");
ticker.onmessage = function(e) {
var type = e.type;
var data = e.data;
18.3 Comet with Server-Sent Events | 515
Client-Side
JavaScript
54
// Now process the event type and event data strings.
}
The event object associated with a message event has a
data
property that holds what-
ever string the server sent as the payload for this event. The event object also has a
type
property like all event objects do. The default value is “message”, but the event
source can specify a different string for the property. A single
onmessage
event handler
receives all events from a given server event source, and can dispatch them, if necessary,
based on their
type
property.
The Server-Sent Event protocol is straightforward. The client initiates a connection to
the server (when it creates the
EventSource
object) and the server keeps this connection
open. When an event occurs, the server writes lines of text to the connection. An event
going over the wire might look like this:
event: bid sets the type of the event object
data: GOOG sets the data property
data: 999 appends a newline and more data
a blank line triggers the message event
There are some additional details to the protocol that allow events to be given IDs and
allow a reconnecting client to tell the server what the ID of the last event it received
was, so that a server can resend any events it missed. Those details are not important
here, however.
One obvious application for the Comet architecture is online chat: a chat client can
post new messages to the chat room with XMLHttpRequest and can subscribe to the
stream of chatter with an EventSource object. Example 18-15 demonstrates how easy
it is to write a chat client like this with EventSource.
Example 18-15. A simple chat client, using EventSource
<script>
window.onload = function() {
// Take care of some UI details
var nick = prompt("Enter your nickname"); // Get user's nickname
var input = document.getElementById("input"); // Find the input field
input.focus(); // Set keyboard focus
// Register for notification of new messages using EventSource
var chat = new EventSource("/chat");
chat.onmessage = function(event) { // When a new message arrives
var msg = event.data; // Get text from event object
var node = document.createTextNode(msg); // Make it into a text node
var div = document.createElement("div"); // Create a <div>
div.appendChild(node); // Add text node to div
document.body.insertBefore(div, input); // And add div before input
input.scrollIntoView(); // Ensure input elt is visible
}
// Post the user's messages to the server using XMLHttpRequest
input.onchange = function() { // When user strikes return
var msg = nick + ": " + input.value; // Username plus user's input
516 | Chapter 18: Scripted HTTP
.NET PDF SDK | Read & Processing PDF files by this .NET Imaging PDF Reader Add-on. Include extraction of text, hyperlinks, bookmarks and metadata; Annotate and redact in PDF documents; Fully support all
active links in pdf; add a link to a pdf in preview PDF Image Viewer| What is PDF advanced capabilities, such as text extraction, hyperlinks, bookmarks and Note: PDF processing and conversion is excluded in NET Imaging SDK, you may add it on
add url pdf; add hyperlink to pdf in
53
var xhr = new XMLHttpRequest(); // Create a new XHR
xhr.open("POST", "/chat"); // to POST to /chat.
xhr.setRequestHeader("Content-Type", // Specify plain UTF-8 text
"text/plain;charset=UTF-8");
xhr.send(msg); // Send the message
input.value = ""; // Get ready for more input
}
};
</script>
<!-- The chat UI is just a single text input field -->
<!-- New chat messages will be inserted before this input field -->
<input id="input" style="width:100%"/>
At the time of this writing, EventSource is supported in Chrome and Safari, and Mozilla
is expected to implement it in the first release after Firefox 4.0. In browsers (like Firefox)
whose XMLHttpRequest implementation fires a readystatechange event (for
ready
State
3) whenever there is download progress, it is relatively easy to emulate
EventSource with XMLHttpRequest, and Example 18-16 shows how this can be done.
With this emulation module, Example 18-15 works in Chrome, Safari, and Firefox.
(Example 18-16 does not work in IE or Opera, since their XMLHttpRequest imple-
mentations do not generate events on download progress.)
Example 18-16. Emulating EventSource with XMLHttpRequest
// Emulate the EventSource API for browsers that do not support it.
// Requires an XMLHttpRequest that sends readystatechange events whenever
// there is new data written to a long-lived HTTP connection. Note that
// this is not a complete implementation of the API: it does not support the
// readyState property, the close() method, nor the open and error events.
// Also event registration for message events is through the onmessage
// property only--this version does not define an addEventListener method.
if (window.EventSource === undefined) { // If EventSource is not defined,
window.EventSource = function(url) { // emulate it like this.
var xhr; // Our HTTP connection...
var evtsrc = this; // Used in the event handlers.
var charsReceived = 0; // So we can tell what is new.
var type = null; // To check property response type.
var data = ""; // Holds message data
var eventName = "message"; // The type field of our event objects
var lastEventId = ""; // For resyncing with the server
var retrydelay = 1000; // Delay between connection attempts
var aborted = false; // Set true to give up on connecting
// Create an XHR object
xhr = new XMLHttpRequest();
// Define an event handler for it
xhr.onreadystatechange = function() {
switch(xhr.readyState) {
case 3: processData(); break; // When a chunk of data arrives
case 4: reconnect(); break; // When the request closes
}
};
18.3 Comet with Server-Sent Events | 517
Client-Side
JavaScript
51
// And establish a long-lived connection through it
connect();
// If the connection closes normally, wait a second and try to restart
function reconnect() {
if (aborted) return; // Don't reconnect after an abort
if (xhr.status >= 300) return; // Don't reconnect after an error
setTimeout(connect, retrydelay); // Wait a bit, then reconnect
};
// This is how we establish a connection
function connect() {
charsReceived = 0;
type = null;
xhr.open("GET", url);
xhr.setRequestHeader("Cache-Control", "no-cache");
if (lastEventId) xhr.setRequestHeader("Last-Event-ID", lastEventId);
xhr.send();
}
// Each time data arrives, process it and trigger the onmessage handler
// This function handles the details of the Server-Sent Events protocol
function processData() {
if (!type) { // Check the response type if we haven't already
type = xhr.getResponseHeader('Content-Type');
if (type !== "text/event-stream") {
aborted = true;
xhr.abort();
return;
}
}
// Keep track of how much we've received and get only the
// portion of the response that we haven't already processed.
var chunk = xhr.responseText.substring(charsReceived);
charsReceived = xhr.responseText.length;
// Break the chunk of text into lines and iterate over them.
var lines = chunk.replace(/(\r\n|\r|\n)$/, "").split(/\r\n|\r|\n/);
for(var i = 0; i < lines.length; i++) {
var line = lines[i], pos = line.indexOf(":"), name, value="";
if (pos == 0) continue; // Ignore comments
if (pos > 0) { // field name:value
name = line.substring(0,pos);
value = line.substring(pos+1);
if (value.charAt(0) == " ") value = value.substring(1);
}
else name = line; // field name only
switch(name) {
case "event": eventName = value; break;
case "data": data += value + "\n"; break;
case "id": lastEventId = value; break;
case "retry": retrydelay = parseInt(value) || 1000; break;
default: break; // Ignore any other line
}
518 | Chapter 18: Scripted HTTP
51
if (line === "") { // A blank line means send the event
if (evtsrc.onmessage && data !== "") {
// Chop trailing newline if there is one
if (data.charAt(data.length-1) == "\n")
data = data.substring(0, data.length-1);
evtsrc.onmessage({ // This is a fake Event object
type: eventName, // event type
data: data, // event data
origin: url // the origin of the data
});
}
data = "";
continue;
}
}
}
};
}
We conclude this exploration of the Comet architecture with a server example. Exam-
ple 18-17 is a custom HTTP server written in server-side JavaScript for the Node
(§12.2) server-side environment. When a client requests the root URL “/”, it sends the
chat client code shown in Example 18-15 and the emulation code from Exam-
ple 18-16. When a client makes a GET request for the URL “/chat”, it saves the response
stream in an array and keeps that connection open. And when a client makes a POST
request to “/chat”, it uses the body of the request as a chat message and writes it,
prefixed with the Server-Sent Events “data:” prefix, to each of the open response
streams. If you install Node, you can run this server example locally. It listens on port
8000, so after starting the server, you’d point your browser to
http://localhost:8000
to connect and begin chatting with yourself.
Example 18-17. A custom Server-Sent Events chat server
// This is server-side JavaScript, intended to be run with NodeJS.
// It implements a very simple, completely anonymous chat room.
// POST new messages to /chat, or GET a text/event-stream of messages
// from the same URL. Making a GET request to / returns a simple HTML file
// that contains the client-side chat UI.
var http = require('http'); // NodeJS HTTP server API
// The HTML file for the chat client. Used below.
var clientui = require('fs').readFileSync("chatclient.html");
var emulation = require('fs').readFileSync("EventSourceEmulation.js");
// An array of ServerResponse objects that we're going to send events to
var clients = [];
// Send a comment to the clients every 20 seconds so they don't
// close the connection and then reconnect
setInterval(function() {
clients.forEach(function(client) {
client.write(":ping\n");
});
18.3 Comet with Server-Sent Events | 519
Client-Side
JavaScript
50
}, 20000);
// Create a new server
var server = new http.Server();
// When the server gets a new request, run this function
server.on("request", function (request, response) {
// Parse the requested URL
var url = require('url').parse(request.url);
// If the request was for "/", send the client-side chat UI.
if (url.pathname === "/") { // A request for the chat UI
response.writeHead(200, {"Content-Type": "text/html"});
response.write("<script>" + emulation + "</script>");
response.write(clientui);
response.end();
return;
}
// Send 404 for any request other than "/chat"
else if (url.pathname !== "/chat") {
response.writeHead(404);
response.end();
return;
}
// If the request was a post, then a client is posting a new message
if (request.method === "POST") {
request.setEncoding("utf8");
var body = "";
// When we get a chunk of data, add it to the body
request.on("data", function(chunk) { body += chunk; });
// When the request is done, send an empty response
// and broadcast the message to all listening clients.
request.on("end", function() {
response.writeHead(200); // Respond to the request
response.end();
// Format the message in text/event-stream format
// Make sure each line is prefixed with "data:" and that it is
// terminated with two newlines.
message = 'data: ' + body.replace('\n', '\ndata: ') + "\r\n\r\n";
// Now send this message to all listening clients
clients.forEach(function(client) { client.write(message); });
});
}
// Otherwise, a client is requesting a stream of messages
else {
// Set the content type and send an initial message event
response.writeHead(200, {'Content-Type': "text/event-stream" });
response.write("data: Connected\n\n");
// If the client closes the connection, remove the corresponding
// response object from the array of active clients
request.connection.on("end", function() {
clients.splice(clients.indexOf(response), 1);
520 | Chapter 18: Scripted HTTP
11
response.end();
});
// Remember the response object so we can send future messages to it
clients.push(response);
}
});
// Run the server on port 8000. Connect to http://localhost:8000/ to use it.
server.listen(8000);
18.3 Comet with Server-Sent Events | 521
Client-Side
JavaScript
Documents you may be interested
Documents you may be interested