Some best practices regarding web component architecture, development, building and publishing.
- Use as little tooling as possible.
- ES modules only.
- Consumable from a CDN.
- Consumable as an NPM package that can be included in JavaScript bundles built from tools like Vite, Rollup, Webpack, etc.
- Each technology should reside in a separate file:
- Much of this is simplified if condensed to one JavaScript file, but then development becomes less enjoyable without the niceties provided by modern editors.
- Deprecation of HTML Imports also makes this more of a hassle than it needs to be.
The structure outlined here is to develop with a separation of concerns, and the separation entails distinct files for each technology:
- HTML
- CSS
- JavaScript/TypeScript
Each component can consist of multiple files and possibly more if you include test files with the source code.
src/
my-component/
styles.css
template.html
element.js
defined.js
my-other-component/
styles.css
template.html
element.js
defined.js
- Your run-of-the-mill CSS file.
- Includes any styling needed by the component's
ShadowRoot
. - Get's imported via a
<link>
insidetemplate.html
orfetch
ed insideelement.js
andprepend
ed as a<style>
element to the component's corresponding template.
- An HTML file with one parent
<template>
element. - Includes any elements and
<slot>
's used by the component. - Gets
fetch
ed insideelement.js
and cloned into the component's shadow DOM.
- A JavaScript or TypeScript file that defines a
class
whichextends
anHTMLElement
. - Defines the lifecycle callbacks invoked for the component's reactions.
- Uses top level
await
to delay execution of the component by dependent modules until the component'stemplate.html
(and possiblystyles.css
) has beenfetch
ed and parsed.
- A JavaScript or TypeScript file that
import
s theclass
fromelement.js
and serves to encapsulate, or call out the side-effect of registering the component in the global scope. - Calls
define
to register the component with theCustomElementRegistry
. - Uses top level
await
to delay execution of the component by dependent modules until the promise returned fromCustomElementRegistry.whenDefined
resolves with the component's constructor. - Optionally accepts and parses query parameters on the import specifier via
import.meta.url
that allows consumers todefine
custom names (or other attributes) for the element before being added to theCustomElementRegistry
.
When publishing a web component to a registry like npm, a script located in the bin directory can be used to move static assets to their appropriate location. Usually the statics will be served by a CDN or web server/reverse-proxy like Nginx. The published script can be used by clients in their CD pipeline when building the application using the web component.
It may be desirable to add a bit of tooling, particularly a bundler that supports loading of static assets like HTML or CSS, to eliminate extra HTTP requests to load such assets. The goal should still be to keep each particular technology in a separate file allowing separation of concerns and all that entails like maintainabiilty, readability, and developer experience. Using a bundler to include the HTML and CSS associated with a web component also negates the need for providing scripts to copy/move these assets for consumers of the published package to a location suitable to their web server.
It should be noted that proper use of HTTP caching can be leveraged to minimize the overhead of additional requests for static assets used by a web component for return visitors if you choose to eschew bundlers altogether.
There is an example of the architecture, development, build, and publication of HTML custom elements outlined in this document at https://github.com/morganney/youtube-vid.
That particular example chooses to use Vite to bundle the HTML and CSS associated with the custom element to eliminate extra HTTP requests as mentioned in the tradeoffs section above. However, an example of a script that can be used to allow consumers to copy the static assets to a location of their choosing is available if your implementation doesn't use a bundler. The original implementation was not using a bundler, but ultimately the decision was made to bundle them with the custom element's JavaScript to not only remove the extra HTTP requests, but also to better colocate the statics with the element's class definition which was not possible by the consuming application's targeted framework (Next.js) without additional scripting.
This is how the consuming application was previously using the provided scripts to copy the statics: https://github.com/morganney/morgan.neys.info/commit/9771143e1c7c7e6f82baf0a11948cba5a1304c3f#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519R12.