Bundle i minifikacja (przykładem AngularJS)

Bundlowanie – robi z wielu plików jeden plik – przyspiesza ładowanie skryptów.
Minifikacja (kompresja) – odchudza plik, wycina białe znaki i komentarze, skraca nazwy zmiennych w JavaScript – jeszcze bardziej przyspiesza ładowanie skryptów 😉
Bundling and Minification in ASP.NET MVC

PM> Install-Package Microsoft.AspNet.Web.Optimization;

Nie będę robił testów, powyższy link wyjaśnia na obrazkach o co chodzi. Podzielę się moimi spostrzeżeniami.

Widziałem ostatnio jak kolega nie docenił tych dwóch technik, więc chciałem o tym trochę napisać. Zrobię to na przykładzie skrytpów JavaScript – działa to analogicznie jak dla styli CSS. Kod C# jest pisany w frameworku Nancy.

Nie trzeba pamiętać o każdym pliczku

Bundlowanie plików ma juz zalety podczas pisania aplikacji. Nie includujemy każdego skryptu bezpośrednio, więc nie musimy pamiętać, żeby go dodać i później zmieniać przy zmianach nazw lub przenosinach do innego folderu.

Zamiast wielu skryptów

<script src="/app/app.js"></script>
<script src="/app/offers/offers.js"></script>
<script src="/app/offers/OfferListCtrl.js"></script>
<script src="/app/offers/OfferCtrl.js"></script>
<script src="/app/offers/digital/DigitalPrinting.js"></script>
<script src="/common/calculations/calculations.js"></script>
<!-- kolejne 30 (300) skryptów -->

Dostajemy jeden

<script src="/Scripts/bundledJs.js?v=GedIBh-USXksmzkpw8b0rBAAO_B2ajuCtSSJGiolCWg1"></script>

Cachowanie JavaScript

Wynikowy plik ma doklejony hash (?v=GedIBh-USXksmzkpw…) obliczany z jego zawartości. Pliki JavaScript są często cachowane przez przeglądarki. Po zmianie przynajmniej jednego pliczku, zostanie zmieniony plik wynikowy i hash. Dla przeglądarki są to jakby dwa różne pliki. Gdyby tego nie było to po zmianie pliku customers.js na serwerze przegląrka ciągle będzie go pobierała ze swojego cache.

Najpierw definicje modułów

Przykładowy widok na część aplikacji w Angularze
Angular scripts structure

Bundlujący kod C#

BundleTable.Bundles.Add(new Bundle("~/Scripts/bundledJs.js")
    .Include("~/app/app.js")
    .Include("~/app/admin/customers/admin.customers.js")
    .Include("~/app/admin/prices/admin.prices.js")
    // more included module definition files
    .IncludeDirectory("~/app", "*.js", true)
);

Pliki admin.customers.js i admin.prices.js to definicje modułów.

// example admin.customer.js:
angular.module('admin.customers', ['ngRoute', 'resources', 'ngMessages'])

Pliki te muszą pojawić się nim zaczniemy się do nich odwoływać w innych plikach.
IncludeDirectory(„~/app”, „*.js”, true) – wybiera wszystkie pliki *.js również w podkatalogach. Plików już raz wybranych (te z modułami) nie wrzuca drugi raz.

Zadziała przypadkiem

Jeśli ktoś nie dzieli kodu JavaScript na moduły (bo np. jest to tylko mała aplikacja, lub nie lubi) to może zadziała coś takiego:

BundleTable.Bundles.Add(new Bundle("~/Scripts/bundledJs.js")
    .IncludeDirectory("~/app", "*.js", true)
);

Zadziała to tylko gdy naszym głównym plikiem konfiguracyjnym (głównym modułem) będzie app.js – tak to bywa na tutorialach. Pliki do bundlowania są sortowanie alfabetycznie więc akurat app.js wyląduje na początku. W przeciwnym wypadku Angular wysypie się w przeglądarce.

Dwa pliki z JavaScript

Tworzę dwa bundlowane pliki JS, jeden dla moich skryptów, drugi dla skryptów zewnętrznych bibliotek. Te drugie załaczam explicite, nie chcę żeby brał wszystkie moduły Angulara które są w katalogu /Scripts. Biorę też zawsze pliki już zminifikowane (*.min.js).

Minifikacja w AngularJS

Angular korzysta z dependecy injection, które wyszukuje odpowiednie serwisy po nazwie. Te pliki nie mogą być po prostu minifikowane, bo się nazwy pozmieniają. Trzeba albo pisać serwisy bardziej explicite albo wykonać dodatkowy krok, który właśnie przerobi nasz kod na bardziej explicite. Na pierwsze szkoda mi czasu, drugie nie wiem jak zrobić na stacku Microsoftych technologi. Godzę się więc z tym, że nie będzie minifikacji napisanego przeze mnie JavaScriptu (nie jest to na razie wielki problem).

Skrypty w <head>

Wrzucam JavaScript do <head> zamiast na sam koniec strony. Robię tak ponieważ w Single Page Aplications kod JavaScript nie jest czymś dodatkowym robiącym wodotryski. Jest to silnik, dzięki któremu wszystko się pokazuje. Nie będzie więc tak (jak w tradycyjnych stronach), że załaduje się markup który zobaczymy a później dociągną się skrypty. W SPA strona zacznie się pokazywać dopiero gdy dociągnięte zostaną skrypty Angulara i naszej aplikacji.

Kod

// Bootstrapper.cs
private void ConfigureBundles()
{
BundleTable.Bundles.Add(new Bundle("~/Content/styles.css")
    .Include("~/Content/Styles/reset.css")
    .Include("~/Content/bootstrap.css")
    .Include("~/Content/bootstrap-theme.css")
    .Include("~/Content/ng-grid.css")
    .Include("~/Content/Styles/main.css")
);

BundleTable.Bundles.Add(new Bundle("~/Scripts/bundleVendorScripts.js")
    .Include("~/Scripts/jquery-2.1.1.min.js")
    .Include("~/Scripts/bootstrap.min.js")
    .Include("~/Scripts/angular.min.js")
    .Include("~/Scripts/angular-route.min.js")
    .Include("~/Scripts/angular-resource.min.js")
    .Include("~/Scripts/angular-animate.min.js")
    .Include("~/Scripts/angular-messages.min.js")
    .Include("~/Scripts/i18n/angular-locale_pl-pl.js")
    .Include("~/Scripts/ng-grid.min.js")
    .Include("~/Scripts/underscore-min.js")
    .Include("~/Scripts/checklist-model.js")
    .Include("~/Scripts/angular-ui/ui-bootstrap-tpls.min.js")
);

// Files that are included explicitly contain module definition
// and need to be on the top
BundleTable.Bundles.Add(new Bundle("~/Scripts/bundledJs.js")
    .Include("~/common/resources/resources.js")
    .Include("~/common/calculations/calculations.js")
    .Include("~/common/services/calculationsService.js")
    .IncludeDirectory("~/common", "*.js", true)
    .Include("~/app/app.js")
    .Include("~/app/admin/customers/admin.customers.js")
    .Include("~/app/admin/prices/admin.prices.js")
    .Include("~/app/admin/users/admin.users.js")
    .Include("~/app/admin/usersListForOffers/admin.usersListForOffers.js")
    .Include("~/app/offers/offers.js")
    .Include("~/app/offers/digital/commonDirectives/commonDirectives.js")
    .IncludeDirectory("~/app", "*.js", true)
);
// IndexModule.cs
public IndexModule()
{
    Get["/"] = _ =>
    {
        return View["Index", new
        {
            Styles = Styles.Render("~/Content/styles.css").ToString(),
            BundledVendorScripts = Scripts.Render("~/Scripts/bundleVendorScripts.js").ToString(),
            BundledScripts = Scripts.Render("~/Scripts/bundledJs.js").ToString(),
        }];
    };
}
// Index.cshtml
<html>
<head>
    ....
    @Html.Raw(@Model.Styles)

    @Html.Raw(@Model.BundledVendorScripts)
    @Html.Raw(@Model.BundledScripts)
    ...

Wynik na stonie

<link href="/Content/styles.css?v=3sSQFGrCHzIKi38WSm3lvPI8fZWtqx1noJpWhGiAAY81" rel="stylesheet">
<script src="/Scripts/bundleVendorScripts.js?v=JQgCloNF1qYWuldL0Z1v4yVsO9Kf6h21JziMHz--KXY1"></script>
<script src="/Scripts/bundledJs.js?v=640Ko0GlTm1XerX1Of9NFOcEJw0ynzGN04nQ_rc3hAE1"></script>

Generuję zbundlowane pliki w IndexModule.cs (BundledScripts = Scripts.Render(„~/Scripts/bundledJs.js”).ToString()), ponieważ nie potrafiłem zrobić tego w Index.cshtml. Problem z nammespace’ami w Nancy. Ktoś miał podobny problem?

Update

Problem z glyphicons przy bundlowaniu

Miałem problem z glyphicons z Bootstrapa. Opis i rozwiązanie — MVC4 Bundles – No Twitter Bootstrap icons. Chodzi o ścieżki bezwzględne to ikonek. Pomogła też zmiana folderu wynikowego pliku CSS. Jeśli jest on taki jak plik bootstrap.min.css to ścieżki względne się nie popsują.

Reklamy
Ten wpis został opublikowany w kategorii Programowanie i oznaczony tagami , , , . Dodaj zakładkę do bezpośredniego odnośnika.