Zum Inhalt springen

JavaScript im Frontend richtig einbinden

Erstellt am 24. Juni 2008 · 18 Antworten · letzte Antwort am 6. Januar 2009

Tags: Frage

Enlightning Man ·

Stichworte: (eigenes und /contrib) JavaScript, Frontend, JSmin Kompression, Prototype, Ajax, Temporäre Dateien

Hallo zusammen,
ich bin grad dabei einige eigene Plugins zu schreiben und auf die Frage gestoßen, was wohl eine "best practice" ist, um JavaScript automatisch komprimiert im Frontend zu benutzen.

Meinen Lösungsansatz hab ich hier mal beschrieben und wäre dankbar für Kommentare, ob dies einer sinnvollen Herangehensweise entspricht, oder ob ich dasselbe vielleicht mit 2 Zeilen TS lösen könnte. Weiß man ja nie 😉

Szenario:
Ich möchte einen Username-Suchfeld-Plugin haben, der beim eintippen ähnliche Nutzernamen vorschlägt, die man dann auswählen kann um zu deren Profilseiten zu navigieren. (Beispiel in meiner Joomla-Implementation: User Suche).

Herangehensweise:
Da mir der xajax Plugin nicht performant genug ist habe ich mich für die eID-Methode entschieden (in eigenen Tests hat xajax 5 mal so lange gedauert, einen einfachen String zurückzugeben (1s vs 200ms)).

Der eigentliche Ajax Request soll über die in Typo3 ja bereits enthaltene Prototype Library getätigt werden mit relativ wenig zusätzlichen client-seitigem code.

Frage:
Die Prototype Library ist derzeit 124kb groß. Komprimiert man sie mit JSmin, welches ja ebenfalls in Typo3 enthalten ist, sind es nurnoch 92kb. Wie ist nun der richtige Weg, um die Typo3 internen libraries aus dem Frontend heraus zu benutzen?

Eigentlich ist die Javascript Minimierung (minify) seit Typo4.2 default mäßig aktiviert. Jedoch merkt man davon im Frontend nichts. Das Backend hat einfache Methoden, die einem zum Beispiel das Laden der System JS-libraries ermöglichen und ich denke, dass die dann auch automatisch den JSmin minify Algorithmus durchlaufen. Auf Frontend Seite wird man jedoch (fast) allein gelassen.

Lösungsvorschlag für eigenen Code:
Die Methode t3lib_div::minifyJavascript() bietet Zugriff auf die JSmin library und kann sehr gut benutzt werden, um komprimierten JS code in der HTML Seite auszuliefern. Der Plugin Code kann dabei so aussehen:

$script = ' 
  function myRequest() {
    var url = "index.php";
    var par1 = "jim";
    var pars = "eID=usersearch&name="+par1;
    var myAjax = new Ajax.Request(url, {method: "get", parameters: pars, onComplete: myResponse});
  }
  
  function myResponse(orgRequest) {
    //wenn Antwort XML: var xmldoc = orgRequest.responseXML;
    var responseText = orgRequest.responseText;
    document.getElementById("some_id").innerHTML = responseText;
  }';
		
//minify using JSmin
$script = t3lib_div::minifyJavaScript($script);
//add to page
$GLOBALS['TSFE']->setJS($this->extKey,$script);

Resultat ist, dass die HTML Seite am Ende schön minimalisierten JavaScript Code enthält.

Nachteile:
- Der Code muss jedes Mal durch JSmin geschickt werden wenn die Seite generiert wird.
- Es geht nur für Code der direkt vorliegt und nicht in externen JS-Dateien steckt, zum Beispiel in eigenen aufwändigeren Skripten, bzw. in 3rd-Party Frameworks wie Prototype.

Effizienter Lösungsvorschlag für komplexere Setups:
Es wäre schön, den JavaScript code, nicht jedes Mal komprimieren zu müssen, sondern die komprimierte Version, vorzugsweise natürlich automatisch, abzuspeichern und einzubinden.

Typo3 bietet dazu folgende interessante Möglichkeit: inline2TempFile

Ersetzt man im obigen Beispiel die unteren Zeilen durch:

//minify using JSmin
$script = t3lib_div::minifyJavaScript($script, $error);
//generate tmp file from inline string
$inlineTmpfile = TSpagegen::inline2TempFile($script,'js');
//add to site header	
$GLOBALS['TSFE']->additionalHeaderData['sup_games'] = $inlineTmpfile;

Die inline2TempFile Methode funktioniert so, dass ein md5 Hash des Strings gebildet wird, der in einer temporären Datei abgelegt werden soll (funktioniert für Javascript und CSS Dateien) und aus diesem wird ein entsprechender Dateiname generiert. Sollte diese Datei bereits im typo3temp Ordner existieren, wird eine in die entsprechenden script oder link tags gewrappte Referenz auf diese Datei zurückgegeben. Bsp:

<script type="text/javascript" src="typo3temp/javascript_26d6cd95e6.js"></script>

Dieser kann natürlich einfach als additionalHeaderData hinzugefügt werden.

Um nun aber eine externe Datei so abzuspeichern würde man nichts gewinnen, da der md5 Hash ja auf dem bereits minify'ten String gebildet wird um die temporäre Datei zu speichern. Man müsste also beispielsweise jedes Mal, die Prototype library öffnen, lesen und minifizieren um zu sehen, dass man es schonmal gemacht hat. --> Blöd. Insbesondere deswegen, weil JSmin für eine komplexe Datei wie die prototype.js schon ein paar Sekunden benötigen kann.

Besser: Man schreibt eine abgewandelte inline2TempFile und benutzt diese:

function main($content,$conf)	{
  //...
  $minProt = $this->minifyFile(PATH_typo3.'contrib/prototype/prototype.js');
  $GLOBALS['TSFE']->additionalHeaderData['sup_games'] = $minProt;
  return $this->pi_wrapInBaseClass($content);
}

function minifyFile($file, &$error='') {
  //generate md5 hash based on filename not content
  $hashedName = 'typo3temp/javascript_'.substr(md5($file),0,10).'.js';
  $output = '<script type="text/javascript" src="'.htmlspecialchars($GLOBALS['TSFE']-">absRefPrefix.$hashedName).'"></script>';';
  // Write file if not exists:
  if (!@is_file(PATH_site.$hashedName))	{
    //Einlesen der Originalversion:
    $str = file_get_contents($file);
    //minify
    $str = t3lib_div::minifyJavaScript($str, $error);
    t3lib_div::writeFile(PATH_site.$hashedName,$str);
  }
  return $output;
}

Mit dieser Methode, wird der md5-Hash auf den Dateinamen der Javascript Datei gebildet anstatt auf den Inhalt. Es ist somit möglich, JS-code einfach in JS-Dateien auszulagern, die bei Generierung der Seite automatisch minifiziert werden, dies jedoch nur, wenn es nicht bereits geschehen sein sollte.

Vorteil: Gute Methode, um beliebige JS-Script Dateien automatisch und bei Bedarf zu minifizieren und so auszuliefern.

Nachteil: Es ist abhängig vom Dateinamen. Sollte sich also der Inhalt der Datei ändern (Development-Setup / Typo3 Update), so muss der Inhalt des Temp Ordners natürlich gelöscht werden, damit die Datei neu erstellt wird.

Kommentare? 🙂

Grüße,
EM

mklappstuhl ·

Hi,
ich sitz grad vor dem selben Problem, bin aber momentan weniger dabei etwas zu programmieren (zwecks mangelnder Kenntnisse).
Das was du oben beschrieben hast macht ja auch http://code.google.com/p/minify/. Davon ausgegangen werde ich wohl demnächst einfach die Dateien erstellen (werden beim o.g. Skript gespeichert) und den Pfad im Template-TS ersetzen.
Deine genannte Lösung wär aber natürlich um einiges besser (vorrausgesetzt, man findet eine Lösung um die Datei einmal 'on-the-fly' zu generieren und dann immer wieder zu verwenden).
Das wäre wirklich ein schönes Feature für das gesamte System.

Grüße ,
Martin

Edit: weiterer Vorteil von Minify ist, dass man beliebig viele JS Dateien in eine Packen kann und somit die HTTP-Requests senkt.
(Funktioniert laut minify wiki alles auch mit .css)

Enlightning Man ·

Hi,
sieht wie ein interessantes Projekt aus. Nach 2 Minuten angucken scheint es für meinen Geschmack aber etwas "sehr" automatisch. Müsste man mal ausprobieren wie man es mit Typo verbinden kann.

Habe aber selbst auch schon drüber nachgedacht, das Script oben so zu erweitern, dass man mehrere JS Dateien in eine einzige minimieren kann. Das sollte eigentlich funktionieren.

Für CSS wäre natürlich eine gute Erweiterung nur ist das ja meist schon eine Template Sache und müsste daher vermutlich mit Hilfe von TypoScript komprimiert werden.

Wäre aber sehr interessant, wenn es mal jemand mit Typo ausprobiert und seine Erfahrungen postet.

Grüße,
EM

Enlightning Man ·

Klar, die gibts, aber wenn man über

$GLOBALS['TSFE']->additionalHeaderData[$this->extKey] = '<script ....';

eigenes JS in einer eigenen Extension einbindet wird es nicht 'geminified'.

Vielleicht ist das auch der falsche Weg, aber mein Initialpost oben hat eigentlich genau das gefragt:

Geht es irgendwie automatisch aus der eigenen Extension heraus?

steffenk ·

am besten Du schaust Dir mal die tslib_pagegen an, dann verstehst Du besser wie was eingefügt wird.
InlineJS:
$GLOBALS['TSFE']->inlineJS[] = '...'
Scripts:
$GLOBALS['TSFE']->JSCode oder
$GLOBALS['TSFE']->additionalHeaderData

letzteres wird nicht minified, aber es ist ja an Dir bereits minified scripte in der extension abzulegen und die zu includen.

Enlightning Man ·

Ah cool, werd ich mir mal anschaun.

Ich finds immer wieder ein bisschen schwierig, solche Informatinen zu finden leider...

Also Danke für den Hinweis 🙂

mklappstuhl ·

Hallo,
also ich habe config.minifyJS = 1 grad mal ausprobiert und funktioniert jetzt nicht. Allerdings wird Attribut (wie man es auch nennen mag) vom t3editor nicht gehighlighted 😉
Ich vermute mal, wenn es funktionieren würde die Datei nur 'minified' würde.
Allerdings hätte man dann immer noch 2 Dateien (HTTP-Requests) und gzip nicht verwendet (was in den meisten Fällen Sinn macht), seh ich das richtig?
Ich möchte Niemandem Etwas vorwerfen aber wieso ist eine Solche, doch meist von positiven Auswirkungen begleitete Funktion nicht im Core enthalten?
(Könnte ja bei Bedarf per TS deaktiviert werden.) 😉

Grüße
Martin

Enlightning Man ·

Hi,

ich habs auch grad mal ausprobiert und muss sagen, dass ich es auch nicht hinbekommen habe.

@steffenk's post:

$GLOBALS['TSFE']->JSCode ist markiert als deprecated und man soll $GLOBALS['TSFE']->additionalJavaScript verwenden.

Mir ist es nicht gelungen, über $GLOBALS['TSFE']->inlineJS etwas zu produzieren, was später im Dokument auftaucht. Habe diese drei Varianten ausprobiert:

$GLOBALS['TSFE']->inlineJS[$this->extKey] = '
		
		/**
		 * Hier gibts viel Kommentare */
		function test() {
		
			/* Ich bin Kommentar */
			alert(1);
			
			
			
		}
		
		';
//dasselbe mit:
$GLOBALS['TSFE']->inlineJS[] = ...
$GLOBALS['TSFE']->inlineJS = ...

Wenn man obigen String in

$GLOBALS['TSFE']->additionalJavaScript[$this->extKey] = ...

einfügt, taucht es in script tags gewrapped im HEAD auf. Ist aber weit entfernt von einer minified Version... (Ich habe per Debugger überprüft, dass der Flag richtig gesetzt ist...)

Hab dann versucht rauszufinden, was das Problem ist und ein durchsuchen der kompletten t3lib und typo3 Ordner hat lediglich folgende sinnvolle Stelle in tslib_pagegen ergeben:

	// Should minify?
		if ($GLOBALS['TSFE']->config['config']['minifyJS']) {			
			$minifyErrorScript = $minifyErrorInline = '';
			$_scriptCode = t3lib_div::minifyJavaScript($_scriptCode,$minifyErrorScript);
			if ($minifyErrorScript) {
				$GLOBALS['TT']->setTSlogMessage($minifyErrorScript, 3);
			}
			if ($_inlineJS) {
				$_inlineJS = t3lib_div::minifyJavaScript($_inlineJS,$minifyErrorInline);
				if ($minifyErrorInline) {
					$GLOBALS['TT']->setTSlogMessage($minifyErrorInline, 3);
				}
			}
		}

Wie man sieht wird hier lediglich das inlineJS gepackt wo ich ncihts hineinbekommen habe.

Wie gehts denn nun??

@Tetramatrix: Händisch ist halt aufwendig und daher fehleranfällig. außerdem nervt es, wenn man größere Projekte hat und evtl. noch Leute, die sich nicht so gut auskennen, die daran aber arbeiten sollen. Ich hab das Script oben mittlerweile so erweitert, dass ich einen Debug flag für die lokale devel-Version setze bei der das nicht automatisch minified wird und den ich auf dem production server ausmachen kann. Alles woran man da noch denken muss is, den entsprechenden Cache bei einem update zu clearen aber das muss man ja eh immer tun.

Stoneage ·

(e.g. removal of unnecessary whitespace/comments), and serve the results with HTTP encoding (gzip/deflate)

LOL! Zuerst wird das JS pseudo-komprimiert (whitespace/comments) und danach gezipped (gzip/deflate) werden???!!! Der Mehrnutzen dürfte minimal sein, wenn nicht sogar geringer ausfallen. Wenn Man/Frau Zeit hat!?

steffenk ·

mein Reden 😉

letzteres wird nicht minified, aber es ist ja an Dir bereits minified scripte in der extension abzulegen und die zu includen.

Enlightning Man ·

Ich nehm an die letzten beiden Posts gingen um was anderes ihr Threadhijacker, ja? 😃

steffenk schrieb

mein Reden 😉

letzteres wird nicht minified, aber es ist ja an Dir bereits minified scripte in der extension abzulegen und die zu includen.

Bitte korrigiert mich, wenn ich mich irre aber ich dachte der Sinn von Utility Funktionen ist es, dem Programmierer a) Arbeit abzunehmen, die er wiederholt ausführen muss und b) fehleranfällige Prozesse zu automatisieren.

Mein Script im ersten Post (welches wirklich nur ein 10-Zeiler ist) tut genau das. Dann hab ihr gesagt, dass es in Typo3 anders gemacht werden soll mittels des config flags. Meine Fragen bleiben also bestehen:

1) Wie bekommt man etwas in das inlineJS, sodass es der config Flag überhaupt beachtet wird?
2) Wieso wird er nicht beachtet, wenn man es als additionalJavaScript (man beachte den Unterschied zu additionalHeaderData) eingebunden wird?

Als Nebenbemerkung an alle, die den Thread hier evtl. mal lesen mögen weil sie sich ähnliche Fragen stellen: Man muss aufpassen, wenn man eine komplette JS Datei als "inlineJS" einbinden möchte, da der aktuelle Stand (Typo3 4.2.1) das Resultat des Minifyings on the fly generiert und somit jedesmal erstellt wenn die Seite generiert werden muss.

Enlightning Man ·

@just2b: schonmal interessanter Link, meine Fragen werden da leider aber auch nicht beantwortet.

Tetramatrix schrieb

Bisschen OT: Aber die Seite wird ja eh gecached. Ich kann mir nicht ganz vorstellen, dass die JS-Datei jedesmal minifiziert wird. #paralyzed#

Klar, da haste vollkommen recht... Setzt halt ordentliches Caching des jeweiligen Plugins voraus (was bei dynamischen Content evtl. problematisch ist).
Fakt ist doch, dass sich etwas wie Javascript im Vergleich zum Content der Seite relativ selten ändert. Wie gesagt, Prototype einbinden dauert ca 4 Sekunden auf meinem System für die Minifizierung. Ist ähnlich aufwendig wie die ganzen GIFBUILDER Bilder zu erzeugen. Da schafft Typo es ja, die sinnvoll zu cachen unabhängig davon, wie der Plugin programmiert ist. Mein Script hat versucht, diese Funktionalität für Javascript Files zu kopieren. Habe immernoch keine Antwort, wie das mit irgendwelchen unkommentierten Superflags gelöst werden könnte 😉

Enlightning Man ·

Mal als Nachtrag zu der eigentlichen Diskussion:

Tetramatrix schrieb

LOL! Zuerst wird das JS pseudo-komprimiert (whitespace/comments) und danach gezipped (gzip/deflate) werden???!!! Der Mehrnutzen dürfte minimal sein, wenn nicht sogar geringer ausfallen. Wenn Man/Frau Zeit hat!?

Hab es mal ausprobiert. Unsere Seite hat, dank des immens großen Prototype und Scriptaculous einen Javascript Footprint von kanpp 346KB. (Geht hier nicht um die Wahl der JS Bibliothek, ist nur Beispiel). Habe es mit JSMin und mod_deflate komprimiert und diese Tabelle erhalten:

Modus                 Größe     % von gesamt   Ersparnis relativ zu Vormethode
keine Komprimierung:  356 kb        100%         0 kb /  0%
JSmin, kein deflate:  272 kb         76%        84 kb / 24%
kein JSmin, deflate:   83 kb         23%       189 kb / 69%
JSmin und deflate:     66 kb         19%        17 kb / 20%

Insgesamt lässt sich der Footprint also auf 19 % der Initialgröße reduzieren und, wie zu erwarten, macht natürlich das mod_deflate den Löwenanteil aus. Nichtsdestotrotz, spart eine vorherige JSmin Komprimierung zumindest in unserem Falle nochmal 17kb, was immerhin einer Ersparnis von zusätzlichen 20% gegenüber der alleinigen deflate Komprimierung bedeutet.

Wenn ich die Wahl habe, meine Seite durch eine einzige Methode nochmal um 20% kleiner zu bekommen, bin ich durchaus ein Mann der dafür Zeit hat. Und der sie, da wir unseren läppischen Terrabyte Traffic im Monat mittlerweile erreichen, auch haben sollte.

Grüße,
Christopher