Going Buildless

Avatar of Pascal Schilp
Pascal Schilp am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Ich führe eine Fernbeziehung. Das bedeutet, ich fliege alle paar Wochen nach England, und jedes Mal im Flugzeug denke ich darüber nach, wie schön es wäre, ein paar Reddit-Posts zu lesen. Ich könnte eine Reddit-App finden, die es mir erlaubt, Posts offline zu cachen (ich bin sicher, es gibt eine da draußen), *oder* ich könnte die Gelegenheit nutzen, etwas selbst zu schreiben und Spaß daran zu haben, die neuesten und besten Technologien und Webstandards zu verwenden!

Darüber hinaus gab es viel Diskussion über das, was ich gerne *going buildless* nenne, was meiner Meinung nach eine wirklich faszinierende Entwicklung ist, bei der Produktionsprojekte ohne Build-Prozess (wie z. B. einen Bundler) erstellt werden.

Dieser Beitrag ist auch eine Hommage an einige großartige Leute in der Web-Community, die großartige Dinge ermöglichen. Ich werde all diese Dinge verlinken, während wir fortschreiten. Beachten Sie, dass dies kein Schritt-für-Schritt-Tutorial sein wird, aber wenn Sie den Code überprüfen möchten, können Sie das fertige Projekt auf GitHub finden.

Unser Endergebnis sollte ungefähr so aussehen

Lassen Sie uns eintauchen und ein paar Abhängigkeiten installieren

npm i @babel/core babel-loader @babel/preset-env @babel/preset-react webpack webpack-cli react react-dom redux react-redux html-webpack-plugin are-you-tired-yet html-loader webpack-dev-server

Ich scherze.

Wir werden nichts davon verwenden.

Wir werden versuchen, so viele Werkzeuge und Abhängigkeiten wie möglich zu vermeiden, um die Einstiegshürde niedrig zu halten. Was wir verwenden werden, ist

  • LitElement – LitElement ist unser Komponentenmodell. Es ist einfach zu bedienen, leichtgewichtig, nah an der Hardware und nutzt Webkomponenten.
  • @vaadin/router – Dies ist ein sehr kleiner (< 7 KB) Router mit einer *fantastischen* Entwicklererfahrung, den ich nur wärmstens empfehlen kann.
  • @pika/web – Dies hilft uns, unsere Module für die einfache Entwicklung zusammenzuführen.
  • es-dev-server – Dies ist ein einfacher Entwicklungsserver für moderne Webentwicklungs-Workflows, erstellt von uns bei open-wc. Obwohl jeder HTTP-Server ausreicht, können Sie gerne Ihren eigenen mitbringen.

Das ist alles! Wir werden auch ein paar Browser-Standards verwenden, nämlich: ES-Module, Web Components, Import Maps, KV-Storage und Service Worker.

Lassen Sie uns nun unsere Abhängigkeiten installieren

npm i -S lit-element @vaadin/router
npm i -D @pika/web es-dev-server

Wir werden auch einen postinstall-Hook zu unserer package.json hinzufügen, der Pika für uns ausführt

"scripts": {
  "start": "es-dev-server",
  "postinstall": "pika-web"
}

🐭 Pika

Pika ist ein Projekt von Fred K. Schott, das darauf abzielt, die nostalgische Einfachheit von 2014 in die Webentwicklung von 2019 zu bringen. Fred macht lauter großartige Dinge. Zum einen hat er pika.dev entwickelt, mit dem man einfach nach modernen JavaScript-Paketen auf npm suchen kann. Er hielt auch kürzlich seinen Vortrag Reimagining the Registry auf der DinosaurJS 2019, den ich Ihnen sehr empfehlen kann.

Pika geht noch einen Schritt weiter. Wenn wir pika-web ausführen, werden unsere Abhängigkeiten als einzelne JavaScript-Dateien in ein neues Verzeichnis web_modules/ installiert. Wenn Ihre Abhängigkeit einen ES "Modul"-Einstiegspunkt in ihrem package.json-Manifest exportiert, unterstützt Pika dies. Wenn Sie transitive Abhängigkeiten haben, erstellt Pika separate Chunks für jeden gemeinsamen Code zwischen Ihren Abhängigkeiten.

Das bedeutet, in unserem Fall wird unsere Ausgabe etwa so aussehen

└─ web_modules/
    ├─ lit-element.js
    └─ @vaadin
        └─ router.js

Prima! Das war's. Wir haben unsere Abhängigkeiten bereit, als einzelne JavaScript-Moduldateien, und das wird uns später in diesem Beitrag das Leben sehr erleichtern, also bleiben Sie dran!

📥 Import Maps

In Ordnung! Nachdem wir nun unsere Abhängigkeiten sortiert haben, machen wir uns an die Arbeit. Wir erstellen eine index.html, die etwa so aussehen wird

<html>
  <!-- head, etc. -->
  <body>
    <reddit-pwa-app></reddit-pwa-app>
    <script src="./src/reddit-pwa-app.js" type="module"></script>
  </body>
</html>

Und reddit-pwa-app.js

import { LitElement, html } from 'lit-element';

class RedditPwaApp extends LitElement {

  // ...

  render() {
    return html`
      <h1>Hello world!</h1>
    `;
  }
}

customElements.define('reddit-pwa-app', RedditPwaApp);

Wir haben einen guten Start. Versuchen wir mal zu sehen, wie das bisher im Browser aussieht, also starten wir unseren Server, öffnen den Browser und... Was ist das? Ein Fehler?

Oh je.

Und wir haben kaum angefangen. Okay, schauen wir uns das mal an. Das Problem hier ist, dass unsere Modulspezifizierer *nackt* sind. Es sind *nackte Modulspezifizierer*. Das bedeutet, dass keine Pfade angegeben sind, keine Dateiendungen, sie sind einfach... ziemlich nackt. Unser Browser weiß nicht, was er damit anfangen soll, also wird ein Fehler ausgelöst.

import { LitElement, html } from 'lit-element'; // <-- bare module specifier
import { Router } from '@vaadin/router'; // <-- bare module specifier

import { foo } from './bar.js'; // <-- not bare!
import { html } from 'https://unpkg.com/lit-html'; // <-- not bare!

Natürlich könnten wir dafür Werkzeuge verwenden, wie webpack, rollup oder einen Entwicklungsserver, der die nackten Modulspezifizierer in etwas Bedeutsames für Browser umschreibt, damit wir unsere Imports laden können. Aber das bedeutet, wir müssen eine Menge Werkzeuge einführen, uns mit der Konfiguration auseinandersetzen, und wir versuchen, hier minimal zu bleiben. Wir wollen einfach nur Code schreiben! Um dies zu lösen, werden wir uns Import Maps ansehen.

Import Maps ist ein neuer Vorschlag, der es Ihnen ermöglicht, das Verhalten von JavaScript-Importen zu steuern. Mit einer Import Map können wir steuern, welche URLs von JavaScript import-Anweisungen und import()-Ausdrücken abgerufen werden, und diese Zuordnung kann in Nicht-Import-Kontexten wiederverwendet werden. Das ist aus mehreren Gründen großartig

  • Es ermöglicht, dass unsere nackten Modulspezifizierer funktionieren.
  • Es bietet eine Fallback-Auflösung, sodass import $ from "jquery"; versuchen kann, zuerst zu einem CDN zu gehen, aber auf eine lokale Version zurückgreifen kann, wenn der CDN-Server ausfällt.
  • Es ermöglicht das Polyfilling von (oder andere Kontrolle über) eingebauten Modulen. (Mehr dazu später, halten Sie sich fest!)
  • Löst das Problem verschachtelter Abhängigkeiten. (Lesen Sie diesen Blogbeitrag!)

Klingt ziemlich gut, oder? Import Maps sind derzeit in Chrome 75+ hinter einem Flag verfügbar, und mit diesem Wissen im Hinterkopf gehen wir zu unserer index.html und fügen eine Import Map zu unserem <head> hinzu

<head>
  <script type="importmap">
    {
      "imports": {
        "@vaadin/router": "/web_modules/@vaadin/router.js",
        "lit-element": "/web_modules/lit-element.js"
      }
    }
  </script>
</head>

Wenn wir zurück zu unserem Browser gehen und unsere Seite aktualisieren, werden keine Fehler mehr angezeigt, und wir sollten unser <h1>Hello world!</h1> auf unserem Bildschirm sehen.

Import Maps ist ein unglaublich interessanter neuer Standard und definitiv etwas, das Sie im Auge behalten sollten. Wenn Sie daran interessiert sind, damit zu experimentieren und Ihre eigene Import Map basierend auf einer yarn.lock-Datei zu generieren, können Sie unser open-wc import-maps-generate Paket ausprobieren und herumspielen. Ich bin wirklich gespannt, was die Leute in Kombination mit Import Maps entwickeln werden.

📡 Service Worker

Okay, wir werden ein wenig in der Zeit vorspringen. Wir haben unsere Abhängigkeiten zum Laufen gebracht, unseren Router eingerichtet und einige API-Aufrufe gemacht, um Daten von Reddit zu erhalten und sie auf unserem Bildschirm anzuzeigen. Die Durchsicht des gesamten Codes sprengt den Rahmen dieses Beitrags, aber denken Sie daran, dass Sie den gesamten Code im GitHub Repository finden können, wenn Sie die Implementierungsdetails lesen möchten.

Da wir diese App erstellen, um Reddit-Threads im Flugzeug lesen zu können, wäre es *großartig*, wenn unsere Anwendung offline funktionieren würde und wir irgendwie Posts zum Lesen speichern könnten.

Service Worker sind eine Art JavaScript-Worker, der im Hintergrund läuft. Sie können ihn sich als sitzend zwischen der Webseite und dem Netzwerk vorstellen. Immer wenn Ihre Webseite eine Anfrage stellt, geht diese zuerst durch den Service Worker. Das bedeutet, wir können die Anfrage abfangen und etwas damit machen! Zum Beispiel können wir die Anfrage zum Netzwerk durchlaufen lassen, um eine Antwort zu erhalten, und sie cachen, wenn sie zurückkommt, damit wir diese gecachten Daten später verwenden können, wenn wir vielleicht offline sind. Wir können auch einen Service Worker verwenden, um unsere Assets *vorab zu cachen*. Das bedeutet, wir können alle kritischen Assets vorab cachen, die unsere Anwendung benötigt, um offline zu funktionieren. Wenn wir keine Netzwerkverbindung haben, können wir einfach auf die gecachten Assets zurückgreifen und trotzdem eine funktionierende (wenn auch offline) Anwendung haben.

Wenn Sie daran interessiert sind, mehr über Progressive Web Apps und Service Worker zu erfahren, empfehle ich Ihnen *dringend*, The Offline Cookbook von Jake Archibald sowie diese Video-Tutorial-Serie hier von Jad Joubran zu lesen.

Lassen Sie uns einen Service Worker implementieren. In unserer index.html fügen wir folgenden Ausschnitt hinzu

<script>
  if ('serviceWorker' in navigator) {
    window.addEventListener('load', () => {
      navigator.serviceWorker.register('./sw.js').then(() => {
        console.log('ServiceWorker registered!');
      }, (err) => {
        console.log('ServiceWorker registration failed: ', err);
      });
    });
  }
</script>

Wir fügen auch eine Datei sw.js im Stammverzeichnis unseres Projekts hinzu. Wir werden also die Assets unserer App vorab cachen, und hier hat Pika uns das Leben wirklich leicht gemacht. Wenn Sie sich den Install-Handler in der Service-Worker-Datei ansehen

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHENAME).then((cache) => {
      return cache.addAll([
        '/',
        './web_modules/lit-element.js',
        './web_modules/@vaadin/router.js',
        './src/reddit-pwa-app.js',
        './src/reddit-pwa-comment.js',
        './src/reddit-pwa-search.js',
        './src/reddit-pwa-subreddit.js',
        './src/reddit-pwa-thread.js',
        './src/utils.js',
      ]);
    })
  );
});

Sie werden feststellen, dass wir die volle Kontrolle über unsere Assets haben und eine schöne, klare Liste von Dateien haben, die wir zum Offline-Betrieb benötigen.

📴 Offline gehen

Richtig. Nachdem wir nun unsere Assets für den Offline-Betrieb gecacht haben, wäre es hervorragend, wenn wir tatsächlich einige Posts speichern könnten, die wir offline lesen können. Es gibt viele Wege, die nach Rom führen, aber da wir ein wenig am Puls der Zeit leben, entscheiden wir uns für: KV-Storage!

📦 Eingebaute Module

Es gibt ein paar Dinge zu besprechen. KV-Storage ist ein eingebautes Modul. Eingebaute Module sind sehr ähnlich zu regulären JavaScript-Modulen, außer dass sie mit dem Browser ausgeliefert werden. Es ist gut zu beachten, dass eingebaute Module zwar mit dem Browser ausgeliefert werden, sie aber *nicht* im globalen Geltungsbereich verfügbar sind und mit std: benannt sind (Ja, wirklich). Dies hat einige Vorteile: Sie verursachen keinen zusätzlichen Overhead beim Start eines neuen JavaScript-Laufzeitkontextes (z. B. eines neuen Tabs, Workers oder Service Workers) und verbrauchen keinen Speicher oder CPU, es sei denn, sie werden tatsächlich importiert, und vermeiden Namenskollisionen mit vorhandenem Code.

Ein weiterer interessanter, wenn auch etwas kontroverser Vorschlag für ein eingebautes Modul ist das std-toast-Element und das std-switch-Element.

🗃 KV-Storage

Okay, damit ist das aus dem Weg geräumt. Lassen Sie uns über KV-Storage sprechen. KV-Storage (oder "Key-Value Storage") basiert auf IndexedDB und ist recht ähnlich zu localStorage, abgesehen von nur wenigen *wesentlichen* Unterschieden.

Die Motivation für KV-Storage ist, dass localStorage synchron ist, was zu schlechter Performance und Synchronisierungsproblemen führen kann. Es ist auch ausschließlich auf String-Schlüssel/Wert-Paare beschränkt. Die Alternative, IndexedDB, ist... schwer zu bedienen. Der Grund, warum es so schwer zu bedienen ist, ist, dass es vor Promises existiert und dies zu einer, nun ja, ziemlich schlechten Entwicklererfahrung führt. Kein Spaß. KV-Storage hingegen ist viel Spaß, asynchron und einfach zu bedienen! Betrachten Sie das folgende Beispiel

import { storage, /* StorageArea */ } from "std:kv-storage";

(async () => {
  await storage.set("mycat", "Tom");
  console.log(await storage.get("mycat")); // Tom
})();

Beachten Sie, wie wir von std:kv-storage importieren? Dieser Import-Spezifizierer ist ebenfalls nackt, aber in diesem Fall ist es in Ordnung, weil er tatsächlich mit dem Browser ausgeliefert wird.

Ziemlich schick. Wir können das perfekt verwenden, um einen "Für offline speichern"-Button hinzuzufügen und einfach die JSON-Daten für einen Reddit-Thread zu speichern und sie abzurufen, wenn wir sie brauchen.

// reddit-pwa-thread.js:52:
const savedPosts = new StorageArea("saved-posts");

// ...

async saveForOffline() {
  await savedPosts.set(this.location.params.id, this.thread); // id of the post + thread as json
  this.isPostSaved = true;
}

Wenn wir also jetzt auf den "Für offline speichern"-Button klicken und zum DevTools "Application"-Tab gehen, sehen wir ein kv-storage:saved-posts, das die JSON-Daten für diesen Beitrag enthält

Und wenn wir zu unserer Suchseite zurückkehren, haben wir eine Liste gespeicherter Beiträge mit dem gerade gespeicherten Beitrag

🔮 Polyfilling

Ausgezeichnet. Wir werden hier jedoch auf ein weiteres Problem stoßen. Am Puls der Zeit zu leben ist schön, aber auch gefährlich. Das Problem, auf das wir hier stoßen, ist, dass kv-storage zum Zeitpunkt des Schreibens nur in Chrome hinter einem Flag implementiert ist. Das ist nicht gut. Glücklicherweise gibt es ein Polyfill, und gleichzeitig können wir eine weitere wirklich nützliche Funktion von Import Maps vorstellen: Polyfilling!

Zuerst installieren wir das kv-storage-polyfill

npm i -S kv-storage-polyfill

Beachten Sie, dass unser postinstall-Hook Pika erneut für uns ausführt.

Fügen wir außerdem Folgendes zu unserer Import Map in unserer index.html hinzu

<script type="importmap">
  {
    "imports": {
      "@vaadin/router": "/web_modules/@vaadin/router.js",
      "lit-element": "/web_modules/lit-element.js",
      "/web_modules/kv-storage-polyfill.js": [
        "std:kv-storage",
        "/web_modules/kv-storage-polyfill.js"
      ]
    }
  }
</script>

Was hier passiert ist, dass immer wenn /web_modules/kv-storage-polyfill.js angefordert oder importiert wird, der Browser zuerst versucht zu sehen, ob std:kv-storage verfügbar ist; wenn dies jedoch fehlschlägt, wird stattdessen /web_modules/kv-storage-polyfill.js geladen.

Also im Code, wenn wir importieren

import { StorageArea } from '/web_modules/kv-storage-polyfill.js';

Das wird passieren

"/web_modules/kv-storage-polyfill.js": [                 // when I'm requested
    "std:kv-storage",                      // try me first!
  "/web_modules/kv-storage-polyfill.js"    // or fallback to me
]

🎉 Fazit

Und wir sollten jetzt eine einfache, funktionierende PWA mit minimalen Abhängigkeiten haben. Es gibt ein paar Kleinigkeiten an diesem Projekt, über die wir uns beschweren könnten, und sie wären wahrscheinlich alle berechtigt. Zum Beispiel hätten wir wahrscheinlich auf die Verwendung von Pika verzichten können, aber es macht uns das Leben wirklich leicht. Man könnte das Gleiche über das Hinzufügen einer Webpack-Konfiguration sagen, aber man hätte den Punkt verfehlt. Der Punkt ist hier, eine unterhaltsame Anwendung zu erstellen, während man einige der neuesten Features nutzt, ein paar Schlagworte streut und eine niedrige Einstiegshürde hat. Wie Fred Schott sagen würde: "Im Jahr 2019 sollten Sie einen Bundler verwenden, weil Sie es wollen, nicht weil Sie es müssen."

Wenn Sie an Pedanterie interessiert sind, können Sie diese großartige Diskussion über die Verwendung von Webpack vs. Pika vs. buildless lesen, und Sie werden einige großartige Einblicke von Sean Larkinn vom Webpack-Core-Team selbst sowie von Fred K. Schott, dem Erfinder von Pika, erhalten.

Ich hoffe, Sie haben diesen Blogbeitrag genossen, und ich hoffe, Sie haben etwas gelernt oder einige neue interessante Leute zum Folgen entdeckt. Es gibt viele aufregende Entwicklungen in diesem Bereich, und ich hoffe, ich konnte Sie dafür genauso begeistern wie mich. Wenn Sie Fragen, Kommentare, Feedback oder Anmerkungen haben, können Sie mich gerne auf Twitter unter @passle_ oder @openwc erreichen und vergessen Sie nicht, open-wc.org zu besuchen 😉.

Ehrenwerte Erwähnungen

Ich möchte ein paar Leute erwähnen, die interessante Dinge tun und die Sie vielleicht im Auge behalten sollten.

  • Guy Bedford, der es-module-shims geschrieben hat, welche, nun ja, ES-Module und Import Maps shimmen. Was meiner Meinung nach eine ziemlich erstaunliche Leistung ist und es mir ermöglicht, einige dieser neuen Technologien zu nutzen, die noch nicht in allen Browsern implementiert sind.
  • Luke Jacksons Vortrag Don’t Build That App! Kein Webpack, keine Sorgen 🤓🤙, wie Luke sagen würde.
  • Danke an Benny Powers und Lars den Bakker für ihre hilfreichen Kommentare und ihr Feedback.