Ohne Frage, das Bundling in ASP.net ist eine feine Sache – so lange Scripts zusammengefasst werden können, die immer benötigt werden und nicht dynamisch per require.js bei Bedarf nachgeladen werden.
Für diese Scripts gibt es 2 Möglichkeiten.
- Die Scripts werden entweder direkt durch einen Script-Minifier gejagt und ausgeliefert – dafür gibt es diverse Tools, die entweder auf die Änderung eines Scripts reagieren oder bei einem Build ausgeführt werden
- Die Scripts werden zur Laufzeit optimiert
Ich möchte hier auf die 2. Möglichkeit eingehen und zeige, wie Scripts zur Laufzeit – unter Verwendung von WebGrease – minimiert werden können.
WebGrease installieren
WebGrease kann über nuget eingebunden werden: Install-Package WebGrease
Der Minifier
Um die Ausgabe an den Client verändern zu können, muss der „Filter“ der Response auf ein Objekt gesetzt werden, dass die Arbeit anstelle des Default-Filters übernimmt.
Response.Filter: Summary: Gets or sets a wrapping filter object that is used to modify the HTTP entity body before transmission. Returns: The System.IO.Stream object that acts as the output filter.
Konkret bedeutete das, dass ein Objekt benötigt wird, das von Stream ableitet, um es dem Filter zuzuweisen. Diesem Objekt wird der Output-Stream, auf den das minimierte Script geschrieben werden soll:
public class JavascriptRequestMinifier : MemoryStream { private readonly Stream _stream; ///<summary>Erzeugt eine Instanz des Objekts</summary> ///<param name="stream">Der Output-Stream</param> public JavascriptRequestMinifier(Stream stream) { this._stream = stream; } //TODO: Write + Flush implementieren }
Als nächstes muss die Methode ‚Write‘ überschrieben werden, um das Schreiben auf den Output-Stream abzufangen.
Das Framework beschreibt den Stream blockweise, was anfangs dazu führte, dass bei größeren Scripts nicht das Ganze optimiert und ausgegeben wurde, sondern nur ein Teil davon. Abhilfe schafft ein StringBuilder, um den Output zu sammeln und ihn komplett in die Ausgabe zu schreiben, wenn der Stream geschlossen wird:
readonly StringBuilder _codeBuilder = new StringBuilder(); public override void Write(byte[] buffer, int offset, int count) { // string aus buffer extrahieren und _codeBuilder zuweisen, falls das script // länger ist _codeBuilder.Append(Encoding.UTF8.GetString(buffer)); }
Abschließend muss die Flush-Methode überschrieben werden, um die gesammelten Daten minimiert an den Client zu senden:
public override void Flush() { Minifier m = new Minifier(); string code = m.MinifyJavaScript( this._codeBuilder.ToString(), new CodeSettings { PreserveImportantComments = false, PreserveFunctionNames = true, MinifyCode = true } ); this._stream.Write( Encoding.UTF8.GetBytes(code), 0, Encoding.UTF8.GetByteCount(code)); this._stream.Flush(); }
Den Filter setzen
Der letzte Schritt, um das Script minimiert auszugeben ist, den Filter zuzuweisen. Die richtige Stelle ist hier in der Global.asax der Event ‚PostReleaseRequestState‘
// Occurs when ASP.NET has completed executing all request event handlers and the // request state data has been stored.
An dieser Stelle muss geprüft werden, ob es sich bei dem ContentType um ein Javascript handelt, dass optimiert werden soll.
Ich habe require.js dahingehend konfiguriert, dass dem Script ein QueryString-Parameter ‚min-require=1‘ angefügt wird. Enthält die Query der Url des Request diesen String, wird eine Instanz des Minifiers erzeugt und dem den Response.Filter zugewiesen:
protected void Application_PostReleaseRequestState(Object sender, EventArgs e){ if (this.Request.Url.Query.Contains("min-require=1")) { this.Response.Filter = new JavascriptRequestMinifier(this.Response.Filter); } }
Damit werden alle Scripts, die dynamisch geladen werden, minimiert an den Client übertragen.