Our experiments in the code camp were heavily guided by the "Micro Frontends in Action" book (see the example code on github https://github.com/naltatis/micro-frontends-in-action-code).
Three independent teams develop the online shop Kwik-E-Mart:
- Product detail page: https://github.com/bal-code-camp-micro-frontend/kwik-e-detail
- Product list incl. recommendations and search: https://github.com/bal-code-camp-micro-frontend/kwik-e-list
- Checkout incl. mini basket and add/remove buttons: https://github.com/bal-code-camp-micro-frontend/kwik-e-checkout
The first approach was using direct links from list to the detail page. There was no checkout yet.
🔗 https://kwik-e-mart-v1-ssi-proxy.apps.baloise.dev/
- The list team exposes a recommendations fragment under /l/recommendations/:id
- The detail page includes recommendations via server side includes:
<!--#include virtual="/l/recommendations/12" -->
We had to use a workaround to include the text in the dom using thymeleaf:
<div class="col s12" th:utext="${product.recommendationFragment}"></div>
- We use nginx to perform the ssi in the project https://github.com/bal-code-camp-micro-frontend/kwik-e-proxy
server {
listen 8080;
ssi on;
location /healthz {
return 200 'woop woop!';
add_header Content-Type text/plain;
}
location /l/ {
proxy_pass https://kwik-e-list.apps.baloise.dev;
}
location /d/ {
proxy_pass https://kwik-e-detail.apps.baloise.dev;
}
location /c/ {
proxy_pass https://kwik-e-checkout.apps.baloise.dev;
}
location / {
root /opt/html;
try_files $uri $uri/ /index.html;
}
}
With this configuration we can use the same url for all applications.
🔗 https://kwik-e-mart-v2-webcomponents-proxy.apps.baloise.dev/
-> Client side composition with web components.
Provider team:
// /c/components/add-to-cart-button.js
const checkoutAddToCartButtonTemplate = document.createElement('template');
checkoutAddToCartButtonTemplate.innerHTML = `
<link href="h/c/components/css/add-to-cart-button.css" rel="stylesheet" />
<button>
<!-- ... -->
</button>
`;
class CheckoutAddToCartButton extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: 'open' });
this.shadowRoot.appendChild(checkoutShoppingCartTemplate.content.cloneNode(true));
}
/* functionality */
}
customElements.define('c-add-to-cart-button', CheckoutAddToCartButton);
User team:
<script src="/c/components/add-to-cart-button.js"></script>
<c-add-to-cart-button product-id="1"></c-add-to-cart-button>
Resulting DOM:
<c-shopping-cart id="1">
#shadow-root ==
<link href="/c/components/css/add-to-cart-button.css" rel="stylesheet">
<button>
<!-- ... -->
</button>
</c-shopping-cart>
Conventions to keep things clean:
- Every team has its own prefix (e.g. team checkout: 'c')
- Every team follows the same path structure for includes etc.
Every user of the c-add-to-cart-button
now only needs to know that the API of this micro frontend component is the attribute product-id
.
Issue: Visually big web components that need some time to initialize cause the web site flicker. Solution: Use skeletons until the actual content is displayed. The responsibility to provide a skeleton that matches the final content is responsibility of the content team. You can e.g. use SSI to include this skeleton before sending the page to the browser.
<script src="/l/components/recommendations.js"></script>
<l-recommendations product-id="4711">
<!--#include virtual="/l/skeletons/recommendations" -->
</l-recommendations>
// c-add-to-car-button onClick handler:
this.dispatchEvent(new CustomEvent('c:cart:changed', { // notice the 'c' prefix for team checkout
bubbles: true, // bubble all the way to the window element
composed: true,
detail: { productId: this.productId }
}));
// c-shopping-cart:
connectedCallback() {
window.addEventListener('c:cart:changed', this.cartChangedEventListener);
}
disconnectedCallback() {
window.removeEventListener('c:cart:changed', this.cartChangedEventListener);
}
cartChangedEventListener = (e) => {
// update shopping cart count
}
Important: Only use your team's global events. If you need to need to notify the user of one of your web components, make it part of the web component API (e.g. <my-component onSomeEvent="usersHandlerFunc()"></my-component>
)
🔗 https://kwik-e-mart-v3-appshell-proxy.apps.baloise.dev/
- Wrapped 3 developements as Webcomponents
- "Click Events" Urls propagation from single developements to upper App Shell issue: shadow dom hides the origin element of an event and returns it self