Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document webpack change: Versioned shared modules for Module Federation #3757

Open
webpack-bot opened this issue May 28, 2020 · 5 comments
Open

Comments

@webpack-bot
Copy link

A pull request by @sokra was merged and maintainers requested a documentation change.

See pull request: webpack/webpack#10960


This PR brings Module Federation to a whole new level. It's a complete reimplementation and redesign of the shared modules.

TL;DR: shared modules are now based on versions.

Before this PR hosts were able to override (overridable) modules of remotes.
Hosts always had priority and 2 remotes could only share modules if the host shares it to both.
That wasn't that bad. The control was clear from host to remote.
But there were weird cases with circular host remote relationships or multiple hosts using the same remote, which allowed to break the control direction.
The system had holes...

After all I think it's best break with the old system in favor of a different approach.

The new system is based on versions of shared modules.
In group of federated builds all parts will agree on the highest version of a shared module.
On the other hand the version of the shared module will be checked against a version requirement based on semver (lite).
If allowed, multiple versions of a shared module might exist and will be consumed based on the required version.

This allows remotes to provide a higher version of a shared module to the federated app.
It also allows two remotes to share a module without the host being involved into that.
And it allows to share two different (major) version of a module, while still reusing compatible versions.
In this end remotes get more decoupled from the host and upgrade to a higher dependency version alone.

There is a singleton mode for shared modules, in which only a single version of the shared module is allowed. This version can be provided from any remote of the application. It might be a deep nested remote that requires a higher version of the shared module. This one would be used for the whole app.

You might choose to only consume a shared module, but don't provide one.
This can make sense to reduce build time and deploy size when the container is always used within a shell that provides these shared modules. E.g. for framework parts.

You can choose between strict and loose version checking for each shared module.
In strict mode a shared module won't be used if the version is not in valid range. If a fallback is provided this will be used instead.
In loose mode shared module will always used, but a warning will be printed. Fallback will only be used when no shared module is available.
When a fallback module is provided strict is the safest choice. It's also the default in this case.
When no fallback module is available, loose mode is the default, as it would thow an error otherwise.
Strict mode can still make sense for modules without fallback, if you have code in place to handle these errors.

You can make shared modules "eager", which doesn't put the modules in a async chunk, but provides them synchronously.
This allows to use these shared modules in the initial chunk. But be careful as all provided and fallback modules will always be downloaded.
There it's wise to provide it only at one point of your app, e. g. the shell.

It's still a valid a approach to wrap your entry point with import("./bootstrap"). When doing so, make sure to inline the entry chunk into the HTML for best performance (no double round trip).
This is now the recommended approach. The old "fix" no longer works as remotes could provide shared modules to the app, which requires an async step before using shared modules.
Maybe we provide some flag for the entry option in future to do this automatically.

There is an initialization phase in which all remotes (and remotes of remotes) have the chance to provide shared modules.
That means once you want to use the first shared module, this will load all remotes (if they are async loaded).
This is important as some deep nested remote might need a higher version of a shared module and it need to have a chance to provide it.

It's possible to have multiple share scopes in which modules are shared. Not sure about a use case for that yet, but it might be handy in future.
For now it's fine to always go with the default share shope.

The container interface has been changed to get and init.

init is an possible async method that is called with one argument: the share scope object. This object is used as share scope in the container and is filled with the provided modules.

To dynamically use a container:

// Initializes the share scope. This fills it with known provided modules from this build and all remotes
await __webpack_initialize_sharing__("default");
const container = window.someContainer; // or get the container somewhere else
// Initialize the container, it may provide shared modules
await container.init(__webpack_share_scopes__.default);
const module = await container.get("./module");

The container might try to provide shared modules, but if the shared module has already been used this will result in a warning and the provided shared module will be ignored. The container might still use it as fallback.

The same way you could dynamically load an A/B test, which provides some newer versions of a shared module.

Examples:

// adds react as shared module
// version is inferred from package.json
// there is no version check for the required version
// so it will always use the higher version found
shared: ["react"]
// adds moment as shared module
// version is inferred from package.json
// it will use the highest moment version that is >= 2.20 and < 3
shared: {
  "moment": "^2.20.0"
}
// adds react as shared module
// version is inferred from package.json
// it will always use the shared version, but print a warning when the shared react is < 16.7 or >= 17
shared: {
  "react": {
    requiredVersion: "^16.7.0",
    singleton: true
  }
}
// adds vue as shared module
// there is no local version provided
// it will emit a warning if the shared vue is < 2.6.5 or >= 3
shared: {
  "vue": {
    import: false,
    requiredVersion: "^2.6.5"
  }
}
// adds vue as shared module
// there is no local version provided
// it throw an error when the shared vue is < 2.6.5 or >= 3
shared: {
  "vue": {
    import: false,
    requiredVersion: "^2.6.5",
    strictVersion: true
  }
}
// adds all your dependencies as shared modules
// version is inferred from package.json in the dependencies
// requiredVersion is used from your package.json
// dependencies will automatically use the highest available package
// in the federated app, based on version requirement in package.json
// multiple different versions might coexist in the federated app
// Note that this will not affect nested paths like "lodash/pluck"
// Note that this will disable some optimization on these packages
// with might lead the bundle size problems
shared: require("./package.json").dependencies
const deps = require("./package.json").dependencies;
// use object spread to change single entries
shared: {
  ...deps,
  react: {
    requiredVersion: deps.react,
    singleton: true
  }
}
shared: {
  "my-vue": { // can be referenced by import "my-vue"
    import: "vue", // the "vue" package will be used a provided and fallback module
    shareKey: "shared-vue", // under this name the shared module will be placed in the share scope
    shareScope: "default", // share scope with this name will be used
    singleton: true, // only a single version of the shared module is allowed
    strictVersion: true, // don't use shared version when version isn't valid. Singleton or modules without fallback will throw, otherwise fallback is used
    version: "1.2.3", // the version of the shared module
    requiredVersion: ^1.0.0" // the required version of the shared module
  }
}

For requiredVersion: ^, ~, >= and exact matching is allowed. Complex ranges are not supported as it would required a significat amount of runtime code to cover that logic.

ModuleFederationRedesign

What kind of change does this PR introduce?
feature, refactoring

Did you add tests for your changes?
yes

Does this PR introduce a breaking change?
yes

What needs to be documented once your changes are merged?
a lot about shared modules in module federation, see above

@lili21
Copy link
Contributor

lili21 commented Nov 29, 2020

should update on webpack.js.org

@Domiii
Copy link

Domiii commented Dec 3, 2020

Looking forward to proper API documentation for MF 🐶

@zshlomyz
Copy link

zshlomyz commented Dec 30, 2020

Just to be sure - in case of a shared module is not marked as a singleton and the version of the module is the same at all of the modules, the shared module will be loaded only once?

@Domiii
Copy link

Domiii commented Dec 31, 2020

@zshlomyz Yes. And warnings will pop up if a different version is requested by any dependency. (Not sure how it is resolved in that case though.)

@lili21
Copy link
Contributor

lili21 commented Feb 9, 2021

how to make shared modules "eager"?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants