Description
Summary
As part of trying out ideas to enable multiple Blazor WebAssembly apps to co-exist on the same page (a la micro frontends as described in #38128), one thing that would be helpful is getting access to the Blazor
and DotNet
JavaScript objects setup as part the Blazor WASM startup script (blazor.webassembly.js) through a means other than the global window
object. Ideally these objects should be retrievable when the Blazor WASM startup script is loaded dynamically, such as when lazy loading a Blazor WASM application in cases like (but not limited to) Blazor-based micro frontends.
Motivation and goals
As I mentioned in my comments here and here, my main motivation for supporting Blazor-based MFEs is to enable teams I work with who are more comfortable with Blazor to continue developing Blazor-based frontend modules that plug into an app shell at runtime in the browser to form one cohesive app portal experience. At the same time, I want to enable teams to build modules using JS-based technologies in those situations where it's more beneficial, such as when the module needs to interop heavily with JS-based components (e.g., Mapbox for maps, Highcharts for time series strip charts, Tableau for dashboards, and Cesium for geo-spatial visualization).
While I understand #38128 has been pushed out of the .NET 8 timeline, I wanted to continue investigating on my end things my teams can do in the short term to make Blazor-based MFEs more feasible. As a proof of concept, I made a set of patches to the Blazor WASM startup script (available on GitHub here), and I've been able to get some basic Blazor-based MFEs working (with a demo app published on GitHub here).
One constraint I see with my current approach is that I need to ensure that I only load one Blazor WASM app at a time, and by "load" I mean import the app's Blazor WASM startup script. The reason is I need to grab a reference to the Blazor
and DotNet
objects that correspond to a particular MFE so that I can properly manage its lifecycle when I mount and unmount that MFE. I believe the one-at-a-time limitation exists due to a possible race condition I call out in my demo app here, but the basic idea is that if I try to load more than one startup script at the same time, I could possibly end up in a situation where one MFE's script will overwrite window.Blazor
and window.DotNet
with its runtime objects before my code has had a chance to capture the corresponding references for the other MFE.
If there were a way to get these references other than through the global namespace, my belief is that this potential race condition can get mitigated.
In scope
Given a URL to a Blazor-based MFE's startup script, I would ideally like to dynamically load it from JavaScript like so and, in the process, get the Blazor
and DotNet
objects created when the startup script is executed by the browser:
const blazorStartup = await import(blazorStartupScriptUrl);
const mfeBlazor = blazorStartup.Blazor;
const mfeDotNet = blazorStartup.DotNet;
// Do some other MFE init, followed by...
await mfeBlazor.start(/* arguments typically passed to Blazor.start */);
I think the ideal way of exposing this would be if the Blazor startup script were a true JS module that exported the Blazor
and DotNet
objects. This would allow JS code to resolve those via a dynamic import using the import()
syntax.
Out of scope
I'm really only considering Blazor WASM apps at this point. Other uses of Blazor really aren't in scope with this request (and I'm not sure how relevant they would be here).
Risks / unknowns
One possible challenge of switching the startup script to a JS module is that <script>
tags would need to specify the type="module"
attribute to ensure the script is executed as a JS module. This would likely be a breaking change for existing Blazor WASM apps.
Just as a straw man, a couple mitigations may include:
- Provide a separate build of the script (e.g., blazor.webassembly.esm.js) that is basically the same script with exports for the
Blazor
andDotNet
objects included. - Make the existing blazor.webassembly.js script simply a shim that dynamically imports the ESM variant. It sounds like based on the last paragraph on dynamic imports described here on MDN, a dynamic import of an ES module should be possible in "classic script" environments within a browser. I'm hand waving a bit here, but that should help avoid breaking existing Blazor WASM applications.
Examples
See the snippet above to get a basic idea of how I would roughly expect this import mechanism to work. For a more complete context of where I would ideally like to use this capability, the bootstrap function in my demo app offers a decent example here. In that example, I'm reading the Blazor
and DotNet
objects from the window
object, but getting them from the module object resolved from the dynamic import of the Blazor startup script would be the ideal outcome.