docs/02-architecture/06-client-side-architecture.md
The JS app can be split into distinct parts:
And then specific Javascript bootstraps used on different pages and required into the app when needed - Article, Article Minute, Crosswords, Liveblog, Gallery, Trail, Profile, Sudoku, Image content, Facia, Football, Preferences, Membership, Ophan, Admin, Main Media, Video Embed, Accessibility.
See below for quick descriptions.
We use polyfill-fastly.io to polyfill many things, the selected polyfills are set in the polyfill.io file.
We use aliases to refer to groups of polyfills. This seems to have disappeared from their documentation, so a list of aliases has been compiled. This was generated by calling listAliases on the now deprecated polyfill-library.
We compile using the webpack babel loaded and the present-env preset. No settings or custom targets are loaded for this.
The inline blocking JS is in the head of the document and will block render until it has finished executing. Each of these scripts are required to render the page correctly on first paint.
Contains the config for the curl AMD module loader. You can find the aliases for certain JS modules in here such as fastdom which helps you avoid having to add the full path for certain modules.
The shouldEnhance JS defines whether we should run the enhanced Javascript.
There are ways to force enhancement, so take a look at the JS, but something of note is that we don't enhance devices running iOS < 8 on any pages and don't enhance iPads on Fronts.
If you put guardian.config in your console, you will see the JS config containing information about analytics, modules, switches, tests and the page. This config object is initially populated serverside and topped-up client side (i.e.: information about the user is only available client-side)
The initial config structure is defined in config.scala.js and javaScriptConfig.scala.js.
In config.scala.js we also set the isModernBrowser flag which is our 'cuts the mustard test' comprising of:
"querySelector" in document
&& "addEventListener" in window
&& "localStorage" in window
&& "sessionStorage" in window
&& "bind" in Function
&& (("XMLHttpRequest" in window && "withCredentials" in new XMLHttpRequest())|| "XDomainRequest" in window)
The commercial JS will only run if isModernBrowser is true.
You'll most likely be using the page config the most often which is defined in JavascriptPage.scala. Be aware that the metadata for a particular page may be overridden with MetaData.make.
Note that if you want to use config in a JS module, you shouldn't use the window object, but include it directly in your module via the config.js utility.
Choose how the browser should render the page before any painting begins. applyRenderConditions.js.
Applies classes to the document based on support including svg, flexbox and replaces js-off with js-on.
Described at the top of the loadFonts.scala.js:
bypass normal browser font-loading to avoid the FOIT.
works like this:
do you have fonts in localStorage?
A util that borrows heavily from loadCSS, it loads CSS async so that non-critical CSS doesn't block rendering.
We polyfill the async attribute to prevent parser blocking by creating a script element and inserting it into the page. This is the main app.js which contains standard and enhanced JS.
Note that in Dev we get curl and require boot.js immediately here but on Prod we concatenate curl and boot.js with bootstraps/standard
Double Note: in head.scala.html we use a link element with prefetch attr and href set to app.js so that we head off and fetch it as early as possible
<link rel="prefetch" href="@Static("javascripts/app.js")">
This is where we would ping cloudwatch.. if we had anything to ping about..
The getUserData util puts the current logged-in user's data in the JS config ready to be used by other JS.
Secrets.
Puts the user's username into the header (increases perceived rendering speed as their username is there before the app.js has to download and parse).
The prepareCmp JS is a stub that will be enhanced by the cmp module loaded in the Commercial bootstrap. Importantly, prepareCmp will create a command queue so that calls to the CMP can be processed once it is fully loaded. it will also define the stub postMessage handler for cross-origin iframe requests.
Gets the Ophan browserId which is used across analytics to tie data together.
The analytics for Dotcom are defined in analytics/base.scala.html.
In javascripts/bootstraps we define all the entry points for each bundle described in tools/tasks/compile/javascript/bundle.js.
The top level entry points which call the bootstrap initialisation of all other bundles are enhanced/main.js, standard/main.js, admin.js (for frontend.gutools, not theguardian.com), commercial.js and video-embed.js (initialised when there is a video embed from videoEmbed.scala.html).
boot: {
options: {
name: 'boot',
out: options.staticTargetDir + 'javascripts/boot.js',
include: 'bootstraps/standard/main',
insertRequire: ['boot'],
exclude: [
'text',
'inlineSvg'
]
}
}
app: {
src: [
options.staticSrcDir + 'javascripts/components/curl/curl-domReady.js',
options.staticTargetDir + 'javascripts/boot.js'
],
dest: options.staticTargetDir + 'javascripts/app.js'
}
To quote the file:
This file is intended to be downloaded and run ASAP on all pages by all readers.
While it's ok to run code from here that requires specific host capabilities, it should manage failing gracefully by itself.
Assume nothing about the host...
This also means you should think very hard before adding modules to it, in particular 3rd party modules.
For this file, performance and breadth of support should take priority over anything…
The standard main.js does a few core things:
The boot.js is the main entry point for the app.
Again, to quote the file:
This module is responsible for booting the application. It is concatenated with curl and bootstraps/standard into app.js
We download the bundles in parallel, but they must be executed sequentially because each bundle assumes dependencies from the previous bundle.
Once a bundle has been executed, all of its modules have been registered. Now we can safely require one of those modules.
Unfortunately we can't do all of this using the curl API, so we use a combination of ajax/eval/curl instead.
Bundles we need to run: commercial + enhanced
Only if we detect we should run enhance.
It uses promises to require and init, in blocking order, standard JS, commercial JS and enhanced JS. As mentioned above it is bundled into app.js and the insertRequire option is used to insert a require call for it.
The commercial JS is its own bundle and is executed immediately after the standard JS.
The main entry point for enhanced JS is in bootstraps/enhanced/main.js
Here we initialise the rest of our Javascript application, requiring the bundles expected by the current page.
For example, we use the page config property of isFront to load facia.js:
// Front
if (config.page.isFront) {
require(['bootstraps/enhanced/facia'], function (facia) {
bootstrapContext('facia', facia);
});
}
or check if there isMedia or a video or audio element exists on the page in order to require the mainMedia JS:
if ((config.isMedia || qwery('video, audio').length) && !config.page.isHosted) {
require(['bootstraps/enhanced/media/main'], function (media) {
bootstrapContext('media', media);
});
}
Each bundle is created via the bundle.js config, e.g.:
facia: {
options: {
name: 'bootstraps/enhanced/facia',
out: options.staticTargetDir + 'javascripts/bootstraps/enhanced/facia.js',
exclude: [
'boot',
'bootstraps/standard/main',
'bootstraps/commercial',
'bootstraps/enhanced/main',
'text',
'inlineSvg'
]
}
}
and each must return an init function that is called from enhanced.js once required.
All the enhanced bootstraps are in bootstraps/enhanced, where you'll be able to see what each bootstrap initialises.
Finally, to tell curl - the module loader - where to fetch a bundle from, when hashed, you will need to add it to the curlConfig.scala.js:
'bootstraps/enhanced/facia': '@Static("javascripts/bootstraps/enhanced/facia.js")'
Run make compile and add assets.useHashedBundles=true to your devOverrides in frontend.conf to test the bundle as it would run on PROD.
There are five projects in the Javascript architecture:
Contains vendor JS from the likes of formstack and the polyfill.io fallback.