Webkrauts Logo

Webkrauts Webkrauts Schriftzug

- für mehr Qualität im Web

Responsive Content

MediaQueryList-Objekt und conditioner.js

Responsive Content

Responsive Webdesign gehört heute zum guten Ton, und so gut wie alle frisch ins Netz gestellten Seiten schmücken sich mit entsprechenden Fähigkeiten. Meist ist damit gemeint, dass das Layout sich dank CSS-Mediaqueries an seine Umgebung anpasst. Was viele darüber vergessen ist, dass auch die übersendeten Inhalte an das Zielgerät angepasst werden wollen. CSS alleine kann das nicht leisten. Wir zeigen euch, wie ihr mit Hilfe von JavaScript auch eure Inhalte responsiv passend ausliefert.

Was viele nicht wissen: Im Zuge mit den Stylesheets hat auch JavaScript gelernt, mit Mediaqueries umzugehen, und zwar durch das neu eingeführte MediaQueryList-Objekt und seine Funktionen. Ein solches MediaQueryList-Objekt erzeugt ihr durch Übergabe einer CSS-Mediaquery:

  1. var mq_large = matchMedia("(min-width: 60em)");

Dieser Beispielcode verknüpft ein neues MediaQueryList-Objekt dauerhaft mit einer Mediaquery, wie ihr sie in CSS per

  1. @media (min-width: 60em) {}

deklarieren würdet, und speichert es zur späteren Befragung in der Variablen mq_large. Damit ist das Setup erledigt und es lässt sich mit der erzeugten MediaQueryList arbeiten. Ihr könnt das Objekt zum Beispiel fragen, ob die verknüpfte Mediaquery aktuell gerade zutrifft. Das macht ihr über die matches-Eigenschaft, die true oder false zurückliefert:

  1. if (mq_large.matches) {
  2.     alert('Dieses Gerät ist mindestens 60em (960px) breit');
  3. }

Dieses bisschen Code reicht schon, um eure Seite auch inhaltemäßig auf das »Mobile First«/»Content First«-Prinzip umstellen. So könntet ihr zum Beispiel erst einmal nur die Basisinhalte zum Browser senden, und bei DOMready entscheiden, ob ihr nicht noch weitere Inhalte per AJAX (via jQuery .load()-Methode) nachladet:

  1. $(document).ready(function() {
  2.   var mq_large = matchMedia("(min-width: 60em)");
  3.   if (mq_large.matches) {
  4.     $("#rechte_spalte").load("/module/rechte_spalte.html");
  5.   }
  6. });

Die kleinen Geräte werden es euch danken, denn jeder DOM-Knoten, der im DOM-Baum herumhängt, kostet knapp bemessenen Speicher und Layoutperformance. Auch ungerendert. Ein weiterer Vorteil dieses Verfahrens: Dem initialen Rendering geht immer ein sogenannter Preparser-Vorgang voraus, bei dem ein blitzschneller Suchalgorithmus das HTML nach zu ladenden Bildern und anderen Ressourcen abgrast und diese lädt, bevor das echte Rendering in Fahrt kommt. Das führt dazu, dass in HTML referenzierte Ressourcen auch dann geladen werden, wenn ihr sie per CSS ausgeblendet habt. Denn vom CSS weiß der Preparser nichts. Mit dem JavaScript-Verfahren umgeht ihr diesen Effekt. Und der dritte Vorteil: Euer Server hat bei mobilen Besuchern deutlich weniger Arbeit zu leisten, so dass die Hauptinhalte schneller rausgeschickt werden können. Selbst die Desktop-Benutzer profitieren davon, weil weniger Serverrechenleistung an die falsche Zielgruppe verschwendet wird. Ebenso kann eine langsame Datenbankabfrage in einem der optionalen Seitenmodule nicht mehr die Auslieferung der gesamten Seite aufhalten.

Es gibt aber auch einen Nachteil: Die Hauptinhalte sind bei größeren Screens zwar auch viel schneller da, dafür dauert es aber länger, bis die optionalen Module angefragt und eingebaut sind. Jeder HTTP-Request kostet Zeit und mehr als sechs arbeitet ein herkömmlicher Server sowieso nicht gleichzeitig ab. Es gibt zwei Mittel dagegen.

Mittel Nummer eins: Ihr bündelt in der JSON-Antwort auf den AJAX-Request die Inhalte verschiedener Module auf einmal:

  1. $desktop_module = array(
  2.   "#linke_spalte" => "<ul><li>…</li></ul>",
  3.   "#rechte_spalte" => "<div><h2>…</h2></div>"
  4. );
  5. echo json_encode($desktop_module, TRUE);

Im JavaScript verteilt ihr anschließend die verschiedenen Blöcke an die entsprechenden Stellen (was diesmal per .get()-Methode stattfinden muss):

  1. $.get("/module/desktop_module.php", function(data) {
  2.   data.each(selektor, html) {
  3.     $(selektor).html(html);
  4.   }
  5. });

Mittel Nummer zwei: Ihr lasst die optionalen Inhalte in dem Haupt-HTML zwar drin, aber kommentiert sie aus:

  1. <div id="rechte_spalte">
  2.   <!--
  3.  <div>
  4.    <h2>Überschrift</h2>
  5.    <p>…</p>
  6.  </div>
  7.  -->
  8. </div>

HTML-Kommentare respektiert der Preparser und überspringt einen auskommentierten Abschnitt einfach. Und auch im Renderingprozess kostet auskommentiertes HTML keine Performance, weil keinerlei DOM-Knoten erzeugt werden, die dann gelayoutet werden wollen. Das bisschen »lebloses« Extra-HTML wirkt sich nicht spürbar auf die Ladezeit aus. Nun bleibt euch nur noch, das HTML bei passender Mediaquery zu entkommentieren, was ihr folgendermaßen bewerkstelligt:

  1. $("#rechte_spalte").
  2. contents().
  3. each(function(index,node) {
  4.   // Wenn Kommentar: entkommentieren
  5.   if (node.nodeType === 8) {
  6.     $(node).replaceWith(node.nodeValue);
  7.   }
  8. });

Diese Lösung ist die schnellere und unaufwendige der zwei, allerdings spart ihr bei dieser Lösung keine Serverzeit mehr ein. Es hängt also vom konkreten Szenario ab, welche Lösung für euch in Frage kommt.

Browserkompatibilität

Um wieder auf das MediaQueryList-Objekt zurückzukommen: Wie sieht es da denn mit der Browserunterstützung aus? Gut sieht’s aus! Chrome, Firefox, Safari und Opera unterstützen die Technik seit Ewigkeiten, die mobilen Browser ebenso (Android ab 3.x). Nur der Internet Explorer kennt MediaQueryList erst ab Version 10. Das macht aber insofern nichts, als dass ihr getrost davon ausgehen könnt, dass alle älteren IEs nur auf Desktops laufen. Zwar ist der Browser auf Windows Phone 7 auch ein IE 9, aber dieses Betriebssystem hat im Gegensatz zu Windows Phone 8 so gut wie gar keinen Marktanteil.

Für die älteren IEs bietet es sich an, den Code um eine sogenannte Feature detection für das MediaQueryList-Objekt zu ergänzen und im Falle des Nichtvorhandenseins die Desktop-Module zu laden:

  1. // Browser ohne matchMedia, oder Desktop
  2. if (!window.matchMedia || matchMedia("(min-width: 60em)").matches) {
  3.   // Desktop Module
  4. }
  5. // Ansonsten, falls Tablet
  6. else if (matchMedia("(min-width: 30em)").matches) {
  7.   // Tablet Module
  8. }

Tipp: Wenn ihr ein MediaQueryList-Objekt nur einmal braucht, könnt ihr wie eben gezeigt auf das Zuweisen des Objekts in eine Variable verzichten, also so:

  1. if (matchMedia("(min-width: 60em)").matches) {}

Alternativ zur Featuredetection setzt ihr einen Polyfill ein, also eine kleine JavaScript-Bibliothek, die mit Hilfe schlauer Tricks in den älteren Browsern das MediaQueryList-Objekt 1:1 nachbildet. Unser Tipp: matchMedia.js.

Reaktion auf Veränderungen

Nun kann es auch vorkommen, dass sich Mediaqueries erst nach dem Laden der Seite verändern. Zum Beispiel könnte ein Nutzer sein Gerät drehen. Oder die Seite wird ausgedruckt, was eine Änderung des Präsentationsmediums darstellt. Auch darauf lässt sich mit Hilfe des MediaQueryList-Objekts reagieren. Zu diesem Zweck nutzt ihr dessen Horchfunktion, und weist diese an, im Falle einer Änderung der Mediaquery eine bestimmte Funktion auszuführen:

  1. function anpassen(mq_orientierung) {
  2.   if (mq_orientierung.matches) {
  3.     // Quermodus
  4.   } else {
  5.     // Hochkantmodus
  6.   }
  7. }
  8.  
  9. var mq_orientierung = matchMedia("(orientation:landscape)");
  10. mq_orientierung.addListener(anpassen);

Das obige Beispiel führt die Funktion namens »anpassen« immer dann aus, wenn die Prüfung auf die Mediaquery ein gegenüber zuvor verändertes Ergebnis zurückliefert. Sprich, die Funktion wird nicht nur dann ausgeführt, wenn plötzlich (orientation:landscape) eintritt, sondern auch dann, wenn die Mediaquery plötzlich nicht mehr zutrifft. Deshalb müsstest ihr in der dann aufgerufenen Funktion abprüfen, was gerade Sache ist.

conditioner.js

Abschließend möchten wir euch noch eine JavaScript-Bibliothek vorstellen, die sich speziell bei größeren/komplexeren Projekten bestens dazu eignet, responsives JavaScript sauber zu strukturieren und aus dem HTML-Markup heraus zu steuern: conditioner.js. Der Conditioner setzt dabei auf das JavaScript-Modularisierungssystem require.js. Die Bibliothek bietet sich somit dann besonders an, wenn ihr in eurem Projekt sowieso schon mit require.js arbeitet. Require.js setzt ein paar einfache Regeln voraus, nach denen eine JavaScript-Datei aufgebaut sein muss, damit sie von require.js als Modul ladbar ist. Das sieht in seiner einfachsten Form so aus:

  1. define({
  2.   // Hier drin passiert die Magie
  3. });

Der Conditioner ermöglicht es nun, im HTML festzulegen, was für Module bei welchen Mediaqueries nachzuladen sind, z.B. so:

  1. <a href="http://maps.google.com/?ll=51.74,3.82"
  2.  data-options="{"map":{"zoom":10, "type":"terrain"}"
  3.  data-conditions="media:{(min-width:30em)}"
  4.  data-module="Googlemaps">
  5.   Auf einer Karte anzeigen</a>

Das Beispiel besteht zunächst nur aus einem einfachen Link zu einer Karte. Die letzten beiden zusätzlichen data-Attribute steuern den Conditioner. Sie bestimmen, dass im Falle einer Gerätebreite von 30em oder mehr die Datei »Googlemaps.js« nachzuladen und auszuführen ist. Progressive Enhancement vom Feinsten!

Innerhalb eines solchen Moduls gibt es vordefinierte Abschnitte, in denen ihr festlegt, was beim Aktivieren geschehen soll, und optional auch, was nötig ist, um es zu deaktivieren, wenn die Bedingungen einmal nicht mehr zutreffen:

  1. define(function(){
  2.  
  3.   // Wird beim Start ausgeführt
  4.   var exports = function(element, options) {
  5.     // "element" zeigt auf das ursprüngliche HTML Element
  6.     // "options" beinhaltet die Daten aus dem data-options-Attribut
  7.   };
  8.  
  9.   // Standardoptionen, falls kein data-options-Attribut existiert
  10.   exports.options = {
  11.     "map": {
  12.       "zoom": 10,
  13.       "type": "terrain"
  14.     }
  15.   };
  16.  
  17.   // Wird vom Conditioner aufgerufen, wenn das Element entfernt wird
  18.   exports.prototype.unload = function() {
  19.     // z.B. Google Maps Element löschen
  20.   };
  21.  
  22.   return exports;
  23.  
  24. });

Der Conditioner versteht sich dabei nicht nur auf klassische Mediaqueries, sondern kennt im Auslieferungszustand fünf Kategorien von Bedingungen:

  • Mediaqueries: media:{"media query"} oder media:{supported} (= Der Browser unterstützt Mediaqueries)
  • Fenster-Breiten: window:{min-width: x}, window:{max-width: x}
  • Element-Eigenschaften: element:{min-width: x}, element:{max-width: x}, element:{seen} (= wenn das Element via display sichtbar geschaltet ist)
  • Anwesenheit eines Zeigegeräts (Maus, Touch, Stylus): pointer:{available}
  • Bestehende Internetverbindung: connection:{any}

Selbstverständlich lassen sich auch mehrere Bedingungen per »and« oder »or« verknüpfen oder mit »not« ins Negative umkehren:

  1. data-conditions="(connection:{any} and media:{(min-width:30em)} and element:{seen}) or not media:{supported}"

Und nicht zuletzt könntet ihr auch eigene Bedingungskategorien schreiben und im Conditioner nutzen (die sogenannten »Custom Monitors«):

  1. <ul id="header" data-conditions="monitor:{minImageCount:5}">
  2.   <li><img...></li>
  3.   <li><img...></li>
  4.   <li><img...></li>
  5.   <li><img...></li>
  6.   <li><img...></li>
  7. </ul>
  1. (function (win, undefined) {
  2.   var exports = {
  3.        
  4.     // Auf welche Events soll gehorcht werden?
  5.     trigger: {
  6.       'resize': window, // resize Event auf window
  7.       'scroll': window // und scroll Event auf window
  8.     },
  9.    
  10.     // Wenn die Events auftreten, wird der im data-conditions-Attribut
  11.     // angegebene Test ausführt (hier: "minImageCount")
  12.     test:{
  13.       minImageCount: function(data) {
  14.       // data ist ein Objekt mit folgenden Eigenschaften:
  15.       // {
  16.       //   element: [HTML Element, das diesen Monitor trägt],
  17.       //   expected: [im Attribut konfigurierter Wert (hier: 5)]
  18.       // }
  19.       var imageCount = data.element.querySelectorAll('img').length;
  20.       return data.expected <= imageCount; // ergibt "true"
  21.       }
  22.     }
  23.   }
  24. }(this));

Damit wären wir am Ende unseres groben Überblicks über responsive Inhalte. Wir hoffen, dass wir euch für diesen Aspekt des responsiven Entwickelns begeistern konnten und wünschen euch viel Spaß bei allen zukünftigen Experimenten!

Kommentare

andreas
am 18.12.2014 - 10:52

hi, das ist nett und wird auch von vielen browsern unterstützt.

nachteil ist, dass man die breakpoints im css und im js definieren muss. man kann es sich leichter machen und sie nur im css definieren (wo sie hin gehören) und per "before" in die seite schreiben und ausblenden. darauf kann man dann per js zugreifen.

Permanenter Link
Christian Schaefer

Christian Schaefer (Autor)
am 18.12.2014 - 13:37

Ja, das bietet sich in der Tat an. Hätte nur den Rahmen hier gesprengt.
Permanenter Link

alexander farkas
am 19.12.2014 - 12:26

Sehr lustig gerade habe ich gestern ein lazysizes plugin (noch beta status) für die zuerst beschriebene Technik (Nachladen von content) geschrieben (https://github.com/aFarkas/lazysizes/tree/gh-pages/plugins/include).

Ein mögliches Performance Problem durch zu viele Requests wird dort übrigens über eine andere Techniken "gelöst": Typisches lazyloading (erst laden wenn im sichtbaren Bereich), Laden in einer priorisierbaren Warteschlange und Vorladen (ebenfalls in einer Warteschlange) nachdem das document fertig geladen ist.

Permanenter Link
Christian Schaefer

Christian Schaefer (Autor)
am 30.12.2014 - 13:31

Wow, schönes Teil! So Bibliotheken darf es ruhig ein paar mehr geben.
Permanenter Link

Nick
am 23.12.2014 - 20:03

Kann man denn davon ausgehen, dass Smartphone/Tablets/<mobile Geräte> immer JavaScript aktiviert haben? Bin da nicht so im Bilde.

Permanenter Link
Christian Schaefer

Christian Schaefer (Autor)
am 30.12.2014 - 13:33

Davon kann grundsätzlich ausgegangen werden, außer vielleicht bei Opera Mini. Nichtsdestotrotz macht es Sinn, so vorzugehen dass alle wesentlichen Inhalte einer Seite ohne JavaScript zu sehen sind. Auch weil sie dann schneller da sind.
Permanenter Link

Die Kommentare sind geschlossen.