Archiv für den Monat Dezember 2014

AngularJS minification mit npm (node.js) und ng-annotate


Wenn ich aktuell AngularJS Module erstelle, dann achte ich bereits darauf das der Code minify fähig ist. Denn durch die Verwendung von Dependency Injection (DI) von AngularJS muss beim Schreiben von AngularJS Modulen bereits darauf geachtet werden das der Code minify fähig ist.

Daher wird aus dem Folgenden nicht minify fähigen Code:

angular.module("AngularStrapModalCtrl", [])
    .controller("AngularStrapModalCtrl", function ($modal, $scope, modalCtrl) {
    $scope.modal = {
        testModalCtrl: modalCtrl
    };
})
.controller("modalCtrl", function ($scope) {

    this.name = "testname";
    $scope.test = "blubb";
});

Der minify fähige Code:

angular.module("AngularStrapModalCtrl", [])
    .controller("AngularStrapModalCtrl", ["$modal", "$scope", "modalCtrl", function ($modal, $scope, modalCtrl) {
    $scope.modal = {
        testModalCtrl: modalCtrl
    };
}])
.controller("modalCtrl", ["$scope", function ($scope) {

    this.name = "testname";
    $scope.test = "blubb";
}]);

Denn die Funktionsparameter die normalerweise über DI aufgelöst werden, werden in einem String Array definiert und damit weiß AngularJS auch nach dem minifien weiterhin das für einen Parameter z.B. mit dem Namen “a” als “$scope” aufgelöst werden soll. Hierbei ist es wichtig das die Reihenfolge der Werte im String Array der Parameter im Funktionsaufruf entspricht.

Wenn man aber auf Module von anderen Entwicklern zurückgreift, dann sind diese Module nicht immer minify fähig und müssten für diesen Zweck von uns von Hand angepasst werden. Hier kann uns der Node Package Manager npm weiterhelfen, der direkt mit node.js installiert wird und eine eigene Konsole zur verfügung stellt. Eine gute Einführung in npm stellt die Seite selbst zur Verfügung.

Denn für npm gibt es das Packet “ng-annotate” welches die oben genannte minify Funktion bereits vollautomatisch für uns ausführt. Installiert wird “ng-annotate” über die npm Konsole welche direkt mit node.js installiert wird.

image

Den Befehl auf der npm Konsole ausführen, um “ng-annotate” zu installieren.

npm install -g ng-annotate

Beim Installieren von npm Paketen kann es zu einem Timeout kommen, vor allem wenn man hinter einem Proxyserver sitzt. Mir hat dabei der folgende Eintrag weitergeholfen:

http://jjasonclark.com/how-to-setup-node-behind-web-proxy

Wenn “ng-annotate” erfolgreich installiert wurde, dann kann man über einen einfachen Befehl auf der npm Konsole unser Angular Modul minify fähig machen.

C:\>ng-annotate –add „c:\Temp\nichtMinifyFaehig.js“ -o „c:\Temp\minifyFaehig.js“

Die Ausgabedatei enthält dann den kompletten AngularJS Modulcode inkl. der Parameterarrays für die minification.

Das ganze lässt sich natürlich entsprechend ausbauen, das dies immer ausgeführt wird wenn das Projekt erstellt wird, … hier sind dem Entwickler wie immer keine Grenzen gesetzt.

AJAX Calls und virtuelle Verzeichnisse mit ASP.NET MVC und dem “base” Tag


In einem meiner letzten Artikel habe ich bereits eine Lösung vorgestellt wie man über ASP.NET die aktuelle URL bereitstellt und in eine globale JavaScript Variable schreibt. Denn sobald man mit virtuellen Verzeichnissen arbeitet funktioniert die Angabe von z.B. “/Home/GetAllItems” nicht mehr als relative URL Angabe. Die bereits von mir vorgestellte Lösung ist recht aufwendig, da man überall bei Pfadangaben auf die globale JavaScript variable zugreifen muss. Der alte Lösungsansatz ist daher meiner Meinung nach die optimale Lösung.

Jetzt bin ich durch weitere Recherchen auf eine einfachere Lösung gestoßen und dafür wird nur das “base” Tag benötigt und ein einfacher MVC Helper “Url.Content”.

...
<head>
    <meta charset="utf-8" />
    <title>My ASP.NET MVC Application</title>
    <base href="@Url.Content("~")" />
    ...
</head>

Jetzt kann man wieder alle URL Angaben wie bisher verwenden, d.h. wenn die Webseite unter

http://localhost/myVirtualDir/Home/Index

liegt, dann wird im Base Tage “/myVirtualDir/” stehen und diese Verzeichnisangabe wird automatisch vor jeden AJAX Request gestellt. Befinde ich mich nicht in einem virtuellen Verzeichnis, dann steht einfach nur “/” im Base Tag und alles funktioniert ebenfalls wie gewohnt. Es ist also keine extra globale Variable notwendig wie in meinem ersten Lösungsvorschlag. Es muss also nur das Base Tag erstellt werden und die restlichen Linkangaben können wie gewohnt verwendet werden.

Bei Stackoverflow habe ich nur gelesen das es im Zusammenhang mit HTML Anchor “#” evtl. Probleme geben soll:

http://stackoverflow.com/questions/1889076/is-it-recommended-to-use-the-base-html-tag

ASP.NET MVC mit AngularJS und UI Router


Damit aus einer AngularJS Anwendung auch eine “richtige” SPA (Single Page Application) wird, benötigt man noch eine passende Router Erweiterung, die sich um die Navigation kümmert und den richtigen Inhalt für die jeweilige URL/Status lädt. Bei der Wahl des passenden Routers gibt es zum einen ngRouter der direkt vom AngularJS Team entwickelt wird oder UI-Router welcher von der Community stammt und auch nested views (verschachtelte Seiten) unterstützt. Ich habe mich in meinen Projekten für den UI-Router entschieden, aufgrund des besseren Funktionsumfangs und da ich wie beim ASP.NET MVC auch weiterhin nested views verwenden möchte, die ngRouter nicht out of the Box unterstüzt.

In den meisten Beispielen die ich bisher gesehen habe, die AngularJS mit ASP.NET MVC und einem Router einsetzen, wurde ASP.NET MVC nur noch verwendet um die AJAX Calls für die Daten bereitzustellen. Alle Views wurden meist in einfache HTML Dateien ausgelagert und man verzichtet damit eigentlich völlig auf die Annehmlichkeiten die einem mit Razor Views zur Verfügung stehen.

Daher verwende ich in meinen Projekten weiterhin Razor Views (cshtml) um z.B. per Authorize Attribut zu bestimmen welche Views nicht ausgeliefert werden, wenn der Benutzer nicht die entsprechenden Rechte besitzt. Oder ich passe die Views bereits entsprechend der aktuellen Benutzerrechte an bevor ich diese an den Client ausliefere.

Damit man AngularJS mit UI-Router und Razor Syntax inklusive nested views auch verwenden kann, muss man die folgenden Einstellungen im MVC Projekt vornehmen.
(Wenn das ganze auch einfacher geht, bin ich gern für einen Tipp dankbar.)

  • Erstellen eines neuen Views mit Beliebigen Namen z.B. “Index.cshtml” in einem beliebigen Controller, der nur den folgenden Inhalt hat:
<div data-ui-view></div>
  • Als Standardroute unseren neuen View eintragen. (In meinem Fall ist es “Index” im “HomeController”)
routes.MapRoute(
    name: "Default",
    url: "{controller}/{action}/{id}",
    defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
  • Für alle restlichen Views bis auf _Layout.cshtml und Index.cshtml wird für “Layout = null” eingestellt. (Das bedeutet für diese Views wird nur das HTML generiert das auch im View enthalten ist. Denn normalerweise wird der Inhalt dieser Seite in der “_Layout.cshtml” Datei an der Stelle wo “@RenderBody” steht von MVC eingefügt und der HTML Code der gesamten Seite zurückgegeben, dies wird durch das Setzten von “null” verhindert.)
@{
    Layout = null;
}

<h2>AddPerson</h2>

...
  • Wenn man nested views verwendet, in denen “@RenderBody” verwendet wird, dann in den nested views das “@RenderBody” entfernen und durch “<div ui-view></div>” ersetzten. (Bei mir findet man dies z.B. in den Untermenüs.)
  • Auch für die nested views wird jetzt im Controller eine passende ActionResult Funktion benötigt, damit der HTML Code abgerufen werden kann. (Vorher wurde dies durch “@RenderBody” von “allein” durch MVC geregelt und es war für nested views keine extra Controllerfunktion notwendig)
//Der neue Index View
 public ActionResult Index()
 {
     return View();
 }

//Der Nested View für das Menü 
 public ActionResult Submenue()
 {
     return View("_HomeSubmenue");
 }
  • Am Ende sollte es nur noch ein “@RenderBody” Element im “_Layout.cshtml” geben, hier wird automatisch unsere “Index.cshtml” Datei durch MVC eingesetzt und ab da übernimmt dann später UI-Router, um den richtigen Inhalte anzuzeigen.

Damit sollten alle ASP.NET MVC Einstellungen abgeschlossen sein. Jetzt kann man sich ganz um das Definieren der passenden Routen bzw. Status im Angular/JavaScript kümmern und die passenden “ui-sref” Links für die Navigation erstellen. Dafür muss natürlich AngularJS und UI-Router installiert und eingebunden sein.

Die App Datei mit den States (Routen) für mein Beispiel sieht dann z.B. folgendermaßen aus:

angular.module("app.main", [
        "ui.router",
        "mainCtrl",
       ... 
])
    .config(['$stateProvider', '$urlRouterProvider', function ($stateProvider, $urlRouterProvider) {
        //http://scotch.io/tutorials/javascript/angular-routing-using-ui-router
        $urlRouterProvider.otherwise("/Home/HighCharts");

        $stateProvider
            //Home Controller
            .state("Home", {
                url: "/Home",
                templateUrl: "Home/Submenue"
            })
            .state("Home.HighCharts", {
                url: "/HighCharts",
                templateUrl: "Home/HighCharts"
            })
            .state("Home.Formvalidation", {
                url: "/Formvalidation",
                templateUrl: "Home/Formvalidation"
            })
            .state("Home.ModelBinderOhneWatch", {
                url: "/ModelBinderOhneWatch",
                templateUrl: "Home/ModelBinderOhneWatch"
            })
            .state("Home.DirectiveTests", {
                url: "/DirectiveTests",
                templateUrl: "Home/DirectiveTests"
            })
            .state("Home.AngularStrapModal", {
                url: "/AngularStrapModal",
                templateUrl: "Home/AngularStrapModal"
            })
            .state("Home.TemplateUrlTest", {
                url: "/TemplateUrlTest",
                templateUrl: "Home/TemplateUrlTest"
            })
            //Account Controller
            .state("Account", {
                url: "/Account",
                templateUrl: "Account/AccountSubmenue"
            })
            .state("Account.PersonsList", {
                url: "/Persons",
                templateUrl: "Account/PersonsList"
            })
            .state("Account.ManageRights", {
                url: "/Rights/:personId?Name",
                templateUrl: "Account/ManageRights"
            })
            .state("Account.AddPerson", {
                url: "/Add",
                templateUrl: "Account/AddPerson"
            });
    }
    ]);

image

Im config Part werden die passenden States für unsere Anwendung festgelegt. Wie genau sich die States zusammensetzten und wie das ganze aufgebaut wird, dazu gibt es einige sehr gute Beispiele im Netz.

Die zugehörige “_Layout.cshtml” sieht mit UI-Router Navigation “ui-sref” dann folgendermaßen aus:

<!DOCTYPE html>
<html lang="en" ng-app="app.main" ng-controller="mainCtrl as main">
<head>
    <meta charset="utf-8" />
    <base href="@Url.Content("~")" />
    @Styles.Render("~/Content/css")
</head>
<body>
    <div class="navbar navbar-default navbar-fixed-top">
        <div class="container">
            <div class="collapse navbar-collapse">
                <ul class="nav navbar-nav">
                    <li ng-class="{'active': main.MenueHomeActive()}">
                        <a ui-sref="Home.HighCharts"><strong>Home</strong></a> 
                    </li>
                    <li ng-class="{'active': main.MenueAccountActive()}">
                        <a ui-sref="Account.PersonsList"><strong>Account</strong></a>
                    </li>
                </ul>
            </div>
        </div>
    </div>

    <div class="container" id="mainContainer">
        @RenderBody()
    </div>

    @Scripts.Render("~/bundles/jquery")
    @Scripts.Render("~/Angular/App")
</body>
</html>

Ein passenden Submenü mit nested view Element “ui-view” und der Subnavigation. Mit Hilfe von “ui-sref-active” wird die CSS Klasse “active” gesetzt, wenn der View angezeigt wird, der in “ui-sref” angegeben wurde.

@{
    Layout = null;
}

<div class="row">
    <div class="col-lg-12">
        <div class="btn-group" role="group">
            <a ui-sref=".PersonsList" ui-sref-active="active" class="btn btn-default">
                <strong>Personen Liste</strong>
            </a>
            <a ui-sref=".AddPerson" ui-sref-active="active" class="btn btn-default">
                <strong>Person erstellen</strong>
            </a>
        </div>
    </div>
</div>

<div ui-view></div>

Das komplette Beispiel findet man wie immer bei mir in CodePlex.