X0r0N
X0r0N

Reputation: 1934

Webpack 5 Module Federation Failure Handling

so ive started to use webpack 5 module federation, and it works pretty well between host and microfronts.

but i noticed that if the microfrontend server is turned off, then in the host app when it tries to fetch that remoteEntry.js and fails, the host application ui will not start at all.

this is understandable if it is a dependency that is unavailable, but i think a better solution would be to have a something like a placeholder... instead of the whole application ui refusing to start because of a broken remote.

is there any solution for this? i think webpack should do a check for the remote and if it fails, it handle it gracefully.

i think its bad that a remote asset can prevent the application from running entirely, if the aim of microfrontends is separation of concern.

Upvotes: 3

Views: 8746

Answers (3)

Ragasudha
Ragasudha

Reputation: 11

Minor changes to Promised based dynamic routes can resolve the issue. Webpack Module Federation Reference: https://webpack.js.org/concepts/module-federation/#promise-based-dynamic-remotes

  1. Add a reject to the promise call and script.onerror = reject;
  2. In Module federation plugin configuration, in the shared object where we pass list of required packages like react, react-dom. Pass the versions directly rather than referring to Host app package.json dependencies.
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        app1: promise new Promise((resolve, reject) => {
          const urlParams = new URLSearchParams(window.location.search)
          const version = urlParams.get('app1VersionParam')
          // This part depends on how you plan on hosting and versioning your federated modules
          const remoteUrlWithVersion = 'http://localhost:3001/' + version + '/remoteEntry.js'
          const script = document.createElement('script');
          script.src = remoteUrlWithVersion
          script.onerror = reject;
          script.onload = () => {
            // the injected script has loaded and is available on window
            // we can now resolve this Promise
            const proxy = {
              get: (request) => window.app1.get(request),
              init: (arg) => {
                try {
                  return window.app1.init(arg)
                } catch(e) {
                  console.log('remote container already initialized')
                }
              }
            }
            resolve(proxy)
          }
          // inject this script with the src set to the versioned remoteEntry.js
          document.head.appendChild(script);
        }
      ),
    },
    shared:{
      "react": {
         "singleton": true,
         "requiredVersion": "17.0.1"
       },
       "react-dom": {
         "singleton": true,
         "requiredVersion": "17.0.1"
       }
    }
  }),
 ],
};

Upvotes: 1

Tony Tettinger
Tony Tettinger

Reputation: 11

The above answer and also the linked solution has an issue that the first rendered component will overwrite the second one.

Upvotes: 1

layonez
layonez

Reputation: 1785

UPDATE:

Actually there is a couple of different ways to handle remotes availability:

  • Promise Based Dynamic Remotes: You can pass a promise to Module Federation plugin config instead of plain url of your remote. This promise and your errors handling logic there will be resolved at runtime. It can be done as in that example:

module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        app1: `promise new Promise(resolve => {
      const urlParams = new URLSearchParams(window.location.search)
      const version = urlParams.get('app1VersionParam')
      // This part depends on how you plan on hosting and versioning your federated modules
      const remoteUrlWithVersion = 'http://localhost:3001/' + version + '/remoteEntry.js'
      const script = document.createElement('script')
      script.src = remoteUrlWithVersion
      script.onload = () => {
        // the injected script has loaded and is available on window
        // we can now resolve this Promise
        const proxy = {
          get: (request) => window.app1.get(request),
          init: (arg) => {
            try {
              return window.app1.init(arg)
            } catch(e) {
              console.log('remote container already initialized')
            }
          }
        }
        resolve(proxy)
      }
      // inject this script with the src set to the versioned remoteEntry.js
      document.head.appendChild(script);
    })
    `,
      },
      // ...
    }),
  ],
};


I think module federation plugin supposed to be used as build time dependency and not includes dynamic modules resolving and failures on purpose.

So at build time it's nice to know that some asset is not there in your app.

But if you really need to have dynamic imports and handle availability of endpoints with your MF's at runtime, than there is nice example of that in module-federation-examples repo

Or simplified version of it:

const loadScope = (url, scope) => {
   const element = document.createElement('script');
   const promise = new Promise((resolve, reject) => {
     element.src = url
     element.type = 'text/javascript'
     element.async = true
     element.onload = () => resolve(window[scope])
     element.onerror = reject
   })`enter code here`
   document.head.appendChild(element)
   promise.finally(() => document.head.removeChild(element))
   return promise
 }

const loadModule = async (url, scope, module) => {
  try {
    const container = await loadScope(url, scope)
    await __webpack_init_sharing__('default')
    await container.init(__webpack_share_scopes__.default)
    const factory = await container.get(module)
    return factory()
  } catch (error) {
    console.error('Error loading module:', error)
    throw error
  }
}

const RemoteButton = React.lazy(() => loadModule(
  'http://localhost:3002/remoteEntry.js',
  'app2',
  './Button'
))

Upvotes: 3

Related Questions