Skip to content
Back to blog

Technology Selection

Comparing the isolation and sharing capabilities of iframe and module federation, and recording the Vite micro-frontend plugin selection plus the dev and build workflows.

Updated

Introduction to Micro-Frontends

There are plenty of basic introductions to micro-frontends online, so I will not repeat them here.

What I want to clarify are the two main goals of micro-frontends. No matter how the technology evolves, the ultimate goals never stray from these two:

  • Isolation: style isolation, technology stack isolation, JS runtime environment isolation, and so on.
  • Sharing: component sharing, logic sharing, page sharing, and so on.

Current tools basically all have a bias, for example:

  • iframe is a typical heavy-isolation, light-sharing approach; sharing requires reuse through UMD.
  • module federation is a typical light-isolation, heavy-sharing approach, with essentially no isolation measures, leaving the business side to handle it.

So sometimes multiple micro-frontend technologies are used together to achieve different goals.

Technology Selection

Let me briefly go over the principles of some technology stacks and the concrete implementations of the corresponding tools. After all:

  • Understanding the principles helps you better identify the scenarios a technology applies to.
  • Understanding the concrete implementation tells you whether specific details can be supported, or how to make them supported.

iframe

As an embedding technology natively supported by W3C, iframe probably needs no further introduction to its principles; you can check MDN iframe directly.

Its biggest advantage is isolation:

  • Good performance and a low barrier to use (native support).
  • A wide range of applicable scenarios (it can isolate almost anything, including CSS, JS, and even DOM events, workers, and so on).
  • Build-tool agnostic (no need to consider the services' build tools or build output differences, and no need to separately consider hot-update compatibility in dev).

For embedding legacy mess code, it is an excellent technology.

But because of its:

  • Outdated communication method (passing events back and forth, poor readability, and no corresponding types)
  • Cumbersome code sharing (requires building a UMD version, with sub-applications reading the dependency package through window)

it is rarely chosen in scenarios that emphasize sharing and communication, or other tools are used to assist, such as tRPC.

module federation

module federation is a highlight of webpack5, reusing shared code by building it separately as an entry. For detailed concepts, refer to webpack module federation.

A more down-to-earth example to explain the principle of module federation is a component library.

Essentially, a component library builds reusable code for the business to reuse, and module federation is similar: it builds the content that needs to be shared as an entry for the business to reuse. The differences are:

  • module federation needs to support dev scenarios, that is, hot updates, runtime forwarding to the corresponding module, and so on.
  • module federation needs to be loaded remotely, rather than directly installing an npm package and outputting it into the build output at build time.

The advantage of module federation is sharing: the same global window, the same dom / cssom, so there are many ways to reuse dependency packages, such as declaring them as external and using importmap to point to the same file, such as UMD modules, such as service workers sharing cache, and so on.

Correspondingly, module federation is also prone to pollution, such as undisposed events, style pollution, and so on. And once it appears, it becomes an intermittent problem: whether the corresponding module is loaded affects whether the pollution appears, or it relates to the number of loads, which is hard to troubleshoot. You need tools to record operation logs or check in advance.

Conclusion

Since the actual application scenario emphasizes sharing, module federation was the final choice.

The build tool needs to be vite rather than webpack, for the following reasons:

  • To reduce the project's transformation cost.
  • The vue community's development for a long time to come will basically be tied to vite; if vite cannot be supported, the project will decouple from the vue community and lag behind.

Tool Selection

According to the awesome-vite list provided by vite officially, and the reply to the micro-frontend feature request, the top recommendation is @originjs/vite-plugin-federation.

And judging from the GitHub homepage of the webpack module federation author, there is another option: @module-federation/vite, maintained by the @module-federation organization.

@originjs/vite-plugin-federation plugin

The issues of @originjs/vite-plugin-federation are quite active. Thanks to this, the tool's limitations can be discovered quickly, and for real-world scenarios they are basically disastrous flaws:

Because of these bugs, in terms of feature fit, development experience, usage cost, and other dimensions, this option is basically unselectable.

Digging into the root causes of these issues, most stem from coupling: the plugin's functionality depends too heavily on vite, and vite is a runtime bundless tool, which means vite's dev and prod are completely different forms, making it hard to handle build work in the dev stage. Meanwhile, because it presets a large number of configurations, the plugin has poor control over the overall build when handling the build process (preset configurations need to be re-overridden, and some cannot be overridden, such as outDir).

Therefore, it is not suitable as a module federation builder; it fails at the underlying scenario adaptation.

Because it uses bundless, many exposes cannot be obtained in dev and have to be obtained through build. Besides causing many runtime bugs, the development experience itself is not friendly enough.

So we need a builder that is decoupled from the build tool and can work independently to package the module federation expose code.

Decoupling from the build tool eliminates most of the problems.

@module-federation/vite plugin

@module-federation/vite is a tool that is decoupled from the build tool and can perform module federation builds independently.

At the bottom layer, it is derived from @angular-architects/native-federation, repackaged as @softarc/native-federation.

Through the ES Build adapter, the build tool esbuild is embedded; when actually building module federation, esbuild is used rather than vite.

So, obviously, this solution also has a problem: the corresponding plugins need to provide an esbuild-compatible version.

For this, you can use UNplugin to migrate plugins with an API similar to vite's, supporting both vite and esbuild at the same time.

If you have time, you can also refer to the ES Build adapter to build a vite/rollup adapter.

@module-federation/vite Workflow

The entire workflow needs to be understood in combination with the principles of module federation. It is divided into dev and prod modes, and the workflows are different.

prod mode

build stage

This stage mainly performs multi-entry builds, passing the dependency packages into the builder as externals. The flow is as follows:

  1. vite proceeds with the normal build process
  2. @module-federation/vite collects dependencies, including shared libs, exposes, and so on
  3. Shared libs and exposes are used as entries respectively and built into single files; the shared libs are passed into the builder as externals, while related information such as name, version, file name, and so on is collected into a manifest
  4. After the build completes, the contents of the manifest are output to remoteEntry.json

server / preview stage

  1. The project initializes remoteEntry.json through the initFederation function, including:
    1. Registering the importmap of the dependency libraries
    2. Initializing the remotes and their remoteEntry.json
  2. Enter the application initialization logic, such as initializing Vue
  3. Load the shared dependency libraries

    The dependency libraries match their addresses through importmap, pointing to the shared libs files built separately in the build stage.

    Because of the importmap used in the project, the shared dependency libraries will point to the same URL address.

    So it can be guaranteed that a dependency library is shared and executed only once.

  4. The application loads remote modules through loadRemoteModule
  5. It checks whether the corresponding service has been initialized; if not, it is initialized automatically
  6. It checks whether the corresponding service has a matching expose; if not, an error is reported; if yes, the content is obtained (through an http request) and returned

dev mode

shared lib pre-build

  1. vite proceeds with the normal build process
  2. @module-federation/vite collects dependencies, only shared libs and so on
  3. The shared libs are used as entries and built into single files; the shared libs are passed into the builder as externals, while related information such as name, version, file name, and so on is collected into a manifest
  4. After the build completes, the contents of the manifest are output to remoteEntry.json

exposes forwarding

Since exposes are not built in dev, when a request arrives for another service's exposes, it is forwarded to the corresponding service's vite server to fetch the content, and the corresponding websocket is connected at the same time.

This means the content of exposes can support hot updates.