Ember Micro-Frontends with single-spa
Tags: ember, singlespa, microfrontends, spa
Published: 2020-10-30 04:19:34

In this article, I am going to walk you through on how to create Micro-Frontends in [Ember.js](https://emberjs.com) using the [single-spa](https://single-spa.js.org) library.

We are going to create a simple application with just only two pages for the list of planets and people references in Star Wars movie. The planets page will list the planets from the films and their details. Similarly the people page will list the characters from the movies and their details. 

You can view the ember-micro-frontends in action [here](https://ember-micro-frontends.surge.sh) and the source code is hosted in this [github repo](https://github.com/ember-micro-frontends).

Before diving into the tutorial, let's make ourselves clear about the tools and technologies we are going to use.

## Micro-Frontends
Micro-Frontends enable us to combine many small apps, empowering teams to choose their technology. It is kinda like the micro-services for backend.

## Ember.js
[Ember.js](https://emberjs.com) is an opinionated, productive, battle-tested JavaScript framework for building modern web applications. It includes everything you need to build rich single page applications with an awesome community and great documentation.

## single-spa
[single-spa](https://single-spa.js.org) is a JavaScript router for front-end microservices.
With single-spa you can use multiple frameworks in a single-page application, allowing you to split code by functionality and have Angular, React, Vue.js, etc. apps all living in harmony. single-spa makes them work together and won't load them until they're needed.

## single-spa-ember
[single-spa-ember](https://single-spa.js.org/docs/ecosystem-ember) is a helper library that helps implement single-spa registered application lifecycle functions (bootstrap, mount and unmount) for for use with Ember.js. For more information, check out the github repository.

## create-single-spa
single-spa offers a [CLI](https://github.com/single-spa/create-single-spa/) for those who prefer autogenerated and managed configurations for webpack, babel, jest, etc. You can also create single-spa apps without the CLI.

## Recommended Setup by single-spa core team
The single-spa core team recommends a default setup to create micro-frontends with any framework based on a set of conventions which will help us to scale our micro frontends in the long run.

We are going to follow the below setup:
```
ember-micro-frontends
|- root-config
|- navbar
|- people
|- planets
|- styleguide
```
### root-config
This is the [host app](https://github.com/ember-micro-frontends/root-config) that manages all the other micro frontends using single-spa. 

Let's see how to create the root-config app using the CLI:
Invoke the `create-single-spa` command at the terminal
```
create-single-spa
```
It will present you with a set of options like directory, type of application. With create-single-spa you can create different type of applications like single-spa-application, in-browser utility and root-config. You can read more about the different type of modules [here](https://single-spa.js.org/docs/module-types).

For this project you have to choose the `single-spa root config` option and follow the other successive prompts.

```
? Directory for new project 
? Select type to generate (Use arrow keys)
  single-spa application / parcel
  in-browser utility module (styleguide, api cache, etc)
❯ single-spa root config
```
Once the generators are done, you will have a `src` folder with two files. `index.ejs` is the html template for our host app where we will have the placeholders for the other micro frontends. Since Ember apps need a DOM element to mount which is known as the root element we will layout the apps inside the `body` tag in this file.

```html
<template id="single-spa-layout">
      <single-spa-router>
        <nav class="topnav">
          <application name="navbar" loader="topNav" error="topNav"></application>
          <div id="navbar"></div>
        </nav>
        <div class="main-content mt-16">
          <route path="people">
            <application name="people"></application>
            <div id="people"></div>
          </route>
          <route path="planets">
            <application name="planets"></application>
            <div id="planets"></div>
          </route>
          <route default>
            <h1 class="flex flex-row justify-center p-16">
              <p class="max-w-md">
                This example project shows independently built and deployed
                microfrontends that use <a href="https://emberjs.com" target="_blank">Ember.js</a> and <a href="https://single-spa.js.org" target="_blank">single-spa</a>. Each nav link
                above takes you to a different microfrontend.
              </p>
            </h1>
          </route>
        </div>
      </single-spa-router>
    </template
```
And the next file will be `root-config.js`. This is where we will register all our Ember micro frontends using the single-spa `registerApplication` api like below:

```js
import { registerApplication, start } from "single-spa";
const planetsApp = registerApplication(
  "planets",
  () => {
    const appName = "planets";
    const appUrl = `http://localhost:4200/planets/assets/planets.js`;
    const vendorUrl = `http://localhost:4200/planets/assets/vendor.js`;
    return loadEmberApp(appName, appUrl, vendorUrl);
  },
  (location) => location.pathname.startsWith("/planets")
);
```
What the above code does is, it registers an Ember micro-frontend named `planets` with single-spa and tells the url for your `app.js` and `vendor.js` files. Since Ember uses two main Javascript assets - app and vendor we need to tell single-spa from where to load these files. Here we are running a local server for the Ember app `planets`.

### navbar
This [app](https://github.com/ember-micro-frontends/navbar) contains a navigation bar to route to the different micro-frontends based on the url.
Create the project with [ember-cli]() new command.
```
ember new navbar
```

Generate two routes for `people` and `planets`

```
ember g route people
ember g route planets
```

Create the navigation markup in `app/templates/application.hbs`
```html
 <div id="nav-wrapper" class="h-16 flex items-center justify-between px-6 bg-burnt-ember text-white">
      <div class="flex items-center justify-between">
        <span class="px-6 font-bold">Ember Micro-Frontends</span>
        <LinkTo @route="index" class="p-6">Home</LinkTo> 
        <LinkTo @route="people" class="p-6">People</LinkTo> 
        <LinkTo @route="planets" class="p-6">Planets</LinkTo> 
      </div>
      <div class="flex items-center justify-between">
        <a
          href="https://github.com/ember-micro-frontends"
          class="externalLink"
        >
          Github project
        </a>
      </div>
    </div>
{{outlet}}

```
You also have to make some changes to your Ember build  by turning off `autoRun` because Ember apps will automatically try to boot as soon as the scripts are parsed and executed. So single-spa recommends to turn off this setting in order to mount/unmount the apps dynamically during run time.

```js
module.exports = function(defaults) {
  let app = new EmberApp(defaults, {
    autoRun: false,
    storeConfigInMeta: false,
    fingerprint: {
      customHash: null
    }
  });

  app.import('node_modules/single-spa-ember/amd/single-spa-ember.js', {
    using: [
      { transformation: 'amd', as: 'single-spa-ember' }
    ]
  });

  return app.toTree();
};
```
And finally we need to hook up the lifecycles for the Ember app with the single-spa library in `app/app.js` at the end of the file.
```js
import singleSpaEmber from 'single-spa-ember';

const emberLifecycles = singleSpaEmber({
  App,
  appName: 'navbar',
  createOpts: {
    rootElement: '#navbar',
  }
});

export const bootstrap = emberLifecycles.bootstrap;
export const mount = emberLifecycles.mount;
export const unmount = emberLifecycles.unmount;
```
We have to do the above step for each Ember app we create.

### people
This [micro frontend](https://github.com/ember-micro-frontends/people) is for the people page and list the people and their details using the Starwars API [SWAPI](https://swapi.dev/).
We are going to fetch the people info from the api in the index route like below:
```js
export default class IndexRoute extends Route {
  queryParams = {
    id: {
      refreshModel: true,
    },
  };

  async model(params) {
    const response = await fetch("https://swapi.dev/api/people/");
    const _people = await response.json();
    _people.results.forEach((p,index) => p.id = ++index);
    const people = _people.results;
    const person = people.find(p => p.id == params.id);
    return { people, person };
  }
}
```

From the api, we will list out the people in a separate component and the details in other component.
```
ember g component people-list
ember g component selected-people
```
The index route template will be something like:
```html
<div>
  <div class="flex">
    <div class="p-6 w-1/3">
      {{#if this.nextPage}}
        <Button
          disabled={{this.disabled}}
          loading={{this.loading}}
          {{on "click" this.fetchPlanets }}
          >
          Fetch More Planets
        </Button>
        {{/if}}
        <PeopleList @people={{@model.people}}/>
    </div>
    <div class="w-2/3 p-6 border-l-2 border-white">
      <div class="selectedPlanet">
        <SelectedPeople @person={{@model.person}} />
      </div>
    </div>
  </div>
</div>
```
The markup for the components will be like, we generate a list of links for the index page to show the selected people information on the right.

```html
<div class="people-list">
  {{#each @people as |p|}}
  <LinkTo @route="index" @query={{hash id=p.id}} class="h-12 flex items-center border-white border-b cursor-pointer no-underline">
    {{p.name}} 
  </LinkTo>
  {{/each}}
</div>
```
And the component to show the people details will have the following markup
```html
<table>
  <tr>
    <td class="font-bold pr-6 w-40">
      Name
    </td> 
    <td>{{@person.name}}</td>
  </tr>
  <tr>
  <td class="font-bold pr-6 w-40">Height</td>
  <td>{{@person.height}}</td>
  </tr>
  <tr>
  <td class="font-bold pr-6 w-40">Mass</td>
  <td>{{@person.mass}}</td>
  </tr>
  <tr>
  <td class="font-bold pr-6 w-40">Hair color</td>
  <td>{{@person.hair_color}}</td>
  </tr>
  <tr>
  <td class="font-bold pr-6 w-40">Gender</td>
  <td>{{@person.gender}}</td>
  </tr>
  <tr>
  <td class="font-bold pr-6 w-40">Birth year</td>
  <td>{{@person.birth_year}}</td>
  </tr> 
  <tr>
  <td class="font-bold pr-6 w-40">Homeworld</td>
  <td><a href="/planets#/?id=1">{{@person.homeworld}}</a></td>
    </tr>
    <tr>
      <td>Films
      </td>
    </tr>
</table>
```

### planets
The [planets](https://github.com/ember-micro-frontends/planets) micro frontend is almost similar to the people micro frontend except it uses the planets api from SWAPI.

```js
export default class IndexRoute extends Route {
  queryParams = {
    id: {
      refreshModel: true,
    },
  };

  async model(params) {
    const response = await fetch("https://swapi.dev/api/planets/");
    const _planets = await response.json();
    _planets.results.forEach((p,index) => p.id = ++index);
    const planets = _planets.results;
    const planet = planets.find(p => p.id == params.id);
    return { planets, planet };
  }
}
```

### styleguide
This [micro frontend](https://github.com/ember-micro-frontends/styleguide) is a special one. Here we will export the Tailwind CSS library utilities to be shared among all the micro frontends for styling with the CSS utility classes from Tailwind.

```css
:root {
  --color-primary: #011627;
  --color-secondary: #8b8c8a;
  --color-danger: #e71d36;
  --color-warning: #ff9f1c;
  --color-info: #2ec4b6;
  --color-background: #182b3a;
  --color-black: #171817;
  --color-overlay: rgba(253, 255, 252, 0.8);
  --color-ember: #e04e39;
  --color-burnt-ember: #9b2918;
  --color-white: #fdfdfd;
}

@tailwind base;

@tailwind components;

@tailwind utilities;

body {
  background-color: var(--color-ember);
  color: var(--color-white);
}

nav.topnav {
  position: fixed;
  width: 100vw;
  top: 0px;
  z-index: 100;
}
```

## Running the app
Once you have done all the above things properly, you can start the Ember microfrontends and see them in action in the local environment by firing up `yarn start` from the `root-config` app.
```
yarn start
```

This will start the `webpack-dev-server` in the port 9000 by booting up the app and you can view the app in `localhost:9000`

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/4are1r8f43ro0c7xytrz.png)

## Known issues
This approach of doing Ember micro-frontends in single-spa has it's own set of challenges.

### History router
As you see in the `planets` micro-frontend we are using query params and hash-based routing in Ember because if we use the history routing in Ember, Ember will take control of the browser history and urls and this might create some problems with routing for the other micro-frontends. Hence I recommend to use hash-routing for your Ember micro-frontends if you are using with single-spa. Please let me know how we can solve the issue in the comments if you have any ideas.

### Ember Data
Another problem I have faced with Ember micro-frontends is using the `ember-data` library and the store that comes with ember-data. Because both the Ember apps have their own ember-data bindings and global objects clashing with each other I got an error something like below during navigating to other pages.

```js
TypeError: Cannot redefine property Inflector
```

### window.require.entries
One more issue I have faced while navigation to previously visited pages is that Ember will reset the module list in `window.require.entries`. Say for example, first you visit the people page, your `window.require.entries` will be populated with the modules from the people Ember app, next if you visit the product app, Ember will reset the entries of the people app and now the `window.require.entries` will only contain the modules from the planets app. So if you try to visit the people page again , you will get a blank page or an error in the console.

## Different Ember versions
I haven't tried building the micro-frontends with different Ember.js versions, I am sure that might bring its own set of problems with it. All the micro-frontends built here are using the same Ember version (3.19).

## Parting Thoughts
Even though the Ember community recommends [Ember Engines](https://ember-engines.com) for running different Ember apps within a single page, this approach of creating micro-frontends have its own set of merits. 

You are not tied with a particular framework implementation, you are free to choose any framework to add to the micro frontends here with other Ember apps. 

You don't have to redeploy other Ember apps if you change your dependencies with respect to the other micro frontends, because single-spa registers your dependencies from where you have hosted the other micro-frontends.

You can dynamically mount/unmount you apps with the [single-spa-inspector](https://single-spa.js.org/docs/devtools) browser addon provided by the single-spa team in your browser. It supports Chrome and Firefox browsers.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/lpfbsnhydv3r9mm2743n.png)

You can also override the apps at runtime inside the browser with the addon for testing any micro-frontend in your local development environments.

Please let me your thoughts in the comments about this approach of building Ember micro-frontends using single-spa and also try out single-spa for building micro-frontends in other frameworks like React, Vue, Svelte, Preact, etc.,

You can find the list of examples in this [page](https://single-spa.js.org/docs/examples)

## References:
- [single-spa](https://single-spa.js.org)
- [single-spa-ember](https://single-spa.js.org/docs/ecosystem-ember)
- [Ember.js](https://emberjs.com)
- [ember-micro-frontends](https://ember-micro-frontends.surge.sh)
- [Github source code](https://github.com/ember-micro-frontends)