{ "version": "https://jsonfeed.org/version/1.1", "title": "fboës - Der Blog | Artikel mit dem Tag \"Webdevelop\"", "home_page_url": "https://journal.3960.org/", "feed_url": "https://journal.3960.org/tagged/webdevelop/feed.json", "description": "Programmierung, Luft- & Raumfahrt, Kurioses: Der Blog von und mit Frank Boës.", "icon": "https://cdn.3960.org/favicon-192x192.png", "favicon": "https://cdn.3960.org/images/tile-128x128.png", "author": { "name": "Frank Boës", "url": "mailto:info@3960.org" }, "authors": [ { "name": "Frank Boës", "url": "mailto:info@3960.org" } ], "language": "de-DE", "_rss": { "about": "http://cyber.harvard.edu/rss/rss.html", "copyright": "© 2008-2023 Creative Commons BY" }, "items": [ { "id": "user/posts/2024-02-03-kleinste-javascript-unit-tester-welt/index.md", "url": "https://journal.3960.org/posts/2024-02-03-kleinste-javascript-unit-tester-welt/", "title": "Der kleinste JavaScript-Unit-Tester der Welt", "content_html": "
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. 😉
", "summary": "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…", "date_published": "2024-02-03T18:14:32+01:00", "date_modified": "2024-02-04T05:06:25+01:00", "author": { "name": "Frank Boës", "url": "mailto:info@3960.org", "avatar": "https://www.gravatar.com/avatar/71fcf51cf2ae9acdd54182d3e367ceca" }, "authors": [ { "name": "Frank Boës", "url": "mailto:info@3960.org", "avatar": "https://www.gravatar.com/avatar/71fcf51cf2ae9acdd54182d3e367ceca" } ], "banner_image": "https://cdn.3960.org/favicon-192x192.png", "language": "de-DE", "image": "https://cdn.3960.org/favicon-192x192.png", "tags": [ "Javascript", "Programmierung", "Webdevelop" ] }, { "id": "user/posts/2023-12-29-wie-funktioniert-tolino-webbrowser/index.md", "url": "https://journal.3960.org/posts/2023-12-29-wie-funktioniert-tolino-webbrowser/", "title": "Wie funktioniert der Tolino-Webbrowser?", "content_html": "In den eBook-Readern von Tolino steckt auch ein Webbrowser. Welche Fähigkeiten hat der eigentlich? Und lohnt es sich, eigens für eBook-Reader optimierte Webseiten zu bauen?
\n\nJeder Browser strahlt seine eigene Browser-Kennung ab: Den User Agent String. Er verrät nicht nur, welche Browser-Software in welcher Version im Einsatz ist, sondern auch meist das Betriebssystem.
\nFür einen Tolino Shine sieht das wie folgt aus:
\nMozilla/5.0 (Linux; Android 8.1.0; de-; tolino shine 4/16.1.0) AppleWebKit/537.36 (KHTML, like Gecko) Verions/4.0 Chrome/61.0.0.0 Mobile Mobile Safari/537.36\n
\nDamit lüften sich mehrere Geheimnisse: Nicht nur ist auf dem Tolino ein Android 8 als Betriebssystem im Einsatz, sondern der Browser ist auch ein veralteter Google Chrome 61. (Zum Zeitpunkt der Untersuchung war gerade der Google Chrome 121 aktuell.) Damit beherrscht er leider nicht alle modernen Tricks von CSS und ECMAScript, ist aber für Brot-und-Butter-Websites immer noch eine sehr gute Wahl.
\nMein Tolino Webbrowser hat eine interessante Eigenheit: Er strahlt an jeden besuchten Web-Server einen eigenen Header ab:
\nX-REQUESTED-WITH: de.telekom.epub\n
\nTatsächlich gibt es möglicherweise historische Gründe für diesen Header, uns hilft dies aber sonst nicht weiter. Viel spannender ist für uns…
\nDie physische Bildschirmgröße des Tolino Shine 4 ist 1072×1448 Pixel. Im Web wird dabei ein Device Pixel Ratio von 1.875 angenommen.
\nNun haben wir auf einem eBook-Reader zwei Herausforderungen, die bei der Gestaltung von Websites berücksichtigt werden müssen:
\nSinnvoll wäre also eine Reaktion des Webbrowsers auf folgende Media Queries:
\n\nUnd hier kommt der etwas traurige Part: In Bezug auf Media Queries gibt es leider keine Anpassungen des Google Chrome auf dem Tolino gegenüber einem normalen Google Chrome auf einem Android-Smartphone oder -Tablet:
\nMedia Query | \nValue | \n
---|---|
Media type | \nscreen | \n
Monochrome | \nfalse | \n
Color | \ntrue | \n
Width | \n572px | \n
Height | \n773px | \n
Resolution (dpi) | \n180 | \n
Colors | \n8 | \n
Orientation | \nportrait | \n
Script | \nfalse | \n
Hover | \nfalse | \n
Pointer | \ncoarse | \n
Prefers reduced motion | \nfalse | \n
Kurz gesprochen gibt es also keine Media Query, mit der das CSS einer Website für den Webbrowser des Tolino zugeschnitten werden kann.
\nMeine Lösung für dieses Dilemma ist wenig schön, aber besser als nichts:
\n(function () {\n if (window.navigator.userAgent.match(/(Tolino|Kindle)/i)) {\n document.body.classList.add("is-ebook-reader");\n }\n})();\n
\nAuf jeder neuen Seite wird mit einer kleinen Zeile JavaScript kurz überprüft, ob es im User Agent String einen Hinweis darauf gibt, dass es sich bei dem besuchenden Browser um einen eBook-Reader handelt. Wenn ja, wird der Seite eine CSS-Klasse is-ebook-reader
hinzugefügt.
Zum Spaß habe ich das hier in diesen Blog eingebaut, und einfach mittels CSS Custom Properties eine kleine Fallunterscheidung in mein CSS gebaut. Eine ganz einfache Lösung könnte aber wie folgt aussehen:
\n:root {\n --color-background: #eee;\n --color-text: #112;\n --color-link: orange;\n --text-decoration-link: none\n}\n\n.is-ebook-reader {\n --color-background: white;\n --color-text: black;\n --color-link: black;\n --text-decoration-link: underline dotted;\n}\n\na {\n color: var(--color-link);\n text-decoration: var(--text-decoration-link);\n}\n
",
"summary": "In den eBook-Readern von Tolino steckt auch ein Webbrowser. Welche Fähigkeiten hat der eigentlich? Und lohnt es sich, eigens für eBook-Reader optimierte…",
"date_published": "2023-12-29T18:14:24+01:00",
"date_modified": "2023-12-30T19:47:58+01:00",
"author": {
"name": "Frank Boës",
"url": "mailto:info@3960.org",
"avatar": "https://www.gravatar.com/avatar/71fcf51cf2ae9acdd54182d3e367ceca"
},
"authors": [
{
"name": "Frank Boës",
"url": "mailto:info@3960.org",
"avatar": "https://www.gravatar.com/avatar/71fcf51cf2ae9acdd54182d3e367ceca"
}
],
"banner_image": "https://cdn.3960.org/favicon-192x192.png",
"language": "de-DE",
"image": "https://cdn.3960.org/favicon-192x192.png",
"tags": [
"CSS",
"Programmierung",
"Webdevelop"
]
},
{
"id": "user/posts/2023-04-10-verabschiedung/index.md",
"url": "https://journal.3960.org/posts/2023-04-10-verabschiedung/",
"title": "Verabschiedung",
"content_html": "",
"summary": "Alle Branches sind gepusht, ich meld' mich ab.\n\nEin Programmierer verabschiedet sich von seinem bisherigen Arbeitgeber",
"date_published": "2023-04-10T18:54:53+02:00",
"date_modified": "2023-04-10T18:54:53+02:00",
"author": {
"name": "Frank Boës",
"url": "mailto:info@3960.org",
"avatar": "https://www.gravatar.com/avatar/71fcf51cf2ae9acdd54182d3e367ceca"
},
"authors": [
{
"name": "Frank Boës",
"url": "mailto:info@3960.org",
"avatar": "https://www.gravatar.com/avatar/71fcf51cf2ae9acdd54182d3e367ceca"
}
],
"banner_image": "https://cdn.3960.org/favicon-192x192.png",
"language": "de-DE",
"image": "https://cdn.3960.org/favicon-192x192.png",
"tags": [
"Programmierung",
"Philosophie",
"Webdevelop"
]
},
{
"id": "user/posts/2023-03-05-24h-uhr-als-web-component/index.md",
"url": "https://journal.3960.org/posts/2023-03-05-24h-uhr-als-web-component/",
"title": "Die 24h-Uhr – als Web Component",
"content_html": "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.
", "summary": "Web Components zu bauen ist gar nicht so kompliziert, selbst ohne Framework. Nach meinen vorherigen Überlegungen zur Verwendung von SVGs in Web Components war…", "date_published": "2023-03-05T18:56:12+01:00", "date_modified": "2023-03-05T18:56:12+01:00", "author": { "name": "Frank Boës", "url": "mailto:info@3960.org", "avatar": "https://www.gravatar.com/avatar/71fcf51cf2ae9acdd54182d3e367ceca" }, "authors": [ { "name": "Frank Boës", "url": "mailto:info@3960.org", "avatar": "https://www.gravatar.com/avatar/71fcf51cf2ae9acdd54182d3e367ceca" } ], "banner_image": "https://journal.3960.org/posts/2023-03-05-24h-uhr-als-web-component/24h-clock.png", "language": "de-DE", "image": "https://journal.3960.org/posts/2023-03-05-24h-uhr-als-web-component/24h-clock.png", "tags": [ "Webdevelop", "Web-Components", "SVG", "Javascript", "CSS", "Geografie", "Outdoor", "Programmierung", "Technologie" ] }, { "id": "user/posts/2023-01-27-javascript-pattern-zum-event-handling/index.md", "url": "https://journal.3960.org/posts/2023-01-27-javascript-pattern-zum-event-handling/", "title": "JavaScript: Pattern zum Event-Handling", "content_html": "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.
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 die 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.
SVGs können nicht nur mit Grafikprogrammen wie Adobe Illustrator oder Inkscape erstellt werden, sondern auch mit einem einfachen Texteditor – oder programmatisch mit jeder Skriptsprache, die man so zur Hand hat. Damit die SVG-Grafik aber in jedem Umfeld funktioniert, sind ein paar Besonderheiten zu beachten:
\n\n<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n<svg version="1.1" viewBox="0 0 100 100" width="210mm" height="210mm" xmlns="http://www.w3.org/2000/svg">\n <title><!-- Document Title goes here --></title>\n <style>\n <!-- CSS goes here -->\n </style>\n <defs>\n <!-- Optional: Pastable objects -->\n </defs>\n <!-- Here comes the actual SVG -->\n</svg>\n
\nDie Grundstruktur eines SVG-Dokuments kann man sich ähnlich wie die eines HTML-Dokuments vorstellen:
\nZuerst kommen Meta-Angaben. Dazu gehört der Dokumenten-Titel, CSS und gegebenenfalls noch <defs>
, falls ihr Grafikkomponenten mehrfach verwenden wollt.
Erst danach sollte die eigentliche Ausgabe von Grafikelementen beginnen.
\nIn SVG hat das <title>
-Tag mehrere Bedeutungen:
<title>
-Tag HTML verwendet, um den Namen eines Dokuments festzulegen.title
-Attributs. Beim Überfahren mit der Maus wird zum Beispiel eine kleiner Tooltip angezeigt.…funktioniert wie in HTML. Dementsprechend sollte man versuchen, nicht jedem Element einzeln mittels stroke
- und fill
-Attributen Styles zuzuordnen, sondern mit CSS das Layout globaler zu definieren.
Tatsächlich gibt es aber ein paar Besonderheiten bei der Verwendung von CSS in SVGs:
\nRGBA-Farben haben die Fähigkeit, darunterliegende Teile einer Grafik durchscheinen zu lassen. Der Alpha-Kanal bestimmt dabei die Durchlässigkeit bzw. Opazität. Leider werden RGBA-Farben nicht in jedem Fall korrekt ausgegeben. Die meisten Browser können es, dafür kaum ein Grafikprogramm oder Bildbetrachter.
\nStattdessen gibt es verschiedene Formen von Opazität bzw. Transparenz.
\nopacity
macht ein gesamtes Element,fill-opacity
nur die Füllung,stroke-opacity
nur die Umrandung opak bzw. transparent.In CSS sieht das wie folgt aus:
\n.black-a-fifty {\n /* fill: rgba(0,0,0,0.5); --- will not always work */\n fill: #000000;\n fill-opacity: 0.5;\n}\n
\nFans von CSS Custom Properties haben bei SVG ebenfalls kein Glück: Die meisten Grafikprogramme können diese Variablen nicht verarbeiten. Stattdessen gibt es aber einen kleinen Ausweg mit dem CSS-Wert currentColor
:
Der Wert für currentColor
wird mit der in SVG sonst irrelevanten CSS-Eigenschaft color
gesetzt (die im Gegensatz zu der gleichnamigen Eigenschaft in HTML nicht aktiv ELemente einfärbt – dafür ist in SVG ja fill
und stroke
da). Der Aufruf von currentColor
wird dann mit dem zuletzt via color
gesetztem Wert gefüllt – ganz wie bei einer CSS Custom Property.
.black {\n color: black; /* sets currentColor to `black` */\n}\n\n.red {\n color: red; /* sets currentColor `red` */\n}\n\n/* These elements will be colored black or red, depending on class */\n\nrect {\n stroke: currentColor;\n fill: currentColor;\n}\n\ntext {\n fill: currentColor;\n}\n
\nIn SVG kann in Theorie nur einzeiliger Text gesetzt werden, beziehungsweise muss jede neue Zeile neu positioniert werden. Mit einem kleinen Trick kann man aber mehrzeiligen Text erstellen, indem jede Zeile in ein <tspan>
-Element gehüllt und diese relativ unter der vorhergehenden Zeile positioniert wird:
<text>\n <tspan x="0">Line 1</tspan>\n <tspan x="0" dy="1.1em">Line 2</tspan>\n <tspan x="0" dy="1.1em">Line 3</tspan>\n <tspan x="0" dy="1.1em">Line 4</tspan>\n</text>\n
\nWichtig ist hier die Definition des Zeilenabstands – diese muss in jedem dy
-Attribut wiederholt werden.
Wenn ihr Links in eurem SVG verwenden wollt, gibt es zwei Wege, wobei je nach Ausgabemedium nur einer von beiden funktioniert… oder auch gar keiner.
\nDie veraltete, aber noch weit verbreitete Methode sind xlink:href
-Attribute, die die Ziel-URL an einem Element festlegen. Damit diese funktionieren, muss zuerst an dem <svg>
-Knoten ein neuer Namespace deklariert werden:
<svg … xmlns:xlink="http://www.w3.org/1999/xlink">\n
\nSobald dieser Namespace bekannt ist, kann das Attribut verwendet werden.
\nIn SVG2 wird stattdessen das aus HTML bekannte <a>
-Tag mit href
-Attribut verwendet.
Für eine maximale Kompatibilität kann man beide Techniken miteinander kombinieren:
\n<a xlink:href="https://www.example.com" href="https://www.example.com">…</a>\n
\nÜbrigens: Die Links für <defs>
und <use>
werden mit dem selben xlink:href
- beziehungsweise href
-Attribut gebaut.
Im Standard sind SVG-Linien eine scharfkantige Sache. Zum Glück gibt es CSS-Eigenschaften, die sowohl die Enden mit stroke-linecap
als auch die Ecken mit stroke-linejoin
abrunden können.
.rounded-line {\n stroke-linecap: round;\n stroke-linejoin: round;\n}\n
\nWenn euer SVG-Dokument auch in Inkscape bearbeitbar sein soll, könnt ihr mit einem zusätzlichen XML-Namensraum kleine Helferlein im Dokument hinterlassen:
\n<svg … xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape">\n
\nJetzt steht euch die Möglichkeit zur Verfügung, die aus Inkscape bekannten Layer in eurem SVG zu verwenden. Dazu müsst ihr eine beliebige Gruppe nur mit zusätzlichen Attributen ausstatten:
\n<g inkscape:groupmode="layer" inkscape:label="Name your layer">…</g>\n
\nWenn eure SVGs nicht nur in modernsten Browsern, sondern auch in älteren Browsern und Grafikprogrammen funktionieren soll, müssen ein paar Spielregeln eingehalten werden. Im Endeffekt ist nach kurzer Umgewöhnung das Erzeugen wunderschöner SVG-Dokumente nicht viel komplizierter als das Erstellen von HTML-Dokumenten.
", "summary": "SVGs können nicht nur mit Grafikprogrammen wie Adobe Illustrator oder Inkscape erstellt werden, sondern auch mit einem einfachen Texteditor – oder…", "date_published": "2022-05-10T19:06:51+02:00", "date_modified": "2022-05-11T08:52:28+02:00", "author": { "name": "Frank Boës", "url": "mailto:info@3960.org", "avatar": "https://www.gravatar.com/avatar/71fcf51cf2ae9acdd54182d3e367ceca" }, "authors": [ { "name": "Frank Boës", "url": "mailto:info@3960.org", "avatar": "https://www.gravatar.com/avatar/71fcf51cf2ae9acdd54182d3e367ceca" } ], "banner_image": "https://cdn.3960.org/favicon-192x192.png", "language": "de-DE", "image": "https://cdn.3960.org/favicon-192x192.png", "tags": [ "SVG", "CSS", "Programmierung", "Webdevelop" ] }, { "id": "user/posts/2022-03-03-gnome-erweiterungen-fuer-ubuntu/index.md", "url": "https://journal.3960.org/posts/2022-03-03-gnome-erweiterungen-fuer-ubuntu/", "title": "Gnome-Erweiterungen für Ubuntu", "content_html": "Die GNOME Shell-Erweiterungen sind für Ubuntu (und andere Linux-Betriebssysteme) ein schöner Weg, um das eine oder andere Verhalten des Betriebssystems aufzubohren.
\n\nDazu muss „GNOME Shell Extensions“ und „GNOME Tweaks“ zuerst installiert werden:
\nsudo add-apt-repository universe\nsudo apt install gnome-tweak-tool\nsudo apt install gnome-shell-extensions\n
\nMeine Lieblings-GNOME-Erweiterungen sind diese:
\nA fundamental principle of building (web) applications is to have a strict separation between logic and output – at least if your goal as a programmer is to stay sane. For outputting stuff most often there is a simple (template) language to prevent mixing too much logic into your templates.
\nFor PHP template engines like Twig or Smarty solve the outputting part. But do we really need a template engine? Is it possible that PHP (without any additional libraries) is quite sufficient to do templating?
\n\nThe main idea of template engines is:
\nif
, while
or for
are more simple to use in a template engine.A perfect example for this philosophy is the template engine Mustache: It only contains a minimum of required methods in order to reduce complexity.
\nBut do we really need the overhead of a proper template engine? Or is it possible to use plain PHP for our templating needs, without adding dependencies like template engines to our project?
\nActually PHP has good capabilities to do templating – its founding idea revolves around templating, as a matter of fact.
\nIt is common practice to put all your templates into a separate folder, and append a common file extension to all template files. For example Symfony with Twig puts all templates into a directory called /templates
and use the file extension .twig
.
Actually there is nothing stopping us from using the same /templates
directory for our templates. As file extension we are using (like Twig and to keep our mental health):
Template output | \nFile extension | \n(MIME-type) | \n
---|---|---|
HTML file | \n*.html.php | \ntext/html | \n
XML file | \n*.xml.php | \ntext/xml or application/xml | \n
JSON file | \n*.json.php | \napplication/json | \n
Text file | \n*.txt.php | \ntext/plain | \n
CSV file | \n*.csv.php | \ntext/csv | \n
This list is easily extended be even more MIME-types and corresponding file extensions. Important for us is that all files end with .php
, so our code editor knows how to handle these files. This also secures the files if by mistake these files get called directly by a web server; files ending with .php
will be executed as PHP, and their PHP secrets will be compiled and not shown to a world-wide audience.
It is good custom to name your template after the controller calling it. Accordingly a HTML template for an index-controller will be index.html.php
, for an user-controller user.html.php
. The XML template for the index-controller becomes index.xml.php
.
Templates not attached to a controller (called snippets or partials) will be prefixed with a _
, e.g. _meta.html.php
. The purpose of these files will be explained later on.
We will take advantage of the fact that PHP returns every line of code not enclosed in PHP tags (<?php ?>
) unaltered back to the browser. That allows us to concentrate on building our beautiful template output:
<!DOCTYPE html>\n<html xmlns="http://www.w3.org/1999/xhtml" lang="en">\n<head>\n</head>\n<body>\n <!-- I am valid PHP -->\n</body>\n
\nThe most important task of templates is to output variables prepared by the controller. Like in every template language we open up a instruction block and execute a function for outputting a variable.
\n<!-- Bad idea -->\n<div><?php echo($title) ?></div>\n
\nPHP kindly allows for shortening the output instruction:
\n<!-- Still a bad idea -->\n<div><?= $title ?></div>\n
\nBut actually the above examples are very bad ideas, because both ignore quoting / escaping – a core function of any good template engine, because only this secures variable output against malicious intentions. Look at the above examples and imagine some HTML tags (or characters like a single <
) inside $title
– as you can see these will be embedded into the HTML output. In most cases this is undesirable, possibly breaking your site or (even worse) allowing attackers to make your site send harmful HTML to your visitor's browsers. Want to see an example?
<!-- See, a bad idea -->\n<div><?= $_GET['search'] ?></div>\n
\nBut PHP has you covered: htmlspecialchars
converts any string to safe HTML (or XML, for that matter). Characters like <
, >
or "
will be safely encoded:
<!-- Better idea -->\n<div><?= htmlspecialchars($title) ?></div>\n
\nBecause this main method of outputting variables will become tedious to write, we are introducing an alias for htmlspecialchars
– the beginning of the world's smallest PHP template engine:
// Template.php\n\n/**\n * Alias for `htmlspecialchars`\n */\nfunction html($s): string\n{\n return htmlspecialchars($s);\n}\n
\nThis function shortens our work to convert variables and will be our new main function to send variable content back to the browser:
\n<!-- Best idea -->\n<div><?= html($title) ?></div>\n
\nPHP offers even more function for quoting / escaping. There is a function for encoding query parameters used in URLs, quoting characters like #
or &
: urlencode
. These can be easily combined with our html
function:
<a href="https://www.example.com?id=<?= html(urlencode($id)) ?>">Test</a>\n
\nSometimes we also have to convert our PHP variables to JavaScript variables. Not explicitly build for that purpose but highly recommendable is a function called json_encode
. It actually converts the given variable to JSON – which is also valid JavaScript. Even better the function also converts PHP arrays and objects to JavaScript arrays and objects:
<script>\nvar data = <?= json_encode($data) ?>;\n</script>\n
\nThe most important control structure in templates are conditions, with your best friend if
to be mentioned first. But „pure“ PHP with its curly brackets used in conjunction with if
tends to make matters confusing in your template, making it hard to see where your conditional block ends:
<?php if (!empty($title)) { ?>\n <div><?= html($title) ?></div>\n<?php } ?>\n
\nDo not despair, PHP has an alternative syntax for controls structures tailored for templating tasks. Instead of starting a block via {
a simple :
is used. And instead of ending a block via }
something like end...;
will be used:
<?php if (!empty($title)): ?>\n <div><?= html($title) ?></div>\n<?php endif ?>\n
\nYour if
-conditions can use any operators and functions known to PHP. One of your most important conditions in templating will be a check if a given variable has content to output. This test is simply executed by using !empty
or isset
.
Of course there is also else
and elseif
available:
<?php if (!empty($results)): ?>\n <h4>We have found <?= html(count($results)) ?> results.</h4>\n<?php else: ?>\n <h4>Sorry, we have found no results.</h4>\n<?php endif ?>\n
\nThen there are lists and tables in your templates. With PHP loops your are able to iterate over an PHP array to output lists and tables. Lucky for you there is also an alternate syntax for looping control structures:
\n<?php if (!empty($list)): ?>\n <ul>\n <?php foreach($list as $index => $item): ?>\n <li><?= html($item) ?></li>\n <?php endforeach ?>\n </ul>\n<?php endif ?>\n
\nThis syntax does a proper job for for
, foreach
or while
:
<h4>Lottery numbers</h4>\n<ol>\n <?php for ($i = 1; $i <= 49; $i++): ?>\n <li>\n <input type="checkbox" name="lottery_<?= html($i) ?>" id="lottery_<?= html($i) ?>" value="1" />\n <label for="lottery_<?= html($i) ?>"><?= html($i) ?></label>\n </li>\n <?php endfor ?>\n</ol>\n
\nPartial templates present in multiple controllers (like a header, footer or navigation) are called… partials (doh!) or snippets. These partials are prefixed with a single _
, for example _header.xml.php
.
These partials or snippets can be embedded into other templates by PHP's very own include
instruction:
<!DOCTYPE html>\n<html xmlns="http://www.w3.org/1999/xhtml" lang="en">\n<head>\n <?php include('_meta.html.php') ?>\n</head>\n<body>\n <?php include('_header.html.php') ?>\n ...\n <?php include('_footer_.html.php') ?>\n</body>\n
\nYou never would have guessed, but PHP also handles translations! The Gettext library is included in PHP and allows to translate strings, which Gettext will look up in a dictionary you are able to build. If a translation is found, Gettext will return it to PHP.
\nMost important for translations is to tell PHP the language / locale you are using. This is done via setlocale
:
setlocale(LC_MESSAGES, 'de_DE'); // German in Germany\nsetlocale(LC_MESSAGES, 'en_GB'); // English in Great Britain\n
\nThis also switches to the correct format for numbers and dates in the given language, as well as using the correct date terms for months and weekdays.
\nThe strings you want translated are passed to the gettext
/ _
function:
<!-- translate to 'Guten Morgen!' -->\n<h4><?= html(_('Good morning!')) ?></h4>\n
\nThe required dictionaries to translate your strings can be created by yourself. The required PO- and MO-files can be created using programmes like Poedit. Poedit also offers to search your whole project for translatable strings to automatically add them to your dictionaries.
\nIf your happen to add variables to your translatable strings things can get a little hairy. This somewhat complex construction allows for a translatable string to also work with variables:
\n<div><?= html(vsprintf(_('There are %d results'), [$count])) ?></div>\n
\nSo here is our next alias to be added to our simple template engine: A shortcut for outputting translatable strings mixed with variables.
\n// Template.php\n\n/**\n * Alias for `vsprintf`, but with HTML escaping and translation\n */\nfunction _html(string $format, array $args = []): string\n{\n return htmlspecialchars($args\n ? vsprintf(_($format), $args)\n : _($format)\n );\n}\n
\n…which makes writing translatable strings mixed with variables somewhat less tedious:
\n<h4><?= _html('Good morning!') ?></h4>\n<div><?= _html('There are %d results', [$count]) ?></div>\n
\nIt is very helpful for developers as well as template builders alike to have a look into complex variable constructs for debugging purposes – think of big objects or arrays to traverse. PHP offers print_r
and var_dump
/ var_export
for this purpose. These functions are helpful, but not in HTML as they do not produce any line breaks, dumping all your structured data into a single line. Again our little template engine comes to the rescue with a small shortcut:
// Template.php\n\n/**\n * HTML dumper für PHP variables\n */\nfunction debug($mixed, bool $extended = false): void\n{\n echo('<pre class="debug" style="margin: 1em 0; border: 1px solid red; background: #fee; color: #000; padding: 1em;">');\n echo(htmlspecialchars($extended\n ? var_export($mixed, true)\n : print_r($mixed, true)\n ));\n echo('</pre>');\n}\n
\nThis allows for complex variables to be dumped in a readable manner:
\n<?php debug($data) ?>\n
\nWhile your at it, you might consider using console.log
for debugging output in the console of your browser.
PHP was build to output HTML. This works by PHP telling the browser that the MIME-type of the document it is sending is text/html
. But actually PHP is perfectly capable to send any other MIME-type header. The function header
allows PHP to send custom headers to your visitor's browsers, replacing default headers while your at it.
The header used to set the MIME-type is Content-Type
. So it is perfectly possible for PHP to send any conceivable MIME-type.
For XML we choose the file extension .xml.php
, for example in sitemap.xml.php
. This template does not differ very much from a standard HTML template, and uses the same quoting / encoding routines. We just have to send a different Content-Type
header and we are serving proper XML:
<?php header('Content-Type: text/xml'); ?>\n<urlset>\n <?php foreach($urls as $url): ?>\n <url>\n <loc><?= html($url) ?></loc>\n </url>\n <?php endforeach ?>\n</urlset>\n
\nOutputting JSON is also quite simple with PHP. For this MIME-type we are using the file extension .json.php
(like in feed.json.php
) and need to set the correct Content-Type
. To properly use quoting / escaping the inbuilt PHP function json_encode
is very helpful, as it converts even complex PHP variables into structured JSON output:
<?php\n header('Content-Type: application/json');\n echo(json_encode($data));\n
\nIf you like your JSON's source to look nice, there is also a parameter called JSON_PRETTY_PRINT
for json_encode
:
<?php\n header('Content-Type: application/json');\n echo(json_encode($data, JSON_PRETTY_PRINT));\n
\nActually PHP's json_encode
precisely converts PHP variable types to the matching JSON types. A PHP integer will be a JSON integer, a PHP string will be a JSON string. So note the difference between 1
and „1“
.
The examples above for fetching templates and setting the correct MIME-type can be solved with a general switch
block:
// e.g. $template = 'index';\n// e.g. $contentType = 'html';\n\nswitch ($contentType) {\n case 'xml':\n header('Content-Type: text/xml');\n break;\n case 'json':\n header('Content-Type: application/json');\n break;\n case 'txt':\n header('Content-Type: text/plain');\n break;\n case 'csv':\n header('Content-Type: text/csv');\n break;\n default:\n $contentType = 'html';\n break;\n}\n\n$templateFilename = __DIR__ . '/templates/' . $template . '.' . $contentType . '.php';\nrequire($templateFilename);\n
\nEVery PHP project needs to have a clean separation between logic and output – but not always you will need a template engine like Twig or Smarty for this separation. Common off-the-shelf PHP (with few small helpers) can be a simple, fuss-free alternative for templating.
\nSo here it is, our small manual of the most important PHP functions for templating:
\nFunction | \nAlias | \nDescription | \n
---|---|---|
htmlspecialchars | \nhtml | \nHTML escaping | \n
urlencode | \n- | \nURL escaping | \n
json_encode | \n- | \nJSON/JavaScript escaping | \n
setlocale | \n- | \nSet language / locale for translation | \n
gettext / _ | \n_html | \nOutputting translation | \n
strftime | \n- | \nOutput localized date format | \n
localeconv | \n- | \nGet number format for current locale | \n
printf | \n_html | \nOutput formatted variables in string | \n
nl2br | \n- | \nConvert line breaks to <br /> | \n
implode | \n- | \nJoins array members to a single string | \n
var_dump | \ndebug | \nDump PHP variables | \n
header | \n- | \nChange Content-Type in browser | \n
The only thing missing from PHP is template inheritance, like in Twig's template inheritance or Smarty's template inheritance. Also have a look at CSS-Trick's comparison of template engines.
\nEs gibt auch eine deutsche Version von diesem Artikel, „Die kleinste PHP-Templating-Engine der Welt“.
", "summary": "A fundamental principle of building (web) applications is to have a strict separation between logic and output – at least if your goal as a programmer is to…", "date_published": "2021-12-17T18:48:53+01:00", "date_modified": "2024-02-02T17:26:09+01:00", "author": { "name": "Frank Boës", "url": "mailto:info@3960.org", "avatar": "https://www.gravatar.com/avatar/71fcf51cf2ae9acdd54182d3e367ceca" }, "authors": [ { "name": "Frank Boës", "url": "mailto:info@3960.org", "avatar": "https://www.gravatar.com/avatar/71fcf51cf2ae9acdd54182d3e367ceca" } ], "banner_image": "https://cdn.3960.org/favicon-192x192.png", "language": "en-US", "image": "https://cdn.3960.org/favicon-192x192.png", "tags": [ "PHP", "Programmierung", "Webdevelop" ] }, { "id": "user/posts/2021-12-04-print-stylesheets-fuer-svg-grafiken/index.md", "url": "https://journal.3960.org/posts/2021-12-04-print-stylesheets-fuer-svg-grafiken/", "title": "Print-Stylesheets… für SVG-Grafiken", "content_html": "Vektor-Grafiken haben für Logos, Diagramme und Symbole die fantastische Eigenschaft, bei beliebig hoher Auflösung relativ kleine Dateigrößen zu verursachen. Und hier folgen ein paar Kniffe, um SVG-Grafiken mit der Hilfe von CSS sowohl am Bildschirm als auch im Druck gut aussehen zu lassen.
\n\nSeitdem die Unterstützung von SVG-Vektor-Grafiken in jedem modernen Browser kein Problem mehr darstellt, ist der Siegeszug von SVG-Grafiken unaufhaltsam. Ob via <img>
, als CSS-Hintergrundbild, oder direkt in ein HTML-Dokument eingebettet: kleine Symbole wie komplexe Grafiken können so responsiv auf den Bildschirm gezaubert werden. Eine Einleitung in das Thema „SVG“ bei CSS-Tricks erklärt die grundsätzlichen Handgriffe.
Fast immer wird eine SVG-Grafik mit einem Vektor-Grafikprogramm (wie zum Beispiel dem kostenlosen Inkscape) erzeugt. Dabei wird oft vergessen, dass SVG-Dateien eigentlich nur aus XML bestehen, das mit Hilfe eines Text-Editors bearbeitet werden kann.
\nInteressanterweise kann innerhalb eines SVG-Dokuments auch CSS verwendet werden. Und mit CSS können wir auch Media-Queries einsetzen.
\nHöchste Zeit, den Text-Editor auszupacken und unserem SVG zusätzliches CSS überzuhelfen!
\nEin einfaches Stylesheet, um beim Druck von HTML- und SVG-Dokumenten im Browser störendes Beiwerk wie die automatisch vom Browser erzeugte Kopf- und Fußzeile loszuwerden, sieht wie folgt aus:
\n@page {\n margin: 0;\n padding: 0;\n}\n@page :footer {\n display: none\n}\n@page :header {\n display: none\n}\n
\nGenau dieses CSS lässt sich mit einem <style>
-Tag direkt in die SVG-Grafik einbauen. Dazu kann man an jeder beliebigen Stelle im Dokument ein solches Tag einfügen, und dann CSS einfügen:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n<svg version="1.1" xmlns="http://www.w3.org/2000/svg">\n <title>…</title>\n <style>\n @page {\n margin: 0;\n padding: 0;\n }\n @page :footer {\n display: none\n }\n @page :header {\n display: none\n }\n </style>\n <!-- your svg here -->\n</svg>\n
\nNatürlich könnt ihr auch jede andere Media-Query in das Dokument einfügen. Zu beachten: Beim Re-Import in ein Grafik-Programm kann dies möglicherweise nicht mehr korrekt interpretiert werden, und eure Grafik wirkt etwas verspielt. Uns interessiert aber sowieso nur das Ergebnis im Browser.
\nUm die Idee weiterzutreiben, kann man für eine SVG-Grafik im DIN A4 eine praktische Druckvorschau mitliefern. Dazu bauen wir uns ein Dokument mit den entsprechenden Abmessungen von 210×297 Millimetern und erzeugen ein Rechteck, dass exakt dem Seitenrand folgt:
\n<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n<svg width="210mm" height="297mm" viewBox="0 0 210 297" version="1.1" xmlns="http://www.w3.org/2000/svg">\n <rect x="0" y="0" width="210" height="297" />\n <!-- your svg here -->\n</svg>\n
\nDieses Rechteck ist nun sowohl auf dem Bildschirm als auch im Druck zu sehen. Interessanterweise können wir so ziemlich jedem SVG-Element mittels class
und id
Attribute mitgeben, die wir (wie aus HTML gewohnt) CSS zuweisen können.
In diesem Fall wollen wir die eigentliche Seite weiß und den nicht druckbaren Bereich grau darstellen. Nicht zuletzt zeigt eine gepunktete Linie die Blattgrenze an:
\n<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n<svg width="210mm" height="297mm" viewBox="0 0 210 297" version="1.1" xmlns="http://www.w3.org/2000/svg">\n <title>…</title>\n <style>\n svg {\n margin: 1em auto;\n background-color: #ddd;\n }\n .page {\n stroke: #999;\n stroke-dasharray: 0.5, 0.5;\n fill: #fff;\n display: inline;\n }\n </style>\n <rect class="page" x="0" y="0" width="210" height="297" />\n <!-- your svg here -->\n</svg>\n
\nDamit wird unsere Kosmetik nun nicht im Druck zu Gesicht bekommen, unterscheiden wir noch zwischen Screen- und Print-Styles:
\n<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n<svg width="210mm" height="297mm" viewBox="0 0 210 297" version="1.1" xmlns="http://www.w3.org/2000/svg">\n <title>…</title>\n <style>\n @page {\n margin: 0;\n padding: 0;\n }\n @page :footer {\n display: none\n }\n @page :header {\n display: none\n }\n .page {\n display: none;\n }\n @media screen {\n svg {\n margin: 1em auto;\n background-color: #ddd;\n }\n .page {\n stroke: #999;\n stroke-dasharray: 0.5, 0.5;\n fill: #fff;\n display: inline;\n }\n }\n </style>\n <rect class="page" x="0" y="0" width="210" height="297" />\n <!-- your svg here -->\n</svg>\n
\nFertig ist die SVG-Grafik mit Media Queries (in diesem Falle ein Spielbrett für Raumschiffe) – sowohl für die Ausgabe am Bildschirm als auch den Druck vorbereitet.
\nDie Kombination aus SVG und CSS kann man natürlich noch weiter treiben. Da so ziemlich jede Media-Query im Browser für SVGs funktioniert, können auch Anpassungen an den Dark Mode oder für verschiedene Monitor-Größen direkt im SVG eingebettet werden.
", "summary": "Vektor-Grafiken haben für Logos, Diagramme und Symbole die fantastische Eigenschaft, bei beliebig hoher Auflösung relativ kleine Dateigrößen zu verursachen.…", "date_published": "2021-12-04T18:23:43+01:00", "date_modified": "2021-12-04T18:23:43+01:00", "author": { "name": "Frank Boës", "url": "mailto:info@3960.org", "avatar": "https://www.gravatar.com/avatar/71fcf51cf2ae9acdd54182d3e367ceca" }, "authors": [ { "name": "Frank Boës", "url": "mailto:info@3960.org", "avatar": "https://www.gravatar.com/avatar/71fcf51cf2ae9acdd54182d3e367ceca" } ], "banner_image": "https://cdn.3960.org/favicon-192x192.png", "language": "de-DE", "image": "https://cdn.3960.org/favicon-192x192.png", "tags": [ "SVG", "CSS", "Idee", "Programmierung", "Webdevelop" ] }, { "id": "user/posts/2021-08-27-merge-einen-git-branch-abgekuerzt/index.md", "url": "https://journal.3960.org/posts/2021-08-27-merge-einen-git-branch-abgekuerzt/", "title": "Merge in einen Git-Branch – abgekürzt", "content_html": "Ob man nun Gitflow, GitHub Flow oder ein selbstausgedachte Abwandlung dieser Modelle verwendet: Einige dieser Modelle benötigen einen Weg, um einen Branch in einen anderen Branch zu mergen. Dieser Handgriff ist in Git mehrteilig – und teilweise etwas nervig.
\n\nGit sieht dafür den Weg vor, in den Ziel-Branch zu wechseln (z.B. main
), und vom Quell-Branch (z.B. feature/XYZ
) mittels git merge
zu ziehen. Das ist in der Regel eine sehr gute Methode, um Fehlbedienungen zu vermeiden.
Bei vielen Git-Workflows wird nun aber davon ausgegangen, dass im Ziel-Branch nicht direkt gearbeitet werden soll. Es ist also sehr wichtig, nach Abschluss des Merges wieder in den Quell-Branch zu wechseln, um dort versehentliche Commits zu vermeiden. Die verschiedenen Branch-Wechsel bedürfen also einiger Konzentration.
\nZudem ist die Schreibarbeit für einen solchen Merge auf der Kommandozeile etwas umfangreicher, will man nichts übersehen und vor allen Dingen alle Zwischenschritte auch auf dem Repo-Server bzw. origin
bekannt machen:
$ git push\n$ git checkout main\n$ git pull\n$ git merge feature/XYZ\n$ git push\n$ git checkout feature/XYZ\n
\nTatsächlich gibt es für Gitflow die Gitflow-CLI-Tools, die genau dieses Problem lösen:
\n$ git flow feature finish feature/XYZ\n
\nDummerweise haben diese Tools den Nachteil, dass das Ziel der Operation fest verdrahtet ist – und außerdem die Installation der Tools voraussetzt. In der Regel scheitert es bei mir daran, dass in dem Projekt gar nicht Gitflow verwendet wird. 😉
\nDer Wunsch ist also: Ein leichtgewichtiges, flexibles Tool, dass sicher einen Quell-Branch in einen Ziel-Branch merged.
\nNicht zuletzt durch die beeindruckende Sammlung von Bash Aliases meines Kollegen Marty kam ich auf die Idee, für meine Wünsche ebenfalls einen Bash-Alias zu schreiben. Analog zu Git Aliases (zu denen ich meine persönliche Sammlung von Git Aliases angelegt habe) kann man mittels Bash-Aliases neue Befehle für die Bash erzeugen.
\nIm einfachsten Fall sind Bash-Aliases Aufrufe, die komplizierte Einzeiler ersetzen. Meine Idee war etwas komplexer, so dass ich stattdessen eine Bash Function verwendet habe, um meine Idee umzusetzen. Ich hatte die folgende Vorstellung:
\nSollten Merge Conflicts auftreten, würde das Programm abbrechen und Gelegenheit zur Korrektur des Konflikts geben.
\nDas finale Script (die aktuellste Version findet sich auch in meinen öffentlichen .bash_aliases
) sieht nun wie folgt aus:
# Put me in `.bash_aliases`, works like an alias.\ngit-merge-to() {\n TARGET_BRANCH=develop\n if [[ "${1}" ]]; then\n TARGET_BRANCH=${1}\n fi\n\n FEATURE_BRANCH=$(git branch | sed -n -e 's/^\\* \\(.*\\)/\\1/p')\n\n if [[ ! "${FEATURE_BRANCH}" ]]; then\n echo -e "\\e[91mERROR\\e[0m: No git branch found"\n return 4\n fi\n if [[ "${FEATURE_BRANCH}" =~ ^(main|master|develop|preview)$ ]]; then\n echo -e "\\e[91mERROR\\e[0m: Branch ${FEATURE_BRANCH} is not a mergable branch"\n git branch\n return 2\n fi\n if [[ "${FEATURE_BRANCH}" == "${TARGET_BRANCH}" ]]; then\n echo -e "\\e[91mERROR\\e[0m: Branches are identical"\n return 3\n fi\n\n echo -en "Merge branch \\e[94m${FEATURE_BRANCH}\\e[0m into \\e[94m${TARGET_BRANCH}\\e[0m? [yn] "\n read CONFIRM\n if [[ ! "${CONFIRM}" =~ ^(y|Y|yes|Yes)$ ]]; then\n echo "Cancelled"\n return 1\n fi\n\n git push\n git checkout ${TARGET_BRANCH}\n git pull\n git merge ${FEATURE_BRANCH} --no-edit\n git push\n git checkout ${FEATURE_BRANCH}\n}\n
\nSobald dieses Script in der .bash_aliases
eingetragen ist, kann man es auf dem lokalen Rechner von jedem Ort aus starten:
$ git-merge-to develop\nMerge branch feature/web-components into develop? [yn] y\n Everything up-to-date\n Switched to branch 'develop'\n Your branch is up-to-date with 'origin/develop'.\n Already up-to-date.\n Merge made by the 'recursive' strategy.\n Writing objects: 100% (6/6), 578 bytes | 578.00 KiB/s, done.\n Switched to branch 'feature/web-components'\n Your branch is up-to-date with 'origin/feature/web-components'.\n$\n
\nNetterweise funktioniert auch die Auto-Vervollständigung des Befehles mittels der Tabulatoren-Taste, so dass ein git-
und TAB den Befehl anzeigt.
Neue Projektmitarbeiter in ein vollkommen verfuddeltes Projekt einarbeiten:
\n", "summary": "Neue Projektmitarbeiter in ein vollkommen verfuddeltes Projekt einarbeiten:\nOnboarding… auf einem sinkenden Schiff.", "date_published": "2020-12-04T18:56:23+01:00", "date_modified": "2020-12-04T18:56:23+01:00", "author": { "name": "Frank Boës", "url": "mailto:info@3960.org", "avatar": "https://www.gravatar.com/avatar/71fcf51cf2ae9acdd54182d3e367ceca" }, "authors": [ { "name": "Frank Boës", "url": "mailto:info@3960.org", "avatar": "https://www.gravatar.com/avatar/71fcf51cf2ae9acdd54182d3e367ceca" } ], "banner_image": "https://cdn.3960.org/favicon-192x192.png", "language": "de-DE", "image": "https://cdn.3960.org/favicon-192x192.png", "tags": [ "Lustiges", "Philosophie", "Webdevelop" ] }, { "id": "user/posts/2020-08-30-pragmatisches-css-fuer-amp-layouts/index.md", "url": "https://journal.3960.org/posts/2020-08-30-pragmatisches-css-fuer-amp-layouts/", "title": "Pragmatisches CSS für AMP-Layouts", "content_html": "Onboarding… auf einem sinkenden Schiff.
Mit Accelerated Mobile Pages hat eine inzwischen nur noch lose an Google angekoppelte Stiftung einen Standard verabschiedet, der das Internet deutlich schneller machen soll – vor allen Dingen im mobilen Bereich. Die Einschränkungen im Bereich Layout (u.a. in Bezug auf CSS) sind für Entwickler aber eine gewisse Herausforderung.
\nZum Glück gibt es SASS – damit können wir ein bestehendes Layouts für eine „konventionelle“ Site pragmatisch für AMP übernehmen und anpassen, ohne ein komplett eigenständiges AMP-Layout bauen zu müssen.
\n\nDazu muss man wissen, wie AMP dafür sorgt, dass die in AMP geschriebenen Seiten schnell werden:
\n<script>
, <iframe>
).Für die Verwendung von AMP gibt es zwei Strategien:
\nWenn wir uns für die Strategie „AMP-Duplikat“ entscheiden, haben wir auf einmal zwei Template-Sets (reguläres HTML und AMP), und deswegen auch zwei verschiedene Layouts. Natürlich können wir versuchen, auf beiden Seiten auch das selbe Layout zu verwenden – aber wie schaffen wir es, die Größenbeschränkung von AMP einzuhalten, und für die eigenen AMP-Tags eigene Styles zu schreiben, ohne zwei verschiedene CSS-Dateien schreiben zu müssen?
\nHier kommt SASS ins Spiel: Mit diesem CSS-Preprocessor können wir nicht nur eine CSS-Datei erzeugen – wir können auch zwei CSS-Dateien erzeugen. Dabei machen wir uns zu nutze, dass SASS jede .scss
-Datei ohne _
am Anfang des Dateinamens in eine CSS-Datei kompiliert.
Unser Setup ist also:
\nstyles.scss
enthält das Layout für die regulären HTML-Seiten, erzeugt styles.css
amp.scss
enthält das Layout für die AMP-Seiten, erzeugt amp.css
SASS bietet darüber hinaus an, mittels @import
den Inhalt anderer SASS-Dateien zu inkludieren. Das machen wir uns im Falle der amp.scss
zu nutze:
// amp.scss\n\n@import "styles";\n
\nJetzt wird SASS die amp.scss
zu einer amp.css
kompilieren, die das selbe Ergebnis hat wie styles.scss
bzw. styles.css
.
Ein Konzept, mit dem jeder CSS-Entwickler vertraut ist, sind Media Queries: Als CSS-Entwickler sperren wir CSS-Regeln in diese Queries ein, um je nach Ausgabegerät bzw. -kanal ein unterschiedliches Layout zu erzwingen.
\nGenau so denken wir nun AMP-CSS: Wir klammern in unserer SASS-Datei styles.scss
nun Blöcke ein, die nicht (oder nur) in der amp.css
auftauchen sollen. Dazu schreiben wir uns zwei kleine Mixins:
// styles.scss\n\n$amp: false !default;\n\n// Only output this block in AMP pages.\n@mixin amp() {\n @if($amp) {\n @content;\n }\n}\n\n// Only output this block in non-AMP pages.\n@mixin no-amp() {\n @if(not($amp)) {\n @content;\n }\n}\n
\nDiese beiden Mixins reagieren auf eine Variable namens $amp
. Im oberen Mixin wird der an das Mixin übergebene Inhalt nur ausgegeben, wenn $amp: true
ist – im unteren Beispiel wird er nur ausgeben, wenn $amp: false
ist.
Davor definieren wir die Variable $amp
. Wichtig ist hier die Verwendung von !default
. Damit verraten wir SASS, dass der Wert dieser Variable nur false
sein soll, wenn $amp
nicht bereits vorher gesetzt wurde.
Übrigens können wir die Mixins auch in eine eigene Datei auslagern (zum Beispiel _mixins.scss
), das Prinzip bleibt aber das gleiche.
Unsere styles.scss
soll nun alle AMP-Regeln nicht ausgeben, und alle Nicht-AMP-Regeln ausgeben. Dementsprechend setzen wir $amp
zu false
.
Derweil in der amp.scss
:
// amp.scss\n\n$amp: true;\n@import "styles";\n
\nHier setzen wir also $amp
auf true
und holen uns danach die Inhalte von styles.scss
– hier wird das dort definierte $amp
überschrieben, und danach die styles.scss
von SASS ganz normal weiter kompiliert. Damit haben wir die selben Inhalte, aber die Mixins werden sich nun genau umgekehrt verhalten.
Damit können wir nun alle CSS-Regeln für unsere regulären HTML-Seiten und AMP-Seiten in der style.scss
schreiben. An den Stellen, wo eine Unterscheidung zwischen regulären und AMP-CSS notwendig wird, verwenden wir die Mixins:
// styles.scss\n\n// AMP & regular CSS\nnav {\n a {\n color: red;\n }\n}\n\n// Hide rule for AMP CSS\n@include no-amp() {\n header {\n background-color: black;\n color: white;\n }\n}\n\nfooter {\n background-color: black;\n color: white;\n // Hide partial rule for AMP CSS\n @include no-amp() {\n font-size: 0.6em;\n }\n}\n\n// Hide rule for non-AMP CSS\n@include amp() {\n amp-img {\n border: 1px solid red;\n }\n}\n
\nDamit müssen viele CSS-Regeln nur einmal definiert werden, und werden in beiden Layouts identisch ausgegeben. Dieser Artikel zum Beispiel verwendet im regulären HTML und AMP-HTML weitestgehend identisches CSS, und kann trotzdem auf die Besonderheiten und Kompressionswünsche von AMP eingehen.
\namp.css
montierenDas so produzierte amp.css
dürft ihr gemäß den Vorschriften von AMP für die Integration von CSS nicht als separates Stylesheet laden. Stattdessen erreicht AMP seine hohe Geschwindigkeit durch die Tatsache, dass das gesamte AMP-CSS in die HTML-Seite eingebunden werden muss. Hier müssen wir also nur eine kleine Template-Anpassung in den AMP-Templates durchführen, die den Inhalt der amp.css
im AMP-Tag <style amp-custom>
am Beginn einer jeden AMP-Seite montiert.
<!doctype html>\n<head>\n …\n <style amp-custom>\n /* Content of amp.css goes here. */\n </style>\n …\n</head>\n
\nÜbrigens empfiehlt es sich spätestens hier, alle überflüssigen Leerzeichen aus der amp.css
zu entfernen, um weiter Platz zu sparen. Eine Ersetzung mit einem regulären Ausdruck (wie z.B. /\\n\\s+/
) lässt hier weiter die Luft raus, oder ihr setzt für den SASS-Compiler den Schalter compressed
.
Die Trennung von Logik und Ausgabe in einer (Web-)Anwendung ist eines der fundamentalen Prinzipien, um als Programmierer seine geistige Gesundheit zu behalten. Für die Ausgabe wird dabei eine zumeist einfache (Template-)Sprache verwendet, die verhindern soll, dass zu viel Logik in die Ausgabe wandert.
\nIn PHP wird dies in der Regel von Template-Engines wie Twig oder Smarty gelöst. Aber warum benötigen wir eigentlich eine Template-Engine? Könnte es sein, dass PHP ohne weitere Zusätze auch eine ganz passable Lösung für Templating parat hält?
\n\nDie Hauptidee hinter den meisten Template-Engines ist:
\nif
, while
und for
.Ein ausgezeichnetes Beispiel für die Philosophie von Template-Engines ist Mustache, das ein absolutes Minimum von Methoden anbietet, um Komplexität zu verhindern.
\nAber brauchen wir in PHP den Overhead einer Template-Engine wirklich? Oder können wir nicht mit ein bisschen Disziplin direkt mit PHP alle unsere Template-Bedürfnisse stillen – ohne uns in Abhängigkeit zu einer weiteren Software-Bibliothek zu begeben?
\nTatsächlich bietet PHP (auch aufgrund seiner Genese) sehr gute Möglichkeiten, Templating damit zu betreiben.
\nTemplate-Engines legen ihre Templates in der Regel in einen eigenen Ordner mit einer eigenen Dateiendung ab. In der Kombination Symfony mit Twig liegen Templates im Projekt in einem Ordner /templates
und haben die Dateiendung .twig
.
Tatsächlich hindert uns nichts daran, unsere PHP-Templates ebenfalls in einem /templates
-Verzeichnis abzulegen. Als Datei-Endung verwenden wir dabei (analog zu Twig und zur Erhaltung der geistigen Gesundheit):
Template-Ausgabe | \nDatei-Endung | \n(MIME-Type) | \n
---|---|---|
HTML-Dateien | \n*.html.php | \ntext/html | \n
XML-Dateien | \n*.xml.php | \ntext/xml oder application/xml | \n
JSON-Dateien | \n*.json.php | \napplication/json | \n
Text-Dateien | \n*.txt.php | \ntext/plain | \n
CSV-Dateien | \n*.csv.php | \ntext/csv | \n
Diese Liste lässt sich natürlich beliebig um neue Dateitypen erweitern. Wichtig ist nur, dass die Dateien auf PHP enden, damit sie einerseits von einem Code-Editor als PHP-Dateien erkannt werden, andererseits beim irrtümlichen Aufruf über einen Webserver auch als PHP ausgeführt werden, und nicht irgendwelche PHP-Geheimnisse verraten.
\nNach alter Sitte benennen wir das Template nach dem Controller. So wird das HTML-Template für den Index-Controller index.html.php
, für den User-Controller user.html.php
benannt. Das XML-Template für den Index-Controller ist dann entsprechend index.xml.php
.
Templates, die keinen Controller zugeordnet werden können (sogenannte Snippets oder Partials) beginnen mit einem _
, also zum Beispiel _meta.html.php
. Was es mit diesen Dateien auf sich hat klären wir weiter unten.
Wir nutzen die Fähigkeit von PHP, dass jede Anweisung, die nicht in PHP-Tags (<?php ?>
) steht, nicht als PHP verstanden wird, sondern direkt an den Browser zurückgegeben wird. Damit können wir innerhalb unserer Template-Dateien uns tatsächlich auf die Ausgabe konzentrieren:
<!DOCTYPE html>\n<html xmlns="http://www.w3.org/1999/xhtml" lang="en">\n<head>\n</head>\n<body>\n <!-- I am valid PHP -->\n</body>\n
\nDie wichtigste Funktion eines Templates ist die Ausgabe von Variablen, die im Controller vorbereitet wurden. Dazu öffnen wir (wie in jeder Template-Sprache) einen Kommandoblock, und führen darin das Kommando zum Ausgeben von Variablen aus.
\n<!-- Bad idea -->\n<div><?php echo($title) ?></div>\n
\nNetterweise gibt es eine PHP-Tag, das diese Ausgabe nochmals verkürzt:
\n<!-- Still a bad idea -->\n<div><?= $title ?></div>\n
\nTatsächlich sind aber beide Wege eine ganz schlechte Idee, weil wir hier sträflich das Quoting / Escaping vernachlässigt haben – eine Kernfunktionalität einer jeden Template-Engine und ein Garant für die Sicherheit unserer Web-Applikation. Im obigen Beispiel wäre es möglich, dass in $title
HTML steckt, was direkt wieder auf der Seite ausgegeben werden kann. In den wenigsten Fällen ist dies erwünscht und setzt voraus, dass man dem Variablen-Inhalt jederzeit vertrauen kann. Beispiel gefällig?
<!-- See, a bad idea -->\n<div><?= $_GET['search'] ?></div>\n
\nPraktischerweise liefert PHP die Funktion htmlspecialchars
mit, die einen String für die Ausgabe in sicherem HTML umwandelt. So werden Zeichen wie <
, >
und "
sicher umgewandelt:
<!-- Better idea -->\n<div><?= htmlspecialchars($title) ?></div>\n
\nDa diese Art der Ausgabe unsere Hauptmethode sein soll, die Schreibweise aber etwas sperrig ist, bauen wir uns einen Alias für htmlspecialchars
. Der Beginn der kleinsten PHP-Template-Engine der Welt:
// Template.php\n\n/**\n * Alias for `htmlspecialchars`\n */\nfunction html($s): string\n{\n return htmlspecialchars($s);\n}\n
\nDiese Funktion kürzt uns die Schreibarbeit ab, um eine Variable sicher auszugeben, und ist ab sofort unsere Hauptfunktion zur Ausgabe von Variablen:
\n<!-- Best idea -->\n<div><?= html($title) ?></div>\n
\nPHP bietet aber noch mehr Funktion für Quoting / Escaping. Unter anderem müssen wir bei der Ausgabe von Query-Parametern in URLs Zeichen wie #
und &
umwandeln. Dafür hat PHP die Funktion urlencode
parat – in Kombination mit unserer html
-Funktion unschlagbar:
<a href="https://www.example.com?id=<?= html(urlencode($id)) ?>">Test</a>\n
\nDann und wann müssen wir auch PHP-Variablen in JavaScript-Variablen umwandeln. Hier bietet sich die (etwas artfremde) Funktion json_encode
an, die PHP-Variablen als JSON ausgibt. Damit wird die Variable nicht nur in korrekte Anführungszeichen gesetzt (die wir wiederum nicht HTML-enkodieren dürfen), sondern erlaubt sogar die Ausgabe von komplexen Variablen wie Arrays:
<script>\nvar data = <?= json_encode($data) ?>;\n</script>\n
\nDie wichtigsten Kontrollstrukturen für das Templating sind Bedingungen. Hier ist das if
der wichtigste Ansprechpartner. „Echtes“ PHP mit seinen geschweiften Klammern ist dabei aber eher unübersichtlich, da zum Beispiel bei Schachtelungen unklar sein kann, welche schließende Klammer für welchen Block zuständig ist:
<?php if (!empty($title)) { ?>\n <div><?= html($title) ?></div>\n<?php } ?>\n
\nPraktischerweise kennt PHP auch eine alternative Syntax für Kontrollstrukturen, bei dem jeder Block hinter einem Kommando nicht mit einem {
, sondern mit einem :
beginnt – und statt mit einem }
mit einem end...;
beendet wird.
<?php if (!empty($title)): ?>\n <div><?= html($title) ?></div>\n<?php endif ?>\n
\nIn den if
-Bedingungen kann man dabei alle Operatoren und Funktionen verwenden, die PHP so kennt. Wichtig ist (wie immer), dass vor dem Verwenden einer Variable getestet wird, ob die Variable überhaupt definiert wird, was mit !empty
oder isset
geschehen kann.
Darüber hinaus stehen natürlich auch else
und elseif
zur Verfügung:
<?php if (!empty($results)): ?>\n <h4>Wir haben <?= html(count($results)) ?> Ergebnisse gefunden.</h4>\n<?php else: ?>\n <h4>Wir haben leider keine Ergebnisse gefunden.</h4>\n<?php endif ?>\n
\nDie Ausgabe von Listen und Tabellen ist ebenfalls eine wichtige Funktion von Templates. Mittels Schleifen kann man zum Beispiel über vorher definierte Arrays iterieren – ebenfalls mit der alternative Syntax für Kontrollstrukturen:
\n<?php if (!empty($list)): ?>\n <ul>\n <?php foreach($list as $index => $item): ?>\n <li><?= html($item) ?></li>\n <?php endforeach ?>\n </ul>\n<?php endif ?>\n
\nDabei funktionieren for
, foreach
und while
ganz wunderbar:
<h4>Lottozahlen</h4>\n<ol>\n <?php for ($i = 1; $i <= 49; $i++): ?>\n <li>\n <input type="checkbox" name="lotto_<?= html($i) ?>" id="lotto_<?= html($i) ?>" value="1" />\n <label for="lotto_<?= html($i) ?>"><?= html($i) ?></label>\n </li>\n <?php endfor ?>\n</ol>\n
\nUm Template-Teile wie zum Beispiel einen Header, Footer oder Navigation in ein Template einzufügen, fügen wir dem Dateinamen des Template-Teils einfach einen führenden _
hinzu, wie zum Beispiel _header.xml.php
.
Ein Template-Teil (auch als Snippet oder Partial bekannt) kann mit dem PHP eigenen include
ins ein anderes Template eingebunden werden:
<!DOCTYPE html>\n<html xmlns="http://www.w3.org/1999/xhtml" lang="en">\n<head>\n <?php include('_meta.html.php') ?>\n</head>\n<body>\n <?php include('_header.html.php') ?>\n ...\n <?php include('_footer_.html.php') ?>\n</body>\n
\nSogar Übersetzungen von Templates beherrscht PHP! Die in PHP vorhandene Gettext-Bibliothek erlaubt es, einen String zu übergeben, für den Gettext in einem beigefügten Wörterbuch nach der korrekten Übersetzung sucht, und diesen zurückgibt.
\nWichtig dafür ist das Setzen der Locale in PHP mittels setlocale
:
setlocale(LC_MESSAGES, 'de_DE');\n
\nDie korrekte Bedienung dieser Funktion sorgt übrigens auch für die Übersetzung von Zahlen- und Datumsformaten, wie auch Datumsbezeichnern.
\nDie eigentlich zu übersetzenden Bezeichner können an die Funktion gettext
bzw. _
übergeben werden.
<!-- translate to 'Guten Morgen!' -->\n<h4><?= html(_('Good morning!')) ?></h4>\n
\nDie dafür notwendigen Wörterbücher kann man spielend leicht selber erstellen. Die dafür notwendigen PO- und MO-Dateien kann man zum Beispiel mit Poedit erstellen. Poedit ist sogar in der Lage, die gesamte PHP-Programmierung nach noch nicht katalogisierten, übersetzbaren Strings zu durchsuchen.
\nWenn wir mit Variablen-Ersetzung arbeiten, wird die Konstruktion etwas schwieriger:
\n<div><?= html(vsprintf(_('There are %d results'), [$count])) ?></div>\n
\nDa wir so ziemlich jeden hardcodierten Text übersetzbar machen wollen, können wir unserer simplen Template-Engine eine weitere Funktion als Abkürzung hinzufügen:
\n// Template.php\n\n/**\n * Alias for `vsprintf`, but with HTML escaping and translation\n */\nfunction _html(string $format, array $args = []): string\n{\n return htmlspecialchars($args\n ? vsprintf(_($format), $args)\n : _($format)\n );\n}\n
\n…was den Aufruf deutlich einfacher macht:
\n<h4><?= _html('Good morning!') ?></h4>\n<div><?= _html('There are %d results', [$count]) ?></div>\n
\nFür Entwickler wie auch Template-Designer ist es hilfreich, sich komplexe Variablen im Frontend zu Debugging-Zwecken ausgeben zu lassen. Dafür bietet PHP die Funktionen print_r
und var_dump
/ var_export
. Dummerweise ist in HTML ein solcher Dump schwer lesbar, da die Zeilenumbrüche in HTML ignoriert werden. Eine kleine Funktion für unsere Template-Engine kann auch diesen Missstand beheben:
// Template.php\n\n/**\n * HTML dumper für PHP variables\n */\nfunction debug($mixed, bool $extended = false): void\n{\n echo('<pre class="debug" style="margin: 1em 0; border: 1px solid red; background: #fee; color: #000; padding: 1em;">');\n echo(htmlspecialchars($extended\n ? var_export($mixed, true)\n : print_r($mixed, true)\n ));\n echo('</pre>');\n}\n
\nDamit können auch komplexe Variablen bis hin zu Objekten lesbar ausgegeben werden:
\n<?php debug($data) ?>\n
\nStatt einer weithin sichtbaren Debug-Ausgabe könnte man natürlich gleich auf die Ausgabe von Debugging-Daten in der Browser-Console ausweichen.
\nPHP erzeugt von Haus aus eine HTML-Ausgabe. Im Browser funktioniert dies so, dass jedes PHP-Skript den Webserver anweist, als MIME-Type text/html
zurückzugeben. Tatsächlich kann PHP aber auch ganz andere MIME-Types ausgeben. Dazu gibt es die Funktion header
, mit der PHP den Webserver anweisen kann, bestehende Header zu ändern bzw. neue zu setzen. Der für uns interessante Header ist Content-Type
.
So können wir PHP anweisen, jeden erdenklichen MIME-Type zurückzugeben.
\nFür XML verwenden wir die Template-Endung .xml.php
, also zum Beispiel sitemap.xml.php
. Diese Templates unterscheiden sich tatsächlich nicht großartig von HTML-Templates, und verwenden sogar die selben Quoting/Escaping-Funktionen. Nur der zusätzliche Content-Type
-Header muss mitgeschickt werden:
<?php header('Content-Type: text/xml'); ?>\n<urlset>\n <?php foreach($urls as $url): ?>\n <url>\n <loc><?= html($url) ?></loc>\n </url>\n <?php endforeach ?>\n</urlset>\n
\nAuch JSON kann PHP für uns ausgeben. Hierfür verwenden wir die Template-Endung .json.php
, wie zum Beispiel feed.json.php
, und senden wieder den korrekten Content-Type
. Um das Quoting/Escaping in den Griff zu bekommen, bauen wir unser JSON nicht von Hand, sondern verwenden die Funktion json_encode
, die auch strukturierte PHP-Variablen strukturiert und proper formatiert ausspuckt:
<?php\n header('Content-Type: application/json');\n echo(json_encode($data));\n
\nFür besonders schönes JSON kann man übrigens den Parameter JSON_PRETTY_PRINT
mitgeben:
<?php\n header('Content-Type: application/json');\n echo(json_encode($data, JSON_PRETTY_PRINT));\n
\nÜbrigens respektiert die Funktion json_encode
tatsächlich PHP-Typen. Damit werden PHP-Integer tatsächlich als JSON-Integer ausgegeben, und PHP-Strings als JSON-Strings. Hier muss also in PHP den Unterschied zwischen 1
und „1“
berücksichtigt werden.
Die obigen Beispiele zum Setzen des korrekten MIME-Types und Holen der Templates kann man natürlich auch allgemein lösen:
\n// e.g. $template = 'index';\n// e.g. $contentType = 'html';\n\nswitch ($contentType) {\n case 'xml':\n header('Content-Type: text/xml');\n break;\n case 'json':\n header('Content-Type: application/json');\n break;\n case 'txt':\n header('Content-Type: text/plain');\n break;\n case 'csv':\n header('Content-Type: text/csv');\n break;\n default:\n $contentType = 'html';\n break;\n}\n\n$templateFilename = __DIR__ . '/templates/' . $template . '.' . $contentType . '.php';\nrequire($templateFilename);\n
\nJedes PHP-Projekt braucht eine saubere Trennung zwischen Logik und Ausgabe – aber für diese Trennung braucht es nicht immer eine Template-Engine wie Twig oder Smarty. Auch handelsübliches PHP kann (mit wenigen, kleinen Helferlein) eine performante, wartungsarme Alternative fürs Templating sein.
\nHier nochmals eine Übersicht über die wichtigsten PHP-Funktionen für Templating:
\nFunktion | \nAlias | \nBeschreibung | \n
---|---|---|
htmlspecialchars | \nhtml | \nHTML-Escaping | \n
urlencode | \n- | \nURL-Escaping | \n
json_encode | \n- | \nJSON/JavaScript-Escaping | \n
setlocale | \n- | \nSetzen der Sprache für Übersetzungen | \n
gettext / _ | \n_html | \nAusgabe von Übersetzungen | \n
strftime | \n- | \nGibt ein lokales Datumsformat aus | \n
localeconv | \n- | \nErmittelt die lokale Ausgabeeinstellungen für Zahlen | \n
printf | \n_html | \nAusgabe von Variablen in Strings | \n
nl2br | \n- | \nKonvertiert Zeilenumbrüche in HTML-<br /> | \n
implode | \n- | \nVerbindet ein Array zu einem String | \n
var_dump | \ndebug | \nDump von PHP-Variablen | \n
header | \n- | \nAusgabe von anderen Content-Type | \n
Einzig beim Thema Vererbung von Templates sind echte Template-Engines deutlich komfortabler, wie Twigs Template-Vererbung und Smartys Template-Vererbung zeigen. Siehe dazu auch den Vergleich der Templating-Engines für PHP auf CSS-Tricks.
\nThere also is an English version of this article, „World's smallest template engine“.
", "summary": "Die Trennung von Logik und Ausgabe in einer (Web-)Anwendung ist eines der fundamentalen Prinzipien, um als Programmierer seine geistige Gesundheit zu behalten…", "date_published": "2020-08-20T18:02:28+02:00", "date_modified": "2022-02-21T18:22:25+01:00", "author": { "name": "Frank Boës", "url": "mailto:info@3960.org", "avatar": "https://www.gravatar.com/avatar/71fcf51cf2ae9acdd54182d3e367ceca" }, "authors": [ { "name": "Frank Boës", "url": "mailto:info@3960.org", "avatar": "https://www.gravatar.com/avatar/71fcf51cf2ae9acdd54182d3e367ceca" } ], "banner_image": "https://cdn.3960.org/favicon-192x192.png", "language": "de-DE", "image": "https://cdn.3960.org/favicon-192x192.png", "tags": [ "PHP", "Programmierung", "Webdevelop" ] }, { "id": "user/posts/2020-07-30-sichere-passwoerter-fuer-leute-sichere-passwoerter-hassen/index.md", "url": "https://journal.3960.org/posts/2020-07-30-sichere-passwoerter-fuer-leute-sichere-passwoerter-hassen/", "title": "Sichere Passwörter für Leute, die sichere Passwörter hassen", "content_html": "Wirklich sichere Passwörter haben in der Regel den Nachteil, dass sie schwer zu merken oder schwer zu schreiben sind. Das muss aber nicht sein: mit diesem einfachen Rezept kann man die Sicherheit seiner Passwörter erhöhen, und muss sich nur ein Passwort merken.
\n\nUnd damit sind nicht Passwort-Manager gemeint. Stattdessen versteht sich dieses einfache Rezept als Anleitung für diejenigen, die auch im Jahr 2020 nur ein Passwort für alle ihre Accounts verwenden. Denkt zum Beispiel an die Passwörter für eure Oma. 😉
\nWohlgemerkt: Das folgende Rezept sollte nur verwendet werden, wenn man keine Lust hat, sich ein wirklich sicheres Passwort zuzulegen. Deswegen nenne ich das folgende, mit meiner Frau zusammen entwickelte Rezept…
\nWir beginnen einfach mal mit der Annahme, dass das Standardpasswort bisher folgendes war:
\npasswort\n
\nDas ist nachgewiesenermaßen ein schlechtes Passwort: Es ist ganze 8 Zeichen lang, steht direkt so in einem Wörterbuch, und ist (laut Kaspersky-Tool zum Testen der Passwortsicherheit) in 20 Sekunden1 geknackt. Aber mit wenigen Tricks und gleichem Merkaufwand kann dieses schlechte Passwort in ein deutlich sichereres Passwort umgewandelt werden.
\nDie Minimalanforderungen an Passwörter sind in der Regel:
\nDie Groß- und Kleinschreibung erledigen wird über einen großen Anfangsbuchstaben, was die Zeit zum Knacken schon auf 39 Sekunden1 anhebt:
\nPasswort\n
\nDa die deutsche Sprache gerne Komposita verwendet, kann man zusätzlich jedes im Passwort vorkommende Nomen einzeln mit einem Großbuchstaben versehen:
\nPassWort\n
\nDas hebt die Zeit zum Knacken schon auf 12 Minuten1 an – diese Regel ignorieren wir aber der Einfachheit halber, und machen mit anderen Rezeptbestandteilen weiter.
\nEin paar Ziffern hat jeder von uns im Kopf. In der Regel Jahreszahlen mit vier Stellen – aber warum nicht eine deutsche Postleitzahl mit fünf Stellen, oder auch die auswendig gelernte Telefonnummer eures ersten Schwarms mit acht Stellen?
\nPasswort09648\n
\nJetzt benötigen wir zum Knacken bereits 16 Tage1.
\nUnd als Sonderzeichen fügen wir zwischen dem Buchstaben- und Ziffernteil sowie am Ende ein einfaches Sonderzeichen ein.
\nPasswort-09648!\n
\nDamit ist unser neues Standardpasswort zwar nicht kompliziert… aber mit 15 Zeichen ist es nun deutlich länger und widersteht Knackversuchen für circa 4 Jahrhunderte1!
\nDas eigentlich wichtige an Passwörtern ist aber, dass für jeden Account ein eigenes Passwort verwendet wird. Andernfalls riskieren wir, dass beim Hack einer Passwort-Datenbank der Hacker danach Zugriff auf alle unsere Accounts hat.
\nDarüber hinaus besteht die Gefahr, dass der Hacker das Passwort einfach mal bei eurem E-Mail-Account ausprobiert – und dann für alle eure Accounts das Passwort über die „Passwort vergessen“-Funktion zurückgesetzt werden kann. Also benötigen wir unterschiedliche Passwörter für alle Accounts.
\nAuch hier greift Omas Passwort-Rezept – als Extra-Zutat erweitert man das Passwort um den Namen des Dienstes:
\nPasswort-09648!google\nPasswort-09648!facebook\nPasswort-09648!web.de\n
\nDamit wird aus unserem 15-Zeichen-Passwort ein Passwort mit über 20 Zeichen Länge, dass mehr als 10.000 Jahrhunderte1 standhält, und durch seine Länge verblüffenderweise sicherer ist als z.B. „&uG4ftL!
“ mit 12 Tagen Knackdauer1. Selbst ein so einfacher Vertreter wie „Klaus-123!google
“ hält immerhin 24 Jahrhunderte1.
Um die Tipparbeit zu reduzieren kann die Extra-Zutat auch nur durch drei Buchstaben ersetzt werden (mit Dank an Mathias für die Inspiration). Das können zum Beispiel die ersten drei Buchstaben des Dienstes sein, oder jeder zweite Buchstabe – was verblüffenderweise bei der bereits erreichten Länge die Sicherheit gar nicht so großartig reduziert1:
\nPasswort-09648!goo\nPasswort-09648!fac\nPasswort-09648!web\n
\nDie Extra-Zutat kann für Schreibfaule noch weiter verkürzt werden, indem man nur den Anfangsbuchstaben des Dienstes verwendet, was zumindest 8 Jahrhunderte hält1:
\nPasswort-09648!g\nPasswort-09648!f\nPasswort-09648!w\n
\n„Moment“, denkt ihr jetzt, „wenn mein Passwort in einem Account geklaut wird, kann man ja einfach erraten, wie es in einem anderen Account aussehen würde.“ Gut mitgedacht, aber gar nicht so wahrscheinlich:
\nTatsächlich speichert kein ernsthafter Dienst euer Passwort, sondern eine Art „Quersumme“ eures Passworts, ein sogenanntes Hash. Euer Passwort passt genau zu diesem Hash, aber von dem Hash kann man nicht unbedingt auf das Passwort schließen. Alleine aus diesem Grund kann euch niemand euer Passwort nachträglich zuschicken. Je länger das Passwort, desto zufälliger wird das Hash, und desto schwerer ist es, auf das ursprüngliche Passwort zurückzuschließen2.
\nPasswort | \nMD5 Hash ohne Salt | \n
---|---|
Passwort-09648! | \nb443a258958c87a19b7fe521de7a6958 | \n
Passwort-09648!g | \nda01b07dd811a00e6576fa4d83b4eee6 | \n
Passwort-09648!goo | \nfd39389348f433257dbde5361d33d757 | \n
Passwort-09648!google | \n0da72493f73589b4e1c96dbff8f7d2e1 | \n
Passwort-09648!facebook | \nc20ed3dc0acd60e05a6d668ad25758fb | \n
Passwort-09648!web.de | \n7a3d0f5304dd9bdad469c025269ca274 | \n
Ergo: Falls jemand das Hash eures Passworts klaut, wird er viel Zeit brauchen, dieses Hash in euer Passwort zurückzuverwandeln, um sich danach in eurem Namen in dem Dienst anzumelden. In der Regel macht sich aber niemand die Mühe, dieses Passwort anzuschauen um dann auszuknobeln, wie das Passwort bei anderen Diensten aussehen könnte.
\nOmas Passwort-Rezept besteht also aus den folgenden Schritten:
\n-
“.!
“.Die ganze schöne Anleitung ändert aber nichts an der Tatsache, dass man die eigenen Passwörter geheim halten muss – und jedes Gerät sicher sein muss, auf dem man ein Passwort eintippt.
\nCSS-Variablen erlauben komfortabel, Farben und andere CSS-Eigenschaften in CSS-Stylesheets zu definieren – wenn da nur nicht ältere Browser wären, die CSS-Variablen nicht unterstützen, und damit den schönen CSS-Plan zerstören.
\nAber keine Bange: SASS und CSS-Variablen sind die perfekte Kombination, um eine bombensichere Unterstützung für CSS-Variablen zu erreichen.
\n\nCSS-Variablen bzw. CSS Custom Properties erlauben eine übersichtliche, wiederverwendbare Definition von CSS-Eigenschaften. Wie schon von SASS, LESS und anderen CSS-Präprozessoren bekannt, kann eine solche Variable mehrfach verwendet werden – eine Änderung an der zentralen Definition ändert zeitgleich und zuverlässig die CSS-Ausgabe:
\n:root {\n --color-background: #ffffee;\n --color-text: #111100;\n}\n\nbody {\n color: var(--color-text);\n background-color: var(--color-background);\n}\n\narticle {\n border: 1px dotted var(--color-text);\n padding: 1em;\n}\n
\nDas ist besonders praktisch, wenn man einem Styleguide folgt, später übersichtlich Werte im CSS austauschen möchte, oder einen zentralen Ansatzpunkt für CSS-Änderung zum Beispiel durch Redakteure haben möchte.
\nGleichzeitig kann die Definition von CSS-Variablen auch in Abhängigkeit von Media Queries (@media
) definiert werden. So kann sich der Inhalt einer Variable für die Druckausgabe oder z.B. für die Ausgabe im Dark Mode ändern.
@media screen {\n :root {\n --color-background: #ffffee;\n --color-text: #111100;\n --color-link: #000899;\n --color-link-hover: #1520c7;\n }\n}\n@media (prefers-color-scheme: dark) {\n :root {\n --color-background: #333;\n --color-text: #fff;\n --color-link: #0eb9e7;\n --color-link-hover: #39cbf6;\n }\n}\n@media print {\n :root {\n --color-link: blue;\n --color-link-hover: blue;\n }\n}\n
\nDamit entfällt die Notwendigkeit, Variablen durch SASS oder LESS definieren zu müssen – und damit in bestimmten Projekten die Notwendigkeit, überhaupt SASS oder LESS zu verwenden…
\n…wenn da nicht (gar nicht so) alte Browser wären, die CSS-Variablen nicht interpretieren können. So sind alle Versionen des Microsoft Internet Explorers, sowie ältere Versionen von Microsoft Edge und Apple Safari nicht in der Lage, CSS-Variablen zu verarbeiten.
\nNetterweise gibt es aber einen Weg, auch diesen Browsern grundsätzliche Wünsche mitzuteilen:
\nbody {\n color: #111100;\n color: var(--color-text);\n background-color: #ffffee;\n background-color: var(--color-background);\n}\n
\nCSS ist so aufgebaut, dass bei einer mehrfachen Zuweisung von CSS-Eigenschaften die letzte Eigenschaft verwendet wird, die erfolgreich vom Browser interpretiert werden konnte.
\nIn unserem obigen Beispiel werden ältere Browser nicht in der Lage sein, eine korrekte Ersetzung für Print und Dark Mode durchzuführen – aber wenigstens grundsätzlich sieht die Seite gut aus. Im Sinne von progressive enhancement (oder graceful degradation) hat man mit dieser Lösung schon einen passablen Job gemacht.
\nDieses Arbeiten mit Fallbacks hat aber zwei Nachteile:
\nNetterweise bieten sich SASS-Variablen an, um CSS-Variablen zu definieren. Unser erster Schritt: Wir definieren die Werte, die wir später als CSS-Variablen verwenden wollen. Dazu benutzen wir aber nicht reguläre SASS-Variablen, sondern stattdessen SASS-Maps. Diese praktischen Konstrukte erlauben die Definition einer Liste von Schlüssel-Wert-Paaren:
\n$cssVariablesScreen: (\n 'color-background': #ffffee,\n 'color-text': #111100,\n 'color-link': #000899,\n 'color-link-hover': #1520c7\n);\n
\n…die in SASS nur wenig komplizierter als eine SASS-Variable ausgegeben werden können:
\nbody {\n color: map-get($cssVariables, color-background);\n}\n
\nWir aber interessieren uns für die Ausgabe der SASS-Variablen als CSS-Variablen. Hier hilft uns die SASS-Map, die in einer Schleife (@each
) alle Schlüssel-Wert-Kombinationen ausgeben kann:
:root {\n @each $name, $value in $cssVariablesScreen {\n --#{$name}: #{$value};\n }\n}\n
\nAuf diese Weise können wir für jede Media Query eine SASS-Map erzeugen, und den dazu passenden Block an CSS-Variablen erzeugen lassen:
\n$cssVariablesDarkMode: (\n 'color-background': #333,\n 'color-text': #fff,\n 'color-link': #0eb9e7,\n 'color-link-hover': #39cbf6\n);\n$cssVariablesPrint: (\n 'color-link': blue,\n 'color-link-hover': blue\n);\n\n:root {\n @media screen {\n @each $name, $value in $cssVariablesScreen {\n --#{$name}: #{$value};\n }\n }\n @media (prefers-color-scheme: dark) {\n @each $name, $value in $cssVariablesDarkMode {\n --#{$name}: #{$value};\n }\n }\n @media print {\n @each $name, $value in $cssVariablesPrint {\n --#{$name}: #{$value};\n }\n }\n}\n
\nUm uns unnötige Wiederholungen von SASS-Code zu vermeiden, verwenden wir SASS-Mixins (@mixin
). So kann das Durchlaufen von SASS-Maps zum Erzeugen von CSS-Variablen wunderbar in ein solches Mixin ausgelagert werden:
@mixin make-css-variables($cssVariables) {\n @each $name, $value in $cssVariables {\n --#{$name}: #{$value};\n }\n}\n\n:root {\n @media screen {\n @include make-css-variables($cssVariablesScreen)\n }\n @media (prefers-color-scheme: dark) {\n @include make-css-variables($cssVariablesDarkMode)\n }\n @media print {\n @include make-css-variables($cssVariablesPrint)\n }\n}\n
\nDamit haben wir zumindest schonmal die Ausgabe der CSS-Variablen – haben aber leider noch nichts gewonnen, was die Fallbacks für ältere Browser angeht.
\nTatsächlich haben wir aber schon alle Teile herumliegen, die folgende Fallbacks möglich machen:
\nbody {\n color: #111100;\n color: var(--color-text);\n background-color: #ffffee;\n background-color: var(--color-background);\n}\n
\nTheoretisch könnten wir uns der Aufgabe in SASS wie folgt nähern:
\nbody {\n color: map-get($cssVariables, color-text);\n color: var(--color-text);\n background-color: map-get($cssVariables, color-background);\n background-color: var(--color-background);\n}\n
\nBei einem genauen Blick erkennt man hier aber, das die beiden Zeilen direkt zusammenhängen, und ganz wunderbar durch das folgende Mixin abgebildet werden können:
\n@mixin variable-fallback($property, $name) {\n #{$property}: map-get($cssVariablesScreen, $name);\n #{$property}: var(--#{$name});\n}\n\nbody {\n @include variable-fallback(color, color-text);\n @include variable-fallback(background-color, color-background);\n}\n
\nMit ein bisschen zusätzlicher Fehlerbehandlung und mehr Flexibilität, welche SASS-Map für den Fallback verwendet werden soll:
\n@mixin variable-fallback($property, $name, $cssVariables: $cssVariablesScreen) {\n @if map-has-key($cssVariables, $name) {\n #{$property}: map-get($cssVariables, $name);\n } @else {\n @warn "Missing CSS variable: #{$name}"\n }\n #{$property}: var(--#{$name});\n}\n
\nCSS-Variablen mit Kompatibilität für Browser, die CSS-Variablen nicht unterstützen, kann man mit wenig Schreibarbeit via SASS lösen. Die Schritt sind:
\n// Defining SASS variables to build CSS variables\n// --------------------------------------------------------------\n\n$cssVariablesScreen: (\n 'color-background': #ffffee,\n 'color-text': #111100,\n 'color-link': #000899,\n 'color-link-hover': #1520c7\n);\n$cssVariablesDarkMode: (\n 'color-background': #333,\n 'color-text': #fff,\n 'color-link': #0eb9e7,\n 'color-link-hover': #39cbf6\n);\n$cssVariablesPrint: (\n 'color-link': blue,\n 'color-link-hover': blue\n);\n\n// Defining mixins for converting SASS variables to CSS variables\n// --------------------------------------------------------------\n\n@mixin make-css-variables($cssVariables) {\n @each $name, $value in $cssVariables {\n --#{$name}: #{$value};\n }\n}\n\n@mixin variable-fallback($property, $name, $cssVariables: $cssVariablesScreen) {\n @if map-has-key($cssVariables, $name) {\n #{$property}: map-get($cssVariables, $name);\n } @else {\n @warn "Missing CSS variable: #{$name}"\n }\n #{$property}: var(--#{$name});\n}\n\n// Convert SASS variables to CSS variables\n// --------------------------------------------------------------\n\n:root {\n @media screen {\n @include make-css-variables($cssVariablesScreen)\n }\n @media (prefers-color-scheme: dark) {\n @include make-css-variables($cssVariablesDarkMode)\n }\n @media print {\n @include make-css-variables($cssVariablesPrint)\n }\n}\n\n// Use CSS variables\n// --------------------------------------------------------------\n\nbody {\n @include variable-fallback(color, color-text);\n @include variable-fallback(background-color, color-background);\n}\n
",
"summary": "CSS-Variablen erlauben komfortabel, Farben und andere CSS-Eigenschaften in CSS-Stylesheets zu definieren – wenn da nur nicht ältere Browser wären, die CSS…",
"date_published": "2020-06-17T18:41:26+02:00",
"date_modified": "2020-06-18T11:41:37+02:00",
"author": {
"name": "Frank Boës",
"url": "mailto:info@3960.org",
"avatar": "https://www.gravatar.com/avatar/71fcf51cf2ae9acdd54182d3e367ceca"
},
"authors": [
{
"name": "Frank Boës",
"url": "mailto:info@3960.org",
"avatar": "https://www.gravatar.com/avatar/71fcf51cf2ae9acdd54182d3e367ceca"
}
],
"banner_image": "https://cdn.3960.org/favicon-192x192.png",
"language": "de-DE",
"image": "https://cdn.3960.org/favicon-192x192.png",
"tags": [
"Webdevelop",
"CSS",
"Programmierung"
]
},
{
"id": "user/posts/2020-06-15-css-variables-dark-mode/index.md",
"url": "https://journal.3960.org/posts/2020-06-15-css-variables-dark-mode/",
"title": "CSS-Variablen und Dark Mode",
"content_html": "Mit dem Dark Mode kann man in so ziemlich jedem Betriebssystem den Wunsch äußern, dass alle Darstellungen möglichst dunkel gehalten werden sollen – in der Regel mit heller Text auf dunklem Hintergrund. Interessanterweise betrifft dieser Wunsch auch Webseiten.
\n\nDer Dark Mode hat inzwischen in so ziemlich jedem Betriebssystem Einzug gehalten. So sind z.B. in Windows 10, Mac OSX, Linux, Android und auch iOS Einstellungen vorhanden, mit denen viele Applikationen sich diesem Farbwunsch anpassen.
\nInteressanterweise wird dieser Wunsch vom Betriebssystem auch auch an Browser weitergereicht. Der Browser entscheidet aber nicht selber, wie eine Seite im Dark Mode umgefärbt werden muss, sondern überlässt die Definition des Verhaltens dem jeweiligen Betreiber der Website, die gerade besucht wird.
\n@media (prefers-color-scheme: dark)
Technisch wird das über eine @media
-Query in CSS gelöst:
@media (prefers-color-scheme: dark) {\n body {\n color: white;\n background: black;\n }\n\n a {\n color: orange;\n }\n}\n
\nDabei müsst ihr als Entwickler nicht zwangsläufig euer Betriebssystem auf Dark Mode umschalten, um eure Styles zu testen. In Chrome kann man den Dark Mode kurzzeitig aktivieren, indem man im Web Inspektor (z.B. via F12 öffnen) mittels Strg+Shift+P die Kommandoleiste aktiviert, und dort „color-scheme“ (oder „dark“) eintippt. Eine Auswahl namens „Emulate CSS prefers-color-scheme: dark“ für die Aktivierung des Dark Mode im Browser erscheint.
\nMit CSS-Variablen bzw. CSS Custom Properties kann man unter anderem auch wunderschöne Farbdefinitionen hinterlegen, die je nach @media
-Query abgeändert werden können. Neben der Verwendung für Print-Styles ist die Verwendung für den Dark Mode eine gute Idee.
Anstatt in eurem CSS an jedem Element Farbcodes zu hinterlegen, die dann für jede Kombination aus Media-Query und Element neu definiert werden müssen, hinterlegt ihr an den einzufärbenden Elementen nur CSS-Variablen. Diese definiert ihr zentral, und könnt diese auch zentral umschalten:
\n@media screen {\n :root {\n --color-background: #ffffee;\n --color-text: #111100;\n --color-link: #000899;\n --color-link-hover: #1520c7;\n }\n}\n@media (prefers-color-scheme: dark) {\n :root {\n --color-background: #333;\n --color-text: #fff;\n --color-link: #0eb9e7;\n --color-link-hover: #39cbf6;\n }\n}\n\nbody {\n color: var(--color-text);\n background-color: var(--color-background);\n}\n\na {\n color: var(--color-link);\n}\n\na:hover {\n color: var(--color-link-hover);\n}\n
\nÜbrigens kann man auch SVG-Grafiken in der Seite mit diesem Trick umfärben lassen, da SVG-Grafiken ebenfalls über CSS ihre Farben verändern können:
\nsvg {\n fill: var(--color-text);\n stroke: var(--color-text);\n}\n
\nMit der obigen Methode können natürlich auch Hintergrundgrafiken ausgetauscht werden. In vielen Fällen ist das aber nicht nötig: Üblicherweise werden beim Bau von Grafiken von Websites viele Grafiken auf weiße Hintergrundfarben freigestellt. Hier sollte man prüfen, ob diese nicht besser auf einen transparenten Hintergrund (als PNG-Grafik) freigestellt werden.
", "summary": "Mit dem Dark Mode kann man in so ziemlich jedem Betriebssystem den Wunsch äußern, dass alle Darstellungen möglichst dunkel gehalten werden sollen – in der…", "date_published": "2020-06-15T18:41:26+02:00", "date_modified": "2020-06-16T09:20:21+02:00", "author": { "name": "Frank Boës", "url": "mailto:info@3960.org", "avatar": "https://www.gravatar.com/avatar/71fcf51cf2ae9acdd54182d3e367ceca" }, "authors": [ { "name": "Frank Boës", "url": "mailto:info@3960.org", "avatar": "https://www.gravatar.com/avatar/71fcf51cf2ae9acdd54182d3e367ceca" } ], "banner_image": "https://journal.3960.org/posts/2020-06-15-css-variables-dark-mode/blackwhite.jpg", "language": "de-DE", "image": "https://journal.3960.org/posts/2020-06-15-css-variables-dark-mode/blackwhite.jpg", "tags": [ "Webdevelop", "CSS", "Programmierung" ] }, { "id": "user/posts/2020-05-17-ausstattung-fuer-home-office/index.md", "url": "https://journal.3960.org/posts/2020-05-17-ausstattung-fuer-home-office/", "title": "Die Ausstattung für das Home-Office", "content_html": "\nMit wenigen Handgriffen kann man den häuslichen Arbeitsplatzes (aka „Home Office“) deutlich angenehmer und produktiver gestalten.
\n\nIn der Regel erhält man von seinem Arbeitgeber einen Laptop nebst Netzteil ausgehändigt – für ein echtes Büro braucht es aber dann doch etwas mehr als einen Tisch, einen Stuhl und eine Lampe.
\nInspiriert von den Home-Offices von Basecamp-Mitarbeitern folgt hier eine Liste der Dinge, mit der der heimische Arbeitsplatz deutlich angenehmer werden kann.
\nGrundsätzlich muss das Home-Office folgende Eigenschaften erfüllen:
\nHome-Office-Arbeit ist sitzende Arbeit – mehr noch als normale Büro-Arbeit. Und so sollte für die 35–40 Stunden pro Woche der Arbeitsplatz so ergonomisch wie möglich eingerichtet werden. Dreh- und Angelpunkt ist ein vernünftiger Schreibtischstuhl.
\nEin Küchenstuhl oder billiger Schreibtischstuhl (ohne verstellbare Rücken- und Armlehnen) kann das Arbeiten aber schnell zur Tortur machen. Tatsächlich ist ein guter Schreibtischstuhl in der Regel eine nicht unerhebliche Investition – aber eine kleine dreistellige Summe ist hier langfristig betrachtet eine gute Investition in die eigene Rückengesundheit.
\nEin anständiger Monitor vergrößert nicht nur die Bildschirmfläche, sonder erlaubt ein ergonomischeres Sitzen bei der Arbeit. Monitore ab 23 Zoll aufwärts sind zu erschwinglichen Preisen zu haben. Wenn man sowieso einen heimischen PC besitzt, hat man möglicherweise auch bereits einen Kandidaten für den Zweitmonitor herumstehen. Denn an vielen Monitoren gibt es mehrere Monitor-Eingänge.
\nSo kann man heimischen PC und Arbeits-Laptop gleichzeitig anschließen, und am Monitor die gerade aktuelle Quelle auswählen – oder der Monitor ist sogar so schlau, auf die zuletzt aktive Quelle zu wechseln.
\nFalls ihr dagegen einen neuen Monitor anschaffen müsst, lohnt eine kurze Überprüfung, ob euer Laptop nicht DisplayPort via USB-C beherrscht. In diesem Falle kann ein USB-C-Monitor angeschafft werden. Im günstigsten Fall hat der Monitor einen USB-Hub integriert, der gleichzeitig als einfache Docking-Station fungiert – weiteres Zubehör für euren Laptop wird nicht an dem Laptop selber eingestöpselt, sondern am Monitor!
\nDie meisten Laptop-Tastaturen und ihre eingebauten Trackpads eignen sich nur bedingt für längere Tipp-Arbeiten. Schon preisgünstige Tastaturen (wie z.B. die Cherry Stream 3.0 ab ~€20,-) machen nicht nur das Tippen einfacher, sondern bieten auch Medienknöpfe, mit denen man z.B. laufende Musik schnell für ein Telefonat pausieren kann.
\nGleiches gilt für preisgünstige Mäuse (wie z.B. die Logitech M500 ab ~€20,-) – zusätzliche Bedienmöglichkeiten wie ein Scrollrad oder die praktische „Zurück“-Taste erleichtern die Bildschirm-Arbeit ungemein.
\nWenn (wie in meinem Fall) noch ein zweiter Rechner auf dem Schreibtisch steht, kann man entweder mehrere Tastaturen und Mäuse anschaffen. Es gibt aber auch Funktastaturen und -mäuse (wie die Logitech MX-Serie), die problemlos zwischen mehreren Empfängern hin- und herschalten können. Damit liegt nur eine Tastatur und eine Maus auf eurem Schreibtisch, die sich Heim- oder Arbeits-PC verbinden können.
\nSelbst wenn der Laptop an einen Monitor angeschlossen ist, kann er trotzdem weiterhin als Zweit-Monitor dienen. Da mit angeschlossener Maus und Tastatur die Tastatur des Notebooks nicht mehr benötigt wird, kann das Notebook auch in eine ergonomischere Position gebracht werden.
\nDafür gibt es entweder einen Stapel Bücher – oder einen Laptop-Ständer (wie z.B. der Nulaxy C3 ab ~€35,-). Die meisten Laptop-Ständer erzeugen dabei unterhalb des Laptops einen Freiraum, in dem man zusätzliche Dinge ablegen kann – wie zum Beispiel Maus und Tastatur, wenn sie nicht mehr in Verwendung sind. Und ganz nebenbei verbessert die Luft unter dem Laptop die Belüftung bzw. Kühlung des Laptops.
\nFür Telefonate und Videokonferenzen sind die in Notebook eingebauten Mikrofone meist nur ein Notbehelf. Interessanterweise ist bei so ziemlich jedem modernen Notebook der eingebaute 3,5mm-Klinkenstecker nicht nur für normale Kopfhörer geeignet, sondern vor allen Dingen für die von Smartphones bekannten Kombinationen aus Kopfhörern und Mikrofonen.
\nDie Klinken an dem Kopfhörer unterscheiden sich darin, dass sie statt zwei schwarzer Ringe auf dem blanken Teil des Steckers drei Ringe haben. In das Notebook eingesteckt wird dann nicht nur die Audio-Ausgabe in die Kopfhörer gelenkt, sondern die Aufnahme über das Mikrofon des Kopfhörers abgewickelt.
\nIhr könnt also den bei eurem Smartphone mitgelieferten Kopfhörer mit 3,5mm-Klinke einfach in euer Notebook einstecke. Das kostet euch keinen Cent und ist zumeist eine überraschend leistungsfähige Lösung.
\nUnd falls ihr schon einen Bluetooth-Kopfhörer mit integrierten Mikrofon besitzt: Die meisten Notebooks haben ebenfalls Bluetooth eingebaut, so dass ihr diese Kopfhörer ebenfalls mit all ihren Features am Notebook nutzen könnt.
\nWenn ihr mit eurem Notebook öfter zwischen Arbeitsplätzen wechselt, kann die Umstöpselei etwas nerven. Folgende Anschlüsse müssen dann imer wieder angedockt werden:
\nAnstatt sich eine teure (und meist nur für den aktuellen Laptop geeignete) Docking-Station zuzulegen, kann ein USB-Hub (ab €10,-) schon Wunder wirken, um die Anzahl der benötigten Kabel zu reduzieren. Einen schon vorhandenen USB-Hub kann man übrigens preisgünstig zu einer Dockingstation aufrüsten, indem man folgende USB- beziehungsweise USB-C-Zubehörteile ansteckt:
\nFalls euch der Sinn weniger nach Bastelei ist: Auch hier lohnt eine kurze Überprüfung, ob euer Laptop USB-C beherrscht. Schon preisgünstige USB-C-Hubs (ab ~€30,-) bieten nicht nur die Möglichkeit, weitere USB-Geräte anzuschließen, sondern auch Netzwerkkabel, Kopfhörer oder (je nach USB-C-Anschluss) Monitore.
\nEtwas teurer sind dann USB-C-Hubs mit Power Delivery, die euren Laptop mit Strom versorgen können. Hier müsst ihr aber überprüfen, ob euer Laptop auch über den USB-C-Port geladen werden kann, und wieviel Watt er für den Betrieb benötigt. In den meisten Fällen wird man um eine Original-Dockingstation nicht herum kommen, wenn die Stromversorgung gewährleistet sein soll.
\nDie Regelungen für die ergonomische Einrichtung von Arbeitsplätzen sollten auch (oder gerade) daheim beherzigt werden. Mit ein paar kleinen Investitionen und Teilen, die man wahrscheinlich sowieso daheim herumliegen hat, ist das schnell bewerkstelligt.
", "summary": "Mit wenigen Handgriffen kann man den häuslichen Arbeitsplatzes (aka „Home Office“) deutlich angenehmer und produktiver gestalten.", "date_published": "2020-05-17T19:42:53+02:00", "date_modified": "2021-10-12T09:41:03+02:00", "author": { "name": "Frank Boës", "url": "mailto:info@3960.org", "avatar": "https://www.gravatar.com/avatar/71fcf51cf2ae9acdd54182d3e367ceca" }, "authors": [ { "name": "Frank Boës", "url": "mailto:info@3960.org", "avatar": "https://www.gravatar.com/avatar/71fcf51cf2ae9acdd54182d3e367ceca" } ], "banner_image": "https://journal.3960.org/posts/2020-05-17-ausstattung-fuer-home-office/homeoffice.jpg", "language": "de-DE", "image": "https://journal.3960.org/posts/2020-05-17-ausstattung-fuer-home-office/homeoffice.jpg", "tags": [ "Homeoffice", "Geckobar", "Idee", "Programmierung", "Technologie", "The Cool", "Webdevelop" ] }, { "id": "user/posts/2020-04-10-web-components-mit-markdown-verwenden/index.md", "url": "https://journal.3960.org/posts/2020-04-10-web-components-mit-markdown-verwenden/", "title": "Web Components mit Markdown verwenden", "content_html": "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.
", "summary": "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…", "date_published": "2020-04-10T19:32:33+02:00", "date_modified": "2020-04-10T20:34:10+02:00", "author": { "name": "Frank Boës", "url": "mailto:info@3960.org", "avatar": "https://www.gravatar.com/avatar/71fcf51cf2ae9acdd54182d3e367ceca" }, "authors": [ { "name": "Frank Boës", "url": "mailto:info@3960.org", "avatar": "https://www.gravatar.com/avatar/71fcf51cf2ae9acdd54182d3e367ceca" } ], "banner_image": "https://cdn.3960.org/favicon-192x192.png", "language": "de-DE", "image": "https://cdn.3960.org/favicon-192x192.png", "tags": [ "Webdevelop", "Web-Components", "Blog", "Javascript", "Programmierung", "Technologie" ] } ] }