Skip to content

escalon/microfrontends

Repository files navigation

Microfrontends Demo

Introductory Slides

Overview

Microfrontends are an approach to integrate functionality of microservices not on data, but on ui. Microfrontends are non-trivial user interfaces built by independent teams which can be combined to achieve an overall business value.

This project demonstrates an application built from microfrontends, using Svelte.js and its Server-Side-Rendering (SSR) facility with hydration.

It consists of three microfrontends which run independently from each other:

  • product (express SSR/Svelte): products and product promotions

  • cart (express SSR /Svelte): shopping cart, independently updatable

  • hostingactions (express static/Svelte): product-specific actions, independently updatable

The overall infrastructure to deliver the microfrontends is made up of Spring boot applications:

  • aggregator (thymeleaf template index.html aggregates microfrontends and provides menu)

  • zuul (makes all parts available to the browser from the same host)

Aggregation

In a microfrontend architecture pages are aggregated from multiple sources and they contain several microfrontends. An aggregator can sit in front of a content management system frome where it can retrieve a site navigation menu, footer etc., but a CMS can also play the role of the aggregator itself.

Aggregation can happen in the backend or on the client. Aggregation in the backend means that the markup of html pages is assembled during delivery. Aggregation in the client means that the browser transcludes markup much like it transcludes images in html pages: The browser fetches parts of the page asynchronously and shows them when they are ready.

There are various technologies and tools which can be used for this kind of aggregation.

You can use a template engine that is able to retrieve html and place it into the page (e.g. for Java jsp or the more modern Thymeleaf), or you can set up server-side includes (SSI), microcaching or even edge side inclusion (ESI) to assemble the html page which will be delivered to the browser.

For declarative client-side transclusion of markup you can use a web component like h-include.js by @gustafnk or the pre-web-component hinclude by @mnot.

For simplicity, the aggregator service in this project uses a template-engine (Thymeleaf) to aggregate the backend part of the page.

No-framework Javascript

"Disappearing" frameworks like Svelte.js or Stencil.js allow to have no framework library in the page, instead relying on standard Javascript (with polyfills for older browsers).

This is a major win for microfrontends because several apps by several teams, written in several disappearing frameworks, can coexist on the page without risking framework version conflicts and without incurring the overhead of several big framrworks that not only need to be downloaded, but also parsed by the browser.

No-framework microfrontends can both be added alongside a classic framework application which dominates the page or they can be used inside a larger application that may or may not be written with a disappearing framework.

Server Side Rendering

Svelte components can be used for Server Side Rendering (SSR) in a node application.

While it is possible to deliver an SPA by just including some bootstrapping element on the page and letting Javascript handle the markup, Server Side Rendering is preferable for non-personal data because the initial page is complete from the start and it is not necessary to wait for Javascript to fill the DOM.

Note
Be wary with caching when delivering personalized, sensitive data in your html or api responses. You must not deliver a page containing sensitive data if it has a non-personalized URL, even if you cache only static parts in your system and assemble them immediately before delivery. You have no control over caches between your site and the user’s browser and you cannot prevent some proxy or a browser in an internet cafe to deliver cached content for a user who was not the original recipient, if the same URL produces different content for different users.
Since browsers cache https contents in the file system, you have to consider privacy issues very carefully here, it is probably best not to allow caching for sensitive data at all. Only cache static resources and use client-side aggregation without caching for all private information.

To make this work in svelte.js, you need to register the svelte compiler in the server application or tell the compiler to create a pre-built commonjs package. It will compile any imported component so that it gets a render(data) method which is used to render the component with the given data; then use the result to build the server response.

In addition, tell the component in the browser that it should hydrate the pre-rendered markup, i.e. instead of creating the markup in the DOM from scratch it should adopt the markup it encounters.

Remove unused CSS

The Svelte compiler removes unused css if you configure it not to cascade styles of a component to child components (default from Svelte 2). That makes it easy for the compiler to decide whether or not a rule is used by a component, since it knows its possible states.

It also supports preprocessing of css, e.g. with a sass or less compiler.

The project shows how Svelte removes unused styles from a larger css system. You can import the style system into your component and let Svelte figure out which parts are actually relevant for the component. See the style tag of CartStatus.html for an example.

Exporting Components

In order to export a component, add a name property to the package’s rollup configuration which becomes the namespace for all exported components:

rollup.config.js
export default {
    input: 'main.js',
    output: {
        file: pkg.main,
        format: 'iife',
        name: 'productcomponents',
        sourcemap: true
    }
}

To make the component available, add an export default statement to the file you have defined as input in rollup.config.js:

main.js
import Product from './components/Product.html';

...

export default {
    Product
};

To allow Svelte applications to import the component in uncompiled form so that they can be built with the compiler version matching that application, either add a svelte property for a single exported component to package.json:

package.json
{
  "name": "singlecomponent", // (1)
  "version": "0.0.1",
  "svelte": "src/MyComponent.html"  // (2)
}
  1. name of the package for imports

  2. MyComponent.html in the /src folder becomes importable as import MyComponent from 'singlecomponent'

or a 'svelte.root' property for multiple exported components:

package.json
{
  "name": "cartcomponents", // (1)
  "version": "0.0.1",
  "svelte.root": "components"  // (2)
}
  1. name of the package for imports

  2. Cart.html in the /components folder becomes importable as import Cart from 'cartcomponents/Cart.html', likewise CartStatus.html and AddToCart.html from the same folder.

See pkg.svelte for more details.

Using Svelte Component in the Browser

When building a Svelte app that uses an external Svelte component, you have to decide whether you want to bundle the component with your app or pick up the component from the browser page at runtime.

If you bundle the external component, it will become part of your Svelte application package. If you need a new version of the external component, you will have to update the component’s version in your application package and build a new version of your application.

If you pick up the external component from the page, the external component can be updated independently of your application, although it appears inside your application.

In order to facilitate long-term caching of a bundle, you can use rollup-plugin-hash.

As Bundled Dependency

Bundling an external component is simple: add the package to package.json so that it will be imported from node_modules, make sure the build finds the component there (e.g. by adding the rollup-plugin-resolve plugin to your rollup.config.js) and write an import statement in your component as usual that references the component in the external package.

import ExternalComponent from 'othercomponent/ExternalComponent.html'
Note
If the external component package has a pkg.svelte or pkg['svelte.root'] entry, the compiler will use the raw html file of the external component for compilation, see Exporting Components. Therefore it is strongly recommended that packages which export components define such an entry. Otherwise the external component will be used in compiled form, which introduces a certain risk of Svelte version conflicts. When compiling components from other packages containing styles that need to be preprocessed, e.g. with sass, it is necessary to add the required preprocessor to your build.

As External Dependency

Picking up the component from the browser page requires that you add a <script src="othercomponent/bundle.js" /> tag to the page. The othercomponent package must of course export the desired components in a distribution suitable for the browser as described in Exporting Components. When the othercomponent/bundle.js script is executed, it will add the exported components to the page in the namespace of the othercomponent bundle.

Your application’s bundling configuration must be told that it should not try to resolve the component from node_modules, rather it should treat it as external dependency and look for it in the global context.

rollup.config.js
{
    input: 'main.js',
    output: {
        file: pkg.browser,
        format: 'iife',
        sourcemap: true
    },
    external: ['hostingactions/EmailAction.html'], // (1)
    globals: {
        'hostingactions/EmailAction.html' : 'hostingactions.EmailAction' // (2)
    }
}
  1. Tells rollup that the component imported as hostingactions/EmailAction.html is a runtime dependency

  2. Tells rollup the identifier it should use to inject the dependency from the browser page, must match the name under which the component is exported from the component module.

Using Svelte Component in SSR

In this demo we let the compiler create a commonjs bundle of the application. See the server.js examples in product, cart and hostingactions for examples.

Creating SSR bundles

The commonjs bundles are created by defining separate build output definitions in rollup.config.js:

rollup.config.js
{
    input: 'CartApp.html',
    output: {
        file: 'dist/ssr/bundle.js',
        format: 'cjs', //(1)
        sourcemap: true,
    },
    external: ['svelte/store.js'],
    plugins: [
        svelte({
            hydratable: true,
            store: true,
            generate: 'ssr', // (2)
            cascade: false,
            // ...
        }),
        // ...
    },
  1. Tells rollup to create a commonjs module

  2. Lets the svelte compiler create components with the SSR api, i.e. with a render() method instead of the browser api.

Svelte Store with SSR

The Svelte Store establishes a common application status across components, which is especially useful when the application runs in the browser. The components refer to data elements in the store where needed.

Using the Svelte Store is possible with SSR too. There are two options. You can enable the store using the SSR option store: true when registering the component and pass the store to the render() method as shown below.

server.js
require('svelte/ssr/register')({
    store: true
}); // enable store on svelte compiler
...
const store = new Store({items: []});
server.get('/cart/', function(req, res) {
    res.write(`
    <!DOCTYPE html>
    <div id="cart">${app.render({}, {store})}</div>
    <script src="/cart/bundle.js"></script>
  `);
    res.end();
});

As an alternative, a component can define declaratively through an import statement that it needs the store, so that an app need not know that its components require a store internally. That is the technique we use in this demo. For examples see AddToCart.html and store.js in cartcomponent.

AddToCart.html
import store from '../store.js' // provides a global store instance bound to sessionstorage
export default {
    store: () => store,
    tag: 'add-to-cart'
}

Svelte Components as Custom Elements

Add a tag property to each component you want to use as custom-element and assign a kebab-case tag name with at least one hyphen in it.

components/Product.html
<script>
    export default {
        tag: 'product-card'
    };
</script>

Since custom elements v1 must be real class files, they cannot be compiled to ES5. That can be achieved by telling buble not to transform classes. Also tell the svelte compiler to create custom components using the customElement option:

rollup.config.js
  plugins: [
    svelte({
      customElement: true
    }),
    buble({transforms: {classes: false}})

For more customElement options see the documentation of the svelte compiler.

URLs

Zuul Proxy

Allows to deliver html page, app bundles and api resources from a common URL. http://localhost:8888/cart/
http://localhost:8888/product/

Aggregator

Aggregates the page with containing microfrontends. For the purposes of this demo, we use thymeleaf as shown below.

src/main/templates/index.html
<div th:replace="http://localhost:8888/cart"></div>
<div th:replace="http://localhost:8888/product"></div>
Warning
By default, Thymeleaf throws an error when a remote template source is not available. For production you would have to adjust that by customizing the url template resolver.

Building

Javascript

For local development and testing you need to create npm links to the cartcomponents and hostingactions module after npm install. Since npm v5 this has to be repeated after npm install.

Tip
In a real-life scenario you would keep those modules in a private or public NPM registry. In that case npm link is not necessary unless you develop several modules simultaneously.

The cart app depends on cartcomponents, the product app depends on cartcomponents and hostingactions.

# make packages linkable
$ cd cartcomponents
$ npm link
$ cd ../hostingactions
$ npm link

# link in packages
$ cd ../cart
$ npm link cartcomponents
$ cd ../product
$ npm link cartcomponents
$ npm link hostingactions

When the dependencies are linked, you can build cartcomponents, cart, hostingactions and product or continuously watch and build them:

# build once
$ npm run build

# continously watch and build
$ npm run watch

Java

Building involves packaging and creating docker images. Make sure Docker is running before you execute Maven:

mvn install

Running

Locally

Run the Java artifacts with the Spring profile 'dev' to make them connect with locally running Node instances. To run them with Maven, cd into aggregator and zuul respectively and execute:

$ mvn spring-boot:run -Dspring-boot.run.profiles=dev

cd into product, cart and hostingactions respectively and execute

$ npm start

The applications should start and listen on the ports 3005 - 3007.

Open the browser:

localhost:8888
localhost:8888/webcomponent

The webcomponent url shows an example where the cart microfrontend is included as a webcomponent.

Docker

To build and push docker images from aggregator and zuul, make sure docker is running, then cd into the respective directories and execute

$ mvn dockerfile:build
$ mvn dockerfile:push

Docker for Windows requires Windows 10 Professional or Enterprise 64 bit with enabled Hyper-V.

Important
On Windows 10 it is necessary to run docker-compose in a standalone terminal window, not in an embedded IDE terminal (notably VSCode or Webstorm), where you will get an IOError: [Errno 0]. See docker/compose#5019
$ docker-compose -f docker/common/docker-compose.yml up
$ docker-compose -f docker/common/docker-compose.yml down

Acknowledgements

Thanks to:

Roadmap

  • Client-side transclusion of Svelte SSR microfrontends

  • Git submodules for cartcomponents and hostingactions so that npm link is not required for building and running

  • Config for dev-only execution of product (exposes ../hostingactions/dist/hostingactions as static resource), should not do that in production

  • Use real ReST backend to store changes

  • use rollup dynamic import to dynamically choose product panel per product type, actions on action panel - choosing with #if means new actions must be added statically before they can be used (is it possible to do it fully dynamically at all? What will be in the DOM?), and choosing the available actions should be up to the services team which writes the launchpad. Two level of ifs:

    1. in product list: product → launchpad type, e.g. hosting launchpad

    2. in action launchpad: product conditions → action type, e.g. if product.domain then EmailAction

  • let each panel retrieve its own data both in SSR and browser when activated - performance issue?

  • styling concepts

    • Master style vs. no master style.

      With master style there is a css which dominates the page. A component cannot isolate itself against that, unless it uses shadow DOM.

      Without master style the pattern lab reigns only over parts which want to be reigned (e.g. classes describing the parts of the page and style which only applies to those), but apps can choose to apply the pattern lab by building their own css prefixed with their own signature class.

    • Style which applies to elements having predefined classes like BEM or Bootstrap makes it necessary to apply those classes inside the component markup, is there an alternative which would allow to process the master css so that it becomes part of the component style, applies to the component markup and is stripped down to the styles really needed by the component?