Microfrontends
"Microfrontends" is an architecture style that applies the ideas of "microservices" to the frontend by splitting an application into smaller parts that can be developed by different teams and can be deployed individually.
It also means that each team can use different tools and frameworks for their microfrontend without interfering with other microfrontends in the same app.
The goal
The goal for microfrontends in the context of ZUi-Web is to allow different teams to use a different version of ZUi-Web without having to know or care about which versions of ZUi-Web is used by other teams.
For the whole app this means that at runtime it should be possible to have different versions of ZUi-Web active at the same time without interfering with each other.
This is especially important because each team has it's own deployment schedule and when one team want's to update ZUi, other teams may not be affected unintentionally.
The issue
The issue is that ZUi-Web is build based on the "web components" standard and this standard does not support multiple versions of the same tag at the same time on the same page.
There is a 1 to 1 relation between tag-name and implementation class for web components.
This means that at runtime there can only be one zui-button
tag.
The reason is that web components have to be registered globally:
class Button extends HTMLElement {
...
}
// notice: registration on "window" scope
window.customElements.define("zui-button", Button)
For this reason, by default it is not possible to use multiple versions of ZUI-Web at the same time in the same app.
This is not a flaw of ZUi-Web but it's the way that the web components standard is defined. ZUi-Web is standard complient and therefore we're affected by this limitation of the standard.
One simple way to bypass this limitation would be to use <iframe>
for each microfrontend. Each iframe has it's own scope for custom elements and therefore isn't affected by this limitation.
However, iframes have other limitations and downsides and the actual projects that are using ZUi-Web (at least those that we know of) aren't using iframes and therefore need another solution.
Solution
Even though the web standard doesn't allow using the same tag twice, we still try to support usage in microfrontends as best as we can.
Starting with version 3.3.0 in addition to the normal @zeiss/zui
and @zeiss/zui-react
packages we will provide two new packages: @zeiss/zui-v
and @zeiss/zui-react-v
with the same version as the normal packages (e.g. @zeiss/zui@3.3.1
and @zeiss/zui-v@3.3.1
).
The @zeiss/zui-v
package includes all ZUi-Web components with exactly the same functionality. The only difference is that all components have a different tagName that includes the version info as suffix.
There are two "flavours" includes, one with only the major version and one with the major and minor version.
<PACKAGE_ROOT>/major
includes components likezui-button-v3
<PACKAGE_ROOT>/minor
includes components likezui-button-v3-3
In the app, you can now either use <zui-button-v3>
or <zui-button-v3-3>
as tagname. You can even use both if you like and you can install @zeiss/zui
as well and use <zui-button>
at the same time.
Notice: Components are still registered globally so you have to make sure that all components that you want to use are imported, e.g.:
import from "@zeiss/zui-v/major"
import from "@zeiss/zui-v/minor"
import from "@zeiss/zui"
Example
Let's look at an example. Say you have a microfrontend app that consists of an app-shell and two microfrontends "mf-a" and "mf-b". In the app-shell you are using "@zeiss/zui@3.3.0" with the normal tagNames (<zui-button>
).
Both microfrontends can use these tagNames as well.
Now "mf-a" want's to update to a newer version while the app-shell cannot update (yet).
In "mf-a" you can npm install @zeiss/zui-v@3.4.0
and add import from "@zeiss/zui-v/minor"
to the setup code of mf-a.
In the code of "mf-a" you can now use both <zui-button>
(which will take the 3.3.0 version) and <zui-button-v3-4>
(which will take the 3.4.0 version) at the same time.
Now imagine that a new major version of ZUi-Web is released and "mf-b" needs a new component from that release. However, there are some breaking changes that prevent the app-shell and "mf-a" from updating. They still need the v3 version.
"mf-b" can now npm install @zeiss/zui-v@4.0.0
and use <zui-fancy-new-component-v4>
.
Notice though: While possible in theory, It's not intended to mix-and-match different versions in the same microfrontend. The idea is that one microfrontend still uses one specific ZUi version but other microfrontends in the same app can use a different version and at runtime there is no clash anymore.
React
@zeiss/zui-react-v
provides react-wrapper components like before. The name of the component itself is not changed so for React users, nothing changes in the code. The react wrapper ZuiButton
is still ZuiButton
but it will reference the zui-button-v3
web component internally. The only thing that React users have to change is the import (and potentially CSS selectors and other references of actual tagNames in the code).
import { ZuiButton } from "@zeiss/zui-react-v/major";
function MyComponent() {
return <ZuiButton>Will be zui-button-v3</ZuiButton>
}
If you want to use the same component with different versions in the same react component, you have to rename the imported components, e.g.:
import { ZuiButton as ZuiButtonMajor } from "@zeiss/zui-react-v/major";
import { ZuiButton as ZuiButtonMinor } from "@zeiss/zui-react-v/minor";
function MyComponent() {
return (
<>
<ZuiButtonMajor>Will be zui-button-v3</ZuiButtonMajor>
<ZuiButtonMinor>Will be zui-button-v3-3</ZuiButtonMinor>
</>
)
}
Icons
Icons in @zeiss/zui-icons
are not effected. The versioning of the icons is handled by the ZUi design team and is independent from our own versioning cycle. All ZUi components, both from @zeiss/zui
and @zeiss/zui-v3
will still use the same normal ZUi icons without any suffix. So it's not possible to have multiple icon versions at the same time.
How to adopt?
- change the import of
@zeiss/zui
to@zeiss/zui-v
(and@zeiss-zui-react
to@zeiss/zui-react-v
in react apps) - Rename all ZUi Tags to use the tagName with suffix. We know that this can be annoying but we try to support you with this process:
We're using this Regex internally to replace all occurrences of tagNames in our code-base:
const tagName = "zui-button" //...
new RegExp(`(?<=^|[^\\w-])${tagName}(?=$|[^\\w-])`, 'g');
// will be replaced
zui-button
zui-button{ some css }
zui-button.some-class
zui-button#some-id
zui-button,div // comma in css selectors
// will not be replaced
--zui-button // css variable
--zui-button-width
zui-button-something // when part of another component name
Starting with version 3.3.0, both @zeiss/zui
and @zeiss/zui-v
are exporting a list of all tagNames via import { zuiTagNames } from "@zeiss/zui"
.
TypeScript users can also use an enumeration type and a type guard: import type { ZuiTagName, isZuiTagName } from "@zeiss/zui"
.
With these tools you should be able to (partly) automate the renaming process.
A script to automatically replace all tagNames could iterate over the zuiTagNames
array and replace each tag with the regex:
// notice: Imported from the normal zui package to get the tagNames without suffix
import { zuiTagNames } from "@zeiss/zui"
let fileContent = ... // load file content
zuiTagNames.forEach( tagName => {
const regex = new RegExp(`(?<=^|[^\\w-])${tagName}(?=$|[^\\w-])`, 'g');
fileContent = fileContent.replaceAll(regex, `${tagName}-v3`)
})
// write updated fileContent back to file
Notice: You should not blindly rename everything that starts with
zui-
because this would also effect zui-icons which don't have suffixed tag names. It would also include other things that aren't changed, for example event names (e.g. "zui-textfield-date-picker-date-selected") or attributes (e.g.<zui-thumbnail zui-internal-has-header>
, although those are marked as "internal" and shouldn't be used anyway).
Internally, we are running a script that renames all tagNames in our code-base at build-time so we don't have to change anything in our code-base at all. Whether this approach is feasible in your project too or if it's better to change the tagNames once and commit those changes is up to you.
How does this work internally?
Internally, we don't do any changes to tagNames in our code-base but instead we have implemented a script, that does this fully automated. This script iterates over all files in the codebase (TypeScript, SCSS) and does a search-and-replace-style replacement of all tagNames (See section above for the regex). The script also triggers a re-building of our react-wrappers and does some changes in the package.json files that are needed for our releases. None of these changes are committed. They are only temporary.
Our pipeline is doing this automatically and is releasing both @zeiss/zui
and @zeiss/zui-v
(and the react counterparts) automatically.
This approach, like every other approach, has some advantages and disadvantages:
Advantages
- relatively easy to implement for us
- limited risk because we don't interfere with basis DOM APIs and the Web Standard
- no runtime overhead (JS/DOM operations)
Disadvantages
- bigger effort for users to adapt. Users have to rename tags in their apps
- CSS selectors are affected
Other Solutions we looked at but didn't implement
In the web standards community there are several ideas how this problem of using different versions of the same web component on the same page could be handled. We've looked into these options but ultimately decided to not implement them (at least for now).
A) Scoped Custom Element Registries
There is a proposal for a new/adjusted web standard called "scoped custom element registries" that is discussed by the Web Standards community. This proposal offers a way to create your own custom element registries in addition to the global registry. This scoped registries are attached to a ShadowDOM and only apply to this ShadowDOM.
To use this proposal, there is a polyfill available.
See:
- https://github.com/WICG/webcomponents/blob/gh-pages/proposals/Scoped-Custom-Element-Registries.md
- https://www.npmjs.com/package/@webcomponents/scoped-custom-element-registry
- https://www.npmjs.com/package/@lit-labs/scoped-registry-mixin
Advantages
- could become a standard in the future and therefore should be kinda future-proof
- use as many different versions at the same time as you like
Disadvantages
- only works in ShadowDOM. This was a deal-breaker for us because at the moment React doesn't work well when rendered in ShadowDOM
- polyfill marked as "Work in Progress" and is not production ready
- a lot of rework needed in our code-base
- no clear timeline when this proposal will be ready
B) Placeholder elements and dynamic loading of components
Based on the polyfill for scoped-custom-elements-registries we came up with a different approach:
Instead of registering an actual component with the tagName zui-button
, we're creating a place-holder component.
This placeholder has some logic to determine which version of the component is requested. Either with an attribute on the component itself (<zui-button version="3.2.1">
) or a parent component <zui-app version="3.2.1"><zui-button>...
). Then the placeholder would load the component class with the requested version and render it.
Advantages
- No code-changes for users. They only need to wrap the whole app in a
<zui-app version="3.2.1">
tag. - No (big) code-changes in our code-base
- use as many different versions at the same time as you like. We could even implement wildcard logic, e.g.
<zui-button version="3.x">
and load the best matching version
Disadvantages
- implementation is very difficult and complex
- need to monkey-patch many standard browser-APIs (e.g.
document.createElement
,document.innerHTML
,HTMLElement
, form APIs...) which comes with a high risk of making mistakes that break other parts of apps. Potentially, browser vendors could even restrict the possibility to monkey-patch those APIs - runtime overhead with potential performance issues: Every time a custom element is rendered, we would need to check the version (DOM lookup) which would likely slow-down rendering
While we stopped following this approach, we still see some benefits and potential in it. Maybe in the future we will give this another try.