19.5. Cookie Alternatives
There are a couple of drawbacks to using cookies for client-side persistence:
-
They are limited to 4 KB of data.
-
Even when cookies are used only for client-side scripting, they are still uploaded to the web server in the request for any web page with which they are associated. When the cookies are not used on the server, it's a waste of bandwidth.
Two cookie alternatives exist. Microsoft Internet Explorer and the Adobe Flash plug-in both define proprietary mechanisms for client-side persistence. Although
neither
is standard, both IE and Flash are widely deployed, which means that at least one of these mechanisms is available in a large majority of browsers. The IE and Flash persistence mechanisms are
briefly
described in the sections that follow, and the chapter concludes with an advanced example that provides persistent storage with IE, Flash, or cookies.
19.5.1. IE userData Persistence
Internet Explorer enables client-side persistence with a DHTML behavior. To access this mechanism, you apply a special behavior to an element (such as
<div>
) of your document. One way to do this is with CSS:
<!-- This stylesheet defines a class named "persistent" -->
<style>.persistent { behavior:url(#default#userData);}</style>
<!-- This <div> element is a member of that class -->
<div id="memory" class="persistent"></div>
Since the
behavior
attribute is not standard CSS, other web browsers simply ignore it. You can also set the
behavior
style attribute on an element with JavaScript:
var memory = document.getElementById("memory");
memory.style.behavior = "url('#default#userData')";
When an HTML element has this "userData" behavior associated with it, new
methods
(defined by the behavior) become available for that element.
To store data persistently, set attributes of the element with
setAttribute( )
and then save those attributes with
save( )
:
var memory = document.getElementById("memory"); // Get persistent element
memory.setAttribute("username", username); // Set data as attributes
memory.setAttribute("favoriteColor", favoriteColor);
memory.save("myPersistentData"); // Save the data
Note that the
save( )
method takes a string argument: this is the (arbitrary) name under which the data is to be stored. You'll need to use the same
name
when you retrieve the data.
Data saved using the IE persistence mechanism can be given an expiration date, just as cookie data can. To do this, simply set the
expires
property before calling the
save( )
method. This property should be set to a string in the form returned by
Date.toUTCString( )
. For example, you might add the following lines to the previous code to specify an expiration date 10 days in the future:
var now = (new Date( )).getTime( ); // now, in milliseconds
var expires = now + 10 * 24 * 60 * 60 * 1000; // 10 days from now in ms
memory.expires = (new Date(expires)).toUTCString( ); // convert to a string
To retrieve persistent data, reverse these steps, calling
load( )
to load saved attributes and calling
getAttribute( )
to query attribute values:
var memory = document.getElementById("memory"); // Get persistent element
memory.load("myPersistentData"); // Retrieve saved data by name
var user = memory.getAttribute("username"); // Query attributes
var color = memory.getAttribute("favoriteColor");
19.5.1.1. Storing hierarchical data
The userData persistence behavior is not limited to storing and retrieving the values of attributes. Any element to which this behavior is applied has a complete XML document associated with it. Applying the behavior to an HTML element creates an
XMLDocument
property on the element, and the value of this property is a DOM
Document
object. You can use DOM methods (see Chapter 15) to add content to this document before calling
save( )
or to extract content after calling
load( )
. Here's an example:
var memory = document.getElementById("memory"); // Get persistent element
var doc = memory.XMLDocument; // Get its document
var root = doc.documentElement; // Root element of document
root.appendChild(doc.createTextNode("data here")); // Store text in document
The use of an XML document enables the storage of hierarchical data; you might convert a tree of JavaScript objects to a tree of XML elements, for example.
19.5.1.2. Storage limits
The IE persistence mechanism allows much more data to be stored than cookies do. Each page may store up to 64 KB, and each web server is allowed a total of 640 KB. Sites on a trusted intranet are allowed even more storage. There is no documented way for an end user to alter these storage limits or to disable the persistence mechanism altogether.
19.5.1.3. Sharing persistent data
Like cookies, data stored with the IE persistence mechanism is available to all web pages in the same directory. Unlike cookies, however, a web page cannot access persistent data saved by pages in its
ancestor
directories using IE. Also, the IE persistence mechanism has no equivalent to the
path
and
domain
attributes of cookies, so there is no way to share persistent data more widely among pages. Finally, persistent data in IE is shared only between pages in the same directory, loaded via the same protocols. That is, data stored by a page loaded with the
https
:
protocol cannot be accessed by a page loaded with regular
http:
.
19.5.2. Flash SharedObject Persistence
The Flash plug-in, versions 6 and later, enables client-side persistence with the SharedObject class, which can be scripted with ActionScript code in a Flash movie.
To do this, create a SharedObject with ActionScript code like the following. Note that you must specify a name (like a cookie name) for your persistent data:
var so = SharedObject.getLocal("myPersistentData");
The SharedObject class does not define a
load( )
method as the IE persistence mechanism does. When you create a SharedObject, any data previously saved under the specified name is automatically loaded. Each SharedObject has a
data
property. This
data
property refers to a regular ActionScript object, and the persistent data is available as properties of that object. To read or write persistent data, simply read or write the properties of the
data
object:
var name = so.data.username; // Get some persistent data
so.data.favoriteColor = "red"; // Set a persistent field
The properties you set on the
data
object are not limited to primitive types such as
numbers
and strings. Arrays, for example, are also allowed.
SharedObject
does not have a
save( )
method, either. It does have a
flush( )
method that immediately stores the current state of the SharedObject. Calling this method is not necessary, however: properties set in the
data
object of a SharedObject are automatically saved when the Flash movie is unloaded. Note also that SharedObject does not support any way to specify an expiration date or lifetime for the persistent data.
Keep in mind that all the code shown in this section is ActionScript code run by the Flash plug-in, not JavaScript code running in the browser. If you want to use the Flash persistence mechanism in your JavaScript code, you need a way for JavaScript to communicate with Flash. Techniques for doing this are covered in Chapter 23. Example 22-12
demonstrates
the use of the ExternalInterface class (available in Flash 8 and later), which makes it trivial to invoke ActionScript methods from JavaScript. Examples 19-3 and 19-4, later in this chapter, use a lower-level communication mechanism to connect JavaScript and ActionScript. The
GetVariable( )
and
SetVariable( )
methods of the Flash plug-in object enable JavaScript to query and set ActionScript
variables
, and the ActionScript
fscommand( )
function sends data to JavaScript.
19.5.2.1. Storage limits
By default, the Flash player allows up to 100 KB of persistent data per web site. The user can adjust this limit down to 10 KB or up to 10 MB. Alternatively, the user can allow unlimited storage or disallow any persistent storage whatsoever. If a web site attempts to exceed the limit, Flash asks the user to allow or deny more storage for your web site.
19.5.2.2. Persistent data sharing
By default, persistent data in Flash is accessible only to the movie that created it. It is possible, however, to loosen this restriction so that two different movies in the same directory or
anywhere
on the same server can share access to persistent data. The way this is done is very similar to how it's done in the
path
attribute of a cookie. When you create a SharedObject with
SharedObject.getLocal( )
, you can pass a path as the second argument. This path must be a prefix of the actual path in the movie's URL. Any other movie that uses the same path can access the persistent data stored by this movie. For example, the following code creates a SharedObject that can be shared by any Flash movie that originates from the same web server:
var so = SharedObject.getLocal("My/Shared/Persistent/Data", // Object name
"/"); // Path
When scripting the SharedObject class from JavaScript, you probably are not interested in sharing persistent data between Flash movies. Instead, you most likely care about sharing data between web pages that script the same movie (see Example 19-3 in the
next
section).
19.5.3. Example: Persistent Objects
This section concludes with an extended example that defines a unified API for the three persistence mechanisms you've studied in this chapter. Example 19-3 defines a PObject class for persistent objects. The PObject class works much like the Cookie class of Example 19-2. You create a persistent object with the
PObject( )
constructor, to which you pass a name, a set of default values, and an
onload
handler function. The constructor creates a new JavaScript object and attempts to load persistent data previously stored under the name you specify. If it finds this data, it parses it into a set of name/value pairs and sets these pairs as properties of the newly created object. If it does not find any previously stored data, it uses the properties of the defaults object you specify. In either case, the
onload
handler function you specify is invoked asynchronously when your persistent data is ready for use.
Once your
onload
handler has been called, you can use the persistent data simply by reading the properties of the PObject as you would do with any regular JavaScript object. To store new persistent data, first set that data as properties (of type boolean, number, or string) of the PObject. Then call the
save( )
method of the PObject,
optionally
specifying a lifetime (in days) for the data. To delete the persistent data, call the
forget( )
method of the PObject.
The PObject class defined here uses IE-based persistence if it is running in IE. Otherwise, it checks for a suitable version of the Flash plug-in and uses Flash-based persistence if that is available. If neither of these options are available, it
falls
back on cookies.
Note that the PObject class allows only primitive values to be saved and converts numbers and booleans to strings when they are retrieved. It is possible to serialize array and object values as strings and parse those strings back into arrays and objects (see http://www.json.org, for example), but this example does not do that.
Example 19-3 is long but well commented and should be easy to follow. Be sure to read the long introductory comment that documents the PObject class and its API.
Example 19-3. PObject.js: persistent objects for JavaScript
/**
* PObject.js: JavaScript objects that persist across browser sessions and may
* be shared by web pages within the same directory on the same host.
*
* This module defines a PObject( ) constructor to create a persistent object.
* PObject objects have two public methods. save( ) saves, or "persists," the
* current properties of the object, and forget( ) deletes the persistent
* properties of the object. To define a persistent property in a PObject,
* simply set the property on the object as if it were a regular JavaScript
* object and then call the save( ) method to save the current state of
* the object. You may not use "save" or "forget" as a property name, nor
* any property whose name begins with $. PObject is intended for use with
* property values of type string. You may also save properties of type
* boolean and number, but these will be converted to strings when retrieved.
*
* When a PObject is created, the persistent data is read and stored in the
* newly created object as regular JavaScript properties, and you can use the
* PObject just as you would use a regular JavaScript object. Note, however,
* that persistent properties may not be ready when the PObject( ) constructor
* returns, and you should wait for asynchronous notification using an onload
* handler function that you pass to the constructor.
*
* Constructor:
* PObject(name, defaults, onload):
*
* Arguments:
*
* name A name that identifies this persistent object. A single pages
* can have more than one PObject, and PObjects are accessible
* to all pages within the same directory, so this name should
* be unique within the directory. If this argument is null or
* is not specified, the filename (but not directory) of the
* containing web page is used.
*
* defaults An optional JavaScript object. When no saved value for the
* persistent object can be found (which happens when a PObject
* is created for the first time), the properties of this object
* are copied into the newly created PObject.
*
* onload The function to call (asynchronously) when persistent values
* have been loaded into the PObject and are ready for use.
* This function is invoked with two arguments: a reference
* to the PObject and the PObject name. This function is
* called *after* the PObject( ) constructor returns. PObject
* properties should not be used before this.
*
* Method PObject.save(lifetimeInDays):
* Persist the properties of a PObject. This method saves the properties of
* the PObject, ensuring that they persist for at least the specified
* number of days.
*
* Method PObject.forget( ):
* Delete the properties of the PObject. Then save this "empty" PObject to
* persistent storage and, if possible, cause the persistent store to expire.
*
* Implementation Notes:
*
* This module defines a single PObject API but provides three distinct
* implementations of that API. In Internet Explorer, the IE-specific
* "UserData" persistence mechanism is used. On any other browser that has an
* Adobe Flash plug-in, the Flash SharedObject persistence mechanism is
* used. Browsers that are not IE and do not have Flash available fall back on
* a cookie-based implementation. Note that the Flash implementation does not
* support expiration dates for saved data, so data stored with that
* implementation persists until deleted.
*
* Sharing of PObjects:
*
* Data stored with a PObject on one page is also available to other pages
* within the same directory of the same web server. When the cookie
* implementation is used, pages in subdirectories can read (but not write)
* the properties of PObjects created in parent directories. When the Flash
* implementation is used, any page on the web server can access the shared
* data if it cheats and uses a modified version of this module.
*
* Distinct web browser applications store their cookies separately and
* persistent data stored using cookies in one browser is not accessible using
* a different browser. If two browsers both use the same installation of
* the Flash plug-in, however, these browsers may share persistent data stored
* with the Flash implementation.
*
* Security Notes:
*
* Data saved through a PObject is stored unencrypted on the user's hard disk.
* Applications running on the computer can access the data, so PObject is
* not suitable for storing sensitive information such as credit card numbers,
* passwords, or financial account numbers.
*/
// This is the constructor
function PObject(name, defaults, onload) {
if (!name) { // If no name was specified, use the last component of the URL
name = window.location.pathname;
var pos = name.lastIndexOf("/");
if (pos != -1) name = name.substring(pos+1);
}
this.$name = name; // Remember our name
// Just delegate to a private, implementation-defined $init( ) method.
this.$init(name, defaults, onload);
}
// Save the current state of this PObject for at least the specified # of days.
PObject.prototype.save = function(lifetimeInDays) {
// First serialize the properties of the object into a single string
var s = ""; // Start with empty string
for(var name in this) { // Loop through properties
if (name.charAt(0) == "$") continue; // Skip private $ properties
var value = this[name]; // Get property value
var type = typeof value; // Get property type
// Skip properties whose type is object or function
if (type == "object" type == "function") continue;
if (s.length > 0) s += "&"; // Separate properties with &
// Add property name and encoded value
s += name + ':' + encodeURIComponent(value);
}
// Then delegate to a private implementation-defined method to actually
// save that serialized string.
this.$save(s, lifetimeInDays);
};
PObject.prototype.forget = function( ) {
// First, delete the serializable properties of this object using the
// same property-selection criteria as the save( ) method.
for(var name in this) {
if (name.charAt(0) == '$') continue;
var value = this[name];
var type = typeof value;
if (type == "function" type == "object") continue;
delete this[name]; // Delete the property
}
// Then erase and expire any previously saved data by saving the
// empty string and setting its lifetime to 0.
this.$save("", 0);
};
// Parse the string s into name/value pairs and set them as properties of this.
// If the string is null or empty, copy properties from defaults instead.
// This private utility method is used by the implementations of $init( ) below.
PObject.prototype.$parse = function(s, defaults) {
if (!s) { // If there is no string, use default properties instead
if (defaults) for(var name in defaults) this[name] = defaults[name];
return;
}
// The name/value pairs are separated from each other by ampersands, and
// the individual names and values are separated from each other by colons.
// We use the split( ) method to parse everything.
var props = s.split('&'); // Break it into an array of name/value pairs
for(var i = 0; i < props.length; i++) { // Loop through name/value pairs
var p = props[i];
var a = p.split(':'); // Break each name/value pair at the colon
this[a[0]] = decodeURIComponent(a[1]); // Decode and store property
}
};
/*
* The implementation-specific portion of the module is below.
* For each implementation, we define an $init( ) method that loads
* persistent data and a $save( ) method that saves it.
*/
// Determine if we're in IE and, if not, whether we've got a Flash
// plug-in installed and whether it has a high-enough version number
var isIE = navigator.appName == "Microsoft Internet Explorer";
var hasFlash7 = false;
if (!isIE && navigator.plugins) { // If we use the Netscape plug-in architecture
var flashplayer = navigator.plugins["Shockwave Flash"];
if (flashplayer) { // If we've got a Flash plug-in
// Extract the version number
var flashversion = flashplayer.description;
var flashversion = flashversion.substring(flashversion.search("\d"));
if (parseInt(flashversion) >= 7) hasFlash7 = true;
}
}
if (isIE) { // If we're in IE
// The PObject( ) constructor delegates to this initialization function
PObject.prototype.$init = function(name, defaults, onload) {
// Create a hidden element with the userData behavior to persist data
var div = document.createElement("div"); // Create a <div> tag
this.$div = div; // Remember it
div.id = "PObject" + name; // Name it
div.style.display = "none"; // Make it invisible
// This is the IE-specific magic that makes persistence work.
// The "userData" behavior adds the getAttribute( ), setAttribute( ),
// load( ), and save( ) methods to this <div> element. We use them below.
div.style.behavior = "url('#default#userData')";
document.body.appendChild(div); // Add the element to the document
// Now we retrieve any previously saved persistent data.
div.load(name); // Load data stored under our name
// The data is a set of attributes. We only care about one of these
// attributes. We've arbitrarily chosen the name "data" for it.
var data = div.getAttribute("data");
// Parse the data we retrieved, breaking it into object properties
this.$parse(data, defaults);
// If there is an onload callback, arrange to call it asynchronously
// once the PObject( ) constructor has returned.
if (onload) {
var pobj = this; // Can't use "this" in the nested function
setTimeout(function( ) { onload(pobj, name);}, 0);
}
}
// Persist the current state of the persistent object
PObject.prototype.$save = function(s, lifetimeInDays) {
if (lifetimeInDays) { // If lifetime specified, convert to expiration
var now = (new Date( )).getTime( );
var expires = now + lifetimeInDays * 24 * 60 * 60 * 1000;
// Set the expiration date as a string property of the <div>
this.$div.expires = (new Date(expires)).toUTCString( );
}
// Now save the data persistently
this.$div.setAttribute("data", s); // Set text as attribute of the <div>
this.$div.save(this.$name); // And make that attribute persistent
};
}
else if (hasFlash7) { // This is the Flash-based implementation
PObject.prototype.$init = function(name, defaults, onload) {
var moviename = "PObject_" + name; // id of the <embed> tag
var url = "PObject.swf?name=" + name; // URL of the movie file
// When the Flash player has started up and has our data ready,
// it notifies us with an FSCommand. We must define a
// handler that is called when that happens.
var pobj = this; // for use by the nested function
// Flash requires that we name our function with this global symbol
window[moviename + "_DoFSCommand"] = function(command, args) {
// We know Flash is ready now, so query it for our persistent data
var data = pobj.$flash.GetVariable("data")
pobj.$parse(data, defaults); // Parse data or copy defaults
if (onload) onload(pobj, name); // Call onload handler, if any
};
// Create an <embed> tag to hold our Flash movie. Using an <object>
// tag is more standards-compliant, but it seems to cause problems
// receiving the FSCommand. Note that we'll never be using Flash with
// IE, which simplifies things quite a bit.
var movie = document.createElement("embed"); // element to hold movie
movie.setAttribute("id", moviename); // element id
movie.setAttribute("name", moviename); // and name
movie.setAttribute("type", "application/x-shockwave-flash");
movie.setAttribute("src", url); // This is the URL of the movie
// Make the movie inconspicuous at the upper-right corner
movie.setAttribute("width", 1); // If this is 0, it doesn't work
movie.setAttribute("height", 1);
movie.setAttribute("style", "position:absolute; left:0px; top:0px;");
document.body.appendChild(movie); // Add the movie to the document
this.$flash = movie; // And remember it for later
};
PObject.prototype.$save = function(s, lifetimeInDays) {
// To make the data persistent, we simply set it as a variable on
// the Flash movie. The ActionScript code in the movie persists it.
// Note that Flash persistence does not support lifetimes.
this.$flash.SetVariable("data", s); // Ask Flash to save the text
};
}
else { /* If we're not IE and don't have Flash 7, fall back on cookies */
PObject.prototype.$init = function(name, defaults, onload) {
var allcookies = document.cookie; // Get all cookies
var data = null; // Assume no cookie data
var start = allcookies.indexOf(name + '='); // Look for cookie start
if (start != -1) { // Found it
start += name.length + 1; // Skip cookie name
var end = allcookies.indexOf(';', start); // Find end of cookie
if (end == -1) end = allcookies.length;
data = allcookies.substring(start, end); // Extract cookie data
}
this.$parse(data, defaults); // Parse the cookie value to properties
if (onload) { // Invoke onload handler asynchronously
var pobj = this;
setTimeout(function( ) { onload(pobj, name); }, 0);
}
};
PObject.prototype.$save = function(s, lifetimeInDays) {
var cookie = this.$name + '=' + s; // Cookie name and value
if (lifetimeInDays != null) // Add expiration
cookie += "; max-age=" + (lifetimeInDays*24*60*60);
document.cookie = cookie; // Save the cookie
};
}
|
19.5.3.1. ActionScript code for Flash persistence
The code in Example 19-3 is not complete as it stands. The Flash-based persistence implementation relies on a Flash movie named
PObject.swf
. This movie is nothing more than a compiled ActionScript file. Example 19-4 shows the ActionScript code.
Example 19-4. ActionScript code for Flash-based persistence
class PObject {
static function main( ) {
// SharedObject exists in Flash 6 but isn't protected against
// cross-domain scripting until Flash 7, so make sure we've got
// that version of the Flash player.
var version = getVersion( );
version = parseInt(version.substring(version.lastIndexOf(" ")));
if (isNaN(version) version < 7) return;
// Create a SharedObject to hold our persistent data.
// The name of the object is passed in the movie URL like this:
// PObject.swf?name=name
_root.so = SharedObject.getLocal(_root.name);
// Retrieve the initial data and store it on _root.data.
_root.data = _root.so.data.data;
// Watch the data variable. When it changes, persist its new value.
_root.watch("data", function(propName, oldValue, newValue) {
_root.so.data.data = newValue;
_root.so.flush( );
});
// Notify JavaScript that it can retrieve the persistent data
now.
fscommand("init");
}
}
|
The ActionScript code is quite simple. It starts by creating a SharedObject, using a name specified (by JavaScript) in the query portion of the URL of the movie object. Creating this SharedObject loads the persistent data, which in this case is simply a single string. This data string is passed back to JavaScript with the
fscommand( )
function that invokes the
doFSCommand
handler defined in JavaScript. The ActionScript code also sets up a handler function to be invoked whenever the
data
property of the root object changes. The JavaScript code uses
SetVariable( )
to set the
data
property, and this ActionScript handler function is invoked in response,
causing
the data to be made persistent.
The ActionScript code shown in the
PObject.as
file of Example 19-4 must be compiled into a
PObject.swf
file before it can be used with the Flash player. You can do this with the
open
source ActionScript compiler
mtasc
(available from http://www.mtasc.org). Invoke the compiler like this:
mtasc -swf PObject.swf -main -header 1:1:1 PObject.as
mtasc
produces a SWF file that invokes the
PObject.main( )
method from the first frame of the movie. If you use the Flash IDE instead, you must explicitly call
PObject.main( )
from the first frame. Alternatively, you can simply copy code from the
main( )
method and insert it into the first frame.
|