Developing Feather-Weight Webservices with JavaScript

Passing JavaScript arguments via the src attribute

The dual <script> idiom—covered in Remotely called JavaScript with local configuration–which is generally necessary in JavaScript based web services is awkward and unfriendly. When simple configuration is all that’s needed, the terse and well-known API that the query string affords would be superior if only JavaScript could use it. With just a few lines added to the remote script, it can.

We’d like to write something direct like this.

<script src="http://elektrum.org/cal.js?show=appts;user=veri"
  type="text/javascript"></script>

Instead we must call two scripts. A configuration script to pass arguments to an imported script which uses them.

<script type="text/javascript">
<!--// 
  user = 'veri';
  show = 'appts';
//-->
</script>

<script src="http://elektrum.org/cal.js"
  type="text/javascript"></script>

A disappointing fact of JavaScript is that an executing script is not itself an object—even though the <script> that contains it is—like everything in the DOM. Therefore, a script doesn’t have direct knowledge of its calling conditions, including its source URI and query string. The location and document objects don’t directly do us any good in this regard.

That’s why we must resort to two scripts when we want to provide a service plus user configuration. Google ads, the omnipresent example, do this. Fifteen lines or so of code to get an ad into a page. Not hard per se but not as easy as one.

Ease is part of affordance in UI and design. The easier path will be the path that users stay on. The separation of code from client is part of that. You could provide your user with some kind of lib.js used with another script call but what if you find a bug or they have problems and your pager number? What if you want to launch a simple enhancement? Getting all your users to install it would be difficult; some would never know v2 was actually worth keeping.

The obstacle challenge

How do we make a script self-aware? The answer is deceptively simple. When a script with a src is loaded in a page, the rest of the page is not yet written. The implication is that no matter how many scripts a page contains, the one currently starting to execute is the last one; so far. So the following reliably makes a script aware of its caller and not just its caller’s page.

var scripts = document.getElementsByTagName('script');
var index = scripts.length - 1;
var myScript = scripts[index];
// myScript now contains our script object

This works in all current JavaScript enabled web browsers. Including but not limited to IE for PC and Mac, FireFox, Safari, and Opera. Another, perhaps obvious, tack is using the id attribute in the script tag, does not, and should not. It’s natural to try to use the id with getElementById() but the HTML specification for script doesn’t contain the id and even though a browser or two supports it, they shouldn’t and you can’t rely on it.

Once past that bit of thinking like the browser, getting the query string is easy because we have access to the script’s source via its myScript.src attribute. We take a perl-ish approach because JavaScript’s regular expressions are powerful and often overlooked.

Getting the query string out

// myScript.src is "http://elektrum.org/cal.js?show=appts;user=veri"
var queryString = myScript.src.replace(/^[^\?]+\??/,'');
// queryString is "show=appts;user=veri"

We take the script’s src and erase—replace with the empty ”—everything from the start, ^, that is not a question mark, [^\?]+, up through a question mark, if there is one, \??. This also safely produces an empty string in queryString if, for whatever reason, there is no query string.

Get the arguments out

We got our query string out. It’s now in our queryString variable. We need to be able to use the arguments programmatically. We parse the query string into an associative array Object by breaking key=value pairs out on the semi-colon (;) or ampersand1 (&), and then splitting them apart on the equals (=).

var params = parseQuery( queryString );

function parseQuery ( query ) {
   var Params = new Object ();
   if ( ! query ) return Params; // return empty object
   var Pairs = query.split(/[;&]/);
   for ( var i = 0; i < Pairs.length; i++ ) {
      var KeyVal = Pairs[i].split('=');
      if ( ! KeyVal || KeyVal.length != 2 ) continue;
      var key = unescape( KeyVal[0] );
      var val = unescape( KeyVal[1] );
      val = val.replace(/\+/g, ' ');
      Params[key] = val;
   }
   return Params;
}

With the newly returned variable params a source like "http://elektrum.org/snow.js?state=nm;town=Taos" will parse out the query string as the equivalent of the following code.

params = new Object({state:'nm'
                    ,town:'Taos'});

It can be used like so.

document.write('Ski conditions in ' + params['town'] +
               ', ' + params['state'].toUpperCase() + '...');

Which would render–

“Ski conditions in Taos, NM…”

The rub

This implementation is not just naïve, it’s technically wrong. Multiple parameters of the same name are allowed in CGI; POST or GET. fish=coelacanth;fish=khuli should pass both values along to the key fish. Our implementation would drive the ceolocanth where 360 million years hasn’t been able: extinction.

The fix

It’s easy enough to parse the values into arrays. That way the key looks up a list of values, which might well only contain one item, instead of a single one. Only one change, and two new lines are needed to make this work.

Params[key] = val;
if ( ! Params[key] ) Params[key] = new Array ();
Params[key].push( val );

push in JS isn’t supported by all older browsers though and it introduces a wrinkle when using the values. They’re arrays now, whether their param[key].length == 1 or not. So you have to be conscious of that when you manipulate or write them out. If you don’t need to support multiple values, don’t bother with the change.

Okay, another potential rub

Anyone who has dealt with JavaScript and caching knows that a good trick to keep a script and its output from getting cached is to put a random query string on it. The server recognizes it isn’t the same call as before and reloads the entire script. This means that if you use the same service many times in a single page with different configuration query strings and the script is large, it might be a page load performance hit as the file is fetched and parsed for every call with a different src.

All together now

Find script, get query string, parse arguments out

var scripts = document.getElementsByTagName('script');
var myScript = scripts[ scripts.length - 1 ];

var queryString = myScript.src.replace(/^[^\?]+\??/,'');

var params = parseQuery( queryString );

function parseQuery ( query ) {
   var Params = new Object ();
   if ( ! query ) return Params; // return empty object
   var Pairs = query.split(/[;&]/);
   for ( var i = 0; i < Pairs.length; i++ ) {
      var KeyVal = Pairs[i].split('=');
      if ( ! KeyVal || KeyVal.length != 2 ) continue;
      var key = unescape( KeyVal[0] );
      var val = unescape( KeyVal[1] );
      val = val.replace(/\+/g, ' ');
      Params[key] = val;
   }
   return Params;
}

Unexpected bonuses

A great side-effect to this style of calling is you can dispense with object scoped checks for imported variables. We’re not importing any script variables at all, like we had to before, Remotely called JavaScript with local configuration. So we also don’t need to fear that we’re going to throw errors by trying to use non-existent, never created or declared, variables. With the dual script idiom we need to check imported variables through another guaranteed object for safety like checking for pangram through window.pangram.

It also means that since you’re no longer polluting the namespace, you can reuse a script over and over in the same page with different parameters. No variable bleed from one invocation to the next like we got in Pangrams in action.

And lastly, it is self-tracking. The query string goes into your weblog so you can see who called the service with what variables at what time. When you use the dual script idiom, you don’t get automatic logging of the variables. They’re only inside the JavaScript. It’s possible to track them with something like Pixeling & usage tracking but that’s more difficult and really only a win for services that are interactive, ie, the user on the client side can interact with dynamic content or a form.

A minor loss sneaked in

When you count on the client to cut and paste a bit of code—or when you’re writing for your own hosts where you have control—you should be using <noscript> tags. That way you can build a failsafe service. If the client’s browser has JavaScript off, you still deliver an image or a text message and link, or even a mini form.

The remote nature of our clients precludes the use of delivering <noscript> tags. If JavaScript in not enabled, then we obviously can’t write out a <noscript> message to say something about why the service isn’t working. This is a noticeable loss.

That said, you should not count on a client for something like the content of <noscript> tags. They could leave it out, or worse, alter it. A major reason to deliver content from a remote source with JavaScript is so it can’t be tinkered with.

What now?

Now that we’ve gone to the trouble to make it possible, when would you really want to use this? Any time where a site is providing dynamic content non-programmatically. By non-programmatically, we mean the end user is only configuring the script once (with the query string). No call to get results to parse alla SOAP or even RSS. Though, that’s not impossible, it would be overly complicated and we’d lose everything we’ve gained in the simplicity JavaScript affords us.

Examples

Or how about a greeking service to help webdevs, which is exactly what we’ll develop next in Configurable greeking.

1 The ampersand (&) is actually deprecated in favor of the semi-colon (;) as the value pair separator in GET strings. The ampersand has been in use so long though that many developers expect it to work, so we show it even though it’s no longer the standard and it’s a little harder to read and deal with in XML. You should consider dropping support for it in your own applications. This discussion is the only place we support it in this manual.
« Pangrams in action · Configurable greeking »
Google
 
Web Developing Featherweight Web Services with JavaScript
This is version 0.57b of this manual. It is a beta version with some gaps. We are grateful for feedback.

The code is the manual has not yet been fully tested against Internet Explorer. Bug reports are welcome.
An Elektrum Press Online