module-federation-examples icon indicating copy to clipboard operation
module-federation-examples copied to clipboard

Can the main application work without remoteEntry in development?

Open sl1673495 opened this issue 3 years ago • 16 comments

In the development environment, if I have multiple remote modules, starting them at the same time is very slow.

Since my remote modules are only render under some route path(dynamic route), Is there any hack way to make my host application works without remoteEntry.js?

sl1673495 avatar Jul 22 '21 06:07 sl1673495

Try placing the script tag for the remoteEntry.js file inside the remote component.

donovanperalta avatar Jul 22 '21 10:07 donovanperalta

Yes you can “point” to remotes deployed to higher environments or production. We do this all the time

You could also shim the remote to return an empty function for any getter. Take a look at promise new promise syntax on webpack docs. You could resolve a fake object that “exports” default with a null function or something.

You could also use try catch to fallback to something else when the import() fails

ScriptedAlchemy avatar Jul 23 '21 05:07 ScriptedAlchemy

Yes you can “point” to remotes deployed to higher environments or production. We do this all the time

You could also shim the remote to return an empty function for any getter. Take a look at promise new promise syntax on webpack docs. You could resolve a fake object that “exports” default with a null function or something.

You could also use try catch to fallback to something else when the import() fails

Thanks, I shared "react" deps with production remoteEntry in my dev environment, But it seems incorrectly use the prod version of react, which caused the error messages to be minimized. How can i avoid that?

sl1673495 avatar Aug 09 '21 09:08 sl1673495

Set your dev mode app to have react shared as eager:true to force webpack to use your local copy

ScriptedAlchemy avatar Aug 24 '21 09:08 ScriptedAlchemy

Since my remote modules are only render under some route path(dynamic route), Is there any hack way to make my host application works without remoteEntry.js?

Wouldn't it be better if we could do it on all environments without a hack? How can we access the remoteEntry.js only when the user navigates to those paths where a new Microfront end is needed for the first time?

Here we have a similar issue that was closed but it was not clear if there is a solution and what it is: https://github.com/module-federation/module-federation-examples/issues/681

skypyxis avatar Jun 28 '22 16:06 skypyxis

1: Use promise new promise if you feel stuck 2: stop statically importing code, since that will cause webpack to load remotes upfront 3: This sounds like user error, remotes only load if something is calling their import. Simple as that.

Things it might be, babel, some other plugin.

Things to try: delete everything and put this, no babel plugins other than the absolute minumum for react to no fail to build, no webpack plugins beyond the bare minimum for an app to serve, no hot reloading plugins, use the @ syntax right from MFP

const someFuntionNEverCalled = ()=>import('app1/thing')
// prevent tree shaking it
console.log(someFuntionNEverCalled)

ScriptedAlchemy avatar Jun 28 '22 22:06 ScriptedAlchemy

Created a new repo with a simplified codebase but the remoteEntry.js files are always loaded ahead, even with the "promise new promise".

Code repo: https://github.com/skypyxis/react-ts-module-federation

Does anyone have a repo where the remoteEntry.js is lazy loaded?

For some reason this still preloads remote entries:

React code:

const CounterAppOne = React.lazy(() => import('app1/CounterAppOne'));

{!showApp1 && <button onClick={() => setShowApp1(true)}>Load App 1</button>}
{showApp1 && (
      <React.Suspense fallback={'loading'}>
          <div className="box">
              <h1>APP-1</h1>
              <CounterAppOne />
          </div>
      </React.Suspense>
)}

webpack:

function lazyLoadRemote(remoteUrl, appName) {
  return `promise new Promise(resolve => {
  const script = document.createElement('script')
  script.src = '${remoteUrl}'

  console.log('lazyLoadRemote', script.src);

  script.onload = () => {
    // the injected script has loaded and is available on window
    // we can now resolve this Promise
    const proxy = {
      get: (request) => window.${appName}.get(request),
      init: (arg) => {
        try {
          return window.${appName}.init(arg)
        } catch(e) {
          console.log('remote container already initialized', e)
        }
      }
    }
    resolve(proxy)
  }
  // inject this script with the src set to the versioned remoteEntry.js
  document.head.appendChild(script);
})`;
}
    new ModuleFederationPlugin({
      name: 'container',
      remotes: {
          // app1: 'app1@http://localhost:3001/remoteEntry.js',
          // app2: 'app2@http://localhost:3002/remoteEntry.js',
          app1: lazyLoadRemote('http://localhost:3001/remoteEntry.js', 'app1'),
          app2: lazyLoadRemote('http://localhost:3002/remoteEntry.js', 'app2'),
    },
    ...

skypyxis avatar Jun 29 '22 15:06 skypyxis

Hey @ScriptedAlchemy this issue also seems similar to the one i am having. I am willing to know when a remote its down, so i am starting with baby steps and trying to use promise new Promise. But even copy and past the examples, i got the error of window.app undefined. This is my webpack config:

new ModuleFederationPlugin({
        name: "manager",
        filename: "remoteEntry.js",
        remotes: {
          fluxograma: `promise new Promise(resolve => {
            // This part depends on how you plan on hosting and versioning your federated modules
            const remoteUrl = 'http://localhost:8081/remoteEntry.js'
            const script = document.createElement('script')
            script.src = remoteUrl
            script.onload = () => {
              const proxy = {
                get: (request) => window.fluxograma.get(request),
                init: (arg) => {
                  try {
                    return window.fluxograma.init(arg)
                  } catch(e) {
                    console.log('remote container error')
                    console.log(e)
                  }
                }
              }
              resolve(proxy)
            }
            // inject this script with the src set to the versioned remoteEntry.js
            document.head.appendChild(script);
          })`,
        },
        shared: {
          vue: {
            singleton: true,
          },
        },
      }),

The code on my component:

<template>
  <button type="button" @click="loadRemote">Load Remote</button>
</template>

<script>
export default {
  methods: {
    loadRemote() {
      import("fluxograma/Tree");
    },
  },
};
</script>

Indeed my http://localhost:8081/remoteEntry.js is loaded: Captura de Tela 2022-07-13 às 22 15 11 Captura de Tela 2022-07-13 às 22 15 15 But when i try to load the exposed component i got the error:

TypeError: window.fluxograma is undefined

That is being thrown at the catch on "window.fluxograma.init(arg)".

Any clue what it may be?

schirrel avatar Jul 14 '22 01:07 schirrel

Init can only be called once. Instead of catching it. Do something like window.remote.didInit = true and just check if it was already initialized or not before calling init again.

Regarding remotes always getting called upfront. I've noticed this as of late - likely a change in webpack startup.

A dirty workaround is to resolve a fake object with no init at all and do everything in the get() so only when a exposed module is used, inject the real remote. Init it, then call get and return the got module.

ScriptedAlchemy avatar Jul 21 '22 06:07 ScriptedAlchemy

hey @ScriptedAlchemy i was doing something wrong, like a said on the issue of the enhanced, i've manage to do this work 😅

schirrel avatar Jul 21 '22 11:07 schirrel

@schirrel @ScriptedAlchemy The snipped provided by schirrel does not work the way I expected, I presume the opening post matches my use case where I'm seeing lazy loaded components eagerly loaded because of something in the webpack runtime.

In react which I am using when I use:

const Component = React.lazy(() => import('child/App'))

I would expect the "child" remote to not load until Component is rendered into the react tree. Instead I am seeing the child remote initiated by a separate runtime the call stack does not match the typical React.lazy api that is prefetching the remote. I don't think it's blocking the main app, but it's causing modules to be downloading that may never be used. Is this intentional behavior? Is there a way to turn in off and lazy load the component until it is utilized in the react tree like it does in traditional react apps?

This is the initiator call stack from a typical react app: image

This is the initiator call stack from a federated module application: image

42shadow42 avatar Aug 09 '22 15:08 42shadow42

@42shadow42 when you say "I would expect the "child" remote to not load until Component is" you mean the component or the remote entry? The snippet i post loads the remote entry, which is in fact required to load always at the start, it only dont block the app shell startup. Because it must be registred for the webpack runtime. But the component itself will only load when required. The code on my snippet is working on production and behaviors as it

schirrel avatar Aug 10 '22 16:08 schirrel

@schirrel I was thinking that the remoteEntry should not be downloaded until it is needed, I have observed that it doesn't execute until necessary, but in large apps it may not be necessary to load all bundles.

42shadow42 avatar Aug 10 '22 18:08 42shadow42

@42shadow42 the remoteEntry files are use small kbs, like an interface only and shouldn't be a problem. @ScriptedAlchemy can correct me, but without the remoteEntry the runtime cant know if it is from a remote or a lib, and so to know if is necessary to load another thing or don't.

schirrel avatar Aug 10 '22 19:08 schirrel

@schirrel Does this mean that there is another file loaded when the remote gets that includes the actual implementation? If so this is a non-issue. I just didn't understand that remoteEntry wasn't the actual bundle.

42shadow42 avatar Aug 10 '22 21:08 42shadow42

Yes @42shadow42. Look at below: my app is using a "cronograma" remote. I have from the start only the remoteEntry load (the second link is because i configured shared wrong). And only when i click to use my remote, the component itself and all its needs is loaded

https://user-images.githubusercontent.com/6757777/184043333-8b6beb89-f759-4abb-9dab-3b16ea53eb8f.mov

schirrel avatar Aug 11 '22 00:08 schirrel