{ "version": "2.0", "xmlns": { "atom": "http://www.w3.org/2005/Atom", "content": "http://purl.org/rss/1.0/modules/content/", "georss": "http://www.georss.org/georss", "gml": "http://www.opengis.net/gml" }, "channel": { "title": "fboës - Der Blog | Artikel mit dem Tag \"Javascript\"", "link": "https://journal.3960.org/", "description": "Programmierung, Luft- & Raumfahrt, Kurioses: Der Blog von und mit Frank Boës.", "language": "de-DE", "copyright": "© 2008-2023 Creative Commons BY", "atom_link": { "href": "https://journal.3960.org/tagged/javascript/rss.json", "rel": "self", "type": "application/rss+json" }, "lastBuildDate": "Sun, 03 Mar 2024 19:00:15 +0100", "atom_updated": "2024-03-03T19:00:15+01:00", "generator": "blogophon", "image": { "url": "https://cdn.3960.org/images/tile-128x128.png", "title": "fboës - Der Blog", "link": "https://journal.3960.org/" }, "items": [ { "title": "Der kleinste JavaScript-Unit-Tester der Welt", "description": "
Schon das kleinste JavaScript-Projekt kann von Unit Testing profitieren. Aber gerade in kleinen Projekten können Lösungen wie Mocha oder Jest sich als viel zu schwergewichtig anfühlen.
\nAber warum eigentlich ein Framework verwenden? Wozu das Projekt mit Dependencies vollstopfen, wenn ein paar elegante Zeilen Code für unsere Zwecke reichen?
", "content_encoded": "Schon das kleinste JavaScript-Projekt kann von Unit Testing profitieren. Aber gerade in kleinen Projekten können Lösungen wie Mocha oder Jest sich als viel zu schwergewichtig anfühlen.
\nAber warum eigentlich ein Framework verwenden? Wozu das Projekt mit Dependencies vollstopfen, wenn ein paar elegante Zeilen Code für unsere Zwecke reichen?
\n\nIn seiner einfachsten Form ist Unit Testing nichts anderes als das Vergleichen von Annahmen mit der Ausgabe von tatsächlichen Code. In JavaScript gibt es dafür eine eingebaute Lösung: console.assert()
.
const starDestroyer = new StarDestroyer();\nconsole.assert(starDestroyer !== null, 'Star destroyer exists');\nconsole.assert(starDestroyer.speed >= 0, 'Star destroyer has speed property with positive Number');\nconsole.assert(starDestroyer.faction === 'imperial', 'Star destroyer is always imperial');\n
\nIm Browser wie auch in Node.js erzeugt diese Methode eine Konsolen-Ausgabe, wenn die Annahme nicht zutrifft.
\nWenn in ein und derselben Datei mehrere Testfälle geprüft werden sollen, ist eine klares Scoping von Variablen notwendig. So wird sichergestellt, das Ergebnisse aus einem Test nicht irrtümlicherweise im nächsten Test verwendet werden. In JavaScript ist die Erzeugung eines neuen Scopes jederzeit mittels {…}
möglich:
{\n const starDestroyer = new StarDestroyer();\n console.assert(starDestroyer !== null, 'Star destroyer exists');\n console.assert(starDestroyer.speed >= 0, 'Star destroyer has speed property with positive Number');\n console.assert(starDestroyer.faction === 'imperial', 'Star destroyer is always imperial');\n}\n\n// No starDestroyer here\n
\nMehrere Tests können so in einer Datei abgehandelt werden.
\nFür automatisiertes Testing ist die Verwendung von console.assert
natürlich zu kurz gesprungen, denn hier kann nur durch Sichtkontrolle überprüft werden, ob alle Tests sauber durchgelaufen sind. Wichtig wäre es also, auf der Kommandozeile einen Exit-Code zu bekommen, der auf einen Fehler hinweist.
Assert
in Node.jsIn Node.js existiert dafür Assert
:
const assert = require('node:assert');\n\n{\n const starDestroyer = new StarDestroyer();\n assert.ok(starDestroyer !== null, 'Star destroyer exists');\n assert.ok(starDestroyer.speed >= 0, 'Star destroyer has speed property with positive Number');\n assert.strictEqual(starDestroyer.faction, 'imperial', 'Star destroyer is always imperial');\n}\n
\nIm Fehlerfall wird assert.AssertionError
geworfen, der auf der CLI einen auch für Automatisierungen wahrnehmbaren Exit-Code erzeugt.
Der Nachteil dieser Lösung ist gut erkennbar: Diese Art von Testing funktioniert nur in Node.js, aber nicht im Browser.
\nGott sei Dank ist die Idee mit dem Werfen von Errors aber gar nicht so komplex. Und damit können wir uns eine kleine Funktion konstruieren, mit der wir ganz grundsätzliches Testing durchführen können:
\nconst assertOk = (assertion, message = 'Assertion') => {\n console.log((assertion ? "✅" : "💥") + " " + message);\n if (!assertion) {\n throw new Error(`"${message}" failed`);\n }\n};\n
\nDamit schlagen wir mehrere Fliegen mit einer Klappe:
\nEtwas aussagekräftiger könnte die Funktion sogar so aussehen:
\nconst assertStrictEqual = (a, b = true, message = '') => {\n message === '' || (message += " | ");\n message += message = `'${a}' equals '${b}'`;\n\n console.log((a === b ? "✅" : "💥") + " " + message);\n if (a !== b) {\n throw new Error(`"${message}" failed`);\n }\n};\n
\nDamit können nicht nur direkt Vergleiche durchgeführt werden, sondern auch gleich eine aussagekräftige Nachricht über den tatsächlichen Inhalt des Tests mit ausgegeben werden.
\nWenn ihr ein Drop-In-Replacement für assert
von Node.js haben wollt, könnt ihr die beiden Funktionen in statische Methoden verwandeln:
const assert = {\n /**\n * Check if `assertion` is `true`. Throw Error on error.\n * @param {Boolean} assertion\n * @param {String} message\n */\n ok(assertion, message = 'Assertion') {\n console.log((assertion ? "✅" : "💥") + " " + message);\n if (!assertion) {\n throw new Error(`"${message}" failed`);\n }\n },\n\n /**\n * Check if `actual` strictly equals `b`. Throw Error on error.\n * @param {any} actual\n * @param {any} expected\n * @param {String} message\n */\n strictEqual(actual, expected, message = '') {\n message === '' || (message += " | ");\n message += message = `'${actual}' equals '${expected}'`;\n\n assert.ok(actual == expected, message)\n }\n};\n
\nOh, und wenn ihr eure Tests noch gruppieren wollt:
\nconsole.group('Testing StarDestroyer');\n{\n const starDestroyer = new StarDestroyer();\n assert.ok(starDestroyer !== null, 'Star destroyer exists');\n assert.ok(starDestroyer.speed >= 0, 'Star destroyer has speed property with positive Number');\n assert.strictEqual(starDestroyer.faction, 'imperial', 'Star destroyer is always imperial');\n}\nconsole.groupEnd();\n
\n…erzeugt eine schön gruppierte Ausgabe eurer Tests.
\nVoilà! Eine Beispiel-Ausgabe:
\nGeoJson.Feature\n ✅ Type matches | 'Feature' equals 'Feature'\n ✅ Feature.properties.title | 'Test' equals 'Test'\n ✅ Feature.geometry.type | 'Point' equals 'Point'\n ✅ No id\nGeoJson.FeatureCollection\n ✅ 'FeatureCollection' equals 'FeatureCollection'\n ✅ Bounding Box West | '6.5664576' equals '6.5664576'\n ✅ Bounding Box South | '51.04571' equals '51.04571'\n ✅ Bounding Box East | '1.53946' equals '1.53946'\n ✅ Bounding Box North | '58.109285' equals '58.109285'\n ✅ FeatureCollection | 'FeatureCollection' equals 'FeatureCollection'\n ✅ Two Features exist | '2' equals '2'\n ✅ Feature.type | 'Feature' equals 'Feature'\n ✅ Feature.properties.title | 'Sailing boat' equals 'Sailing boat'\n
\nNatürlich ersetzt diese sehr simple Herangehensweise nicht wirklich echte Unit-Test-Frameworks. So ist zum Beispiel der Vergleich von Objekt-Strukturen nicht möglich, das Testen von Exceptions oder auch die Verwendung von Mocking und ähnlichen Features. Aber zumindest senkt es die Hürde, überhaupt mit Testing zu beginnen. 😉
", "link": "https://journal.3960.org/posts/2024-02-03-kleinste-javascript-unit-tester-welt/", "pubDate": "Sat, 03 Feb 2024 18:14:32 +0100", "atom_published": "2024-02-03T18:14:32+01:00", "atom_updated": "2024-02-04T05:06:25+01:00", "guid": "user/posts/2024-02-03-kleinste-javascript-unit-tester-welt/index.md", "author": "info@3960.org (Frank Boës)", "categories": [ "Javascript", "Programmierung", "Webdevelop" ] }, { "title": "Die 24h-Uhr – als Web Component", "description": "Web Components zu bauen ist gar nicht so kompliziert, selbst ohne Framework. Nach meinen vorherigen Überlegungen zur Verwendung von SVGs in Web Components war es also höchste Zeit, eine 24-Stunden-Uhr zu bauen.
", "content_encoded": "Web Components zu bauen ist gar nicht so kompliziert, selbst ohne Framework. Nach meinen vorherigen Überlegungen zur Verwendung von SVGs in Web Components war es also höchste Zeit, eine 24-Stunden-Uhr zu bauen.
\n\n24h-Uhren sind analoge Uhren, die einen feinen Unterschied zu herkömmlichen Uhren aufweisen. An Stelle von 12 Stunden pro Umdrehung des Stundenzeigers zeigen sie 24 Stunden an. Damit braucht also der Stundenzeiger einen kompletten Tag für einen Vollkreis.
\nDamit ist es möglich, die vierundzwanzig Stunden auf dem Ziffernblatt grafisch in Tag, Dämmerung und Nacht einzuteilen. Da diese sich in Bezug auf das Datum und die geografische Position verändern, brauchte die Uhr also auch diese Informationen. In meinem Fall habe ich eine Skala auf dem Stundenring angebracht, die sich für die Nacht dunkelblau, für die Dämmung mittelblau und für den Tag hellblau verfärbt.
\n\n
Der Bau als Web Component erlaubt es nun, die Uhr mit relativ wenig HTML in eine beliebige Seite einzubauen. Unter der Github-Seite zur 24h-Uhr findet sich eine knappe Anleitung, der Einbau einer Web Component muss aber nicht komplizierter sein als:
\n<twentyfour-hours-clock></twentyfour-hours-clock>\n
\nWeitere Attribute erlauben es, die Component vorher mit Werten zu bestücken – die sich auch im Nachhinein mit JavaScript ändern lassen:
\n<twentyfour-hours-clock width="128" height="128" datetime="2011-10-10T14:48:00" longitude="auto" latitude="auto"></twentyfour-hours-clock>\n
\nAuch das Einfärben, Vergrößern und Verkleinern von Einzelteilen der Web Component sind mit CSS Custom Properties möglich:
\n<style>\ntwentyfour-hours-clock {\n --color-watchhand: pink;\n --color-night: #111111;\n}\n</style>\n<twentyfour-hours-clock></twentyfour-hours-clock>\n
\nDie Web Component selber wurde mit TypeScript gebaut. Tatsächlich wäre auch der Bau direkt in Vanilla JavaScript möglich gewesen. Meine letzten Abenteuer in TypeScript sind aber so angenehme Erfahrungen gewesen, dass ich die Unterstützung bei der Typisierung in JavaScript nicht mehr missen möchte, und die Web Component in TypeScript gebaut habe. Inzwischen halte ich bei JavaScript die Verwendung von TypeScript für so hilfreich, dass ich auf ESLint zur Überprüfung meiner Programmierung bei Privatprojekten gerne verzichte.
\nEin funktionierendes Beispiel für die fertige Uhr findet sich auf der Demonstrationsseite für die 24h-Uhr.
", "link": "https://journal.3960.org/posts/2023-03-05-24h-uhr-als-web-component/", "pubDate": "Sun, 05 Mar 2023 18:56:12 +0100", "atom_published": "2023-03-05T18:56:12+01:00", "atom_updated": "2023-03-05T18:56:12+01:00", "guid": "user/posts/2023-03-05-24h-uhr-als-web-component/index.md", "author": "info@3960.org (Frank Boës)", "categories": [ "Webdevelop", "Web-Components", "SVG", "Javascript", "CSS", "Geografie", "Outdoor", "Programmierung", "Technologie" ] }, { "title": "JavaScript: Pattern zum Event-Handling", "description": "Manche JavaScript-Applikationen haben eine erhebliche Zahl an Event-Listenern. Diese ordentlich und übersichtlich zu erfassen und abzuarbeiten kann eine Herausforderung sein – bis wir uns die Anleitung zu addEventListener
genau durchlesen.
Manche JavaScript-Applikationen haben eine erhebliche Zahl an Event-Listenern. Diese ordentlich und übersichtlich zu erfassen und abzuarbeiten kann eine Herausforderung sein – bis wir uns die Anleitung zu addEventListener
genau durchlesen.
Wie schon in dem Artikel „Event-Handling mit JavaScript – und ohne jQuery“ angemerkt, ist ein gutes Verständnis von addEventListener
sehr hilfreich, um sich in der eigenen JavaScript-Applikation keine Performance-Ärger einzubrocken – und gleichzeitig die Übersicht zu behalten, wenn die Anzahl der Listener schnell ansteigt.
Gehen wir davon aus, dass ihr klassen- bzw. objektorientiert arbeitet. Schnell werdet ihr auf die Herausforderung treffen, dass this
innerhalb des Listeners eine andere Bedeutung hat als außerhalb.
Natürlich könntet ihr mit .bind()
den Kontext neu setzen. Oder ihr könntet mit anonymen Arrow-Funktionen arbeiten, die aber den üblichen Nachteil von anonymen Funktionen bei Listenern mitbringen.
MDN's Anleitung für addEventListener
verrät uns, dass als Listener drei verschiedene Dinge eingetragen werden können:
null
handleEvent
-Methode verfügt.Die letzte Option erlaubt es uns, this
mit wenig Aufwand vorhersehbar einzusetzen.
Tatsächlich ist die Übergabe des gesamten Objekts als Listener und die Definition einer handleEvent
-Methode überraschend simpel:
// 1st example: Multiple event listeners attached,\n// one handler\nclass App {\n constructor() {\n document.querySelectorAll('a, button').forEach(function(element) {\n element.addEventListener('click', this);\n });\n }\n\n handleEvent(event) {\n // do something\n }\n}\n
\nInnerhalb von handleEvent
kann nun this
mit dem zu erwartenden Bezug auf das aktuelle Klassen-Objekt verwendet werden, während die für das Event notwendigen Eigenschaften (ebenfalls regulär) in dem Parameter event
stehen.
Ärgerlicherweise werden nun aber alle so registrierten Events mit ein und dem selben Listener bedient. Zum Glück verrät uns aber jedes Event, für welches Element es ausgelöst wurde – die sogenannte Event-Delegation. Eine Fallunterscheidung z.B. nach ID kann uns also helfen, die einzelnen Aktionen auseinander zu halten:
\n// 2nd example: Multiple event listeners attached,\n// one handler with delegation per id\nclass App {\n constructor() {\n document.querySelectorAll('a, button').forEach(function(element) {\n element.addEventListener('click', this);\n });\n }\n\n handleEvent(event) {\n switch (event.target.id) {\n case 'example-1':\n // do something\n break;\n case 'example-2':\n // do something other\n break;\n }\n }\n}\n
\n…und aus dem Nachteil ist ein Vorteil geworden, da jetzt die einzelnen Event-Listenern nichts anderes als Fälle in einer switch
-Anweisung geworden sind.
Jetzt können wir die Vielzahl der im DOM verteilten Event-Listener eigentlich auch gleich auf einen einzigen reduzieren, da der Listener sowieso nochmals überprüft, welches Element genau getroffen wurde. Dabei machen wir uns Event-Bubbling zu Nutze, und montieren an zentraler Stelle einen Event-Listener, der alle Interaktionen abfängt, und dann mittels Event-Delegation den korrekten Ansprechpartner auswählt.
\nDie zentralste Stelle ist natürlich… der <body>
. Damit erschlagen wir übrigens gleich den Fall, dass nachträglich dynamisch DOM-Elemente auf der Seite hinzugefügt werden. Denn der auf dem <body>
montierte Listener fängt natürlich auch erst nachträglich im DOM montierte Elemente ab.
// 3rd example: Single event listeners attached,\n// one handler with delegation per id\nclass App {\n constructor() {\n document.body.addEventListener("click", this);\n }\n\n handleEvent(event) {\n switch (event.target.id) {\n case 'example-1':\n // do something\n break;\n case 'example-2':\n // do something\n break;\n }\n }\n}\n
\nNoch weiter abstrahieren lässt sich das Delegations-Beispiel, wenn wir vorher die unterschiedlichen Event-Typen auseinandersortieren:
\n// 4th example: Single event listeners attached,\n// one handler triggers subhandlers per event type\nclass App {\n constructor() {\n document.body.addEventListener("input", this, true);\n document.body.addEventListener("click", this, true);\n document.body.addEventListener("change", this, true);\n }\n\n handleEvent(event) {\n // @see https://developer.mozilla.org/en-US/docs/Web/API/Event\n switch (event.type) {\n case 'click' : this.handleEventClick(event); break;\n case 'input' : this.handleEventInput(event); break;\n case 'change': this.handleEventChange(event); break;\n }\n }\n\n handleEventClick(event) {\n switch (event.target.id) {\n case 'example-1':\n // do something\n break;\n case 'example-2':\n // do something\n break;\n }\n }\n\n handleEventInput(event) {\n switch (event.target.id) {\n case 'example-3':\n // do something\n break;\n case 'example-4':\n // do something\n break;\n }\n }\n\n handleEventChange(event) {\n switch (event.target.id) {\n case 'example-5':\n // do something\n break;\n case 'example-6':\n // do something\n break;\n }\n }\n}\n
\nDieses Pattern lässt sich natürlich auch ohne Objekte einsetzen, und mit regulären Funktionsaufrufen durchführen. Dabei verliert es aber natürlich die Möglichkeit, this
ohne Gehirn- oder Code-Akrobatik zu verwenden.
Unser schönes Beispiel geht davon aus, dass wir die Auswahl der eigentlich durchzuführenden Aktion an der id
des Elements festmachen, dass das Event ausgelöst hat. Tatsächlich können wir das aber an jedes Attribut koppeln. Damit sind auch class
-Attribute mögliche Ziele.
<dialog>\n <button class="modal-close">Close<button>\n \n <label for="date">Input</label>\n <input id="date" type="date" class="change-date" />\n\n <button class="set-to-current">Set to current time</button>\n</dialog>\n
\nDas erlaubt uns zum Beispiel, die erste Klasse eines Elements darüber entscheiden zu lassen, was getan werden soll:
\n// 5th example: Single event listeners attached,\n// one handler triggers subhandlers per `class` attribute\nclass App {\n constructor() {\n document.body.addEventListener("input", this, true);\n document.body.addEventListener("click", this, true);\n document.body.addEventListener("change", this, true);\n }\n\n handleEvent(event) {\n // Get first `class` of element\n const handler = e.target.classList.item(0);\n switch (handler) {\n case 'modal-close' : this.handleModalClose(event); break;\n case 'change-data' : this.handleChangeDate(event); break;\n case 'set-to-current': this.handleSetToCurrent(event); break;\n }\n }\n\n handleModalClose(event) {\n // do something\n }\n\n handleChangeDate(event) {\n // do something\n }\n\n handleSetToCurrent(event) {\n // do something\n }\n}\n
\nAber wie wäre es mit einem eigenen data
-Attribut, zum Beispiel in Form eines data-handler
?
<dialog>\n <button data-handler="modal-close">Close<button>\n \n <label for="date">Input</label>\n <input id="date" type=="date" data-handler="change-date" />\n\n <button data-handler="set-to-current">Set to current time</button>\n</dialog>\n
\nIn diesem Fall hängen wir an alle HTML-Elemente ein data-handle
, mit dem wir zum Beispiel auch direkt benennen können, mit welchem Handler gearbeitet werden soll:
// 5th example: Single event listeners attached,\n// one handler triggers subhandlers per `data-handler` attribute\nclass App {\n constructor() {\n document.body.addEventListener("input", this, true);\n document.body.addEventListener("click", this, true);\n document.body.addEventListener("change", this, true);\n }\n\n handleEvent(event) {\n // Bubble up to closest DOM element with `data-handler`\n const handler = e.target.closest('[data-handler]')?.dataset.handler;\n switch (handler) {\n case 'modal-close' : this.handleModalClose(event); break;\n case 'change-data' : this.handleChangeDate(event); break;\n case 'set-to-current': this.handleSetToCurrent(event); break;\n }\n }\n\n handleModalClose(event) {\n // do something\n }\n\n handleChangeDate(event) {\n // do something\n }\n\n handleSetToCurrent(event) {\n // do something\n }\n}\n
\nAls angenehmer Nebeneffekt können wir uns auf die Elemente beschränken, die auch wirklich ein data-handler
-Attribut haben – oder im DOM nach oben wandern, bis wir ein Element gefunden haben, dass über ein solches Attribut verfügt. \n.closest()
ist hier eine wirkliche Hilfe.
Mit dem Gampead API Test könnt ihr die tatsächlichen Rohwerte der Gamepad
API auslesen – und nebenbei eure Gamepads, Joysticks, Schubkontrollen, Fußpedale und sonstige Controller testen.
Mit dem Gampead API Test könnt ihr die tatsächlichen Rohwerte der Gamepad
API auslesen – und nebenbei eure Gamepads, Joysticks, Schubkontrollen, Fußpedale und sonstige Controller testen.
Moderne Browser haben inzwischen eine Vielzahl interessanter JavaScript-Schnittstellen. Höchste Zeit, die Gamepad
-API genau zu begutachten.
Moderne Browser haben inzwischen eine Vielzahl interessanter JavaScript-Schnittstellen. Höchste Zeit, die Gamepad
-API genau zu begutachten.
Die Gamepad
-API ist eine der Zutaten, die moderne Browser für die Spiele-Entwicklung bereit stellen. Neben Grafik (inklusive Virtual Reality), Sound und Socket-Verbindungen gibt es damit alles, was das Spiele-Entwickler-Herz begehrt.
Anleitungen für der Gamepad
-API gibt es viele: Allen voran die Dokumentation zur Gamepad API auf MDN lässt wenig Fragen offen. Ich habe mich aber in einen etwas speziellen Bereich vorgewagt. Inspiriert von einem Retro Flug-Simulator im Browser \nhabe ich mir die Frage gestellt: Wie sieht es aus, wenn ich einen Flugsimulator für den Browser baue, und dort Unterstützung für Joysticks, Schubkontrollen oder Fußpedale integrieren möchte?
Kurz gefasst: Gar nicht so übel.
\nDie Dokumentation zur Gamepad API auf MDN bespricht in den meisten Beispielen nur Gamepads. Der schöne Artikel „Using The Gamepad API In Web Games“ aus dem Smashing Magazine erklärt deutlich mehr die Verwendung von Joysticks & Co – aber auch hier bleiben ein paar Fragen offen.
\nTatsächlich funktioniert die Gamepad API mit so ziemlich jedem USB/Bluetooth-Eingabegerät, das wie ein Gamepad angesprochen werden kann. Darunter fallen Joysticks, Schubkontrollen, Ruder-Pedale und so ziemlich jeder andere HOTAS-Controller, den Flugsimulator-Fans kennen.
\nDer Vorgang ist in JavaScript verblüffend einfach: Mit der JavaScript-Methode Navigator.getGamepads()
erhaltet ihr ein Array aller an eurem PC/Smartphone/sonstigen Endgerät angeschlossenen Controller.
Die Controller verraten folgendes über sich:
\ngamepad.id
: Das ist die ID des Eingabegeräts, die die meisten Browser in einen verständlichen Namen übersetzen – wenn auch jeder Browser ein etwas anderes Ergebnis ausgibt. 😖gamepad.buttons
: Dieses Array verrät euch, wie viele Buttons das Eingabegerät hat.gamepad.axes
: Dieses Array verrät euch, wie viele Achsen das Eingabegerät hat.gamepad.mapping
: Hier verrät der Browser mit dem Schlüsselwort „standard“
, ob der Controller auf ein Standard-Gamepad umbelegt werden konnte.gamepad.timestamp
: Der Zeitpunkt, an dem das letzte Mal eine Messung auf dem Gamepad durchgeführt wurde. Das wird später wichtig, um die Zeit zwischen den Messungen zu verfolgen, und damit z.B. die Dauer eines Tastendrucks zu erkennen.\n
Das Standard-Mapping ist eine große Hilfe für die Implementation von Gamepads in den Browser: Dabei wird die Achsen- und Button-Belegung des Gamepads auf ein erwartbares Layout eines Standard-Gamepads umgebogen. Die Belegung beinhaltet:
\nDer Haken ist, das ein und dasselbe Gamepad nicht von jedem Browser diesem Layout zugeordnet wird. So wird ein Xbox-Controller unter Firefox/Windows, Chrome/Windows und Chrome/Ubuntu als Standard-Controller erkannt, unter Firefox/Ubuntu dagegen nicht. 😖
\nDas kann fatale Auswirkungen haben: Ein und dasselbe Gamepad kann zum Beispiel im Standard-Mapping die analogen Trigger als Buttons mit Analogwerten melden, während der nächste Browser ohne Standard-Mapping die Buttons streicht und stattdessen zwei zusätzliche Achsen sendet (die dann auch noch die Reihenfolge durcheinander bringen). Oder das D-Pad wird nicht als Sammlung von vier Knöpfen sondern als zwei Achsen gemeldet.
\nHöchste Zeit also, einen Blick auf den lokalen Fuhrpark an HOTAS-Controller zu werfen. Mit dem selbstgebauten Gamepad API Test kann jedes beliebige Eingabegerät so dargestellt werden, wie der Browser dieses Gerät auswertet.
\n\n
Ziemlich schnell zeichnet sich dabei ein Muster ab. Jeder Joystick bietet verlässlich folgendes an:
\nBereits dahinter scheiden sich die Geister. In den meisten Fällen könnt ihr euch noch knapp auf die folgende Konvention verlassen:
\nInteressanterweise lassen es sich Joystick-Hersteller nicht nehmen, einige Achsen zu überspringen. Ein vermeintlicher 5-Achsen-Joysticks kann beim Auslesen der API trotzdem neun Achsen senden, wenn der Hersteller des Joysticks das für eine gute Idee hält.
\n\n
Weitere Eingabegeräte wie Schubkontrollen versuchen sich, auch an diese Konvention zu halten. Hier ist aber deutlich mehr Glück gefragt.
\n\n
Ganz verrückt sieht es dann mit Fußpedalen aus, die über gar keine Buttons, aber dafür eine Achse für die Verschiebung der Füße und zwei Achsen für die jeweiligen Pedale haben.
\nDen Zustand des Gamepads und all seiner Achsen und Buttons zu kennen ist für euer Spiel oder eure Simulation nicht immer hilfreich, denn ihr bekommt die absoluten Roh-Daten des Controllers. Diese solltet ihr in der Regel in ein besser handhabbares Format umwandeln.
\n\nDie Programmier-Beispiele in diesem Artikel sind natürlich sehr simpel gehalten; in einer echten Umgebung kann deutlich mehr Abstraktion hilfreich sein.
\n
Analoge Eingabeinstrumente haben in der Regel kleine Ungenauigkeiten. Manchmal zentrieren sie nicht richtig, manchmal flattern die Ausgabewerte etwas. Um diese kleinen Fehler zu korrigieren werden sogenannte dead zones eingerichtet.
\nDead zones oder dead bands ignorieren bestimmte Werte einer Achse, und schneiden diese ab. Somit kann zum Beispiel eine leicht verkalibrierte Mitte eures Sticks trotzdem als 0
ausgelesen werden, oder ein Stick saubere -1
und +1
übermitteln, obwohl er aus Altersschwäche nur +0.9995
erreicht.
Ein fantastischer Artikel zu diesem Thema ist „Joystick input and using deadbands“ von Mimir Games. Einfach gesprochen brauchen wir zwei Arten von Todzonen:
\ninnerThreshold
: Bei selbst zentrierenden Achsen sollte im einer kleinen Zone in der Mitte keine Eingabe genommen werden.outerThreshold
: Generell sollte das obere und untere Ende des Wertebereichs vorzeitig einen Vollausschlag erzeugen.Eine sehr einfache Funktion für diesen Zweck sieht wie folgt aus:
\nconst axisDeadzone = function(axis, outerThreshold, innerThreshold = 0) {\n if (innerThreshold > 0) {\n const multiplier = (axis > 0 ? 1 : -1);\n axis = Math.max(0, (Math.abs(axis) - innerThreshold) / (1 - innerThreshold)) * multiplier;\n }\n\n if (outerThreshold > 0) {\n axis = Math.max(-1, Math.min(1, axis / (1 - outerThreshold)));\n }\n\n return axis;\n}\n
\nDie Gamepad
-API liefert euch immer eine Momentaufnahme des Gamepads, die ihr so oft wie möglich abfragt. Der zeitliche Abstand zwischen diesen Abfragen ist nicht direkt vorhersehbar, sondern wird ebenfalls gemessen. Dafür übermittelt das Gamepad den gamepad.timestamp
.
Den Effekt einer Achs-Eingabe können wir einfach mit der Zeitspanne multiplizieren, der zwischen den letzten beiden Messungen vergangen ist; selbst wenn unser Gamepad etwas länger für die Messung braucht, ist der Effekt im Spiel trotzdem der selbe.
\nZuerst sollten wir uns eine Variable anlegen, in der wir uns den letzten Timestamp merken können. In der Abfrageschleife (dem Game-Loop) können wir nun die Zeit zwischen den letzten beiden Updates ermitteln:
\nlet lastTimestamp = 0;\nlet posX = 0;\nlet posY = 0;\n\nfunction gameLoop () {\n const gamepad = navigator.getGamepads()[0]; // this is just a stub ;)\n\n // Find time elapsed since last gamepad update\n const elapsedTime = lastTimestamp === 0\n ? 0 \n : timestamp - lastTimestamp;\n // Store current timestamp for next time\n lastTimestamp = timestamp;\n\n // Move Pac Man / Thunderblade / Turrican / whatever relative to elapsed time\n posX += gamepad.axes[0] * elapsedTime;\n posY += gamepad.axes[1] * elapsedTime;\n\n window.requestAnimationFrame(gameLoop); // Restart loop\n}\n
\nButtons klingen trivial, sind es aber nicht. Die Gamepad
-API übermittelt, ob ein Button gerade jetzt gedrückt wird. Ob er vorher schon gedrückt wurde und für wie lange, oder ob er in der aktuellen Messung das erste Mal gedrückt wurde – diese Information müssen wir uns selber erarbeiten.
Folgende Zustände sind für uns hilfreich:
\npressed
: Der Button wird gerade gedrückt gehaltentriggered
: Der Button wird gerade zum ersten Mal gedrücktreleased
: Der Button wird gerade wieder losgelassenIn einem Skript könnte das wie folgt aussehen:
\nlet lastButtonStates = [];\n\nfunction gameLoop () {\n const gamepad = navigator.getGamepads()[0]; // this is just a stub ;)\n\n // Get button states\n const buttonStates = gamepad.buttons.map((button, index) => {\n return {\n pressed: button.pressed,\n triggered: lastButtonStates[index]\n ? (button.pressed && lastButtonStates[index].pressed !== button.pressed)\n : button.pressed,\n released: lastButtonStates[index]\n ? (!button.pressed && lastButtonStates[index].pressed !== button.pressed)\n : false\n }\n });\n\n // Have Pac Man / Thunderblade / Turrican / whatever do stuff\n buttonStates[0].triggered && turrican.jump();\n buttonStates[1].pressed && turrican.fire();\n buttonStates[2].pressed && turrican.chargeSuperpower();\n buttonStates[2].released && turrican.releaseSuperpower();\n\n window.requestAnimationFrame(gameLoop); // Restart loop\n}\n
\nMit den Informationen aus triggered
und released
könnt ihr noch die Zeitdauer des Gedrückthaltens ableiten, und zum Beispiel auch in einem bestimmten Intervall bei gedrückter Taste Aktionen ausführen.
Einige unscheinbar Kontrollen an eurem Joystick sind in der Auswertung initial etwas kompliziert zu verstehen. Mit einer kleinen Trickkiste könnt ihr euch aber schnell Übersicht verschaffen.
\nSchubkontrollen sind eine ganz normale Achse wie zum Beispiel ein Joystick, mit dem Unterschied, dass sie sich nicht selber zentrieren. Sie haben den selben Wertebereich wie jede andere Achse auch. Der value
kann zwischen -1
..+1
liegen.
Dieser Wertebereich ist für eine Schubkontrolle in der Regel nicht so hilfreich, aber mittels einer einfachen Funktion auf den Bereich 0
..1
transformierbar:
const axisToThrottle = function(axis) {\n return (axis + 1) / 2;\n}\n
\nEinige Buttons (auch gerne Trigger genannt) haben nicht nur einen festen Auslösepunkt, sondern auch einen analoge Achse verbaut. So sind bei den meisten Gamepads die unteren Schultertasten als analoge Trigger ausgeführt. Dies erlaubt euch als Spieleentwickler genau zu messen, wie weit der Button gedrückt wurde.
\nAuf einem Standard-Gamepad wird dies am GampepadButton
als value
gemeldet:
gamepad.button[4] = {\n pressed: true,\n value: 0.4321\n};\n
\nDer value
liegt dann zwischen 0
..1
.
Verflixt wird es, wenn das Eingabegerät nicht als Standard-Gamepad erkannt wird; denn tatsächlich ist der Trigger gar kein Button, sondern eine Achse:
\ngamepad.axes[3] = -0.1358;\n
\n…wobei der Wert zwischen -1
..1
liegt, womit er einen anderen Wertebereich umfasst als in seiner GampepadButton
-Form.
Eine einfache Transformation macht uns die Achse aber wieder zum Button:
\nconst axisToButton = function(axis) {\n return {\n pressed: (axis > -1),\n value: (axis + 1) / 2,\n touched: (axis > -1)\n };\n}\n
\n\n
Jedes Vier-Wege-Kreuz auf eurem Gamepad, Joystick oder Schubkontrolle kann eine kleine Überraschung für euch bereit halten. Tatsächlich gibt es mehrere Wege, wie sie in der Gamepad
-API auftreten können:
Wenn ein Vier- bzw. Acht-Wege-Kreuz als eine einzige Achse auftritt, hat der Hersteller sich etwas besonderes überlegt: Jeder Wert zwischen -1..+1
steht für Ausrichtung des Coolie hats zwischen 0°..315°. In Ruheposition sendet er dagegen einen unmöglich hohen Wert, den ihr verwerfen solltet.
Der value
auf der Achse beginnen mit -1
auf der 0°-Position. Pro 90°-Drehung erhöht sich der value
um 0.5715
, bis er auf 315° bei +1
endet.
Um aus diesem analogen Wert einen von acht Zuständen zu machen hilft eine kleine Funktion:
\n// 7 0 1\n// 6 ∗ 2\n// 5 4 3\nconst axisToEightWay = function(axis) {\n return (axis > 1) \n ? undefined // centered state\n : Math.round((axis + 1) * 7/2); // 0 means "top", 4 means "bottom"\n}\n
\nDiese acht Zustände können auch in zwei Achsen umgerechnet werden:
\nconst eightWaytoAxes = function(value) {\n if (value === undefined) {\n return [0,0];\n }\n\n let x = value % 4 ? 1 : 0;\n x *= value > 4 ? -1 : 1;\n \n let y = ((value +2) % 4) ? 1 : 0;\n y *= value < 2 || value > 6 ? -1 : 1;\n \n return [x,y];\n}\n
\nWenn euch vier Zustände reichen, braucht es eine etwas andere Formel:
\n// 0\n// 3 + 1\n// 2 \nconst axisToFourWay = function(axis) {\n return (axis > 1) \n ? undefined // centered state\n : Math.round((axis + 1) * 7/4) % 4; // 0 means "top", 2 means "bottom"\n}\n
\nDas Auslesen eines Joysticks hat jede Menge Fallstricke. Aus gutem Grund spekulieren Spielekonsolen und viele Spiele auf das Standard-Gamepad, um sich Ärger beim Mapping zu ersparen.
\nAndersherum stellt sich auch im Browser die Herausforderung, dass wir nicht bei jedem angeschlossenen Controller das genaue Mapping vorher kennen können. So oder so werdet ihr nicht darum herum kommen, dem Spieler einen Konfigurationsdialog anzubieten, mit dem die Zuordnung von Spielfunktion zu Controller-Button/Achse festgelegt werden kann. Bei der Speicherung des Mappings kann die Web Storage API weiterhelfen, damit der Aufwand nur einmalig anfällt.
\nEin Mapping könnte jedenfalls wie folgt aussehen:
\nconst mapping = {\n buttons: {\n radio: 2,\n flapsUp: 4,\n flapsDown: 6,\n trimLeft: 7,\n trimRight: 5\n },\n axes: {\n roll: 0,\n pitch: 1,\n throttle: 3,\n yaw: 4,\n freeLook: 9\n }\n}\n
\nEin Zugriff auf die konfigurierten Achsen ist damit auch deutlich übersichtlicher:
\nlet roll = 0;\nlet pitch = 0;\n\nfunction gameLoop () {\n const gamepad = navigator.getGamepads()[0]; // this is just a stub ;)\n\n // ...\n\n roll += gamepad.axes[mapping.axes.roll] * elapsedTime;\n pitch += gamepad.axes[mapping.axes.pitch] * elapsedTime;\n\n window.requestAnimationFrame(gameLoop); // Restart loop\n}\n
\nWichtig für die Konfiguration von Achsen ist es, die genaue Art der Achse zu kennen:
\nIn den meisten Fällen werdet ihr nur wenige absolute Achsen in einem Spiel benötigen. Beim Mapping ist es aber sinnvoll, für einen über eine absolute Achse einstellbaren Wert auch ein Mapping für relative Achsen anzubieten. Als Beispiel kann die Schubkontrolle dienen (die in einer Simulation ihren Wert 1:1 auf die simulierte Schubkontrolle überträgt), die auch durch einen kleinen Stick gesteuert werden könnte (was in der Simulation die simulierte Schubkontrolle nach oben oder unten schiebt).
\nNicht zuletzt müsst ihr beim Mapping berücksichtigen, dass die Anzahl der vorhandenen Achsen und Knöpfe mit dem Eingabegerät des Nutzers nicht abdeckbar ist. Hier bieten sich sogenannte Modifier an: Ähnlich wie die Shift-, Alt- und Strg-Taste die Bedeutung einer Taste auf der Tastatur verändern kann, kann ein gedrückt gehaltener Button die Bedeutung der Achsen und Buttons eines Controllers verändern.
\nDas Mapping-Objekt für Modifier sowie der Konfigurationsdialog sind natürlich deutlich komplexer, können aber selbst Retro-Controller mit wenigen Achsen mit einer erstaunlichen Vielzahl von Funktionen belegen.
\nGame Controller im Browser mittels der Gamepad
-API anzusprechen ist nicht kompliziert – die Interpretation der Werte und Berücksichtigung der verschiedenen Controller hingegen schon. Ein robustes Handling im JavaScript erspart euch und euren Nutzern unschöne Erlebnisse.
Die einzelnen Skripte aus diesem Artikel gibt es übrigens als kompletten GamepadHelper
zum Download. Diese Bibliothek kann eine gute Grundlage für ein eigenes Mapping sein.
Der Artikel „How we build the site and use Web Components“ deutet im Nebensatz eine wunderbare Methode an, um in Content-Management-Systemen auf Markdown-Basis redaktionell Web Components zum Einsatz zu bringen.
", "content_encoded": "Der Artikel „How we build the site and use Web Components“ deutet im Nebensatz eine wunderbare Methode an, um in Content-Management-Systemen auf Markdown-Basis redaktionell Web Components zum Einsatz zu bringen.
\n\nMarkdown hat eine oft gar nicht benötigte Eigenschaft: Inline-HTML. In Markdown auftretendes HTML wird nach dem Umwandeln von Markdown in HTML vollkommen unverändert wieder ausgegeben. Also wird folgendes Markdown…
\nThis **Markdown example** will <i>output HTML tags</i> as well.\n
\n…zu folgendem HTML:
\n<p>This <strong>Markdown example</strong> will <i>output HTML tags</i> as well.</p>\n
\nWir erinnern uns: Web Components sind durch JavaScript-Bibliotheken definierte HTML-Tags, die mit ihrem eigenen Verhalten und Layout versehen sind. So kann man Web Components wie HTML im Markdown verwenden, z.B. word-count
:
<word-count>\n Lorem ipsum _dolor_ sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et _dolore magna_ aliquyam erat, sed diam voluptua.\n</word-count>\n
\n…wird damit zu…
\n<word-count>\n <p>Lorem ipsum <em>dolor</em> sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et <em>dolore magna</em> aliquyam erat, sed diam voluptua.</p>\n</word-count>\n
\n…was wiederum im Browser dann den obigen Absatz mit einem kleinen Wort-Zähler dahinter anzeigt.
\nDieser Weg funktioniert unabhängig von der Art des eingesetzten Content Management Systems, solange das CMS redaktionelles Markdown verwendet. Für den Redakteur ist diese Lösung so einfach oder so kompliziert wie einen Wordpress-Shortcode einzusetzen.
\nDazu muss die zu verwendende Web-Component natürlich vorher geladen sein. Dies kann entweder im Template mit einem simplen <script>
-Aufruf geschehen – oder (je nach Sicherheitseinstellung des CMS') auch direkt im Markdown des Artikels:
<script type="module" src="https://unpkg.com/@lrnwebcomponents/word-count@2.6.5/word-count.js?module"></script>\n
\nSo oder so steht Redakteuren nun eine hinreichend bedienbare Methode zur Verfügung, in ihren Artikel Web Components zu verwenden. Alleine für diesen Zweck hatte ich schon ein paar Ideen, welche Web Components Redakteuren helfen könnten:
\n<twitter-tweet>
zum Einbetten von Tweets<video-embed>
zum Einbetten von Videos, zum Beispiel YouTube<map-embed>
zum Einbetten von Karten, wie z.B. Google Maps<pull-quote>
zum Anzeigen von aus dem Text herausgezogenen Zitaten<linkbox-category>
oder <linkbox-tag>
zum Einbetten einer Linkbox, die verwandte Artikel anzeigt<chess-board>
oder <bar-chart>
zum Darstellung von Spezialgrafiken wie einem Schachbrett oder Säulengrafiken (wir erinnern uns: Web Components können auch SVG-Grafiken anzeigen)Zu berücksichtigen wäre nur, dass einige ältere Browser Web Components nicht darstellen können. Das betrifft zum Beispiel auch einige RSS-Reader und E-Mail-Clients, so dass Artikel in diesem Umfeld nur eingeschränkt dargestellt werden. Das kann aber sogar erwünscht sein – denn dann wird das innerhalb des Web-Component-Tags verwendete Markdown bzw. HTML ausgegeben.
", "link": "https://journal.3960.org/posts/2020-04-10-web-components-mit-markdown-verwenden/", "pubDate": "Fri, 10 Apr 2020 19:32:33 +0200", "atom_published": "2020-04-10T19:32:33+02:00", "atom_updated": "2020-04-10T20:34:10+02:00", "guid": "user/posts/2020-04-10-web-components-mit-markdown-verwenden/index.md", "author": "info@3960.org (Frank Boës)", "categories": [ "Webdevelop", "Web-Components", "Blog", "Javascript", "Programmierung", "Technologie" ] }, { "title": "Ideen für Web Components", "description": "Schlaue Beispiele gefällig, was man mit Web Components alles bauen könnte, um HTML zu erweitern?
", "content_encoded": "Schlaue Beispiele gefällig, was man mit Web Components alles bauen könnte, um HTML zu erweitern?
\n\nWeb Components eignen sich ganz hervorragend, um von der Darstellung und/oder Funktionalität her komplexe Aufgaben gekapselt und wiederverwendbar zu lösen. Zudem können Web Components (ähnlich wie <iframe>
, <object>
und viele andere neue Tags) in ihrem Inneren einen Fallback enthalten, so dass Browser ohne die Fähigkeit zur Darstellung von Web Components trotzdem etwas anzuzeigen haben.
Außerdem können Web Components dieses Fallback gleichzeitig via slot
-Mechanismus auswerten, um die Fallback-Inhalte entweder wieder anzuzeigen, oder aber aus dem HTML noch weitere Informationen zu ziehen.
Im Sinne des progressive enhancement könnte man also mit Web Components viele alltägliche Probleme lösen:
\n<twitter-status status="https://twitter.com/Interior/status/463440424141459456">\n <blockquote>Sunsets don't get much better than this one over @GrandTetonNPS. #nature #sunset</blockquote>\n <cite>US Department of the Interior</cite>\n</twitter-status>\n
\nDiese Komponente zieht datenschutzzkonform Tweets von Twitter – und als Fallback braucht sie gar keine weiteren Requests, da der Content bereits auf der Seite zu sehen ist. 😉
\n<video-embed url="https://www.youtube.com/embed/G2dGWH90aew?autoplay=1">\n <a href="https://www.youtube.com/embed/G2dGWH90aew?autoplay=1">Youtube-Video</a>\n</video-embed>\n
\nDiese Komponente verwandelt mittels einfacher Magie den Link zu einem Video in eine Video-Einbettung. Redakteure werden es lieben, nicht mehr mit Embed-Codes herumzuhantieren, sondern einfach den Link zum Video aus der URL-Leiste ihres Browsers kopieren zu können – und trotzdem im fertigen Artikel ein eingebettetes Video zu sehen. (Ein Trick, den das Blogophon auch beherrscht.)
\nMit lite-youtube
schon ähnlich umgesetzt: Mit dieser Komponente bekommt man eine deutlich datenschutzkonforme Darstellung eines eingebetteten Videos von YouTube, Viemo oder ähnlichem – ähnlich wie in diesem Blog YouTube-Videos in die Seite eingebettet werden.
<code-highlighted lang="javascript">\n <pre>\n <code>\n …\n </code>\n </pre>\n</code>\n
\nMit dieser Komponente kann ich Code-Beispiel mit Syntax-Highlighting versehen. In dem lang
-Attribut wird dabei die verwendete Programmiersprache vermerkt.
<map-embed provider="google-maps" coordinates="53.246, 10.412">\n <a href="https://www.google.de/maps/place/53°14'45.6"N+10°24'43.2"E">Die wunderschöne Stadt Lüneburg</a>\n</map-embed>\n
\nKarten in redaktionellen Kontext sind immer wieder hilfreich, um Orte und ihre Umgebung darzustellen. Als Fallback kann immer noch ein Link zu einer Karten-Plattform angeboten werden.
\n<input-coordinates provider="google-maps">\n <input name="coordinates" type="coordinates" step="0.001" value="53.246, 10.412" />\n</input-coordinates>\n
\nGetreu meiner Idee, ein Eingabefeld für Geo-Koordinaten in HTML anzubieten, könnte diese Komponente eine Geo-Lokalisierung und Kartendarstellung beinhalten, um den Nutzer die Auswahl eines Standorts zu erlauben.
\n<textarea-special type="text/html">\n <textarea name="html">\n …\n </textarea>\n</textarea-special>\n
\nWie lange haben wir schon auf einen einfach im Browser funktionierenden Rich-Text-Editor gewartet. Die Komponente ist sogar so schlau, das generierte HTML wieder in das über ihm liegende Formular zurückzugeben.
\nTheoretisch könnte sie ja aber auch ganz andere Dinge zur Bearbeitung anbieten. text/markdown
? text/csv
? text/yaml
? Warum nicht? Das könnte man alles über das type
-Attribut lösen.
Web Components erlauben in modernen Browsern, eigene Tags mit beliebig komplexen Verhalten zu definieren. Und wie der Name „Komponente“ schon nahelegt, kann man diese kleinen Bibliotheken beliebig oft weiterverwenden, sobald diese einmalig geladen wurden.
\nMit einigen kleinen Kniffen kann die Entwicklung solcher Komponenten noch schneller von der Hand gehen.
", "content_encoded": "Web Components erlauben in modernen Browsern, eigene Tags mit beliebig komplexen Verhalten zu definieren. Und wie der Name „Komponente“ schon nahelegt, kann man diese kleinen Bibliotheken beliebig oft weiterverwenden, sobald diese einmalig geladen wurden.
\nMit einigen kleinen Kniffen kann die Entwicklung solcher Komponenten noch schneller von der Hand gehen.
\n\nFür den Einstieg in das Thema Web Components empfiehlt sich die Lektüre der ausgezeichneten Einführung von Google und CSS-Tricks. Andere Anleitungen basieren teilweise auf älteren Ideen, die getrost ignoriert werden können.
\nUpdate 2020–09: Die Vielzahl der Wege, wie man eine Web Component bauen kann, hat webcomponents.dev veranlasst, eine Auflistung aller möglichen Wege zum Erstellen einer Web Component zusammenzustellen.
\nAußerdem lohnt es sich immer, moderne Beispiele anzuschauen. Ich für meinen Teil habe entsprechend versucht, einen Horizontal Situation Indicator als Vanilla-JavaScript Web Component mustergültig zu bauen. Der daraus resultierende Quellcode der „Horizontal Situation Indicator“ Web Component wird im Rahmen dieses Artikels immer wieder als Beispiel herangezogen.
\n\nDie größte Hürde für den angehenden Komponenten-Bauer ist das Verständnis, wie all die schönen Teile zusammenpassen.
\nDa eine Web Compoment gekapselt ist, kann nur über eine vorher definierte Schnittstelle von außen Zugriff auf ihr Verhalten genommen werden. Es ist also sehr sinnig, die Konzeption und den Bau einer Web Component wie den Bau einer Schnittstelle bzw. eines Interfaces zu verstehen.
\nAls Beispiel nehmen wir einfach die fertige Implementation der HSI-Web-Component:
\n<horizontal-situation-indicator id="hsi" heading="45.0" heading-select="0.0"></horizontal-situation-indicator>\n
\nBeim Bau einer Web Component müssen neben dem Namen der Web Component auch die Attribute der Web Component und ihre möglichen Werte definiert werden.
\nDiese Attribute verwandeln sich innerhalb der JavaScript-Repräsentation der Web Component in Properties, die mit den Attributen synchronisiert sind:
\nconst el = document.getElementById('hsi');\nel.getAttribute('heading'); // "45.0"\nel.heading; // "45.0"\n\nel.setAttribute('heading', '60.0');\nel.getAttribute('heading'); // "60.0"\nel.heading; // "60.0"\n\nel.heading = '135.0';\nel.getAttribute('heading'); // "135.0"\nel.heading; // "135.0"\n
\nBei dem Zugriff auf eine Property mit einem -
im Namen funktioniert der Zugriff leicht anders:
el.getAttribute('heading-select');\n//el.heading-select existiert nicht\nel['heading-select']; // korrekte Schreibweise in `[]`\n
\nDie Schreibweise mit []
erlaubt auch den dynamischen Zugriff auf Properties:
let attrName = 'heading-select';\n\nel.getAttribute(attrName);\nel[attrName];\n
\nDarüber hinaus kann der Entwickler einer Web Component noch festlegen, dass die Komponente JavaScript-Methoden anbietet. Diese erlauben zum Beispiel von außen der Komponente zu befehlen, komplexe Prozesse innerhalb der Komponente zu erledigen.
\nel.synchronizeHeading();\n
\nDa JavaScript keine Sichtbarkeiten wie public
und private
für Methoden hat, hat sich als Konvention herausgebildet, private Methoden mit einem _
zu beginnen.
Außerdem kann eine Web Component noch JavaScript-Events erzeugen, die außerhalb der Komponente registriert werden können. So emittiert z.B. das <video>
-Tag Events, wenn ein Video beendet wurde, was ohne dieses Event außerhalb des Tags keiner wissen könnte.
Web Components können fast alles darstellen, was auch regulär in einem Browser dargestellt werden kann. Besonderes Augenmerk muss aber darauf gelegt werden, dass alle benötigten Bestandteile in der einen JavaScript-Datei enthalten ist, die die Web Component definiert.
\nAm Einfachsten einzubinden sind die folgenden Dinge:
\nAlle anderen Ressourcen (Bilder, Videos, Töne) können mit einem Trick eingeschmuggelt werden: Mittels Data URLs können Binär-Dateien in Base64-Zeichenketten umgewandelt werden, die dann z.B. im src
-Attribut eines <img>
eingebunden werden können.
Besonders spannend: SVG-Bilder sind nicht nur schön kompakt in Bezug auf ihren Speicherplatz, sondern können auch direkt in das HTML eingebunden werden – benötigen also den Base64-Trick nicht.
\nIn der Tat gibt es eine ganze Menge Tools, um das Zusammensetzen der einzelnen Teile einer Web Component zu unterstützen. In der Regel reicht aber ein kleiner flotter Node.js-Mehrzeiler als Web Component Build Tool, der aus einzelnen Dateien die eigentliche Web Component zusammensetzt. Die Kurzfassung:
\nconst fs = require('fs');\n\nlet source = fs.readFileSync(`horizontal-situation-indicator.js`).toString();\nlet templateCss = fs.readFileSync(`src/horizontal-situation-indicator.css`).toString();\nlet templateSvg = fs.readFileSync(`src/horizontal-situation-indicator.svg`).toString();\n\nsource = source.replace(/(<style>).*(<\\/style>)/ms, templateCss);\nsource = source.replace(/(<\\/style>).*(`)/ms, templateSvg);\nfs.writeFileSync(`horizontal-situation-indicator.js`, source);\n
\nDas tatsächliche Skript ist zwar etwas komplexer, das Grundprinzip ist aber ein denkbar einfaches: Die Entwicklung von SVG und CSS (oder jedem anderen Dateitypen) findet in separaten Dateien statt, die mit dem obigen Skript einfach in das JavaScript der Web Component hineinkopiert werden. Unter anderem könnte hier auch die Konvertierung von Binär-Daten in ihre Base64-Entsprechung durchgeführt werden.
\nget
und set
für jede Property abkürzen?Da jeder Web Component eine Liste der zu synchronisierenden Attribute / Properties mit der Methode observedAttributes
bekannt gemacht werden muss, kann genau diese Liste im constructor
auch zum programmatischen Erzeugern von Gettern / Settern verwendet werden.
this.constructor.observedAttributes.forEach((attrName) => {\n Object.defineProperty(this, attrName, {\n get() {\n return this.getAttribute(attrName);\n },\n set(attrValue) {\n this.setAttribute(attrName, attrValue);\n }\n });\n});\n
\nDiese Methode hat in einigen Web-Components-Frameworks möglicherweise Nachteile – für die Vanilla-Nutzung ist sie aber weitestgehend ungefährlich.
\nDa Web Components sowieso nur in aktuellen Browsern zuverlässig funktionieren, kann man sich gleichzeitig auch auf fortgeschrittene CSS-Möglichkeiten verlassen. Um CSS innerhalb der Komponente von außen zu beeinflussen, verwende ich CSS-Variablen bzw. CSS-Custom-Properties. Innerhalb des CSS' des Komponente definiere ich sie direkt an der DOM-Wurzel der Komponente:
\n:host {\n --background-color: black;\n --foreground-color: white;\n --heading-select-color: cyan;\n --stroke-width: 0.5;\n}\n/*…und verwende diese CSS-Custom-Properties dann später in Variablen - bei mir z.B. als SVG-CSS-Eigenschaften:*/\n\n#background {\n fill: var(--background-color);\n}\n* {\n fill: var(--foreground-color);\n}\n*[stroke] {\n stroke-width: var(--stroke-width);\n}\n\n#heading-select {\n fill: var(--heading-select-color);\n}\n
\nWer nun auch immer diese Komponente verwendet, kann diese CSS-Custom-Properties von außen beeinflussen:
\nhorizontal-situation-indicator {\n --heading-select-color: red;\n --stroke-width: 1;\n}\n
\nBei der Beispiel-Implementation von <horizontal-situation-indicator>
kann auch bewundert werden, wie durch JavaScript diese CSS-Custom-Properties am lebenden Objekt verändert werden, und in der Komponente sich alles fröhlich verfärbt.
Ganz nebenbei haben wir für die Komponente eine weitere Schnittstelle geschaffen – in diesem Fall eine Styling-Schnittstelle.
\nUpdate: Andererseits können aber auch einzelne DOM-Knoten ohne CSS-Properties zum expliziten Styling freigegeben werden. Eine Anleitung zum Freigeben von DOM-Knoten aus dem Shadow-DOM zum CSS-Styling bei CSS-Tricks zeigt die notwendigen Anpassungen im HTML:
\n<div part="style-me">…</div>\n
\n…und dem CSS im Eltern-Dokument:
\nhorizontal-situation-indicator::part(style-me) {\n font-weight: bold;\n}\n
\nAuch hier hat wieder der Autor der Komponente die Herrschaft über die Elemente, die er nach außen freigibt – wie bei einer Schnittstelle.
\nDer eigentliche Clou der HSI Web Component ist die generelle Fähigkeit von JavaScript, DOM-Elemente und ihre Eigenschaften zu verändern. Dies können sowohl CSS-Eigenschaften als auch generelle Attribute von DOM-Elementen sein.
\nBei SVG bieten sich die folgenden Operationen an:
\nstroke
und stroke-width
zur Beeinflussung von Linienfill
zur Veränderung der Füllfarbeopacity
zur Veränderung der Durchsichtigkeit eines Elementstransform
<text>
-Knoten mittels .textContent
Bei SVG gibt es dabei die Möglichkeit, nicht nur via CSS diese Eigenschaften zu beeinflussen, sondern auch durch das Setzen von Attributen innerhalb des SVGs an einzelnen SVG-DOM-Knoten.
\nAuch das ist in der Beispiel-Implementation von <horizontal-situation-indicator>
zu bestaunen – hier sind die Attribute der Komponente mit Animationsmethoden verknüpft, so dass Änderungen an den Attributen bzw. Properties der Web Component zeitgleich die Darstellung des eingeschlossenen SVGs ändert.
Da eine Web Component im Endeffekt eine Schnittstelle ist, muss es dazu eine Schnittstellen-Dokumentation geben. Ohne diese Dokumentation können andere Entwickler, die die Komponente verwenden möchten, nicht zuverlässig wissen, wie die Komponente zu bedienen ist.
\nAls Minimum muss eine Dokumentation enthalten:
\n## Properties\n\n| Name | Type | Default | Description |\n| -------------- | ------- | ------- | ------------ |\n| `heading` | `float` | `null` | Lorem ipsum… |\n\n## Methods\n\n| Name | Parameters | Description |\n| -------------- | ---------- | ------------------- |\n| `revHeading` | none | Lorem ipsum… |\n\n## Events\n\n| Name | Description |\n| -------------- | -------------------------------- |\n| `synchronized` | Lorem ipsum… |\n\n## Styling\n\n```css\ncomponent-name {\n --background: color; /* Lorem ipsum… */\n}\n\ncomponent-name::part(part-name) {} /* Lorem ipsum… */\n```\n
\nDer fertige Horizontal Situation Indicator als Web Component ist in einem GitHub-Repository gelandet, und einen Blick auf die fertige Implementation der HSI-Web-Component erlaubt einen interaktiven Blick auf die Zusammenhänge in der Komponente.
", "link": "https://journal.3960.org/posts/2020-04-05-svg-web-components/", "pubDate": "Sun, 05 Apr 2020 18:41:26 +0200", "atom_published": "2020-04-05T18:41:26+02:00", "atom_updated": "2023-03-05T10:14:07+01:00", "guid": "user/posts/2020-04-05-svg-web-components/index.md", "author": "info@3960.org (Frank Boës)", "categories": [ "Webdevelop", "Web-Components", "SVG", "Javascript", "CSS", "Programmierung", "Technologie", "The Cool", "Fliegerei" ] }, { "title": "Event-Handling mit JavaScript – und ohne jQuery", "description": "Als Web-Entwickler fügen wir im Laufe eines Projektes einer Website eine zumeist nicht unerhebliche Anzahl an JavaScript-Event-Handlern hinzu – sei es mit jQuery oder regulärem JavaScript (You Might Not Need jQuery). Abhängig von der gewählten Methode lässt sich damit… die Performance einer Website gründlich ruinieren.
\nAber das muss nicht sein – wie dieser Überblick über die Montage von Event-Handlern in JavaScript / jQuery zeigt.
", "content_encoded": "Als Web-Entwickler fügen wir im Laufe eines Projektes einer Website eine zumeist nicht unerhebliche Anzahl an JavaScript-Event-Handlern hinzu – sei es mit jQuery oder regulärem JavaScript (You Might Not Need jQuery). Abhängig von der gewählten Methode lässt sich damit… die Performance einer Website gründlich ruinieren.
\nAber das muss nicht sein – wie dieser Überblick über die Montage von Event-Handlern in JavaScript / jQuery zeigt.
\n\nGanz grundsätzlich muss jeder Prozessschritt beim Hinzufügen eines Event-Handlers richtig angewendet werden. Der ganze Vorgang besteht aus drei Schritten:
\nIn jedem dieser Schritte lässt sich zum Teil massiv optimieren.
\nUm einen Event-Handler montieren zu können, muss dieser an ein Element angekoppelt werden – in der Regel ist dies ein DOM-Element. Dazu gibt es verschiedene Methoden, DOM-Elemente zu selektieren. Je nach gewählter Methode ist dies mehr oder weniger performant.
\n\nFaustformel: Je eindeutiger das Suchmerkmal und je kleiner die Menge der zu durchsuchenden Elemente, desto schneller ist die Suche.
Die schnellste Methode ist dabei die Selektion über ein id
-Attribut – die langsamste dagegen die Suche nach einem beliebigen Attribut, im schlimmsten Fall mit der Prüfung, ob dieses Attribut einen bestimmten Wert beinhaltet.
Methode | \njQuery | \nJavaScript | \nNeues JavaScript | \n
---|---|---|---|
ID | \n$('#x') | \ndocument.getElementById('x') | \ndocument.querySelector('#x') | \n
Klasse | \n$('.x') | \ndocument.getElementsByClassName('x') | \ndocument.querySelectorAll('.x') | \n
Tag | \n$('x') | \ndocument.getElementsByTagName('x') | \ndocument.querySelectorAll('x') | \n
Attribut | \n$('[x]') | \nn/a | \ndocument.querySelectorAll('[x]') | \n
CSS (s.u.) | \n$('x y') | \nn/a | \ndocument.querySelectorAll('x y') | \n
Sowohl die jQuery-Methode $(…)
als auch die JavaScript-Methoden .querySelector()
/ .querySelectorAll()
unterstützen die Auswahl per CSS-Selektor. Damit können auch kompliziertere Suchen im DOM durchgeführt werden, wie z.B. mit nav a
das Auffinden aller <a>
in einem <nav>
. Zum Glück ist die Browser-Unterstützung für .querySelector()
/ .querySelectorAll()
inzwischen sehr gut.
Zu beachten ist, dass die JavaScript-Methoden .querySelector()
/ .querySelectorAll()
je nach Browser um ein Mehrfaches langsamer sind als ihre „einfachen“ Geschwister .getElementById
, .getElementsByClassName
und .getElementsByTagName
.
Interessanterweise kann jede Methode nicht nur auf das gesamte Dokument angewendet werden, sondern auf eine bereits bestehende Selektion. Damit kann die Suche stark beschleunigt werden.
\nIn jQuery existiert dafür die .find()
-Methode, die analog zu .on()
funktioniert…
var navigation = $('nav');\nvar navLinks = navigation.find('a');\nvar navBolds = navigation.find('b');\n
\n…in regulärem JavaScript bleiben die Methoden identisch zu den für die Suche im Dokument verfügbaren Methoden:
\nvar navigation = document.querySelector('nav');\nvar navLinks = navigation.querySelectorAll('a');\nvar navBolds = navigation.querySelectorAll('b');\n
\nDiese Methode kann sehr hilfreich sein, wenn später sowieso DOM-Manipulation an übergeordneten DOM-Elementen notwendig werden.
\nZu beachten ist bei regulärem JavaScript, dass die Methoden .getElementById()
und .querySelector()
ein einzelnes Element
bzw. einen einzelnen Node
(d.h. ein DOM-Element) zurückgeben, während alle anderen Selektions-Methoden eine HTMLCollection
bzw. NodeList
zurückgeben, die vereinfacht gesagt Arrays von Node
s sind.
Für das Hinzufügen von Event-Listenern bietet jQuery die Methode .on()
, und JavaScript die Methode .addEventListener()
an. (Wir ignorieren die Methoden zum Hinzufügen von Event-Handlern direkt via HTML-Attribut, da dadurch eine unglückliche Verkettung von Content (HTML) und Verhalten (JavaScript) entsteht.)
In beiden Fällen verfügt die Selektion über eine Methode, der man nur den Event-Typ und den eigentlichen Event-Listener übergeben muss.
\n$('nav').on('click', function() {\n $(this).addClass('active');\n})\n
\nDer selbe Aufruf ist in Vanilla-JavaScript etwas mehr Schreibarbeit, aber ansonsten identisch:
\ndocument.querySelector('nav').addEventListener('click', function(event) {\n event.target.classList.add('active');\n});\n
\nZu beachten in JavaScript: Event-Listener können nur einem einzelnen DOM-Element hinzugefügt werden – jQuery erlaubt es, am Stück mehreren DOM-Elementen ein und denselben Event-Listener hinzuzufügen.
\nIn beiden Fällen steht im Event-Listener mit $(this)
bzw. event.target
das DOM-Element direkt zur Verfügung, auf dem das Event ausgelöst wurde. Voraussetzung ist bei JavaScript, dass der Event-Listener als ersten Parameter eine Variable namens event
gesetzt bekommen hat.
Ein nicht unwahrscheinlicher Anwendungsfall ist, verschiedene Event-Typen mit dem selben Event-Handler bedienen zu wollen. In jQuery kann man an die .on()
-Methode eine Liste an verschiedenen Event-Typen übergeben:
// Fires on `click` `keyup` `blur`\n$('nav').on('click keyup blur', function() {\n $(this).addClass('active');\n})\n
\nIn JavaScript ist ein bisschen mehr Gehirnschmalz notwendig, denn hier müssen wir jeden Event-Listener mit einem einzelnen Aufruf hinzufügen. Das könnte man in einer Schleife tun…
\n// Bad example: Fires on `click` `keyup` `blur`\n['click', 'keyup', 'blur'].forEach(function(eventType) {\n document.querySelector('nav').addEventListener(eventType, function(event) {\n event.target.classList.add('active');\n });\n});\n
\n…und handelt sich auf diese Weise zwei Performance-Killer ein: Einerseits wird in jedem Schleifendurchlauf document.querySelector
neu ausgewertet, andererseits wird jedes Mal Speicher für eine neue, anonyme Funktion reserviert. Glücklicherweise kann man beide Konstruktionen aus dem Schleifenkörper herausziehen:
// Better example: Fires on `click` `keyup` `blur`\nvar eventTarget = document.querySelector('nav');\nvar eventListener = function(event) {\n event.target.classList.add('active');\n};\n\n['click', 'keyup', 'blur'].forEach(function(eventType) {\n eventTarget.addEventListener(eventType, eventListener);\n});\n
\nDas Array abgerollt sieht dann sogar noch übersichtlicher aus, und zeigt plastisch den Vorteil der vorherigen Deklaration von Event-Ziel und -Listener:
\n// Best example for readability: Fires on `click` `keyup` `blur`\nvar eventTarget = document.querySelector('nav');\nvar eventListener = function(event) {\n event.target.classList.add('active');\n};\n\neventTarget.addEventListener('click', eventListener);\neventTarget.addEventListener('keyup', eventListener);\neventTarget.addEventListener('blur', eventListener);\n
\nWenn sich auf einer Seite mehrere DOM-Objekte befinden, die wir mit einem identischen Event-Handler ausstatten wollen, so gibt es in jQuery die folgende Methode.
\n// Add Event Handler to all `.btn`\n$('.btn').on('click', function() {\n $(this).addClass('active');\n})\n
\nBesonders spannend: Bei .on()
und .querySelectorAll()
können auch mehrere CSS-Selektoren, durch Kommata getrennt, gleichzeitig abgefragt werden. Mit header a, footer a
kriegt man eine Liste aller <a>
in <header>
und <footer>
zurück.
In Vanilla-JavaScript wird das Hinzufügen zu Event-Listenern zu mehreren DOM-Elementen etwas umständlicher, weil ein Event-Handler immer nur einem DOM-Objekt hinzugefügt werden kann. Wenn wir also eine Liste von DOM-Objekten haben, müssen wir jedem DOM-Objekt beim Durchlaufen einer Schleife einen Handler verpassen:
\n// Add Event Handler to all `.btn`\ndocument.querySelectorAll('.btn').forEach(function(btn) {\n btn.addEventListener('click', function(event) {\n event.target.classList.add('active');\n });\n});\n
\nWarum ist das in JavaScript eigentlich so deutlich weniger bequem? Der Grund ist ganz einfach: Diese Konstruktion hat massive Performance-Auswirkungen. Wir fügen damit eine größere Anzahl von Event-Handlern hinzu, die alle das Gleiche tun, aber unterschiedliche DOM-Objekte beobachten müssen. Damit muss der Browser mehr Dinge beobachten. Bei einer 10×10 Zellen umfassenden Tabelle kann ein Event-Listener für eine Tabellenzelle also auf insgesamt 100 Events verteilt werden.
\nFür diesen Fall bietet jQuery eine performante Alternative: Statt jedes Element einzeln mit einem Event-Handler auszustatten, wird einfach ein DOM-Objekt ausgewählt, dass im DOM oberhalb der zu beobachtenden DOM-Objekte liegt. Dieses übergeordnete Objekt wird über alle Events informiert, die unterhalb von ihm stattfinden – das sogenannte „Event Bubbling“.
\nIn jQuery wird der Filter für das eigentliche Event-Ziel als zusätzlicher Parameter von .on()
übergeben.
// Add Event Handler to `nav`, fire if `.btn` was clicked\n$('nav').on('click', '.btn', function() {\n $(this).addClass('active');\n})\n
\nIn Vanilla-JavaScript wird das Event-Ziel innerhalb des Event-Listeners gefiltert:
\n// Add Event Handler to `nav`, fire if `.btn` was clicked\ndocument.querySelector('nav').addEventListener('click', function(event) {\n if (event.target.matches('.btn')) {\n event.target.classList.add('active');\n }\n});\n
\nKollege Malte wies mich noch darauf hin, dass ein Klick natürlich auch auf ein in dem eigentlich interessanten Element liegenden Element stattgefunden haben kann (zum Beispiel der Klick auf ein Bild, das in einem Link liegt, den wir überwachen wollen). Die Lösung dafür ist mit .closest()
einfach zu bewerkstelligen:
// Add Event Handler to `nav`, fire if `.btn` or element inside `.btn` was clicked\ndocument.querySelector('nav').addEventListener('click', function(event) {\n var targetBtn = event.target.closest('.btn');\n if (targetBtn) {\n targetBtn.classList.add('active');\n }\n});\n
\nBezogen auf unser Beispiel mit den 100 Tabellenzellen haben wir gerade aus 100 Event-Handlern einen einzigen Event-Handler gemacht. Das spart nicht nur Speicher, sondern erlaubt es auch, auf DOM-Elemente zu reagieren, die beim Hinzufügen des Event-Handlers noch gar nicht im DOM existierten. Wenn zum Beispiel zu unserem <nav>
-Element erst nach dem Hinzufügen des Event-Handlers neue <… class=„btn“>
zum Beispiel via AJAX hinzugefügt werden, wird unser Event-Handler auf dem <nav>
auch diese Elemente mit verarbeiten, da er ja auf alle Elemente unterhalb von ihm reagiert.
Wie schon in dem Artikel „Simple & Boring“ von Chris Coyier hat auch Bastian Allgeier eine Lanze für Einfachheit in der Programmierung gebrochen. Sein Artikel „Simplicity (II)“ dürfte vielen altgedienten Programmierern aus der Seele sprechen.
\nTatsächlich bemerke ich sowohl in der privaten als auch beruflichen Programmierung den Trend, für mehr Geschwindigkeit ein neues Tool einzusetzen… das kleine Probleme verursacht, die durch ein weiteres Tool gelöst werden müssen… das kleine Probleme verursacht, die durch ein weiteres Tool gelöst werden müssen…
\n\nDer Artikel dreht sich zwar primär darum, was diese Abhängigkeiten gerade für ältere Projekte bedeuten (nämlich, dass Abhängigkeiten nach ein paar Jahren sich nicht wieder auslösen lassen, weil die dafür benötigten Versionen an Tools nicht mehr zur Verfügung stehen), inzwischen bemerke ich aber auch bei aktuellen Projekten die Probleme, die übermäßige Abhängigkeiten für die Entwicklungsgeschwindigkeit bedeuten können, wenn auch nur ein Teil ausfällt.
", "content_encoded": "Wie schon in dem Artikel „Simple & Boring“ von Chris Coyier hat auch Bastian Allgeier eine Lanze für Einfachheit in der Programmierung gebrochen. Sein Artikel „Simplicity (II)“ dürfte vielen altgedienten Programmierern aus der Seele sprechen.
\nTatsächlich bemerke ich sowohl in der privaten als auch beruflichen Programmierung den Trend, für mehr Geschwindigkeit ein neues Tool einzusetzen… das kleine Probleme verursacht, die durch ein weiteres Tool gelöst werden müssen… das kleine Probleme verursacht, die durch ein weiteres Tool gelöst werden müssen…
\n\nDer Artikel dreht sich zwar primär darum, was diese Abhängigkeiten gerade für ältere Projekte bedeuten (nämlich, dass Abhängigkeiten nach ein paar Jahren sich nicht wieder auslösen lassen, weil die dafür benötigten Versionen an Tools nicht mehr zur Verfügung stehen), inzwischen bemerke ich aber auch bei aktuellen Projekten die Probleme, die übermäßige Abhängigkeiten für die Entwicklungsgeschwindigkeit bedeuten können, wenn auch nur ein Teil ausfällt.
", "link": "https://bastianallgeier.com/notes/simplicity-part-2", "pubDate": "Fri, 13 Sep 2019 18:23:11 +0200", "atom_published": "2019-09-13T18:23:11+02:00", "atom_updated": "2019-10-17T18:51:03+02:00", "guid": "user/posts/2019-09-13-programmierung-zurueck-zur-werkbank/index.md", "author": "info@3960.org (Frank Boës)", "categories": [ "CSS", "Javascript", "Meinung", "PHP", "Programmierung", "Webdevelop", "Geckobar" ] }, { "title": "Bilder und iFrames einfach mit Lazy-Loading ausstatten", "description": "Lazy-Loading ist eine beliebte Technik, um die gefühlte Geschwindigkeit einer Internetseite zu erhöhen. Statt alle Bilder einer Webseite schon beim Laden der Seite mitzuladen, werden nur die Bilder geladen, die auch tatsächlich sichtbar sind. Damit verringert man gerade auf langen Seiten die initial geladene Menge an Bildern.
\nBisher hatte das mit etwas Aufwand zu tun, und auf jeden Fall mit JavaScript. Netterweise gibt es inzwischen eine deutlich einfachere Lösung.
", "content_encoded": "Lazy-Loading ist eine beliebte Technik, um die gefühlte Geschwindigkeit einer Internetseite zu erhöhen. Statt alle Bilder einer Webseite schon beim Laden der Seite mitzuladen, werden nur die Bilder geladen, die auch tatsächlich sichtbar sind. Damit verringert man gerade auf langen Seiten die initial geladene Menge an Bildern.
\nBisher hatte das mit etwas Aufwand zu tun, und auf jeden Fall mit JavaScript. Netterweise gibt es inzwischen eine deutlich einfachere Lösung.
\n\nDie bisherigen Lösungen gingen davon aus, dass irgendeine Form von LazyLoad-JavaScript auf der Seite montiert wurde, und das HTML eines jeden Bildes abgeändert werden musste:
\n<!-- Load when visible -->\n<img src="example-lowres.jpg"\n data-src="example-highres.jpg" class="lazyload"\n alt="" width="240" height="240" />\n
\nModerne Browser unterstützen aber auch ein Attribut namens loading
, dass das Ladeverhalten von <img>
und <iframe>
steuert, ohne zusätzliches Javascript. Genaue Details kann man einem Blog-Post des Chrome-Entwicklers Addy Osmani über Lazy-Loading entnehmen, im HTML sieht das aber schlicht und ergreifend wie folgt aus:
<!-- Load when visible -->\n<img src="example.jpg" loading="lazy" alt="" width="240" height="240" />\n<iframe src="example.html" loading="lazy"></iframe>\n\n<!-- Load as soon as possible -->\n<img src="example.jpg" loading="eager" alt="" width="240" height="240" />\n<iframe src="example.html" loading="eager"></iframe>\n
\nDanach sollte man beim Besuch einer Seite, die mit loading
-Attributen versehen ist, beim Öffnen des Inspektors bemerken, dass erst beim Scrollen auf der Seite weiter unten befindliche Bilder bzw. iFrames nachgeladen werden – oder halt, wenn der Browser sich gerade langweilt.
Damit entfällt in Google Chrome, Mozilla Firefox, Opera und dem aktuellen Microsoft Edge die Notwendigkeit, JavaScript für Lazy-Loading auf der eigenen Seite zu montieren (so wie in allen Browsern, die zukünftig dieses Feature unterstützen). Zudem können Browser ohne diese Möglichkeit bzw. ohne JavaScript immer noch die selben Inhalte sehen.
\nWichtig ist aber, nicht jedes Element mit Lazy-Loading auszustatten. Demnach scheint es für Browser Performance-Probleme für Bilder / iFrames im sichtbaren Bereich zu geben, wenn dort Lazy-Loading verwendet wird. Eine halbwegs smarte Lösung würde also zumindest die erste Ressource auf der Seite nicht mit Lazy-Loading ausstatten.
\nUm in einem gesamten Content-Block jedes <img>
und <iframe>
mit dem passenden loading
-Attribut zu versehen, reicht folgende kleine Funktion:
const lazyloadAttributes = function(html, loading = 'lazy') {\n return html.replace(/(<(?:img|iframe) )/g, '$1loading="' + loading + '" ');\n};\n
\nUm einen Automatismus für das Ignorieren der ersten X Bilder / iFrames für Lazy-Loading zu erreichen, führen wir den neuen Paramter ignoreLoadingFor
ein, der standardmäßig für das erste passende Element keine Ersetzung vornimmt. Wenn wir ignoreLoadingFor
auf 2 setzen, werden entsprechend die ersten zwei Elemente ignoriert. Mit etwas Geschick kann man so zum Beispiel auch in Schleifen nur das erste Element in dem ersten Schleifenelement nicht ersetzen.
const lazyloadAttributes = function(html, loading = 'lazy', ignoreLoadingFor = 1) {\n return html.replace(/(<(?:img|iframe) )/g, function(all) {\n if (ignoreLoadingFor > 0) {\n ignoreLoadingFor --;\n return all;\n }\n return all + 'loading="' + loading + '" ';\n });\n};\n
\nDamit werden in einem Block die ersten X Elemente nicht mit Lazy-Loading versehen, alle nachfolgenden Elemente aber schon.
\nGleichsam können in PHP z.B. redaktionelle Texte durch diese Funktion durchgeleitet werden, um überall Lazy-Loading hinzuzufügen:
\nfunction lazyloadAttributes($html, $loading = 'lazy')\n{\n return preg_replace('/(<(?:img|iframe) )/is', '$1loading="' . $loading . '" ', $html);\n}\n
\n…und mit dem Mechanismus zum Ignorieren der ersten Bilder / iFrames für Lazy-Loading:
\nfunction lazyloadAttributes(string $html, string $loading = 'lazy', int $ignoreLoadingFor = 1): string\n{\n $html = preg_replace('/(<(?:img|iframe) )/is', '$1loading="' . $loading . '" ', $html);\n if ($ignoreLoadingFor > 0) {\n $html = preg_replace('/(<(?:img|iframe)) loading=".+?"/is', '$1', $html, $ignoreLoadingFor);\n }\n return $html;\n}\n
\n…wenn auch etwas weniger elegant als in JavaScript.
\nEigentlich gibt es wenig Gründe, dass Attribut nicht einzusetzen. Gerade auf länglichen Übersichtsseiten kann das Erlebnis für den Besucher deutlich verbessert werden. Und für Mobilgeräten mit geringer Bandbreite kann der Geschwindigkeitszuwachs immens sein.
", "link": "https://journal.3960.org/posts/2019-04-10-bilder-iframes-einfach-mit-lazy-loading-ausstatten/", "pubDate": "Wed, 10 Apr 2019 19:02:20 +0200", "atom_published": "2019-04-10T19:02:20+02:00", "atom_updated": "2021-10-21T15:24:22+02:00", "guid": "user/posts/2019-04-10-bilder-iframes-einfach-mit-lazy-loading-ausstatten/index.md", "author": "info@3960.org (Frank Boës)", "categories": [ "Webdevelop", "Für Facebook", "Javascript", "Programmierung" ] }, { "title": "JSON-RSS 2018 – Eine neue Standardisierung", "description": "Schon vor sehr langer Zeit habe ich über JSON-RSS nachgedacht. Dabei ging es mir darum, den Newsfeed-Standard RSS statt mit XML in dem deutlich leichtgewichtigeren und vor allen Dingen mit JavaScript direkt lesbaren JSON zu erzeugen. Inzwischen habe ich ein paar neue Erkenntnisse zu dem aus 2011 stammenden JSON-RSS, und nenne diesen „Standard“ einfach JSON-RSS 2018.
", "content_encoded": "Schon vor sehr langer Zeit habe ich über JSON-RSS nachgedacht. Dabei ging es mir darum, den Newsfeed-Standard RSS statt mit XML in dem deutlich leichtgewichtigeren und vor allen Dingen mit JavaScript direkt lesbaren JSON zu erzeugen. Inzwischen habe ich ein paar neue Erkenntnisse zu dem aus 2011 stammenden JSON-RSS, und nenne diesen „Standard“ einfach JSON-RSS 2018.
\n\nDie Grundidee ist es, das XML mittels einer allgemein gültigen Übersetzungsregel in JSON zu übersetzen. Ein paar Besonderheiten gibt es aber, da XML deutlich anders strukturiert ist als JSON. Die (etwas unscharfen) Regeln sehen wie folgt aus:
\n<item>
als items
. Die Eigenschaft muss dann ein Array von Objekten enthalten, wobei jedes Objekt die Eigenschaften des ursprünglichen Tags enthalten muss._content
ausgegeben wird.xmlns
wird in JSON durch ein xmlns
-Objekt gelöst, in dem die einzelnen Namensräume zu Objekteigenschaften werden.:
mit _
geschrieben, z.B. <geo:fields>
als geo_fields
.Umgemünzt auf JSON-RSS 2018 ergibt dies beispielhaft die folgende Ausgabe:
\n{\n "version": "2.0",\n "xmlns": {\n "atom": "http://www.w3.org/2005/Atom",\n "content": "http://purl.org/rss/1.0/modules/content/",\n "georss": "http://www.georss.org/georss",\n "gml": "http://www.opengis.net/gml"\n },\n "channel": {\n "title": "fboës - Der Blog | Startseite",\n "link": "https://journal.3960.org/",\n "description": "Programmierung, Raumfahrt und Kurioses: Der Blog von und mit Frank Boës.",\n "language": "de-DE",\n "copyright": "© 2008-2018 Creative Commons BY",\n "atom_link": {\n "href": "https://journal.3960.org/rss.json",\n "rel": "self",\n "type": "application/rss+json"\n },\n "lastBuildDate": "Tue, 14 Aug 2018 21:57:32 +0200",\n "atom_updated": "2018-08-14T21:57:32+02:00",\n "generator": "blogophon",\n "image": {\n "url": "https://cdn.3960.org/images/tile-128x128.png",\n "title": "fboës - Der Blog",\n "link": "https://journal.3960.org/"\n },\n "items": [\n {\n "language": "de-DE",\n "title": "Meine Git-Werkzeugkiste",\n "description": "Lorem ipsum...",\n "content_encoded": "<p>More lorem ipsum...</p>",\n "link": "https://journal.3960.org/posts/2018-07-19-git-werkzeugkiste/",\n "pubDate": "Thu, 19 Jul 2018 19:40:55 +0200",\n "atom_published": "2018-07-19T19:40:55+02:00",\n "atom_updated": "2018-08-14T08:10:35+02:00",\n "guid": "user/posts/2018-07-19-git-werkzeugkiste.md",\n "author": "info@3960.org (Frank Boës)",\n "categories": [\n "Programmierung",\n "Webdevelop",\n "Git",\n "Für Tumblr"\n ]\n }\n ]\n }\n}\n
\nDieser Blog produziert ein lebendes JSON-RSS-Beispiel in dem „JSON-RSS 2018“-Standard.
\nAber bei aller Begeisterung für das Gedankenexperiment „JSON-RSS“ empfehle ich doch, auf den seit 2017 existierenden und sich immer noch rapide verbreitenden Standard JSON Feed zu setzen. Neben einer zunehmenden Verbreitung auf Publikations-Seite (u.a. in Wordpress) und einer allgemein akzeptierten Standardisierung gibt es immer mehr Bibliotheken, die das Lesen von JSON-Feeds ermöglichen.
\nDarüber hinaus hat JSON-Feed von vorne herein Erweiterungsmöglichkeiten, die ähnlich wie Namespacing in XML funktionieren.
\nÜbrigens ist RSS gerade wieder in aller Munde: Die Diskussion, ob RSS tot sei, hat in letzter Zeit zu neuen Artikeln geführt:
\nMeiner Meinung nach kann RSS niemals sterben, wenn man sich die fantastischen Möglichkeiten für seine Nutzung anschaut, die sich auf technischer Ebene eröffnen.
", "link": "https://journal.3960.org/posts/2018-08-15-json-rss-neue-standardisierung/", "pubDate": "Wed, 15 Aug 2018 19:50:44 +0200", "atom_published": "2018-08-15T19:50:44+02:00", "atom_updated": "2020-02-03T12:33:05+01:00", "guid": "user/posts/2018-08-15-json-rss-neue-standardisierung/index.md", "author": "info@3960.org (Frank Boës)", "categories": [ "Webdevelop", "Blog", "Idee", "Javascript", "Programmierung", "Technologie" ] }, { "title": "PHP in Javascript übersetzen", "description": "Schon vor längerer Zeit hatte ich mir darüber Gedanken gemacht, wie man PHP-Klassen in Javascript-Objekte übersetzt. Jetzt habe ich einen kleinen Helfer bauen können, der einen Großteil der Arbeit automatisiert.
", "content_encoded": "Schon vor längerer Zeit hatte ich mir darüber Gedanken gemacht, wie man PHP-Klassen in Javascript-Objekte übersetzt. Jetzt habe ich einen kleinen Helfer bauen können, der einen Großteil der Arbeit automatisiert.
\n\nWie man am Beispiel eines kleinen Vigenère-Listings sieht, sind die Strukturen von PHP-Klassen und Javascript-Objekten eigentlich sehr gleichartig. Mit der folgenden Javascript kann man einen Großteil der Transformation durchführen, und muss danach nur noch einige Details anpassen.
\n/**\n * Afterwards you will need to:\n * - add `var` in front of uninitialized variables\n * - move default values for function parameters into function body like `param = param || 'default';`\n * - add `,` after every object property (variables & methods)\n * - replace PHP functions\n * @param {String} php [description]\n * @return {String} [description]\n */\nvar transformPhpToJs = function(php) {\n return php\n .replace(/<\\?php\\s*/g, '')\n .replace(/\\?>\\s*/g, '')\n .replace(/(protected|public|private|const) (\\S+)\\s?=/g, '$2 :')\n .replace(/(protected|public|private|const) /g, '')\n .replace(/(function) (\\S+)\\s?/g, '$2: $1')\n .replace(/(class) (\\S+)\\s?/g, 'var $2 = ')\n .replace(/\\$/g, '')\n .replace(/SELF::/g, 'this.')\n .replace(/->/g, '.')\n .replace(/array\\(([\\s\\S]*?)\\);/g, '[$1];')\n .replace(/=>/g, ':')\n .replace(/empty\\((.+?)\\)/g, '!$1')\n .replace(/preg_replace\\(['"](.+?)['"],(.+?),\\s?(.+?)\\s?\\)/g, '$3.replace(/$1/g,$2)')\n .replace(/preg_match\\(['"](.+?)['"],\\s?(.+?)\\s?\\)/g, '$2.match(/$1/)')\n ;\n}\n
\nDiese Funktion könnt ihr z.B. in die F12
-Konsole von eurem Browser eingeben, und danach euren PHP-Quelltext im Browser übersetzen lassen. Folgender Aufruf gibt euch das transformierte PHP zurück:
transformPhpToJs(`INSERT YOUR PHP HERE`);\n
\nGeht dafür wie folgt vor:
\nF12
eure Entwicklerkonsole.Enter
.Natürlich muss das Ergebnis noch nachbearbeitet werden, aber für die meisten Fälle erledigt es zumindest die groben Arbeiten. Danach könnt ihr z.B. daran gehen die richtige NodeJS-Modul-Struktur einzubauen.
\nUpdate: Mathias hat mich auf Locutus aufmerksam gemacht:
\n\nLocutus is a project that seeks to assimilate other languages’ standard libraries to JavaScript.
Hier findet ihr viele PHP-Funktionen als Javascript-Module umgesetzt. Die ganze Bibliothek ist zwar mit Augenzwinkern zu verstehen, für schnelle Erfolgserlebnisse eignet sie sich aber alle Mal.
", "link": "https://journal.3960.org/posts/2017-08-18-php-javascript-uebersetzen/", "pubDate": "Fri, 18 Aug 2017 19:02:00 +0200", "atom_published": "2017-08-18T19:02:00+02:00", "atom_updated": "2020-01-31T16:55:40+01:00", "guid": "user/posts/2017-08-18-php-javascript-uebersetzen.md", "author": "info@3960.org (Frank Boës)", "categories": [ "PHP", "Javascript", "Programmierung", "NodeJS", "Für Tumblr" ] }, { "title": "Backreferences in regulären Ausdrücken", "description": "Niemand auf diesem Planeten wird jemals ernsthaft behaupten, ein Meister der regulären Ausdrücke zu sein. Selbst nach jahrelangem Einsatz findet man immer wieder neue Techniken. Meine jüngste Erkenntnis: Backreferences in regulären Ausdrücken.
", "content_encoded": "Niemand auf diesem Planeten wird jemals ernsthaft behaupten, ein Meister der regulären Ausdrücke zu sein. Selbst nach jahrelangem Einsatz findet man immer wieder neue Techniken. Meine jüngste Erkenntnis: Backreferences in regulären Ausdrücken.
\n\nIm Blogophon (der Blog-Software zu dieser Seite) gibt es eine größere Bibliothek, die HTML in noch viel schöneres HTML verwandeln soll. Dazu wiederum werden Unmengen an regulären Ausdrücken verwendet.
\nEin spezieller Ausdruck ist dafür zuständig, alle Vorkommen von „…“
und ‚…‘
in ein <kbd>
-Tag einzuwickeln. Der erste Versuch sah wie folgt aus:
.replace(/("|'|')(.*?)("|'|')/g, '<kbd>$0</kbd>')\n
\nDummerweise ist der Ausdruck falsch, denn so findet er auch „…‚
und ‘…“
. Die Lösung für dieses Dilemma sind dann separate Ersetzungen:
.replace(/(")(.*?)(")/g, '<kbd>$0</kbd>')\n.replace(/(')(.*?)(')/g, '<kbd>$0</kbd>')\n.replace(/(')(.*?)(')/g, '<kbd>$0</kbd>')\n
\nWie man unschwer erkennt, ist das ein deutlicher Verstoß gegen das DRY-Prinzip, aber augenscheinlich notwendig, um eine Ersetzung nur durchzuführen, wenn Anfangs- und Endzeichen identisch sind.
\nBeim Lesen des Buches „Secrets of the Javascript Ninja“ von John Resig, Bear Bibeault und Josip Maras bin ich neben vielen anderen erhellenden Kapiteln auch auf das Kapitel über reguläre Ausdrücke gestoßen. Und hier werden im Nebensatz Backreferences erläutert.
\nMit einer Backreference kann man exakt den Inhalt einer einmal in einem regulären Ausdruck gefundenen Gruppe nochmals finden. Um z.B. den Inhalt von Gruppe 1 nochmal zu finden, verwendet man \\1
, von Gruppe zwei dementsprechend \\2
.
Das obige Beispiel ist damit korrekt:
\n.replace(/("|'|')(.*?)(\\1)/g, '<kbd>$0</kbd>')\n
",
"link": "https://journal.3960.org/posts/backreferences-regulaeren-ausdruecken/",
"pubDate": "Fri, 10 Feb 2017 19:32:09 +0100",
"atom_published": "2017-02-10T19:32:09+01:00",
"atom_updated": "2017-10-05T10:30:09+02:00",
"guid": "user/posts/backreferences-regulaeren-ausdruecken.md",
"author": "info@3960.org (Frank Boës)",
"categories": [
"Programmierung",
"Javascript",
"Für Tumblr",
"NodeJs",
"Performance"
]
},
{
"title": "Node.js Pattern: Das Modul",
"description": "Nach mehrmonatigen Experimentieren habe ich für mich endlich das gelungene Pattern gefunden, mit dem ich in Node.js meine Module baue.
", "content_encoded": "Nach mehrmonatigen Experimentieren habe ich für mich endlich das gelungene Pattern gefunden, mit dem ich in Node.js meine Module baue.
\n\n// module-name.js\n\n'use strict';\n\nconst someNodeModule = require('some-node-module');\nconst someOwnModule = require('./some-own-module');\n\n/**\n * [modulName description]\n * @constructor\n */\nconst modulName = function (config) {\n const external = {};\n const internal = {};\n\n /**\n * [someVariable description]\n * @type {[type]}\n */\n external.someVariable = null;\n\n /**\n * [someOtherVariable description]\n * @type {[type]}\n */\n internal.someOtherVariable = null;\n\n /**\n * [someFunction description]\n * @params {[type]} [name] [description]\n * @return {[type]} [description]\n */\n external.someFunction = function () {\n };\n\n /**\n * [someOtherFunction description]\n * @params {[type]} [name] [description]\n * @return {[type]} [description]\n */\n internal.someOtherFunction = function () {\n };\n\n return external;\n};\n\nmodule.exports = modulName;\n
\nDer Aufruf ist dann ganz einfach:
\n\nconst moduleName = require('./moduleName');\n\nmoduleName({});\n
\nEine Erweiterung des Objekts um neue Eigenschaften geht ebenfalls flott von der Hand:
\n// some-other-module-name.js\n\n'use strict';\n\nconst moduleName = require('./moduleName');\n\n/**\n * [someOtherModuleName description]\n * @constructor\n */\nconst someOtherModuleName = function (config) {\n const external = moduleName(config);\n const internal = {};\n\n // Add extra variables and methods here\n\n return external;\n};\n\nmodule.exports = modulName;\n
\nBei dieser Konstruktion sind folgende Punkte erfüllt:
\npublic
und private
Variablen und Methoden. Diese sind hier als external
und internal
bezeichnet, da public
und private
reservierte Wörter sind.new
.Natürlich gibt es noch unzählige andere Methoden. Aber bei dieser Methode finde ich die Mischung aus Übersichtlichkeit und Erweiterbarkeit für meine Belange am Besten.
\nUpdate: Für ECMAScript 6 wurden alle Variablendeklarationen mit const
durchgeführt. Wenn die Deklarationen abwärtskompatibel sein sollen, können diese mit var
gemacht werden.
Da der Microsoft Internet Explorer 8 global inzwischen eine Verbreitung unter 2% zu haben scheint, kann man bei vielen Projekten inzwischen so programmieren, als ob es ihn nicht mehr gäbe. Das erlaubt den Zugriff auf viele neue, wichtige Browser-Features, und erspart Unmengen an Work-Arounds und Polyfills.
", "content_encoded": "Da der Microsoft Internet Explorer 8 global inzwischen eine Verbreitung unter 2% zu haben scheint, kann man bei vielen Projekten inzwischen so programmieren, als ob es ihn nicht mehr gäbe. Das erlaubt den Zugriff auf viele neue, wichtige Browser-Features, und erspart Unmengen an Work-Arounds und Polyfills.
\n\nMeine Quelle für das Vorhandensein von bestimmten Features in bestimmten Browsern ist „Can I Use“. Dementsprechend kann man schön mit einem schnellen Vergleich zwischen dem Internet Explorer 8 und z.B. dem aktuellen Chrome feststellen, welche Features man nun vollkommen bedenkenlos benutzen darf.
\nvw
und vh
als Breiten- und Höhenangaben relativ zum Browserfenster.calc
Leider haben wir nun Ärgernisse wie den Internet Explorer 9 vor der Nase. Deswegen funktionieren folgende Features nach wie vor nicht:
\naccept
-Attribut an Input-FeldernGerade für PHP-Programmierer ist die eventbasierte Programmierung in Javascript bzw. node.js eine Umstellung im Denken. Dabei sind Events die besondere Stärke, wenn man sie denn richtig anfasst.
\nDabei „horcht“ ein Stückchen Programmierung mittels eines Event-Listeners, ob ein bestimmtes Event ausgelöst wird. Wenn man das Javascript von Browsern noch kennt, sind Event-Listener eigentlich schon bekannt. Zumindest in node.js gibt es aber eine deutlich kompaktere Schreibweise für bestimmte Vorgänge.
", "content_encoded": "Gerade für PHP-Programmierer ist die eventbasierte Programmierung in Javascript bzw. node.js eine Umstellung im Denken. Dabei sind Events die besondere Stärke, wenn man sie denn richtig anfasst.
\nDabei „horcht“ ein Stückchen Programmierung mittels eines Event-Listeners, ob ein bestimmtes Event ausgelöst wird. Wenn man das Javascript von Browsern noch kennt, sind Event-Listener eigentlich schon bekannt. Zumindest in node.js gibt es aber eine deutlich kompaktere Schreibweise für bestimmte Vorgänge.
\n\nMein Anwendungsfall: Ich warte auf eine bestimmte Anzahl von Events, und löse mein eigenes Event aus, wenn alle meine Sub-Events erfolgreich abgeschlossen haben. Bisher sah das so aus (schon mit der Kraft von Array.forEach
):
var files = ['a.txt','b.txt','c.txt'];\n var processed = 0;\n\n var checkProcessed = function(err) {\n if (err) {\n console.log("Error!");\n }\n if (++processed === files.length) {\n console.log("Done!");\n }\n };\n\n files.forEach(function(file) {\n fs.writeFile(file, "Test test test", checkProcessed);\n });\n
\nSeit geraumer Zeit bzw. Node 4.x gibt es nämlich Promise
. Der eigentlich Aufbau eines Promise wird an vielen Stellen hinlänglich erklärt. Zusammen mit Array.map
erlaubt das folgende Konstruktion:
var files = ['a.txt','b.txt','c.txt'];\n\n // Making promises\n var promises = files.map(function(file) {\n return new Promise (\n function (resolve, reject) {\n fs.writeFile(file, "Test test test", function(err) {\n if (err) {\n reject;\n } else {\n resolve;\n }\n });\n }\n );\n });\n\n // Checking promises\n Promise\n .all(promises)\n .then(function() {\n console.log("Done!");\n })\n .catch(function(err) {\n console.log("Error!"); \n })\n ;\n};\n
\nNoch einfacher wird dieses Beispiel, wenn die Methode, auf die man wartet, nicht ein Event, sondern direkt ein Promise zurückgibt:
\n var fsp = require('fs-promise');\n var files = ['a.txt','b.txt','c.txt'];\n\n // Making promises\n var promises = files.map(function(file) {\n return fsp.writeFile(file, "Test test test");\n });\n\n // Checking promises\n Promise\n .all(promises)\n .then(function() {\n console.log("Done!");\n })\n .catch(function(err) {\n console.log("Error!"); \n })\n ;\n};\n
\nViele node.js-Bibliotheken, die vormals mit Events funktionierten, gibt es inzwischen auch als Variante mit Promises. So existiert z.B. das Projekt „Modernize node.js“, dass viele Events in Promises umwandelt.
", "link": "https://journal.3960.org/posts/nodejs-pattern-promise/", "pubDate": "Sun, 28 Aug 2016 19:05:24 +0200", "atom_published": "2016-08-28T19:05:24+02:00", "atom_updated": "2020-01-31T16:53:25+01:00", "guid": "user/posts/nodejs-pattern-promise.md", "author": "info@3960.org (Frank Boës)", "categories": [ "NodeJs", "Anleitung", "Javascript", "Programmierung" ] }, { "title": "Node.js Pattern: Array.map()", "description": "Wenn man jahrelang Javascript nur für Browser programmiert hat (bzw. für ältere Browser), beginnt man unweigerlich in node.js mit den selben Konstruktionen. Inzwischen gibt es aber eine Handvoll Methoden, die das Leben deutlich einfacher machen – und das nicht nur in node.js, sondern auch in neueren Browsern.
\nHeute: Wie erzeugt man aus einem Array ein neues Array?
", "content_encoded": "Wenn man jahrelang Javascript nur für Browser programmiert hat (bzw. für ältere Browser), beginnt man unweigerlich in node.js mit den selben Konstruktionen. Inzwischen gibt es aber eine Handvoll Methoden, die das Leben deutlich einfacher machen – und das nicht nur in node.js, sondern auch in neueren Browsern.
\nHeute: Wie erzeugt man aus einem Array ein neues Array?
\n\nVormals sah das Erzeugen eines neuen Arrays aus einem bereits existierenden Array wie folgt aus:
\n\nvar a = ['a','b','c'];\nvar b = [];\n\nfor (var i = 0; i < a.length; i ++) {\n var el = a[i];\n // do something with el\n b.push(el);\n}\n
\nDie Methode Array.map()
erlaubt folgende Konstruktion:
var a = ['a','b','c'];\n\nvar b = a.map(function(el, i) {\n // do something with el\n return el;\n});\n
\nDamit eigenet sich die Funktion z.B. zum Filtern oder Umformen von Arrays.
\nmap
ist Bestandteil von ECMAScript 5.1 bzw. Javascript 1.6. Damit ist Array.map
eigentlich in jedem Browser verfügbar (bis auf den Internet Explorer 8 und ältere).
Wenn man jahrelang Javascript nur für Browser programmiert hat (bzw. für ältere Browser), beginnt man unweigerlich in node.js mit den selben Konstruktionen. Inzwischen gibt es aber eine Handvoll Methoden, die das Leben deutlich einfacher machen – und das nicht nur in node.js, sondern auch in neueren Browsern.
\nHeute: Wie durchläuft man am Einfachsten ein komplettes Array?
", "content_encoded": "Wenn man jahrelang Javascript nur für Browser programmiert hat (bzw. für ältere Browser), beginnt man unweigerlich in node.js mit den selben Konstruktionen. Inzwischen gibt es aber eine Handvoll Methoden, die das Leben deutlich einfacher machen – und das nicht nur in node.js, sondern auch in neueren Browsern.
\nHeute: Wie durchläuft man am Einfachsten ein komplettes Array?
\n\nVormals sah das Durchlaufen eines Array wie folgt aus:
\n\nvar a = ['a','b','c'];\n\nfor (var i = 0; i < a.length; i ++) {\n var el = a[i];\n // do something with el\n}\n
\nDie Methode Array.forEach()
erlaubt folgende Konstruktion:
var a = ['a','b','c'];\n\na.forEach(function(el, i) {\n // do something with el\n});\n
\nforEach
ist Bestandteil von ECMAScript 5.1 bzw. Javascript 1.6. Damit ist Array.forEach
eigentlich in jedem Browser verfügbar (bis auf den Internet Explorer 8 und ältere).