Hello all,
This proposal places itself in the context of my work on client-side component management.
See previous related discussions:
- Improve the importmap declaration to support eargerly loaded modules
- Client-side component-manager library
When working on this topic, I realized that the order in which the modules are loaded is important.
High level view
A high level view of how the components are loaded is the following:
- The component manager service is loaded, and exposed as, for instance,
"component-manager" - Javascript modules providing components import
"component-manager"and use its APIs to register components - Finally, the component manager imports
"component-manager"and call the resolve method to resolve all the registered components (e.g., only load the components with the highest priority in case same role and name)
Relevant technical concepts
Module loading
JavaScript modules are guaranteed to be loaded in their order of declaration. In the example below, moduleA is always going to be loaded before moduleB.
<script type="module" src="./src/moduleA.js"></script>
<script type="module" src="./src/moduleB.js"></script>
Dependencies resolution
An exception to this module loading order is in case of import/export dependencies between modules. In the example below, if moduleA and moduleB import framework.
The (network) loading order will be moduleA -> moduleB -> framework.
But, the execution order will be framework -> moduleA -> moduleB since A and B waits for the imported module to be available before being executed.
The console will always print
A
B
index.html
<script type="module" src="./src/moduleA.js"></script>
<script type="module" src="./src/moduleB.js"></script>
<script type="module" src="./src/framework.js"></script>
moduleA.js
import {log} from "framework";
log("A");
moduleB.js
import {log} from "framework";
log("B");
framework.js
export function log(x) {
console.log(x);
}
Proposal
In conclusion, the declaration order for the component manager service and the modules registering components is not significant. There is an explicit import/export relationship between them, and we can be sure that the service is going to be available when registering the components (steps 1 and 2 in the high level view).
What’s more difficult is to guarantee that step 3 happens strictly after step 2 (i.e., after all the components are registered).
Option 1: special case
The easiest option is to consider that this need for a given module to be loaded last is exceptional.
We declared a new UIExtension with a lower priority than JavascriptImportmapUIExtension that injects a dedicated script.
Option 2: feature
We define a feature provider/consumer concepts, where providers and consumer declared shared IDs.
When generating the scripts elements, we guarantee that all providers of a given ID are declared before their consumers.
For instance, all modules providing client-side components would have the components ID as providers, and the resolver module would have the components ID as consumer.
Cons:
- This forces all the modules providing components to declare this special ID. This feels error-prone as it is easy to forget. It is also verbose as I’m expecting most of our modules to provider components overtime
Option 3: priority
This version is close to option 2, but instead of declaring relationships between module, some modules can declare a priority (positive integer, default 1000).
Modules are sorted by their priority, then by alphabetical order of their WebJar ID (groupId:artifactId)
Pros:
- Less verbose than option 2 as only module needed a special ordering need to use this property
- More generic than option 1
Cons: - Risk of YANGNI over option 1 if it happens that we only set a priority 0 for the use case present above
- More generic than option 1, but also more complex to implement and maintain
Conclusion
+0 for option 3, unless we find other interesting use cases.
+1 for option 1 as long as we don’t find other use cases for option 3.
-1 for option 2 that seems overengineered and hard to maintain.
Thanks, WDYT?