aem-boilerplate
aem-boilerplate copied to clipboard
feat: a minimal plugin and template system
Use case
As a developer, I want to be able to easily organize my code into different files, and execute some of the logic in each phase of the page load as needed.
In particular, I want a way to:
- easily define
templates
that have some JS logic - add
plugins
for more advanced features like experimentation, conversion tracking, tag management, etc. - load
templates
andplugins
only when they are needed - control in what phase a
plugin
is loaded to avoid negatively impacting performances if it is not needed immediately
Technical Requirements
-
plugins
andtemplates
should use the same logic to reduce code duplication -
plugins
andtemplates
should re-use the block loading approach to load both JS and CSS files, if applicable -
plugins
should offer a way to "override" code methods, for instance to add icon spriting on top of regular icon decoration - developers should be able to decide where exactly in each phase the plugins are run so they can control pre/post behavior according to the project needs
- performance impact should be negligible
- plugins and templates should offer a minimal API on the
window.hlx.*
namespace - the system should help in making plugins and templates easier to unit test (using mostly pure functions)
Proposal
Introduce 2 new namespaces on window.hlx.*
:
-
window.hlx.templates
-
add(id, url)
: register a new template -
get(id)
: get the template -
includes(id)
: check if a template exists
-
-
window.hlx.plugins
-
add(id, config)
: add a new plugin -
get(id)
: get the plugin -
includes(id)
: check if a plugin exists -
load(phase)
: load all plugins for the current phase (eager
,lazy
ordelayed
) -
run(method)
: runs the specified method on the plugins (loadEager
,loadLazy
orloadDelayed
)
-
Plugins and templates would follow the same API:
-
export default function init(document, options)
: logic executed immediately when the plugin/template is loaded -
export function loadEager(document, options)
: logic executed in theeager
phase -
export function loadLazy(document, options)
: logic executed in thelazy
phase -
export function loadDelayed(document, options)
: logic executed in thedelayed
phase
where this
is set to an execution context that contains most lib-franklin.js
helper functions, so we can reduce imports in the plugins (makes it easier to mock in tests, and also avoids dependency cycles).
Or the slightly more verbose version below for arrow functions:
-
export default (document, options, context) => {}
-
export const loadEager = (document, options, context) => {}
-
export const loadLazy = (document, options, context) => {}
-
export const loadDelayed = (document, options, context) => {}
ℹ️ All those exports are optional and are only executed if present.
Extracting a new loadModule(name, cssPath, jsPath, args)
method that both the loadBlock
and the new plugin system use.
Usage
Adding Templates
Add a new template to the project:
window.hlx.templates.add('foo', '/templates/foo.js');
or the shorthand version:
window.hlx.templates.add('/templates/bar.js');
or the bulk version:
window.hlx.templates.add([
'/templates/bar.js',
'/templates/baz.js'
]);
or the the module approach that loads both js and css:
window.hlx.templates.add('/templates/bar');
// loads both /templates/bar/bar.css and /templates/bar/bar.js
Adding Plugins
Add a new inline plugin to the project:
window.hlx.plugins.add('inline-plugin-baz', {
condition: () => true, // if defined, the condition is evaluated before running any code in the plugin
loadEager: () => { … },
loadLazy: () => { … },
loadDelayed: () => { … },
});
Add a regular plugin to the project that will always load (no condition
):
window.hlx.plugins.add('plugin-qux', { url: '/plugins/qux/src/index.js' });
or the module approach that loads both js and CSS:
window.hlx.plugins.add('plugin-qux', { url: '/plugins/qux' });
or the shorthand version:
window.hlx.plugins.add('/plugins/qux');
Add a plugin that only loads in the lazy phase
and also passes some additional options:
window.hlx.plugins.add('plugin-qux', {
url: '/plugins/qux/src/index.js',
load: 'lazy', // can be `eager`, `lazy` or `delayed`. defaults to `eager` if omitted
options: { corge: 'grault' },
});
Once plugins have been loaded via window.hlx.plugins.load(phase)
, we can then run individual methods as required via window.hlx.plugins.run('loadEager')
, etc.
Plugin Template
/plugins/qux.js
// document: to keep it consistent with the loadEager in the scripts.js file
// options: additional options passed to the plugin when it was added
// context: passes a plugin context which gives access to the main lib-franklin.js APIs and avoids cyclic dependencies
// it also turns all of those into pure functions that are easier to unit test
export default (document, options, context) => {
console.log('plugin qux: init', options, context);
};
export const loadEager = (document, options, context) => {
console.log('plugin qux: eager', options, context);
};
export const loadLazy = (document, options, context) => {
console.log('plugin qux: lazy', options, context);
};
export const loadDelayed = (document, options, context) => {
console.log('plugin qux: delayed', options, context);
};
~When using proper functions, the this
variable is set to the context, so we can ignore the last parameter. This is mostly for backward compatibility with existing approaches that were passing it down that way until now, but also offers a slightly shorter API.~
Or with proper functions:
/plugins/qux.js
// document: to keep it consistent with the loadEager in the scripts.js file
// options: additional options passed to the plugin when it was added
// context: passes a plugin context which gives access to the main lib-franklin.js APIs and avoids cyclic dependencies
// it also turns all of those into pure functions that are easier to unit test
export default function init(document, options, context) {
console.log('plugin qux: init', options, context);
};
export function loadEager(document, options, context) {
console.log('plugin qux: eager', options, context);
};
export function loadLazy(document, options, context) {
console.log('plugin qux: lazy', options, context);
};
export function loadDelayed(document, options, context) {
console.log('plugin qux: delayed', options, context);
};
Examples
A barebone demo site built for this PR:
- https://github.com/ramboz/helix-plugins-demo/blob/main/scripts/scripts.js#L18-L49
- https://main--helix-plugins-demo--ramboz.hlx.page/
We have a few sites that are being instrumented with this:
- https://github.com/hlxsites/wknd/pull/31
- https://github.com/adobe/helix-website/pull/405
- https://github.com/pfizer/libraryfranklinpfizer/pull/267
- https://github.com/hlxsites/petplace/pull/316
- https://github.com/BambooHR/bamboohr-website/pull/602
- https://github.com/hlxsites/bitdefender/pull/398
- https://github.com/hlxsites/danaher-ls-aem/pull/307
- https://github.com/hlxsites/mammotome/pull/559
- https://github.com/hlxsites/moleculardevices/pull/1263
- https://github.com/hlxsites/maidenform/pull/646
- https://github.com/hlxsites/24petwatch-crosswalk/pull/7
We also do have a few plugins that already follow this API:
- https://github.com/adobe/aem-experimentation
- https://github.com/adobe/aem-rum-conversion
- https://github.com/adobe/aem-martech-loader
As well as a few template mechanisms that could leverage it:
- https://github.com/hlxsites/panera/blob/94c980384caf25cf45848982e1b1b4fe433b41f8/scripts/scripts.js#L76-L80
- https://github.com/hlxsites/vg-macktrucks-com/blob/032be707c746aa121a2c0efa3f529f3c1770532f/scripts/scripts.js#L284C16-L308
Those could all be aligned on a consistent system.
Test URLs:
- Before: https://main--helix-project-boilerplate--adobe.hlx.page/
- After: https://main--helix-plugins-demo--ramboz.hlx.page/
Hello, I'm Franklin Bot and I will run some test suites that validate the page speed. In case there are problems, just click the checkbox below to rerun the respective action.
- [ ] Re-run PSI Checks
Page | Scores | Audits | |
---|---|---|---|
/ | ![]() |
Performance comparison:
At first glance, there is no noticeable impact. Plugin version is even slightly faster, but that is likely just the variance in the test
@kptdobe Shall I also port this over to aem-lib? Or should we first validate and discuss the approach here for a bit?
I think it is fine to discuss it here for now because this goes beyond aem-lib and has / should have an impact on the boilerplate. While I understand the idea, I do not really understand some of the PR content with the foo.js, bar.js, grault, qux... I assume this is not meant to be merged and is here as an example. This makes the whole PR hard to read to know what is needed, what is a sample (maybe we should create a sample project based on the PR boilerplate illustrating the extension capabilities)
While migrating aem-lib, I realised it contains mainly "helper" methods and we still have the core of the loading sequence and logic in the boilerplate. This is the key element for high performance sites and this is given to all projects as part of script.js. Also, we tell all customers this is the place they have to change stuff to cover some of their project needs, i.e. it is easy for them customise / broke the loading sequence.
I like the plugin idea if this is initially used as part of the boilerplate to protect the loading sequence but still allows some level of customisation of the initial sequence + add your own custom logic. In other terms, scripts.js should become minimal to what is fundamentally project specific (load header, load footer ?) using the plugin approach.
@kptdobe Agree on all point! And yes, the foo/bar stuff are just examples. I'll work on creating a cleaner demo, so it's easier to read
@kptdobe All done. PR should be cleaner now and the project-level logic is now in a separate repo.
I initially also wanted to move the page loading logic into the lib, and only expose hooks in the scripts.js
, but I had some pushback from @davidnuescheler so preferred to defer this to a later iteration. Also helps reduce the scope a bit.
Page | Scores | Audits | |
---|---|---|---|
/ | ![]() |
||
/?0.5814969076944914 | ![]() |
||
/?0.7093454368913195 | ![]() |
Page | Scores | Audits | |
---|---|---|---|
/ | ![]() |
||
/?0.7093454368913195 | ![]() |
||
/?0.5814969076944914 | ![]() |
Page | Scores | Audits | |
---|---|---|---|
/ | ![]() |
||
/?0.5814969076944914 | ![]() |
||
/?0.7093454368913195 | ![]() |
@davidnuescheler @rofe @kptdobe @fkakatie Would love to have your thoughts on the current proposal to see how to fine-tune this.
The current is:
- we agree on the current architecture proposal
- @ramboz and team will adjust their plugins to this architecture and deploy the whole thing to 3 projects
- if everything works as expected, we'll work on the final PR: still requires to explose this PR in 2 pieces, one for https://github.com/adobe/aem-boilerplate, and one for https://github.com/adobe/lib-aem
And we also agreed to remove the this
context hack on proper functions in favor of a more consistent use of the context
last parameter on the main exports
[WIP] Follow-up PRs:
- https://github.com/adobe/aem-lib/pull/23
- https://github.com/adobe/aem-boilerplate/pull/275
@ramboz any update on this?
@rofe We've tried it on a dozen websites so far, and it's working fine. But I remember discussions with @trieloff to try to simplify this and leverage more the CustomEvent
API. This would also align better with how the RUM library has evolved.
I'm planning on having a revised proposal accordingly… but it's not exactly at the top of my priority list at the moment.
Closing in favor of #374