This post has originally intended to be one of the chapters of the second part of the series.
However, it became so long that I decided to publish it separately.
I also didn't want to place too much theory in the practical part.
Today you will discover, what you will really gain by placing a widget in an iframe ( I can assure you that there are many benefits), and you will also learn a short history of my failure in the project.
I know the theory is boring, but trust me - it's worth getting acquainted with.
Let's start!
As I mentioned before, an iframe allows you to completely separate HTML documents from each other. What does this mean for us? Let's take a closer look at it.
CSS code separation - don't touch my classes
Let's assume that we want to embed the widget without an iframe.
The loading script could download the HTML and CSS code from the server and then inject it directly onto the page. This, however, carries with it a certain danger.
Remember that most often we do not have control over the website on which the widget will be loaded. The website could be written for example using a dedicated Bootstrap-based theme. The widget can also use Bootstrap, but with a different theme. If we embed such a widget on the page directly and add its styles, we will cause a disaster. Depending on the order in which the styles are placed, either the appearance of the page or the widget will break, or both!
Auto-generating CSS class names for the widget using CSS-in-JS can be a solution for it.
However, performance may suffer, besides, not everyone or CSS-in-JS :)
HTML code separation - stay away from my forms
Seemingly not a big problem. After all, we can use one div that will act as a container for the widget (similar to how we do it in the case of an iframe).
We have a reference to such a container and from the Javascript, we can identify elements belonging to the widget and perform operations on them.
Moreover, if we use React, after rendering the component to the main div, the rest will do "itself".
Let's look at it from another point of view. The page on which the widget is embedded can get references to elements using the document.querySelector or document.getElementsByTagName methods.
If the selector is too imprecise, the script on the page will accidentally get the widget's elements. For example, it can remove some of them or change their appearance. In an extreme case, this may cause the widget to stop working at all.
If the widget contains forms and it's embedded on a page that also contains forms, it can lead to the situation where the identifiers of the form elements will duplicate. This will cause the field labels to point to the least expected element.
Example:
The contact form on the page has a field with the id="email" attribute.
Below on the same page, there is a newsletter subscription widget, which also has a field with the same id.
After clicking on the label for the email field in the widget, the field from the contact form will be focused instead of the widget field.
That is because a browser stops looking for an element when it encounters the first element with a matching id.
Storage separation - the history of the project setback
While the separation of CSS and HTML code can be dealt with somehow, the separation of the storage is not so easy.
Resources such as localStorage, sessionStorage, indexedDB, WebSQL, Cookies, and Cache Storage are assigned to the domain (origin).
This means that if we load the page from the site.example.com and the widget from the widget.example.com,
both the widget will not be able to access the localStorage of the page, and the page will not have access to the localStorage of the widget.
There are ways to get around this and the developer has control over what resources and with whom to share.
I discuss that in the third part of this series
Browsers storages also have their limitations when it comes to the amount of available space. These restrictions are also bound to the domain.
If the website takes up all the space of its localStorage, the widget from the same domain will no longer have the space that may be needed for it to works.
This may seem like an imaginary problem to you, but it is not. I experienced this painfully the hard way a long time ago.
How we broke the widget
A long time ago in a company, where I worked, our widget was not loaded in an iframe but attached directly to the page.
It was installed on the customer's website - a large online store. This website used a third-party cache service that made heavy use of localStorage.
So intensively that it eventually completely stuffed it up.
One time, we got a call from an annoyed customer that the widget was not working.
You should know that the widget was quite an important part of the customer's website.
After a quick check on our side, of course, we did not find any errors. The widget was loading and working properly as usual.
You probably know, how the matter looks like when a customer gets mad, and the only thing you can say is: "It works for me"? 😀
After short scuffles with the customer, we finally came to an agreement.
We received screenshots of the browser console of the computer where the problem was occurring.
It turned out that our script crashed on an unhandled exception due to the stuffed up localStorage.
We can only guess how many clients of the store were affected by this issue.
However, we did not have to guess how long it took us to redesign the entire architecture to the one based on the iframe 😉.
ServiceWorker separation - a widget without the internet
When registering the ServiceWorker we can provide the "scope" parameter, which defines the scope of the website that it will control.
The scope is here the URL. However, this parameter can be used only to narrow the scope, not to extend it.
Moreover, it is not possible to register a ServiceWorker from an origin other than the origin of the registering website.
It follows, that the use of the ServiceWorker in a widget that is loaded directly into the page (without using an iframe) is much more difficult.
If we use an iframe, and the widget comes from a different origin, we can use ServiceWorkers without any problems.
History separation - watch out for routing
Each iframe has its own session history. If your widget is an extensive application, such as chat, it will likely have a routing mechanism.
The window.history object is most often used for this.
Placing a widget in an iframe guarantees that the widget will have its history independent of the history of the website. Without it, it is practically impossible to use window.history for routing, as any history change will affect the widget hosting page.
Of course, you can make a widget with a different routing, but this is an extra complication.
Security - keep an eye on your data
If the content of the frame comes from a different domain than the page on which the frame is located, then web browsers take care of security.
They don't allow data to leak between the page and the iframe.
On the other hand, secure communication using postMessage is possible, and we have control over what domains we can send messages between.
Both the iframe and the page can define the domains from which they will accept messages sent using postMessage.
Iframes also accept many security-related attributes, such as:
- allow - defines whether the iframe can use fullscreen, microphone, payment API, etc.
- referrerpolicy - sets what will be sent in the "Referrer" header when getting the frame source
- sandbox - various additional constraints for the iframe content
You can find a complete list of attributes and their values in the MDN.
Taking into account all the above-mentioned reasons, you can see that it is worth placing the widget in the iframe.
The last important advantage of this approach is the ability to choose any framework you can use to create a widget.
Since your widget will be fully separated, it doesn't matter whether you will use React, Svelte or Angular, or even vanilla Javascript.
I'm glad you got here. I hope that this small dose of theory will be useful in your work and inspire you to create new things.
If you have not read the other posts in this series yet, I invite you to read them.
Top comments (0)