Sandboxing 3rd party JavaScript widgets

The outside world is evil, I mean it. But it is also old, very old. And your cool, magical widget has to survive out there. How can you make sure of it? Last week we published a blog post on building 3rd party widgets in JavaScript. Let’s recap briefly the first, in order and importance, basic goal we want score!

Our widget should be isolated from other code. It must not interfere with code on the host page, and host code should not interfere with your widget. This is the most fundamental and important goal of any widget.

Though absolutely critical, this goal -sandboxing your code- is not an easy task at all. Furthermore, as it happens with software problems, there’s no silver bullet. This is an excellent blog post about the choices you have to sandbox you code nowadays. The short story is this: use iframes. And that’s what we’re going to do. But before, let’s try something else.

Please, display good manners. Thanks!

Your widget is going to be a guest (that’s part of the nature of any widget, right?). And common courtesy rules demand that your widget show some respect and, therefore, do not pollute the host’s environment. Wait, I’m pretty sure you’ve heard of a technique to do that:

Closures! I don’t want to get too technical at this point, but you should really learn about closures. For the sake of brevity let’s accept this simple (or rather simplistic) definition of “closure” from Wikipedia: “A function that has an environment of its own”. That sounds good for sandboxing! Closures offer a way to keep all of your code outside of the global namespace. Your customers can intialize your widget new MagicalWidget() and don’t have to worry about PI to be defined in the global scope, only MagicalWidget is.

We can take a step further and create an immediately-invoked function expression (IIFE) for better code isolation:

You probably know that Browserify and webpack wrap your code in a closure. So go ahead and bundle up your widget with one of them.

So far so good. You are respectful and do not pollute the host’s environment. But, are you protected from the host? Well, if the host defines window.PI = 0 you’ll see there’s nothing to worry about. But what about this scenario?

Woah! Now we’re in trouble. I warned you, the world is an ugly, hostile place. Closures and IIFE offer nice code isolation, but don’t feel safe!

On the other hand, we also have to deal with stylesheet conflicts. You could create a CSS sandbox with cleanslate or something like that.

Let me tell you that I have tried this approach in an alpha release of the MagicBell widget and it worked like a charm! We tested it in several environments and I can tell that bundlers out there do a really nice work isolating your code. Then we added Honeybadger for error tracking. A few minutes after releasing that version we realized we were reporting all errors, including those of other libraries imported by the host. True, that could be fixed, but is it worth it?

Unfortunately, it’s not possible to provide a real isolated environment for your widget using this approach.

It is difficult to see the picture when you are inside the frame

But sometimes you don’t want (or need) to see the picture, like when you’re developing 3rd party JavaScript widgets. iFrames represent “a nested browsing context”. So we can build a real sandbox for our widget with iframes. Let’s try something simple:

At this point, let’s get back to our code from the previous blog post and revisit the webpack configuration. To make our lives easier, we’re going to bundle two files with webpack: one for the app code (whatever your widget is about), and another for the iframe wrapper:

// ./webpack.config.js...module.exports={entry:{app:'./src/js/app.js',// Rename index.js to app.jswidget:'./src/js/widget.js',},output:{path:path.resolve(__dirname,'dist'),// An absolute path to the output directoryfilename:'[name].magicalwidget.js',// The name to use for the output filelibrary:'MagicalWidget'},...};

Note: I’m going to call widget to the code that we use to wrap our app inside an iframe.

Now, we distribute two files: widget.magicalwidget.js and app.magicalwidget.js. But your customers only need to import widget.magicalwidget.js. That script is going to take care of creating the sandbox and importing your app code.

Personalization

Everyone likes, needs, wants to personalize their gadgets. And you want to brag about how flexible your widget is! So, let’s try passing options to this magical widget:

Try clicking, it throws the error “ReferenceError: SQRT2 is not defined”. The execution context of the callback function has changed!

Respect, trust, communication

We made a lot of progress since we started this post. We respect our host, we can trust each other, now we gotta solve a communication problem. Well, the postMessage API provides a way to communicate between our widget iframe and the host’s HTML page. postmate implements a nice wrapper around this API. Let’s add it to our project npm i postmate --save and use it:

There’s a bit more of code to make it work, but this can give you a good idea about how we can solve our communication problems and build a solid relationship with the host page.

* * *

This has been a pretty long post, sorry about that. Let’s recap briefly:

Unfortunately, closures and IIFE do not provide real isolated environments.

iframes are likely your better choice to build a solid sandbox for your widget. Just remember they are not silver bullets.

Use the postMessage API to ensure communication flows freely.

We wanted to describe some techniques that worked and didn’t work for us as well as the issues you might face while developing your awesome widget. We’d be really glad to hear any questions or other techniques you might have tried. Follow us on Twitter and ship lots of awesomeness to your customers!