Hemanth's Scribes

web

Dynamic Remotes, Webpack Module Federation

Author Photo

Hemanth HM

Thumbnail

The webpack module federation post resulted in many folks asking me if remote URLs need to be known at build-time? Or is there a way to specify the remotes after building, as runtime configuration somewhere.

Well, we have an easy way to load the component dynamically, here is a suggested method from the docs:

function loadComponent(scope, module) {
  return async () => {
    // Initializes the shared scope. Fills it with known provided modules
    // from this build and all remotes
    await __webpack_init_sharing__("default");
    
    const container = window[scope]; // or get the container somewhere else
    
    // Initialize the container, it may provide shared modules
    await container.init(__webpack_share_scopes__.default);
    
    const factory = await window[scope].get(module);
    const Module = factory();
    
    return Module;
  };
}

loadComponent("Comic", "XKCD");

Diving Into the Details

__webpack_init_sharing__

__webpack_init_sharing__ is defined as:

{
  __webpack_init_sharing__: {
    expr: RuntimeGlobals.initializeSharing,
    req: [RuntimeGlobals.initializeSharing],
    type: "function",
    assign: true
  }
}

Where RuntimeGlobals.initializeSharing is exported as:

/**
 * The sharing init sequence function (only runs once per share scope).
 * Has one argument, the name of the share scope.
 * Creates a share scope if not existing
 */
exports.initializeSharing = "__webpack_require__.I";

__webpack_share_scopes__

__webpack_share_scopes__ is defined as:

{
  __webpack_share_scopes__: {
    expr: RuntimeGlobals.shareScopeMap,
    req: [RuntimeGlobals.shareScopeMap],
    type: "object",
    assign: false
  },
}

shareScopeMap would be:

/**
 * an object with all share scopes
 */
exports.shareScopeMap = "__webpack_require__.S";

__webpack_share_scopes__.default would resolve into:

{
  "react-dom": {
    "17.0.2": {
      "from": "home-app",
      "eager": true,
      "loaded": 1
    }
  },
  "react": {
    "17.0.2": {
      "from": "home-app",
      "eager": true,
      "loaded": 1
    }
  }
}

__webpack_require__

In case you are more curious:

Object.keys(__webpack_require__);
// ["m", "c", "n", "d", "f", "e", "u", "g", "o", "l", "r", "nmd", "S", "I", "p"]

window[scope]

Getting back to window[scope], in this context scope is Comic:

Object.getOwnPropertyNames(window[scope]);
// ["get", "init"]

init accepts the shared scope object which is used as a shared scope in the remote container and is filled with the provided modules from a host. It can be leveraged to connect remote containers to a host container dynamically at runtime.

await window[scope].get(module) would resolve into a function that on invocation would result in the Module:

() => ((__webpack_require__(/*! ./app.jsx */ \"./app.jsx\")))
{__esModule: true, Symbol(Symbol.toStringTag): "Module"}

Module.default would be the component that is shared.

Key Points

The most important point to be mindful is that we need to load the container before attempting to dynamically connect a remote container.

So, basically it boils down to:

  1. Dynamically injecting the remote js file to the head, so that it loads.
  2. Loading the component for the given scope and module as highlighted above.
  3. (Lazy) loading the component.

P.S: The patterns in the above sandbox is heavily influenced by dynamic-system-host sample, also the sandbox example uses System custom component and is very verbose for better understanding. In the real world usage you need not be so verbose and might just have a single function that does all the three steps.

#webpack#javascript#module-federation#microfrontends
Author Photo

About Hemanth HM

Hemanth HM is a Sr. Machine Learning Manager at PayPal, Google Developer Expert, TC39 delegate, FOSS advocate, and community leader with a passion for programming, AI, and open-source contributions.