Mikrofrontend – implementujemy

Implementacja wcześniej wspomnianej idei, choć wydaje się trudna na pierwszy rzut oka, wcale taka nie jest. Zasadniczo powstaje wiele rzeczy do okoła które wymagają uwagi tj. mechanizmy do aktualizacji komponentów bez pobierania ich cały czas od nowa. Odpowiednie proxy routujące ruch na np. odpowiednie wersje endpointów czy serwisów itd.

Zajmijmy się jednak podstawową częścią. Umieścimy podstronę stworzoną w Angularze w aplikacji Vue, tak aby się ze sobą komunikowały bez przeszkód.

Część agregacyjna - Vue

W istniejącej aplikacji a'la wordpress zróby sobie komponent(ekran) dodający możliwość tworzenia artykułów:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
    <create-article></create-article>
</template>

<script>
export default {
  name: "CreateArticleComponent",
  props: {
  },
  data() {
    return {
      names: ""
    };
  }
};
</script>
W podświetlonej linii znajduje się coś dziwnego - to tag który reprezentuje webcomponent który będzie ściągany z serwisu odpowiedzialnego za tworzenie artykułów. Sam komponent Vue nie jest tak na prawdę potrzebny, ale w przypadku potrzeby przekazania jakichś extra parametrów do wewnątrz, takie wydzielone miejsce może okazać się przydatne i zadba o ład w aplikacji. Do tego wiadomo które komponenty zostały obsłużone a które nie.

Niektóre frameworki jako że implementują customowy system zdarzeń (np. React) bywają problematyczne przy tym podejściu. Dlatego postanowiłem dodać w globalnym scopie eventbusa za pomocą którego aplikacja agregacyjna Vue i komponenty w każdej innej technologii będą mogły się bez problemu porozumieć. Ja zdecydowałem się na użycie biblioteki eventbusjs i zadeklarowałem ją w ten sposób:

1
2
3
4
5
6
7
import EventBus from 'eventbusjs';
import * as authService from './services/authorizationService';

function configureBus() {
    window.EventBus = EventBus;
    window.readToken = authService.readToken;
};
Następnie przy wczytywaniu strony uruchamiana jest utworzona funkcja. Można też zauważyć kolejną funkcję której użyłem - readToken(). Pobiera ona token autoryzacyjny wykorzystywany do identyfikacji użytkownika. Za równo szyna i ta metoda są używane przez konwencję, więc należy być ostrożnym.

Część dotyczącą routingu pominę ponieważ jest standardowa. Komponent do tworzenia artykułów publikuje zdarzenie informujące o tym fakcie o nazwie create-article.publishedSuccessfully. Aby aplikacja odpowiednio zareagowała należy dodać handler, wyglądający np. tak:

1
2
3
4
5
6
function createArticleSuccessfully() {
    window.EventBus.addEventListener("create-article.publishedSuccessfully", (e) => {
        var selectedId = e.target.articleId;
        router.push({ name: 'article', params: { articleId: selectedId } });
    }, this);
}
Przeniesie on użytkownika na nowoutworzony wpis za pośrednictwem routera i innego komponentu. Z kolei ostatnią rzeczą jaka nam pozostaje do zrobienia jest dodanie adresu serwisu i pobranie komponentu potrzebnego do obsługi. Wygląda to następująco:
1
2
3
4
    window.createArticleServiceAddress = 'http://localhost:8080';
    var imported = document.createElement('script');
    imported.src = window.createArticleServiceAddress + '/createArticle.js';
    document.head.appendChild(imported);

Część komponentu - Angular

W tej części dzieje się wszystko co interesuje nas od strony działania samego komponentu. Nie będę opisywać dokładnie jego implementacji ani nawet jej umieszczać, ponieważ jest bardzo standardowa. Można stworzyć gotową aplikację wykorzystującą dependency injection i wszystkie dobrodziejstwa, przynajmniej ja nie zauważyłem jakichkolwiek problemów z ich używaniem.

Zwrócę tylko uwagę na publikowany event:

1
2
3
4
5
6
7
8
9
10
11
12
  publish() {
    this.service.postArticle(
      {
        isPublic: this.isPublic,
        text: this.outputNormal,
        title: this.title
      }).then(data => {
        window.alert("Created succesfully");
        (<any>window).EventBus.dispatch("create-article.publishedSuccessfully", data);
      });
  }
}

Pozostaje wygenerować webcomponent i umiescić go na serwerze! Tu niestety zamieszczę większy fragment konfiguracji w app.module.ts. Dotyczy on głównie zadeklarowania używanych komponentów w aplikacji angualrowej (declarations) oraz (co ważne) komponentów wejściowych (entryComponents). Na samym końcu trzeba stworzyć komponenty i zdefiniować ich nazwy. Całość przedstawia poniższy fragment - zwrócić należy uwagę na fakt że eksportujemy ich wiele, a nie tylko jeden.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@NgModule({
  declarations: [
    AppComponent,
    ArticleListComponent,
    ListedArticleComponent,
    CreateArticleComponent,
    ArticleDetailsComponent,
    EditArticleComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule
  ],
  // bootstrap: [AppComponent],
  entryComponents: [    
    ArticleListComponent,
    CreateArticleComponent,
    ArticleDetailsComponent,
    EditArticleComponent
  ],
  providers: [ArticlesService]
})
export class AppModule {
  constructor(private injector: Injector) {
  }

  ngDoBootstrap() {
    const articleListComponent = createCustomElement(ArticleListComponent, { injector: this.injector });
    const articleDetailsComponent = createCustomElement(ArticleDetailsComponent, { injector: this.injector });
    const createArticleComponent = createCustomElement(CreateArticleComponent, { injector: this.injector });
    const editArticleComponent = createCustomElement(EditArticleComponent, { injector: this.injector });
   
    customElements.define('article-list', articleListComponent);
    customElements.define('article-details', articleDetailsComponent);
    customElements.define('create-article', createArticleComponent);
    customElements.define('edit-article', editArticleComponent);
  }
}
Teraz trzeba zrobić z tego paczkę. W tym celu posłuże się znalezionym (nie pamiętam gdzie!) skryptem build-script.js, który łączy pliki wynikowe w jeden.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const fs = require('fs-extra');
const concat = require('concat');


(async function build() {
    const files = [
        './dist/articles/runtime.js',
        './dist/articles/polyfills.js',
        './dist/articles/main.js'
    ]

    await fs.ensureDir('elements');
    await concat(files,'elements/elements.js')
   
})()

Na koniec w pliku package.json dodajemy skrypt uruchamiający proces generowania webcomponentów:

1
    "elements": "ng build --prod --output-hashing=none  && node ./build-script.js"
produkcyjnie

Podsumowanie

Choć należało przejść przez wiele etapów, całość nie jest skomplikowana. Oczywiście zdaję sobie sprawę że tworzenie funkcji do pobierania tokenów autoryzacyjnych w globalnym scopie nie jest dobrą praktyką, ale jest zwyczajnie proste. Zadbać należy o rzeczy takie jak aktualizacja, przekazywanie adresów serwisów, zamiast dodawania ich na sztywno. Pomocne mogą się okazać odpowiednio skonfigurowane instancje nginx. Muszę stwierdzić jednak, że efekt końcowy choć nie powala (moje zdolności artystyczne nie istnieją), to jest całkiem przyjemny. W użytkowaniu stworzonej przez siebie aplikacji nie było jakichś trudności związanych z przekazywaniem danych. Pomocne było jednak stworzenie ściągi z publikowanymi zdarzeniami i danymi które przesyłają.

Mam też mieszane uczucia, bo i komercyjnie nie uczestniczyłem w projekcie frontendowym i jakoś tak czuję że wszystko jest nieco "połatane na ślinę", a może to przez javascript? Nieważne, polecam każdemu tego typu zabawę - osobiście sporo się dowiedziałem. A sam fakt łączenia frameworków jest na prawdę rewelacyjny z punktu widzenia dydaktycznego 🙂

Close Menu