LightningJS is a safe, fast, and asynchronous embed code intended for third-party Javascript providers.
Globals, prototypes and mismatched library versions can cause lots of compatibility headaches for third-party code.
With LightningJS, all of that third-party code lives in its own separate
window
context. If the code needs to do DOM manipulation, it still has
access to the original document via window.parent
.
Slowdowns in embedded third-party code should never impact the original document.
Traditional embed techniques can block window.onload
if the server responds
slowly, even when the embed itself is asynchronous.
With LightningJS, third-party server response time has zero impact on the
original document. It should even be safe to embed the code at the top of the
<body>
of the document for an added speed boost :)
When customers are using your third-party API, asynchronicity can make usage a
bit more complicated. Some libraries require manual creation of a callstack
(e.g. Google Analytics var _gaq=[]; _gaq.push(...)
), and others try to hook
into script.onload
events.
With LightningJS, the third-party namespace is immediately available as a callable function. All calls return objects that adhere to the CommonJS Promise API. Just a few modifications to the existing API will enable these deferred calls.
Let's say that we are Pirates Incorporated, purveyors of all things piratey on the interwebs. When using LightningJS, our embed code will look something like this:
<!-- begin embed code -->
<script type="text/javascript">/*{literal}<![CDATA[*/
/*** lightningjs-embed.min.js ***/
window.piratelib = lightningjs.require("piratelib", "//static.piratelib.com/piratelib.js");
/*]]>{/literal}*/</script>
<!-- end embed code -->
Our customers can call methods on piratelib
immediately, even though none of
our code has actually loaded yet:
piratelib("fireWarningShot", {direction: "starboard"})
This calls the fireWarningShot
method on our API. At some point, we decide to
return a value to our customers that indicates whether the warning shot was seen.
We also decide to throw exceptions in cases where the warning shot failed.
Since LightningJS already implements a promise API,
we can use the .then(fulfillmentCallback, errorCallback)
method to handle return
values and exceptions:
piratelib("fireWarningShot", {direction: "starboard"}).then(function(didSee) {
if (!didSee) {
// arrr, those landlubbers didn't see our warning shot...we're no
// scallywags, so run another shot across the bow
piratelib("fireWarningShot", {direction: "starboard"});
}
}, function(error) {
if (error.toString() == "crew refused") {
// blimey! it's mutiny!
}
})
The LightningJS API is pretty simple:
lightningjs.require
imports third-party codelightningjs.modules
dictionary of all imported moduleslightningjs.provide
exports third-party code as a LightningJS modulelightningjs.expensive
designates specific exported methods to execute only after the original document is completely finished
This step-by-step should get you up and running pretty quickly:
To make your own embed code, just start with embed.min.js
and then underneath
that code you can simply use lightningjs.require
to pull in your library.
For example: if you named your library namespace "piratelib", then your
final embed code might look something like this:
<!-- begin embed code -->
<script type="text/javascript">/*{literal}<![CDATA[*/
/*** lightningjs-embed.min.js ***/
window.piratelib = lightningjs.require("piratelib", "//static.piratelib.com/piratelib.js");
/*]]>{/literal}*/</script>
<!-- end embed code -->
The extra "guards" around the code ensure that you avoid any issues with template engines (e.g. Drupal, Joomla, etc) and XHTML parsers...as a third-party provider it pays to be prepared for all kinds of document environments.
Keep in mind that your code will be loaded in its own window context.
If you need to access the globals on the original document, you should grab the
window.parent
object. You should also use window.parent.document
to
manipulate the DOM.
Some older codebases may rely on implicit globals in the original document
If this is the case with your library, you could try wrapping your code in
a with
context:
with (window.parent) {
// your existing codebase goes here
}
If you want to utilize the asynchronous API that the embed code creates for your
namespace, you can do so by pasting the code from lightningjs-bootstrap.min.js
at the top of your codebase.
After doing this, you need to tell LightningJS which methods need to be made
available for asynchronous calling using the lightningjs.provide
method.
For example, you could expose your fireWarningShot
method like so:
/*** lightningjs-bootstrap.min.js ***/
/*** piratelib library code ***/
lightningjs.provide("piratelib", {
fireWarningShot: function(options) {
return piratelib.fireWarningShot(options);
}
});
Tools like Google PageSpeed and YSlow sometimes penalize pages for executing
too much Javascript before the page loads. Therefore, you may want to avoid
executing certain CPU or parsing intensive methods until after window.onload
so that your library does not cause any execution penalty for the original document.
To avoid executing expensive methods too early, just decorate these methods
using lightningjs.expensive
. LightningJS will ensure that calls to these
methods are delayed until after the original document completely loads:
lightningjs.provide("piratelib", {
loadToTheGunwalls: lightningjs.expensive(function() {
// this method instantiates lots of grog and iterates
// through each bottle in a loop
piratelib.stockTheGalley();
while (piratelib.bottlesOfGrogOnTheWall) {
piratelib.takeOneDownAndPassItAround();
}
}),
});
Exhaustive browser support really important for LightningJS. To that end, this loading approach has been battle-tested in production by both Meebo and Olark across thousands of websites and browsers. The included tests have been verified to pass in every browser we could easily get our hands on:
- Firefox 2+ (tested in 2.0, 3.0, 3.6, 4.0, 5.0, 6.0, 7.0)
- Chrome 12+ (tested in 12, 13, 14, 15)
- Internet Explorer 6+ (tested in 6, 7, 8, 9)
- Safari 4+ (tested in 4.0, 5.0, 5.1)
- Opera 10+ (tested in 10, 11.5)
- Mobile Safari 5+ (tested in 5.0, 5.1)
The development cycle should be pretty straightforward:
- Edit
lightningjs-embed.js
andlightningjs-bootstrap.js
directly - Add your test cases in
test/test.js
- you might need to add methods to the test library in
test/testlib.js
- Run the development server by executing
bin/serve
and visiting http://localhost:1167 in your browser - Click on the links for testing and benchmarking pages to see the effects of your changes
- don't forget to check multiple browsers, especially older ones like IE6
- Rebuild the components before committing your patches, by executing
bin/build
Feel free to fork, make changes, and send us a pull request...you know the drill.
Loading third-party Javascript is the core goal of LightningJS, and that functionality has been battle-tested in production scenarios. However, there are still some areas that need work:
- gathering test results on more browsers (older browsers, more mobile browsers)
- better testing of the Promise API in production environments
- exposing the original document's
ready
event to the third-party code - working around additional server-side templating systems so that more end-users can paste code into their favorite CMS
There are lots of other relevant ideas that may be outside the scope of LightningJS:
- designing an easy backwards-compatibility layer for existing third-party libraries
- integration with Caja sandboxing
Incidentally - though is it really a coincidence? - you can work on cool stuff like this here at Olark :)
Most of the LightningJS concepts build on work done by the fine Javascript hackers at Meebo. They even wrote up a great blog post detailing the reasoning behind the different parts of the Meebo embed code.
You can also find most of these techniques in use by the Olark embed code, which added cookie-controlled cachebreaking of the library code download and also sidestepped some common templating engine incompatibilities.
The LightningJS project distills this work for the open-source community by:
- simplifying the process of including multiple arbitrary Javascript files
- allowing out-of-the-box handling of asynchronous library calls
- removing dependency on the Meebo build system
For information on traditional third-party embed code (as well as other important topics), you could check out the Third-party JavaScript book, written up by a couple engineers from Disqus.
Other loaders exist with some similar goals, though many are not as well-suited for third-party providers. You should check out these alternatives particularly if you are trying to load files from your own domain: