Isolating scripts on a page
A simple and effective technique for isolating script contexts within a page
Motivation
Recently I had multiple projects where I needed some kind of widget-like code to be injected into different pages. I came up with multiple solutions, but they were flawed one way or another. The trivial solution would be to write a js file and inject it to the target page, just like most libraries work. This comes with almost no technical problems, but I soon found out that host pages tend to redefine basic objects, making even vanilla js unreliable and it makes the whole widget unreliable.
The other extreme solution would be to put everything into an IFrame as it would properly sandbox the code and the visuals. This approach would fall short in two aspects:
- The visuals needs to comply with the rest of the page
- The widget area is non rectangular
For the first point, there is a long forgotten initiative called seamless IFrame, but browsers already deserted it. And the other point is even more hopeless. So I needed to find some other way to achieve it.
Solution
My solution is to incorporate an IFrame to isolate the scripts but otherwise use the original DOM to display the contents. This reduces the IFrame to a simple container of scripts, and there is no need to display it.
Inserting the IFrame
The first problem is to insert the code into an IFrame. The easy solution would be to simply add a new
HTML file to the server and reference it, but I wanted a simply copy-pasteable and self-contained
piece of code. There is an attribute called srcdoc, but it
is not completely supported. Luckily it can be achieved with Javascript, with a little hack. It turned
out that inserting a <script>
into another script is not allowed by the browsers, the end tag needs
to be escaped, and unescaped during the insert. The resulting template would be like this:
<script id="template" type="x-tmpl"></script>
And it makes the IFrame insertion to this:
(function() {
function initIFrame(){
var iframe = document.createElement('iframe');
var html = document.querySelector("#template").innerHTML.replace(/</g, '<');
iframe.style.visibility = "hidden";
iframe.style.width = "1px";
iframe.style.height = "1px";
iframe.style.position = "absolute";
iframe.style.top = "0";
iframe.style.left = "0";
document.body.appendChild(iframe);
iframe.contentWindow.document.open();
iframe.contentWindow.document.write(html);
iframe.contentWindow.document.close();
}
window.onload = (function(pre){
return function(){
pre && pre.apply(this,arguments);
initIFrame();
};
})(window.onload);
})();
This makes the injected.js script to be inserted to the page, has access to the parent window, and as an added bonus it works cross-domain too.
Accessing the parent frame
For a simple example, let's kick off a simple AngularJs app! First, the required library script needs to be inserted to the template:
<script src='https://ajax.googleapis.com/ajax/libs/angularjs/1.4.3/angular.min.js'>
And the HTML:
<body>
<div ng-controller="MyController">
<h2></h2>
</div>
</body>
The last thing is to define the controller and bootstrap the app:
angular.module('myApp', [])
.controller('MyController', ['$scope', function ($scope) {
$scope.welcome = 'Hello World!';
}]);
angular.element(window.parent.document).ready(function() {
angular.bootstrap(window.parent.document, ['myApp']);
});
Visiting with a browser we can not tell that it is not a regular Angular app, but it's code is fully isolated from the rest of the page.
Templating from the host
Oftentimes the host page would want to customize the appearances with custom templates. This makes the logic fully isolated, while retains the ability to alter the visuals. As the IFrame can access the host, it is just a matter of parameter passing. This is most easily done at the IFrame template part:
<script>
window.greeterTemplateId='greeter-template';
</script>
.directive('greeter', function() {
return {
template: window.parent.document.querySelector('#'+window.greeterTemplateId).innerHTML
};
});
Restricting CSS
One of the advantages of this method is that it allows the visuals to be the same as the host. Sometimes it is not wanted, and the content should refrain from inherit styling. There are many css reset libraries, and there is an interesting library to provide style resetting called cleanslate.
Conclusion
This tutorial should give a good overview and a basis for a solution that is likely to work in a wide variety of use cases. If you'd prefer a more drop-in solution, then there is a project called sandie for this purpose.
Using isolation techniques should be used scarcely as it prevents script reuse. If you have a page with a lots of parts each loading the same libraries makes it sluggish and bulky. That said, there are valid use cases when it comes handy.