Passing arbitrary data from the server to the client on page load is an essential function of modern, rich client-side applications. Over my course in various projects I've seen all kinds of setups and use cases, here is the gist of my experience.
Why Do We Need To Pass Data From The Server?
This is just one example of why a "channel" of communication between your back-end infrastructure and your JS App is required.
Trying to explain this concept to people, I often get asked "Why not make an AJAX call?". The most obvious reason is time. It would take an enormous amount of time for the page to fully load, the JS application to initialize, make the AJAX call, wait for the response and then execute the payload of the response.
Why suffer this penalty when at page-load the server already knows the answer and is also a given certainty that the client will make the question. The less obvious reason is that from a REST API perspective, how would you name that call?
How To Pass Data From The Server
There are many ways to perform that task. The most simple being to create an Object Literal in the global context which will then be parsed by your JS App.
On the back-end you need to have an entity that will perform two main functions:
- Accept data from anywhere. From any part in the execution flow you need to pass data to the JS Application, you should be able to do so fast and easily. e.g.
- Output the stored data in proper format. When rendering the template to serve to the visitor you should call the output method of the entity to get a proper output for all the stored data. e.g.
client.data.output()should output the
scripttags along with the JS Object Literal that contains the data. Take special care if you are aggressively cache the templates and or whole pages.
How To Structure The Passed Data
Creating a large Object Literal is one way to go, but it will not get you far. Let's see why...
It is generally advisable to create a "receptor" entity on the JS Application too. You want to avoid every part of your JS code accessing the global object (
GLOBAL). Here is why:
Why A Receptor Is Required
If your application is rather large, it will be difficult, if not impossible, to accurately control the order of accessing the passed data from the server. And order matters. For example if one of the information you pass to your JS App has to do with the environment (Devel, Staging, Live), you want this information to get passed as soon as possible, earlier than any other data that may exist in the Object.
As your application grows, the order of execution can become critical in ways you cannot predict right from the start.
Another reason you need a Receptor mechanism is to control the protocol of communication between the server and the JS App. As you application grows, so are your needs; and thus more sophisticated mechanisms are required. You need to have the proper abstractions in place so you can easily serve your needs as they evolve.
Ok, So How Should The Passed Data Be Structured?
Having a Receptor mechanism implies that each key in the passed data object will execute some code that will parse the data and make sense of it. Therefore it is safe to say that each key is essentially an operation waiting to happen.
There will be cases where such an operation may need to be invoked more than one times. For example if you are feeding your JS App with i18n data, you want to pass the default / core language pack and then the more specific to the current page lang pack.
All things considered an Array is a good way to go. An array containing Object Literals with two static keys:
op stands for Operation and
val for value, any value:
Assuming we have a "Receptor" in place there are two ways we can architect how this engine behaves.
- Pushing all the logic and payload inside the Receptor engine so that it knows what to do with each Operation passed and when to do it.
- Have the Receptor be agnostic of the Operation's context and plainly accept callbacks for each operation.
I know you are already sold for solution 2, no need to debate that.
So the Receptor engine should allow for external parts of the JS App to hook to specific Operations. And it should also account for the order of which these hooks are executed. This means the internal Receptor's API should look like this:
app.receptor.hook(operation, callback, priority);
How To Tie Everything Up
As we've seen in a past post "You Don't Need the DOM Ready Event" and the order that you position your elements is of paramount importance. Therefore the best way to go with tying everything up is:
- Define the global array
- Load your JS Application
- As the App evaluates, assign all the hooks to the Receptor
- After your App has finished evaluating, give the Receptor engine the go ahead and run the hooks.
Anything Ready Out There?
I got your backs!
Server 2 JS is a library created exactly for this purpose. At a ridiculous ~800bytes you get all the described functionality and more (Ready option, Garbage Collection).
If you are working on a new project, give it a try, it's well worth its weight in bytes ;)
Addressing Security Concerns
However there are a few points to be made here. The data passed inside the
script tags are developer written lines, nothing raw from an external source. The values contained in the Operations should be properly sanitized, html entities enforced.
It is one thing to talk about security, a topic I am deeply knowledgeable and sensitive about, and another thing entirely to spread Fear Uncertenty and Doubt. The fact that there are SQL Injection vulnerabilities out there does not mean we must stop writing SQL all together.
Variations of the technique described in this post are actually used by top Alexa websites, including Google, Twitter, Reddit and the list goes on. If you use CSP in your headers then that's an explicit decision that you made that will plainly force you to think of another way of performing the same, in principal, functionality that is described in this post.
Be responsible and informative, don't spread fear and excommunicate knowledge.blog comments powered by Disqus