Ostatio w poście napisałem jak łączyć arkusze stylów z użyciem Tanka Nancy Optimization. Podobnie można także robić z plikami javascript. Jednakże Tanka Nancy Optimization framework zawiera pewien błąd, który powoduje zwracanie błędnych pakietów zawierających połączone pliki css lub js.
Jak to? Jak to się stało?
Błąd polega na tym, że połączone i spakowane wiązki są przechowywane w cache, ale klucz do cache jest generowany w dość ubogi sposób i do tego z błędem. Bierze on pod uwagę jedynie czas modyfikacji pliku. Ponadto tworząc FileInfo nie jest brana pod uwagę absolutna ścieżka, co skutkuje pobieraniem własności nieistniejącego pliku, a czas ostatniej modyfikacji pliku, który nie istnieje to 1601-01-01. Wynikiem tego zamieszania jest to, że CacheKey zależy jedynie od ilości plików w wiązce.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// skopiowano z https://github.com/pekkah/Tanka.Nancy.Optimization/blob/master/src/Tanka.Nancy.Optimization/Bundle.cs public string GetCacheKey() { var builder = new StringBuilder(); foreach (string file in Files) { var fileInfo = new FileInfo(file); builder.Append(fileInfo.LastWriteTimeUtc.ToFileTimeUtc()); } string hashThis = builder.ToString(); SHA1 hasher = SHA1.Create(); byte[] hash = hasher.ComputeHash(Encoding.UTF8.GetBytes(hashThis)); return BitConverter.ToString(hash).Replace("-", ""); } |
Studium przypadku
Podczas migracji projektu z ASP.NET MVC do stworzyłem kilka wiązek:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
public class BootstrapScriptBundle : ScriptBundle { public const string PATH = "/bundles/bootstrap.js"; public BootstrapScriptBundle() : base(PATH) { Include("/Scripts/bootstrap.js"); Include("/Scripts/respond.js"); } } public class JQueryScriptBundle : ScriptBundle { public const string PATH = "/bundles/jquery.js"; public JQueryScriptBundle() : base(PATH) { Include("/Scripts/jquery-2.1.0.js"); } } public class ModernizrScriptBundle : ScriptBundle { public const string PATH = "/bundles/modernizr.js"; public ModernizrScriptBundle() : base(PATH) { Include("/Scripts/modernizr-2.7.2.js"); } } |
A następnie użyłem ich w pliku razora:
1 2 3 4 5 6 |
... @Scripts.Render("/bundles/modernizr.js") ... @Scripts.Render("/bundles/jquery.js") @Scripts.Render("/bundles/bootstrap.js") ... |
Zgodnie z oczekiwaniami wynikowy plik HTML zawierał tagi:
1 2 3 4 5 6 |
... <script src="/bundles/modernizr.js"></script> ... <script src="/bundles/jquery.js"></script> <script src="/bundles/bootstrap.js"></script> ... |
Strona jednak nie działała, debugger komunikował brak jquery. Ku mojemu zdziwieniu serwer to samo dla /bundles/modernizr.js
jak i dla /bundles/jquery.js
.
Analizując genezę problemu potwierdza się, że wszystkie wiązki zawierające po tyle samo plików źródłowych (tutaj słownie po jednym pliku źródłowym) dostają taki sam klucz cache i są nierozróżnialne z punktu widzenia serwera.
Workaround?
Zgłosiłem buga autorowi i trzeba czekać lub poprawić samemu i przekompilować. Najprostsze rozwiązanie typu „przyspawaj mufkę” to tworzenie pakietów zawierających różne ilości plików źródłowych. W razie czego upychamy sianem, czyli tworzymy puste pliki js lub css.Takie rozwiązanie polecam każdemu, kto ma na załatanie 3 minuty i popiera zasadę, że rozwiązania na ślinę i taśmę klejącą są najtrwalsze i najrozsądniejsze :).
Wersja dla twardzieli
Dla tych co chcą poprawić źródła projektu Tanka.Nancy.Optimization podaję 2 skuteczne kroki:
Po pierwsze: w pliku Bundle.cs dokonaj poniższych zmian
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// dodaj pole statyczne public static global::Nancy.IRootPathProvider rootPathProvider; // dodaj metodę private string GetFullPath(string path) { string rootPath = rootPathProvider.GetRootPath(); return System.IO.Path.Combine(rootPath, path.TrimStart('/')); } // błędną metodę GetCacheKey podmień na public string GetCacheKey() { var builder = new StringBuilder(); foreach (string file in Files) { var fileInfo = new FileInfo(GetFullPath(file)); builder.Append(fileInfo.LastWriteTimeUtc.ToFileTimeUtc()); builder.Append(file); } string hashThis = builder.ToString(); SHA1 hasher = SHA1.Create(); byte[] hash = hasher.ComputeHash(Encoding.UTF8.GetBytes(hashThis)); return BitConverter.ToString(hash).Replace("-", ""); } |
Po drugie: do projektu Tanka.Nancy.Optimization dodać klasę o nazwie Startup lub podobnej i poniższej treści:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
using Nancy; using Nancy.Bootstrapper; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Tanka.Nancy.Optimization { public class Startup : IApplicationStartup { public void Initialize(IPipelines pipelines) { } public Startup(IRootPathProvider rootPathProvider) { Bundle.rootPathProvider = rootPathProvider; } } } |
I po przekompilowaniu i podstawieniu nowych bibliotek cache działa poprawnie.
Powyższe zmiany są dostępne na moim forku projektu https://github.com/isukces/Tanka.Nancy.Optimization.