Bernhard Häussner
Tags: Artikel mit dem Tag «Webdev» durchstöbern

My MySQL API onion for PHP

26.07.2009, 17:42
MySQL Onion

MySQL Onion

A huge part of web applications is usually the interaction with the SQL database. This is why I want as little work as possible connecting, escaping values, getting the right tables an so on in PHP. But it should stay simple and allow modular approaches. Therefor I'm using some nested APIs for doing queries easily:

PDO

The very fist thing I am using is PDO. It can handle many RDBMS, but I am most of the times using MySQL or SQLite. By using PDO as an API for the following layers I can make sure most of the code will work for many RDBMSs. PDO even simplifies transactions and prepared statements. Here's some sample PHP code using PDO:

$pdo=new PDO('mysql:host='.$host.';dbname='.$db, $user, $passwort);

$pdo->exec('UPDATE test SET foo="bar" WHERE id=4');

$satement=$pdo->prepare('SELECT * FROM blogeintraege WHERE id=:id');
$satement->bindValue(':id',3,PDO::PARAM_INT);
$satement->execute();
$data=$satement->fetchAll();

PDO Simplifier

The next layer is a class that will hold a MySQL database Connection (a PDO Object) and offer some simple functions for doing e.g. a simple prepared statement. Instead of binding each values manually, you can throw an array in.

It also includes a cache, if you want to run statements more than once. It can append a prefix to all queried tables and checks dynamically inserted tables for validity to avoid SQL-injections and MySQL errors. It is used like that:

$res=$db->sql("SELECT * FROM blogeintraege");
$res=$db->sql(
  "SELECT * FROM #test WHERE id=:id",
  array('id'=>$id),array('id'=>PDO::PARAM_INT),
  array('test'=>'blogeintraege'),
  array('limits'=>array(0,$l),'buffered'=>false)
);

For one array element this does not look too tiny, but the more values are bound, the more useful it gets. And it is very useful if you already have your values in an array, like $_GET.

Note that nearly everything is optional. The table array can contain more tables, for example you can have an array of tables for different languages, if they are in different tables. The bind-types don't need to be specified too. You can even leave out everything except the query as shown in the fist line of code. The Result will by default be returned as a nice array (the GROUP_CONCAT fields are array'ed too) but you can use all other PDO fetch types.

This layer follows a rather functional approve, so I needed another layer for accessing the central sql()-Function in an OOP manner. This should avoid some runtime errors and you can modify the SQL in a modular system.

Statement builder

So I created a wrapper object, that holds a pointer to the database and will construct the parameters for sql(). This comes in handy as more and more optional parameters are added.

The PDO Simplifier has a method to build such statement-objects called sqlO(). This is how the wrapper is used:

$db->sqlO('INSERT INTO blogtaglinks SET ##,type=3')
   ->setSet(array('ID_tag'=>$lasttagid,'ID_entry'=>$id))
   ->exec();
$res=$db->sqlO("SELECT * FROM #test WHERE id=:id")
        ->setData(array('id'=>$id))
        ->setDataTypes(array('id'=>PDO::PARAM_INT))
        ->setTables('test'=>'blogeintraege'),
        ->setLimits(0,$l)
        ->setBuffered(false)
        ->exec();
);

As you can see, it is a little more code, but the code is pretty self-explanatory and now one can build the sets and the other parameters as arrays and then include them easily in the statements.

A bit different: Zend Framework's $db->select() approach

A next step would be to build queries with a single API. This is a feature implemented by the Zend Framework, where you can build your SQL with some API functions and it will even work across various databases:

select = $db->select()
  ->from('blogeintraege',array('id','Titel'))
  ->where('id < ?', $id)
  ->order('id DESC')
  ->limit(0,10);

Well doesn't that look nice?

Kommentare: keine

MacMahonMosaik Online Game

09.07.2009, 18:05
Mac Mohan Mosaik

Mac Mohan Mosaik

Da ich beim Känguru-Wettbewerb 2009 mitgemacht habe, habe ich auch das MacMahonMosaik in die Hände bekommen. Es ist ein kleines Rätsel ähnlich wie Sudoku. Weil es interessant zu Modellieren ist, habe ich das MacMahonMosaik als Online-Spiel erstellt. Hier ein paar Details zu Implementierung:

Modell

Jedes Quadrat im Spiel hat 4 Kanten, die in 3 verschiedenen Farben markiert sein können. Daher lässt sich jede Anordnung als 4-Tupel einer 3-Menge auffassen. Diese 81 Tupel wiederum können den natürlichen Zahlen von 0-80 zugeordnet werden. Somit habe ich als Objekte zunächst die 24 Quadrate (die anderen fallen weg, da sie durch Drehung erreicht werden können), die jeweils eine Zahl speichern. Sie müssen sich nun z.B. drehen können. Dazu wird die Zahl in das Tupel umgerechnet und die Einträge des Tupels um eine Stelle verschoben, wobei der letzte Eintrag der erste wird. Um zu Überprüfen, ob zwei Quadrate aneinander passen, dreht man eines der beiden zweimal und schaut dann einfach ob in der gewünschten Richtung die selben Farben stehen. Dadurch sieht die Prüffunktion im Javascript sehr kurz aus:

p.checkMatch=function(other,pos){
  var mycodes=this.getColorCodes(); // Tupel aus Zahl
  var o=other.clone(); // Neues Quadrat mit selber Zahl
  var othercodes=o.rotate().rotate().getColorCodes(); // anderes Q. 2x drehen
  return (mycodes.charAt(pos)==othercodes.charAt(pos)); //prüfen
}

Zusätzlich haben die Quadrate noch ein paar Darstellungsrelevante optionale Eigenschaften und Methoden. Ist ihnen ein <Canvas>-Element im HTML-Baum delegiert, können sie ihm die passenden Event-Handler für das interaktive Drehen und Bewegen zuweisen und eine graphische Repräsentation ihrer selbst zeichnen. Und sie speichern einige Daten für drag & drop.

Doch beim Ziehen und Ablegen kommt das zweite Objekt ins Spiel: Das Feld in dem die Quadrate abgelegt werden können. Wann immer man ein Quadrat bewegt, frag dieses Quadrat beim Gitter nach, ob es sich einrasten kann. Das Gitter gäbe dann die Pixel-Koordinaten des Einrastpunkts auf dem Bildschirm und die Koordinaten innerhalb des Gitters zurück. Wird das Quadrat dann fallen gelassen, registriert es sich im Gitter.

Das Gitter hat dazu ein zweidimensionales Feld hinterlegt, in dem es Pointer zu den abgelegten Quadraten speichern kann. Wenn jetzt ein Quadrat im Raster abgelegt wird und sich registriert hat, weist es das Raster an, sich zu revalidieren - Das Gitter ruft für jedes Quadrat und je seine vier Nachbarn die Prüffunktion auf. (Da das „Passen“ symmetrisch ist, ließe sich die Komplexität des Algorithmus vielleicht noch optimieren, doch das hätte wieder mehr Code zur Folge und bei 24 Einheiten...) Da das Gitter nur Zeiger zu den Quadraten speichert, ist bei einer Drehung eines Quadrats keine weitere Registrierung nötig. Falls also ein Quadrat angeklickt wird, rotiert es sich, aktualisiert die eigene Darstellung und gibt den Revalidierungsbefehl an das Raster.

Für die Darstellung hat das Gitter auch eine recht einfache Methode, die den Feldhintergrund rötlich färbt, sollte ein Fehler vorliegen und eine Gratulation meldet, sollten alle Quadrate an einem passenden Ort sein, also das Spiel gewonnen.

Ablaufdiagramm

Hier mal ein kleiner Flowchart. Man beginne das Lesen bei den Event-Handlern: (erstellt mit U+2500-U+257F: Box Drawing)

Quadrat
├ Zahl ◁──────┐ {Umrechnung}
├ (Tupel) ◁───┘
├ Drehen() ◀────────┐◁──┐─┐▷─────┐
├ Prüfen(Quadrat) ──◆───┊─┊──────┊──┐
├ [Darstellung]     │   │ │      │  │
│  ├ Visualisieren◁─┊───┊─┘      │  │
│  └ [Handler]      │   │        │  └──┐
│     ├ Klick ──────┊───┘        │     │
│     ├ Packen  ────┊────────────┊──┐  │
│     ├ Bewegen ◀───┊───┐        │  │  │
│     └ Ablegen ────┊─────────┬▷─┤  │  │
└ Kopieren() ►──────┘   │     │  │  │  ▼ bool
                     [XY,XY]  │  │  │  │
Gitter                  │     │  │  │  │
├ Feld[X,Y]             │     │  │  │  │
├ KoordinatenBei(XY) ───┘     │  │  │  │
├ Registrieren(Quadrat)  ◀─XY─┘  │  │  │
│ └ Un-Registrieren() ◀─────XY───┊──┘  │
├ Alle Prüfen()  ►────────┐─▷────│─────┘
└ Darstellung erneuern() ─┘  ◁───┘
  └ Färben(), Gratulation()

Wie das Gitter überprüft, ob auch am Rand rote Kanten liegen? Statt weiteren Code für den Rand zu basteln, wird das Gitter mit voll-roten Quadraten ohne graphische Repräsentation außerhalb das Gitters vorbelegt. Das hat den kleinen Nebeneffekt, das auch out-of-bounds-Fehler im Prüfalgorithmus vermieden werden, da ja z.B. bei negativen Gitterkoordinaten -1 stets ein solches „virtuelles“ Quadrat liegt.

Wie man also sieht, kann man an diesem Spiel schon einige lustige Code-Spar-Methoden benutzen. Eigentlich sparen sie nicht nur Code, sondern auch Nachdenken, wenn man sie „sieht“.

Javascript Umsetzung

JS-Closures habe ich in letzter Zeit immer mehr zu schätzen gelernt. (Man muss in PHP übrigens auch nicht mehr lange darauf verzichten) Sie lassen den Programmierer Funktionen, also mehr oder weniger Code, wie ein String oder eine Zahl an andere Funktionen übergeben oder in Variablen spichern, sodass mit anonymen Funktionsobjekten als Closures in JS eigene Kontrollstrukturen definiert werden können, wie diese zweifache for-Schleife, um über Koordinaten bzw. Zweidimensionales zu loopen:

function loopXY(w,h,cb) {
  for (var x=0;x<w;x++) {
    for (var y=0;y<h;y++) {
      cb(x,y);
    }
  }
}
var m=[];
loopXY(5,7,function(x,y){
  m.push("("+x+","+y+")");
});
alert(m);

Wie man sieht stehen in Javascript in den Closures alle äußeren Variablen zur Verfügung. Das wiederholte verwenden des Namens einer äußeren Variable führt trozdem noch dazu, dass in der Closure diese lokal verwendet wird und außen der alte Wert erhalten bleibt. Ein bischen gewöhnungsbedürftig ist die Verwendung von this in JS. Im oben gezeigten Code würde z.B. in der Schleife this auf window zeigen. Das lässt sich mit der Function.call oder der Function.apply-Funktion kompensieren, wie in diesen Code-Schnippseln für Event-Handler.

Für die Erstellung von Objekten (außer reinen Datenspeichern, die mit {} einfach erstellt werden können) verwende ich folgende Grundstruktur:

function Square(id) { // constructor
	this.id=id; // Eigenschaften initialisieren
}
(function(){
  var p=Square.prototype; // shortcut zum Prototype
  p.setId=function(id) { // einfacher Setter
    this.id=id;
    return this; // macht Setter chainable
  };
  var privat; // kann nur von den Objektmethoden gesehen werden
  function privatauch() {} // dito
})();
var s=new Square (3);

Allerdings bin ich mir nicht sicher, ob das wirklich besser ist, als alles in Konstruktoren zu packen.

Der Code des Onlinegames ist vielleicht nicht ganz so interessant zu lesen, wie es war ihn zu erstellen, aber ich denke, hinein schauen lohnt sich. Ich bin natürlich immer interessiert an weiteren Javascript-Techniken und Tipps zur Code-Gestaltung.

Kommentare: keine

MySQL-Queries sparen mit GROUP_CONCAT

22.06.2009, 20:10
Ein MySQL-Query mit GROUP_CONCAT

Ein MySQL-Query mit GROUP_CONCAT

Fast jede (Web-)Anwendung, die eine Datenbank wie MySQL benutzt, muss irgendwie das Problem lösen, zu einem Artikel mehrere Tags, zu einem Eintrag mehrere Kommentare, zu einem Wort mehrere Synonyme zu finden, die sich in einer weiteren Tabelle befinden. Jedes Mal, wenn mehrere Zeilen Abgefragt werden sollten, die wieder mehrere untergeordnete Zeilen haben, bedeutet das meistens für jedes Element einen weiteren Query zu starten, oder eine zweite Abfrage mit einer weiteren Liste zu starten, oder eine riesige Reihe Datensätze abzufragen und der Anwendung das Gruppieren zu überlassen. Mit GROUP_CONCAT (Dokumentation) lässt sich in MySQL (fast) alles in ein SQL-Query zusammenfassen.

Einführung

Nehmen wir eine einfache Datenbankstuktur an, wie die eines Blogs. Es gibt blogeintraege mit einem Titel, blogtags mit einem Namen. Dann gibt es noch eine Tabelle, die beides verbindet, z.B. blogtaglinks. Jeder Datensatz hat natürlich eine eindeutige ID. Wenn man jetzt alle Blogeinträge auflisten will, könnte man das wie folgt machen:

SELECT Titel FROM blogeintraege

Um die Anzahl der Verbundenen Tags herauszufinden, wäre ein solcher Join üblich:

SELECT Titel, COUNT(distinct(t.ID))
FROM blogeintraege b, blogtags t, blogtaglinks l
WHERE b.ID = l.ID_entry and t.ID = l.ID_tag
GROUP BY b.ID
+-------------------------+-----------------------+
| Titel                   | COUNT(distinct(t.ID)) |
+-------------------------+-----------------------+
| Hello World!            |                     2 |
| The CMS                 |                     2 |
| To be Released Later... |                     1 |
+-------------------------+-----------------------+
3 rows in set (0.01 sec)

Hier werden jeweils alle Spalten, die den selben Blogeintrag repräsentieren gruppiert und dann aus diesen Gruppen die Anzahl der verschiedenen Tags abgefragt. Dabei ist COUNT die Gruppenfunktion, die die nun versteckten, „weg-gruppierten“, Datensätze verarbeitet - hier zählt.

Die GROUP_CONCAT-Funktion

MySQL und andere RDBMSs kennen für Zahlen einige Funktionen, die Summe, Durchschnitt etc. gruppierter Zahlen berechnen können. MySQL kennt darüber hinaus die GROUP_CONCAT()-Funktion, welche die „versteckten“ Werte (also auch CHAR) in einer Spalte zusammen zu hängen. Mit dieser Funktion wäre es also möglich, die Tags direkt aufzulisten:

SELECT Titel, GROUP_CONCAT(distinct(t.name))
FROM blogeintraege b, blogtags t, blogtaglinks l
WHERE b.ID = l.ID_entry and t.ID = l.ID_tag
GROUP BY b.ID
+-------------------------+---------------------------------+
| Titel                   | GROUP_CONCAT(distinct(t.name))  |
+-------------------------+---------------------------------+
| Hello World!            | Projekt: Mein Blog,Persönliches |
| The CMS                 | PHP,Projekt: Mein Blog          |
| To be Released Later... | Projekt: Mein Blog              |
+-------------------------+---------------------------------+
3 rows in set (0.00 sec)

Jetzt wird anstatt der Anzahl der Tags eine Komma-getrennte Liste der Tags angezeigt. Das ist insofern ein recht großer Fortschritt, da wir nicht nochmal eine zweite Abfrage machen müssen, die uns jeweils die Namen der Tags heraus sucht.

Weiter Ausbauen

Jetzt wird es Zeit ein bisschen zu Optimieren. In diesem Beispiel macht es wenig Sinn, aber theoretisch könnten zwei Tags den gleichen Namen haben. In diesem Fall würde uns MySQL aber den Namen nur einmal anzeigen, da distinct() doppelte Werte aussortiert. Wir würden also weniger Tags bekommen, als ein COUNT(distinct(t.ID)) anzeigt. Das können wir lösen, indem wir die distinct()-Funktion auf die IDs der Tags anwenden:

SELECT Titel, GROUP_CONCAT(distinct(t.ID),'-', t.name)
FROM blogeintraege b, blogtags t, blogtaglinks l
WHERE b.ID = l.ID_entry and t.ID = l.ID_tag
GROUP BY b.ID

Das würde uns für jeden Blogeintrag eine Liste erstellen, wie 42-Webbrowser,28-Productivity,27-Design. Die eigentlich überflüssigen IDs werden später noch entfernt, z.B. mit PHP.

Wenn wir zusätzlich noch die Erstellungsdaten z.B. der letzten drei Kommentare abfragen wollen, zeigt sich, warum wir distinct() verwenden müssen: Sonst wird jeder Tag so oft angezeigt, je nach dem, wie viele Kommentare ein Eintrag hat. Wenn wir also noch eine Tabelle benutzen, wie blogkommentare mit creation_time, und uns jeweils die letzten Erstellungszeiten anzeigen lassen wollen, benutzen wir:

SELECT Titel, GROUP_CONCAT(distinct(t.ID),'-', t.name), 
GROUP_CONCAT(distinct(c.ID),'-',c.creation_time ORDER BY creation_time DESC)
FROM blogeintraege b, blogtags t, blogtaglinks l, blogkommentare c
WHERE b.ID = l.ID_entry and t.ID = l.ID_tag and c.entry_id = b.id
GROUP BY b.ID

Jetzt haben wir noch eine weitere Spalte mit einem Wert, wie 34-2008-10-20 18:28:41,33-2008-10-19 23:22:37. Doch je mehr Spalten wir abfragen, desto unübersichtlicher werden die Listen. Außerdem hängt ja immer noch die ungewollte, aber doch notwendige id davor.

Verarbeitung mit PHP

Diese minderen kosmetischen Makel lassen sich dann in der Anwendung noch beheben. Hier zeige ich, wie ich mit PHP die Listen in Arrays lade und sortiere:

function arrayifyGrpConcats($q,$resultset) {
  $regexp='/\\bgroup_concat\\((.*?) separator \'(?<sep>.*?)\'\\) +as +(?<name>\w+)\\b/i';
  if ( preg_match_all($regexp,$q,$matches,PREG_SET_ORDER) ) {
    foreach ($matches as $grpCoClause) {
      if (isset($grpCoClause["name"],$grpCoClause["sep"])) {
        $name=$grpCoClause["name"];
        if (strpos($name,'_',1)===false) {
          $foldername=$name;
          $subname=false;
        } else {
          $foldername=substr($name,0,strpos($name,'_',1));
          $subname=substr($name,strpos($name,'_',1)+1);
        }
        $s=$grpCoClause["sep"];
        foreach ($resultset as $id=>$row) {
          $val=$row[$name];
          unset($resultset[$id][$name]);
          if ($subname) {
            if (!isset($resultset[$id][$foldername])) {
              $resultset[$id][$foldername]=array();
            }
            $vals=explode($s,$val);
            for ($i=0, $len=count($vals);$i<$len;$i++) {
              if (!isset($resultset[$id][$foldername][$i])) {
                $resultset[$id][$foldername][$i]=array();
              }
              $resultset[$id][$foldername][$i][$subname]=preg_replace('/\\d+-/','',$vals[$i],1);
            }
          } else {
             $resultset[$id][$foldername]=explode($s,$val);
          }
        }
      }
    }
  }
  return $resultset;
}

Diese Funktion kann direkt nach der Abfrage auf die Ergebnisse angewendet werden. Dazu übergibt man ihr das Result als durchnummeriertes Array mit je einem assoziativen Unterarray (Dictionary) pro Datensatz, der als Schlüssel die Spaltennamen und als Werte den jeweiligen Spaltenwert enthält. Die Funktion sucht dann im Query nach GROUP_CONCAT-Aufrufen der From group_concat(... separator ',') as iwas. Das Trennsymbol wird explizit angegeben, damit es hier keine Missverständnisse gibt.

Man sollte bedenken, dass MySQL mit dem Separator im Wert nichts macht. Wie man im SQL sieht habe ich das Problem mit dem Trennzeichen im Wert „gelöst“, indem ich es aus dem Kommentartext einfach gelöscht habe. Eine ausgefeiltere Methode habe ich bei stackoverflow gefunden.

Als Alias nach as kann man fast alles angeben. Die Funktion wird diesen Namen als Spaltennamen im Result-Array verwenden.

Als kleines Extra habe ich noch eingebaut, dass die Funktion optional mehrere Spalten zusammen nimmt, wenn sie ein Alias der Form gruppe_name erkennt: Sie wird dann im Resultset-Array unter gruppe ein nummeriertes Array anlegen, das ein assoziatives Arrays enthält mit den verschieden name. Wenn wir also zu einem Kommentar nicht nur das Datum, sondern auch den Autor wissen wollen, könnten wir group_by... as comm_time, group_by... as comm_name verwenden, um die Daten für jeden Kommentar zusammen zu halten.

Im Endeffekt kann das dann so aussehen:

SELECT b.id, b.Titel AS Titel,
group_concat(DISTINCT t.ID,'-', t.name separator ', ') as tags_Name, 
group_concat(DISTINCT t.id separator ', ') as tags_id, 
group_concat(DISTINCT c.ID,'-',c.creation_time separator ', ') as comm_time, 
group_concat(DISTINCT c.ID,'-',REPLACE(LEFT(c.Text,20),',','') 
     separator ', ') as comm_text 
FROM blogeintraege b, blogtaglinks l, blogtags t, blogkommentare c
WHERE b.ID = l.ID_entry and t.ID = l.ID_tag and c.entry_id=b.id
GROUP BY b.ID LIMIT 1,2

Was uns dann dieses Ergebnis liefert: (print_r)

Array
(
    [0] => Array
        (
            [id] => 2
            [Titel] => The CMS
            [tags] => Array
                (
                    [0] => Array
                        (
                            [Name] => PHP
                            [id] => 3
                        )

                    [1] => Array
                        (
                            [Name] => Projekt: Mein Blog
                            [id] => 2
                        )

                )

            [comm] => Array
                (
                    [0] => Array
                        (
                            [time] => 2008-10-19 23:22:37
                            [name] => Bernhard H.
                            [text] => Also das Kommentarsy
                        )

                    [1] => Array
                        (
                            [time] => 2008-10-20 18:28:41
                            [name] => Bernhard H.
                            [text] => So ich hab es jetz 
                        )

                )

        )

    [1] => Array
        (
            [id] => 10
            [Titel] => To be Released Later...
            [tags] => Array
                (
                    [0] => Array
                        (
                            [Name] => Projekt: Mein Blog
                            [id] => 2
                        )

                )

            [comm] => Array
                (
                    [0] => Array
                        (
                            [time] => 2008-10-20 19:48:50
                            [name] => Bernhard H.
                            [text] => Cool das klappt auc
                        )

                )

        )

)

Ich hoffe mit dieser Methode in Zukunft Datenbankabfragen zu vermindern und damit die Geschwindigkeit der Anwendungen weiter zu erhöhen. Zwei kleine Sachen noch:

SELECT @@group_concat_max_len

Dieser Wert ist die maximale Länger der mit GROUP_CONCAT gebildeten Spaltenwerte und lieget meist bei 1024. Außerdem kann es nötig sein den Spaltenwerte von BLOB zu konvertieren: CAST(... as CHAR).

Kommentare: keine

Ein paar Code-Schnipsel

15.06.2009, 19:30
Javascript

Javascript

In diesem Post nur ein paar kleine Code-Auszüge und Techniken für Webanwendungen. Inhalt:

  • Data URI scheme für Favicons & CSS-Farbverläufe
  • HTML-Attribut contenteditable zum einfachen Editieren
  • Javascript: Globale Variablen einfach vermeiden
  • JSON in Script-Tags
  • JSON-Parser bei Bedarf laden
  • jQuery nach hinten verlegen
  • jQuery mouserest-Plugin

Data URI scheme für Favicons & CSS-Farbverläufe

Eine mir bisher unbekannte Funkion moderner Browser (ab IE8) sind data: URLs. Anders als normale http:// oder file:// URLs repräsentieren sie keinen Link, sondern die Daten sind komplett in der URL enthalten. Damit man auch Binärdaten verwenden kann (oder sich das escapen von HTML-Tags, „"“ etc. sparen), ist es möglich optional mit Base64 zu encodieren. Für ein Favicon sieht das dann so aus:

<link rel="shortcut icon" href="data:image/png;base64,iVBORw[...]RK5CYII="/>

Und für eine Graphik im CSS:

body {background:url(data:image/png;base64,iVBORw[...]ElFTkSuQmCC);}

Das lohnt sich bei kleinen Farbverläufen oder Symbolen, da ein zusätzliches Request zum Server mehr Bytes brauchen würde, als die Base64-Codierung produziert. Da bei base64 jeweils 3 Bytes zu 4 werden, sieht die Rechnung für die maximale PNG-Größe so aus:

PNG*(4/3) + 55 B = HTTP-HADER
mit HTTP-HADER:=600B PNG=1635 B

Da man im CSS mittels Sprites sowieso Requests sparen kann und durch Kombination größere PNGs bekommt, lohnt es sich hier also nur bei kleinen Projekten. (Oder bei riesigen Cookies, was die Request-Größe in die Höhe treibt. Da würde ich dann aber eine static-Subdomaine empfehlen. )

Als weiteren Vorteil kann man eine ganze Seite in eine HTML-Datei packen, jedoch kann man auch die Bilder nicht mehr so leicht bearbeiten.

HTML-Attribut contenteditable zum einfachen Editieren

Auch ein spaßiges Feature sind editierbare HTML-Elemente. Man kann das HTML-Attribut contenteditable für einzelne HTML-Elemente setzten oder gleich für die ganze Seite:

<body contenteditable="true">

Das Attribut ist zwar erst mit HTML5 valid, wird aber schon von einigen älteren Browsern unterstützt.

Javascript: Globale Variablen einfach vermeiden

Mit wachsender Zahl an Javascript-Bibliotheken wird die Wahrscheinlichkeit der Kollision globaler Variablen immer größer. Zwar ist es unwahrscheinlich, dass man z.B. $ überschreibt, aber manche Javascript Frameworks haben sich ja sogar zueignen gemacht, die Prototypen der Sprachfunktionen zu erweitern, was natürlich beim Einsatz eigener/anderer Prototypen-Erweiterungen nicht-überschaubare Probleme erzeugen kann. Das lässt sich aber vermeiden, indem man eine Art jQuery für andere Aufgaben erzeugt. Dazu kann man z.B. die ersten ~50 Zeilen von jQuery grob kopieren1, einen eigenen Framework-Namen einsetzen und dann eigene Funkionen einbauen, was man dann z.B. so verwenden kann:

var foo=Array(1,2,3,4);
alert(myFW(foo).contains(4));

Hier benutzt man dass die Funktion myFW einen Weiteren Konstruktor aus sich selbst aufrufen kann.

Es gilt zusätzlich globale Variablen zu vermeiden. Doch man kann für einzelne Komponenten Kürzel als Arrays verwenden, die dann die Globalen speichern. So kann man auch im DOM-Explorer von Firebug einen Überblick über globale Variablen behalten und die Komponenten können kommunizieren.

1 Ich vermute, wer den ganzen jQuery-Code aufmerksam gelesen hat, kann mehr als man in allen Javascript Büchern zusammen lernt.

Um den Speicherbedarf nicht unnötig in die Höhe zu treiben kann man die Variablen-Sichtbarkeit immer recht klein halten, damit die GC die Daten wieder fressen kann, wenn sie nicht mehr gebraucht werden. In JS sind übrigens auch Funktionen nicht global und löschbar. Funktionen, die nur beim Initialisieren gebraucht werden, kann man also mit delete funktionsName; wieder aus dem Speicher nehmen.

Ähnlich der main()-Funktion in anderen Programmiersprachen kann man in Javascript den Code zusätzlich in eine gleich aufgerufene, anonyme Funktion hüllen:

(function(){
  var nichtglobal;
  // code
})();

So kann man ganz normal „globale“ Variablen und Funktionen verwenden, aber ein anderes Script bemerkt davon nichts.

JSON in Script-Tags

Wenn man für Javascript Daten übertragen will, dann bietet sich JSON an, da es einfach Daten austauschen kann und in vielen Programmiersprachen implementiert ist. Da gibt es die eine Methode, JSONP, welche die Daten in einer Variable speichert, bzw. an ein Callback übergibt. Alternativ kann man auch die Daten in <script type="text/x-json" id="dafaultData">-Tags im DOM platzieren und dann mit Javascript auslesen und parsen

JSON-Parser bei Bedarf laden

Da moderne Browser jetzt native JSON-Parser bekommen, muss man den JSON-Parser nicht immer laden. Das könnte man so lösen:

<script>
  if(!this.JSON||typeof JSON.stringify !== 'function'||typeof JSON.parse !== 'function'){
    document.write("<scr"+"ipt type=\"text/javascript\" src=\"json2.js\"></scr"+"ipt>");
  }
</script>

jQuery nach hinten verlegen

Ganz praktisch hat es sich erwiesen, das Javascript nach dem meisten Markup zu laden. So wird durch CSS und HTML schonmal die Seite aufgebaut und dann erst werden die Javascript-Verbesserungen geladen.

Viele Webanwendungen, z.B. Google Mail, zeigen zunächst einen Ladebildschirm. Dadurch fühlt sich die Anwendung gleich viel langsamer an. Schneller wird die Anwendung wenn zunächst der grobe Aufbau steht, mit HTML und CSS zuerst geladen, dann ein kleines Skript erste Programmteile implementiert, sodass die Optik fertig erscheint. Dann läd man jQuery und dann erst weitere Javascripts (die auf jQuery basieren). So kann sich die Seite blitzschnell aufbauen, ohne dass erst jQuery geladen werden muss.

jQuery mouserest-Plugin

Nicht wirklich ein „Plugin“ aber eine kleine Hilfsfunktion. Sie führt ein Callback aus, nachdem die Maus einige Zeit auf dem Element war:

jQuery(function(){ // plugins
  jQuery.fn.mouserest=function (callback,time) {
    jQuery(this).mouseenter(function(e){
      this.dsthTo=setTimeout(function (obj){callback.apply(obj)},time,this);
    }).mouseleave(function(){
      clearTimeout(this.dsthTo);
    });
  }
});

Das ist ganz praktisch, denn wenn man die Zeit (in ms) auf 200 setzt, lässt sich verhindern, dass bestimmte Effekte beim bloßen Überfahren auftreten. Sondern der Event wird erst ausgelößt, wenn die Maus eine kurze Zeit auf dem Element war. Im callback kann man übrigens durch das apply() mit this auf das DOM-Element zugreifen, wie es in jQuery üblich ist.

Kommentare: 2 Einträge

jQuery Shortcuts (hoverfade Plugin)

01.06.2009, 15:53
jQuery hoverfade Plugin

jQuery hoverfade Plugin

Als ich zum Ferienbeginn ein paar Sachen an meinem Blog gerichtet habe, habe ich zum ersten mal die Plugin-Funktion von jQuery (aktiv) benutzt. Es gibt bestimmte Javascript-Effekte, die man immer wieder benutzt. In jQuery sind einige davon schon Enthalten. Doch man baut dann im Endeffekt doch immer einige jQuery-Funktionen zusammen. Ich z.B. „verstecke“ gerne Dinge auf Webseiten, indem ich sie leicht transparent mache und dann bei Mouseover wieder voll anzeigen lasse. Deshalb habe ich dafür ein jQuery „Plugin“ gebastelt:

jQuery.fn.hoverfade = function(hvspeed,hvopacity,uhspeed,uhopacity) {
  return this.each(function(){
    jQuery(this).css({
      opacity:uhopacity
    }).hover(function(){ 
      jQuery(this).stop().fadeTo(hvspeed,hvopacity);
    },function(){ 
      jQuery(this).stop().fadeTo(uhspeed,uhopacity);
    });
  });
};

Man kann es natürlich kaum Plugin nennen, sondern eher ein kleiner Shortcut, sodass man nicht immer schreiben muss:

$("selector").css({
   opacity:0.3
}).hover(function(){ 
   $(this).fadeTo("fast", 1);
},function(){ 
   $(this).fadeTo("slow", 0.3);
});

Sondern nur noch folgendes braucht:

$("selector").hoverfade("fast",1,"slow",0.3);

Wie man sieht ist das Erstellen von solch kleinen Plugins recht einfach: man hängt einfach in das jQuery.fn-Objekt die Funktion ein, die dann den häufig verwendeten Code enthält, und schon kann man sie als Makro auf alle jQuery-Elemente anwenden. Natürlich kann man auch Parameter an die Funktion übergeben, um eine gewisse Konfiguration zuzulassen.

Außerdem empfiehlt es sich in Plugins statt dem kurzen $ das explizite jQuery zu verwenden, damit man das Plugin auch verwenden kann, wenn $ z.B. ein anderes JS-Frameworks ist. In der Plugin-Funktion ist this das jQuery-Objekt auf das das Plugin angewendet wurde. Damit der Code immer klappt, sollte man noch mit each über die enthaltenten Elemente loopen. Im each-Closure zeigt this dann allerdings auf das DOM-Element, weshalb man es (meistens) mit $(this) wieder zu einem jQuery-Objekt machen muss. Damit das Plugin auch wirklich immer läuft, sollte man es vielleicht auch noch von JSLint checken lassen.

OT: andere Änderungen an der Seite

Ich habe übrigens neben den Icons für die Social Networks (Damit die Blogeinträge weiter oben stehen) auch noch ein paar andere Sachen verändert: Die automatischen ids, die nur die <input> mit den <label> verbinden, habe ich verkürzt, damit sie nicht immer so hässlich im Markup stehen. Dann habe ich im CSS alle Schriftgrößen auf Pixel umgestellt. Ich hoffe jetzt natürlich, dass meine Seite weiterhin in allen Browsern (fast) gleich aussieht.

Meine Editor-Toolbar wird auch immer voller. Jetzt habe ich mir noch Buttons dazu gemacht, um encodeierte <> und [] einzufügen. Außerdem habe ich endlich (halb) vimeo-Support im CMS. Ist ja grausam immer diesen ganzen <embed>-Kram erstmal in valides HTML umzuschreiben und dann hat man immer sehr viel HTML-Code zwischen dem Text. Außerdem kann ich so jetzt z.B. ganz einfach mal die Schriftfarbe von allen eingebetteten vimeo-Videos ändern.

Eigentlich sollte es ja so sein, dass im Content der Blogposts nichts ist, das die Darstellung beeinflusst, doch man braucht bei eingebetteten Videos offensichtlich immer Breite (ist fest) und Höhe (leider nicht fest), weshalb ich dann habe: [[vimeo:4767190|338|]]. Um alles 100%ig zu machen müsste ich natürlich aus der oEmbed API die genauen Video-Daten holen, aber das ist dann fast doch ein bisschen zu viel Aufwand. Was mich allerdings wundert: Während diese seltsame embed-Box auf der vimeo-Seite nicht-valides HTML ausspuckt, gibt die API, z.B. http://vimeo.com/api/oembed.xml?url=http%3A//vimeo.com/4767190&width=540 schönes valides HTML XHTML heraus.

Kommentare: keine
 
Χρόνογραφ
© 2008-2017 by Bernhard Häussner - Impressum - Login
Kurz-Link zur Homepage: http://1.co.de/