Teil 3 - Seitenunterteilung und Routing
Dieses Lab ist Teil der Amenity Finder Serie, einer umfassenden Anleitung, die Schritt für Schritt zeigt, wie man eine Web-Applikation mithilfe von Web-Components entwickelt. In jedem Teil der Serie werden spezifische Aspekte der Entwicklung behandelt und es werden bewährte Methoden und Techniken vermittelt, um eine moderne und skalierbare Webanwendung aufzubauen.
Im dritten Teil der Serie unterteilen wir die Amenity Finder Applikation in einzelne Seiten (URLs) und implementieren das Routing. Wir sorgen dafür, dass unsere Applikation mehrere Ansichten unterstützt und der Benutzer nahtlos zwischen ihnen navigieren kann.
Vorschau
Wenn du dieses Lab bereits durchgearbeitet hast, wirst du im vierten Teil der Amenity Finder Serie lernen, wie wir die Suchseite entwickeln und die Parametrisierung implementieren, um Suchanfragen abzusetzen.
Vorbereitung
Ziele
Die Hauptziele dieses Labs sind:
- Implementierung eindeutiger URLs für jede einzelne Seite (View) der Applikation: Wir werden eine Routing-Funktionalität einrichten, die es ermöglicht, auf jede Seite der Applikation über eine eindeutige URL zuzugreifen. Dadurch können Benutzer direkt zu einer bestimmten Seite navigieren, indem sie die entsprechende URL eingeben oder einem Link folgen.
- Bereitstellung einer Navigation, die es dem Benutzer ermöglicht, zwischen den einzelnen Seiten zu navigieren: Wir werden eine Navigationsleiste oder ein Menü erstellen, die es dem Benutzer ermöglicht, auf einfache Weise zwischen den verschiedenen Seiten der Applikation zu wechseln. Dies erleichtert die Navigation und verbessert die Benutzererfahrung, indem es den Zugriff auf verschiedene Funktionen und Inhalte ermöglicht.
Branch
Der dritte Teil des Amenity Finder Labs ist auf GitHub verfügbar.
Wichtige Links
Wenn du weitere Informationen zu den behandelten Themen suchst, findest du hilfreiche Ressourcen unter den nachfolgenden Links:
Aufgabenstellung
Du hast jetzt die Möglichkeit, die Aufgabenstellung eigenständig zu lösen. Wenn du deine Lösung abgeschlossen hast oder wenn du sofort weiterlesen möchtest, stellen wir im nächsten Abschnitt unseren Lösungsvorschlag für den dritten Teil der Amenity Finder Serie vor.
Aufgaben
- Definiere Views als eigene Web-Components: Erstelle die Web-Components
<home-view>
,<search-view>
und<results-view>
, um die verschiedenen Ansichten der Applikation darzustellen. Jede dieser Komponenten repräsentiert eine spezifische Ansicht der Applikation. - Wechsel der Navigation über das
currentView
Property in der AmenityFinder-Komponente:- Binde das click-Event der
<mwc-list-item>
-Elemente an eine neue Methode. - Diese Methode sollte den Aufruf der Methode
_navigateTo(viewName)
auslösen. - Die
_navigateTo
-Methode ist dafür zuständig, den Wert descurrentView
-Properties zu aktualisieren. - Erweitere die
render()
-Methode in der AmenityFinder-Komponente um den Aufruf der Methode_renderCurrentView()
. - Abhängig vom Wert des
currentView
-Properties sollte das entsprechende Custom Element angezeigt werden.
- Binde das click-Event der
Optional
Durch diese optionale Erweiterung können die einzelnen Seiten (Views) der Applikation über eindeutige URLs abgerufen werden.
- Um das Routing zu implementieren, ist es erforderlich, das Routing-Modul Page.js zu installieren.
- Nach der Installation musst du die Routen für die verschiedenen Seiten definieren. In diesem Fall sind die folgenden Routen relevant:
- Die Startseite:
/
- Die Suchseite:
/search
- Die Ergebnisseite:
/results
- Die Startseite:
- Zum Schluss sollst du die Navigation noch mit den diesen definierten Routen verknüpfen.
Unser Lösungsvorschlag
Schritte
Das Routing der Applikation wird in vier Schritten implementiert:
- Applikation in separate Seiten unterteilen
- Routing einbauen (optional)
- Deployment für Routing anpassen (optional)
- Layout optimieren (optional)
Applikation in separate Seiten unterteilen
Unsere Applikation hat bereits eine Navigation. Wir unterscheiden darin zwischen 3 Views. Jede dieser Views soll unter einer konkreten URL erreichbar sein. Wenn ein Benutzer eine dieser URLs in seinem Browser eingibt, soll er auf dieser View in der Applikation landen. Wir definieren die folgenden Views und URLs:
View | URL | Beschreibung |
---|---|---|
Home | / | Startseite |
Search | /search | Seite für die Suche |
Results | /results | Seite für Suchresultate |
Web-Components für Views erstellen
Wir legen als Erstes für jede View eine eigene Webkomponente (Klasse) an. Damit wir zwischen wiederverwendbaren Komponenten und den Views bereits in der Ordnerstruktur besser unterscheiden können, legen wir die Klassen in einem eigenen Verzeichnis src/views
an. Für jede View erstellen wir eine Datei mit dem folgenden Inhalt und passen die Namen und den Inhalt der render()
Methode für an die jeweilige View an:
import { html, LitElement } from 'lit';
export class HomeView extends LitElement {
render() {
return html`Home…`;
}
}
customElements.define('home-view', HomeView);
Unsere Verzeichnisstruktur im src
Ordner sieht nun ungefähr so aus:
src/
└── views/
├── HomeView.js
├── SearchView.js
└── ResultsView.js
Aktuelle View in der Applikation definieren
Damit wir den Zustand für die aktuelle View in der Applikation tracken können, definieren wir ein currentView
Property. Das Default soll bei diesem Property der Wert home
sein. Als Default-View soll nämlich unsere Startseite angezeigt werden:
--- src/AmenityFinder.js
+++ src/AmenityFinder.js
static get properties() {
return {
showSidebar: { type: Boolean },
+ currentView: { type: String },
};
}
und
--- src/AmenityFinder.js
+++ src/AmenityFinder.js
constructor() {
super();
this.showSidebar = false;
+ this.currentView = 'home';
}
render() {
Nun müssen wir noch die ausgewählte View als Hauptinhalt in unserer Applikation darstellen. Dafür müssen wir in der render()
Methode den Teil:
<div>
<p>Main Content!</p>
</div>
mit dem Inhalt der ausgewählten View austauschen. Wir erstellen eine neue Methode in der AmenityFinder
Klasse _renderCurrentView()
. Diese Methode soll, abhängig vom Wert des currentView
Property, den Inhalt für die ausgewählte View liefern:
_renderCurrentView() {
switch (this.currentView) {
case 'home':
return html`<home-view></home-view>`;
case 'search':
return html`<search-view></search-view>`;
case 'results':
return html`<results-view></results-view>`;
default:
return ``;
}
}
und binden diese noch in unserer render()
Methode entsprechend ein:
--- src/AmenityFinder.js
+++ src/AmenityFinder.js
></mwc-icon-button>
<div slot="title">Title</div>
</mwc-top-app-bar>
- <div>
- <p>Main Content!</p>
- </div>
+ <main>
+ ${this._renderCurrentView()}
+ </main>
</div>
</mwc-drawer>
`;
Obwohl wir nun den Inhalt Home…
definiert im HomeView.js
sehen sollten, funktioniert dies noch nicht. Wir verwenden zwar den richtigen Tag <home-view>
. Der Browser kennt diesen Tag allerdings noch nicht, weil wir im AmenityFinder
die Komponente noch nicht importiert haben. Das holen wir mit den folgenden vier Zeilen noch nach:
--- src/AmenityFinder.js
+++ src/AmenityFinder.js
import '@material/mwc-list/mwc-list-item.js';
import '@material/mwc-icon-button';
+import './views/HomeView.js';
+import './views/ResultsView.js';
+import './views/SearchView.js';
+
export class AmenityFinder extends LitElement {
static get properties() {
return {
Nun sollten wir den Inhalt von <home-view>
in unserer Applikation sehen:
Das Wechseln der Views über Anpassungen im Programmcode sollte nun auch funktionieren. Setzen wir den Wert von currentView
zum Beispiel auf results
, sollte der Inhalt von <results-view>
angezeigt werden usw.
Damit der Wechsel der Views auch im UI funktioniert, binden wir in AmenityFinder.js
für die mwc-list-item
Elemente die Methode _navigateTo()
als click
Event Handler:
<mwc-list-item @click="${() => this._navigateTo('home')}">Home</mwc-list-item>
<mwc-list-item @click="${() => this._navigateTo('search')}">Search</mwc-list-item>
<mwc-list-item @click="${() => this._navigateTo('results')}">Results</mwc-list-item>
Und in der _navigateTo Methode setzen wir den Wert des currentView
Properties und schliessen die Sidebar:
_navigateTo(view) {
this.currentView = view;
this.showSidebar = false;
}
Routing einbauen (optional)
SPA Routing im Development-Server aktivieren
Damit unsere Routes im @web/dev-server auch bei einem Page-Load einer Unterseite funktionieren, müssen wir das appIndex
property in der Konfiguration web-dev-server.config.mjs
auf unsere Index-Datei setzen:
--- web-dev-server.config.mjs
+++ web-dev-server.config.mjs
/** Set appIndex to enable SPA routing */
- // appIndex: 'demo/index.html',
+ appIndex: 'index.html',
plugins: [
/** Use Hot Module Replacement by uncommenting. Requires @open-wc/dev-server-hmr plugin */
WICHTIG: Falls der Server am Laufen ist, muss er für das Einlesen der angepassten Konfiguration neu gestartet werden.
Routing Library auswählen
Damit wir die Logik wie das Parsen der URL, das Interpretieren der Parameter usw. nicht selber schreiben müssen, wählen wir dafür eine Library aus. Für unser Beispiel eignet sich Page.js sehr gut:
Tiny ~1200 byte Express-inspired client-side router.
Ihr könnt selbstverständlich auch eine andere Library eurer Wahl verwenden. Als Beispiel: eine mächtigere Alternative, die allerdings auch etwas mehr Komplexität mit sich bringt, ist der Vaadin Router.
Bei der Auswahl einer Library sollte stets darauf geachtet werden, wie gross resp. «schwer» sie ist. Jede Abhängigkeit, die wir in unserer Applikation einbauen, hat Auswirkungen auf die Performance und auch Ladezeit. Dies fällt einem bei schnellen Internetleitungen und grossen Desktop-Devices nicht unbedingt auf, werden die Devices und Internetanschlüsse aber kleiner und mobiler, gewinnt die Leichtgewichtigkeit von Libraries an Bedeutung. Mit Page.js sind wir mit ~6 kB (minified) resp. ~2 kB (minified + gzipped) sehr gut unterwegs.
Data-driven Routing
Das Routing in unserer Applikation soll «Data-driven» sein. Das heisst, Daten resp. der Zustand unserer Applikation definiert, was angezeigt wird. Der Router ist zuständig, um die URLs zu parsen und die Daten für die Applikation daraus abzuleiten. So stellen wir sicher, dass unsere Logik und die Komponenten vom Router entkoppelt bleiben.
Mit unserem currentView
Property und der dazugehörigen Render-Methode haben wir einen Grundstein für das «Data-driven» Routing gelegt. Nun bauen wir noch Page.js ein um die Verbindung zur URL im Browser herzustellen. Zuerst installieren wir die Abhängigkeit mit:
$ npm install page@^1.11.6
und importieren die Library in unsere Applikation:
import page from 'page';
Danach können wir, zentral an einem Ort, unsere Routes definieren. Wir erstellen dafür eine neue Methode _initializeRoutes()
und initialisieren die Routes gem. der Dokumentation von Page.js wie folgt:
_initializeRoutes() {
page('/', () => {
this.currentView = 'home';
});
page('/results', () => {
this.currentView = 'results';
});
page('/search', () => {
this.currentView = 'search';
});
page();
}
Jedes Mal, wenn im Browser die URL z. B. /results
ist, ruft Page.js den definierten Callback auf. In diesem setzen wir unsere Daten (den Wert des currentView
Property) auf den jeweils richtigen Wert für die dazugehöre View. Damit unsere erste, rudimentäre Version unseres Routing funktioniert, müssen wir die neue Methode _initializeRoutes()
noch aufrufen. Ein guter Ort dafür ist der Konstruktor:
--- src/AmenityFinder.js
+++ src/AmenityFinder.js
super();
this.showSidebar = false;
this.currentView = 'home';
+
+ this._initializeRoutes();
}
render() {
Wir können unsere URLs danach bereits testen. Wenn wir im Browser auf http://localhost:8000/search gehen, sollten wir den Inhalt unserer <search-view>
Komponente sehen, die URL http://localhost:8000/results sollte den Inhalt von <results-view>
anzeigen usw.
Navigation mit Routing verbinden
Zum Schluss wollen wir noch die Navigation in der Sidebar mit dem Routing verbinden. Bei jedem Klick auf einen Navigationspunkt soll die URL wechseln und die entsprechend richtige View angezeigt werden. Dafür erstellen wir eine neue Methode _navigateToUrl(url)
. Sie nimmt einen Parameter entgegen, die URL zu welcher navigiert werden soll, und führt über die Page.js-Library das Navigieren zur entsprechenden URL aus:
_navigateToUrl(url) {
page(url);
}
Diese Methode führen wir nun bei jedem Klick auf einen Navigationspunkt mit dem entsprechenden Wert für den url
Parameter aus:
--- src/AmenityFinder.js
+++ src/AmenityFinder.js
>
<span slot="title">Navigation</span>
<mwc-list>
- <mwc-list-item>Home</mwc-list-item>
- <mwc-list-item>Search</mwc-list-item>
- <mwc-list-item>Results</mwc-list-item>
+ <mwc-list-item @click="${() => this._navigateToUrl('/')}">Home</mwc-list-item>
+ <mwc-list-item @click="${() => this._navigateToUrl('/search')}">Search</mwc-list-item>
+ <mwc-list-item @click="${() => this._navigateToUrl('/results')}">Results</mwc-list-item>
</mwc-list>
<div slot="appContent">
<mwc-top-app-bar>
Danach sollte die Navigation über das Menu in der Sidebar bereits funktionieren. Falls euer <mwc-drawer>
das Attribut type="modal"
hat, ist noch etwas unschön, dass die Sidebar beim Navigieren nicht zugeklappt wird. Dies können wir mit ein paar einfachen Anpassungen noch ergänzen.
Sidebar beim Navigieren schliessen
Wir führen eine neue Methode _closeSidebar()
ein. Diese setzt unser showSidebar
Property auf false
und wir können sie in der _navigateToUrl
Methode noch aufrufen. Wiederverwenden können wir die neue Methode übrigens auch beim bereits gebundenem @MDCDrawer:closed
Event. Das ganze sieht dann wie folgt aus:
--- src/AmenityFinder.js
+++ src/AmenityFinder.js
render() {
return html`
- <mwc-drawer
- hasHeader
- type="modal"
- .open="${this.showSidebar}"
- @MDCDrawer:closed="${() => {
- this.showSidebar = false;
- }}"
- >
+ <mwc-drawer hasHeader type="modal" .open="${this.showSidebar}" @MDCDrawer:closed="${this._closeSidebar}">
<span slot="title">Navigation</span>
<mwc-list>
<mwc-list-item @click="${() => this._navigateToUrl('/')}">Home</mwc-list-item>
page();
}
_navigateToUrl(url) {
page(url);
+ this._closeSidebar();
+ }
+
+ _closeSidebar() {
+ this.showSidebar = false;
}
}
Unsere finale Version sieht nun etwa wie folgt aus:
Deployment für Routing anpassen (optional)
Falls wir unsere Applikation nach wie vor über Surge deployen, stellen wir fest, dass Direktlinks zu unseren im AmenityFinder
definierten Seiten (z. B. https://amenity-finder.surge.sh/search) nicht funktionieren.
Dies liegt daran, dass wir unsere Routes in unserer Single-Page-Applikation definiert haben. Diese wird im Moment nur unter /
(der Server liefert hier automatisch das index.html
aus) ausgeliefert. Wir müssen dem Server beibringen, dass alle Pfade nach dem Host jeweils an unsere Applikation, konkret das index.html
, umgeschrieben werden sollen. In der Dokumentation von Surge ist dies im Abschnitt “Adding a 200 page for client-side routing” zu finden.
Surge leitet also alle Requests auf ein File 200.html
weiter, sofern eine solche Datei existiert. Wir erstellen dafür bei jedem Build eine Kopie unseres index.html
und speichern diese als 200.html
ab. Wir können diesen Schritt mit dem folgenden postbuild
npm-Skript einbauen:
"postbuild": "cp dist/index.html dist/200.html"
Layout optimieren (optional)
Der Hauptinhalt jeder Seite klebt im Moment noch etwas am Rand. Mit einer minimalen CSS Ergänzung können wir diese kleine Unschönheit einfach beheben:
--- src/AmenityFinder.js
+++ src/AmenityFinder.js
:host {
min-height: 100vh;
}
+
+ main {
+ padding: var(--amenity-container-padding, 1rem);
+ }
`;
}
Wir verwenden dabei das --amenity-container-padding
CSS Custom Property mit einem Default-Wert von 1rem
. Wir haben so eine Variable, die wir ev. später bei weiteren Containern für das Padding wiederverwenden können. Gleichzeitig erlauben wir das Theming unserer Applikation «von aussen», ausserhalb unseres Shadow DOM.
Anwendungsbeispiel
Als Beispiel können wir (oder jemand, der unsere Komponenten oder Applikation einbindet) im index.html
das Custom Property auf einen anderen Wert setzen. Das neue Padding wird über Shadow DOM Grenzen hinweg durch die ganze Applikation propagiert:
--- index.html
+++ index.html
* @see https://material.io/develop/web/docs/theming
*/
--mdc-theme-primary: #004996;
+
+ --amenity-container-padding: 4rem;
}
</style>
<title>amenity-finder</title>
Fantastisch!
Du hast den dritten Teil der Amenity Finder Serie erfolgreich abgeschlossen. In diesem Teil haben wir die Amenity Finder Applikation in einzelne Seiten (Views) unterteilt und das Routing implementiert. Du hast gelernt, wie du mithilfe des Routers die einzelnen Seiten über eindeutige URLs aufrufen kannst. Jetzt können Benutzer nahtlos zwischen den verschiedenen Seiten der Applikation navigieren. Im vierten Teil werden wir die Suchseite inklusive Parametrisierung zum Absetzen der Suche aufbauen.