uwc part 4: Dynamic element hydration
Bryan Ollendyke
Posted on July 16, 2020
haxtheweb / unbundled-webcomponents
"The magic script" - Unbundled Web components for lazy-hydration routines hitting maximal browsers
How this repo works will be broken out into a few posts on the different pieces. First, let's start with how Dynamic Element Hydration works. Let's define that as follows.
Dynamic Element Hydration - the browser noticing an undefined, web component and then injecting the definition to "hydrate" the tag in the DOM.
Because the "Magic Script" (see last post) has 2 lines to integrate, let's look at what's inside that build.js file that's so "magic"
window.process = {env: {NODE_ENV: "production"}};
var cdn = "./";
var ancient=false;
if (window.__appCDN) {
cdn = window.__appCDN;
}
window.WCAutoloadRegistryFile = cdn + "wc-registry.json";
try {
var def = document.getElementsByTagName("script")[0];
// if a dynamic import fails, we bail over to the compiled version
new Function("import('');");
// insert polyfill for web animations
var ani = document.createElement("script");
ani.src = cdn + "build/es6/node_modules/web-animations-js/web-animations-next-lite.min.js";
def.parentNode.insertBefore(ani, def);
var build = document.createElement("script");
build.src = cdn + "build/es6/node_modules/@lrnwebcomponents/wc-autoload/wc-autoload.js";
build.type = "module";
def.parentNode.insertBefore(build, def);
} catch (err) {
var legacy = document.createElement("script");
legacy.src = cdn + "build-legacy.js";
def.parentNode.insertBefore(legacy, def);
}
Breaking this file down into a few concepts
- 1st we establish the location of the wc-registry.json file via
window.WCAutoloadRegistryFile
. For our standard integrations in this article, this points to https://cdn.webcomponents.psu.edu/cdn/wc-registry.json - 2nd we attempt establish if we need polyfills using a "hack"
new Function("import('');");
.- This evaluates if a dynamic import is possible. If the platform cannot do a dynamic import, we know that we have a super old, ES5 or partial ES6 browser, something we'll go into detail in another post
- 3rd we add in an animation polyfill
- 4th we inject a
script type="module"
tag that references the wc-autoload.js web component singleton
wc-registry.json
This JSON file contains an object that is tag-name => bare import location of that tag name
. This file is generated by our gulp tooling in the repo seen here. Basically its a glob that finds web component definitions via customElements.define
and uses this to form tag name => file location
.
This means that anything in your package.json file (referenced here is accent-card
as an example) will be available in your node_modules and get roped into the unbundled routine in the end.
wc-autoload Singleton
The heavy lifting and logic of the hydration all stems from a singleton element called wc-autoload
. The injected script type="module"
portion at the end of our steps is below
var build = document.createElement("script");
build.src = cdn + "build/es6/node_modules/@lrnwebcomponents/wc-autoload/wc-autoload.js";
build.type = "module";
def.parentNode.insertBefore(build, def);
The autoloader will process this json file and create the registry. You can see the code here but it follows the following logical loop:
- Event listener for the webpage to load which then calls
window.WCAutoload.requestAvailability()
- This self-appends a a single wc-autoloader tag to the dom which is then in control of listening for changes
- in its
connectedCallback
it drops in a mutation observer
connectedCallback() {
// listen for changes and then process any new node that has a tag name
this._mutationObserver = new MutationObserver(mutations => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
this.processNewElement(node);
});
});
});
// support window target
if (window.WCAutoloadOptions) {
this.options = window.WCAutoloadOptions;
}
setTimeout(() => {
// support window target
if (window.WCAutoloadTarget) {
this.target = window.WCAutoloadTarget;
} else {
this.target = document.body;
}
// listen on the body and deep children as well
this._mutationObserver.observe(this.target, this.options);
}, 0);
}
Mutation Observer
This mutation observer defaults to monitoring document.body
for changes. This is the critical piece that makes this all work (hence, "magic"): When a change is noticed in the body, it will run processNewElement
against each addedNode
.
Example
- DOM loads
-
my-app
is noticed being added into the dom. - wc-registry.json has
{ "my-app": "@yourorg/my-app/some-location/my-app.js" }
- The observer notices the match in the registry (using another singleton called dynamic-import-registry and performs
import("@yourorg/my-app/some-location/my-app.js");
to dynamically hydrate the element
There's also logic that runs on initial load to ensure that all nodes that are undefined
at run time, yet live in the registry, are dynamically hydrated.
wheeeww
Wow, that was a lot. But we're not done with the unbundled build routine. This is how we get a common integration and default to evergreen browsers but some questions remain:
- How do we handle polyfill'ed targets (ES5 / ES6-ish)?
- How do we compile to the three targets in an unbundled manner?
- How does HAX and other applications leverage this approach in a production environment?
Next up, I'll go into how we figure out which version to ship the user on the front end and then we'll dig into how Polymer CLI is used to compile the assets into ES5, ES6, and ES8 versions.
Posted on July 16, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.