Lad os antage, at jeg er fortrolig med at udvikle klientsideapplikationer i jQuery, men nu vil jeg gerne begynde at bruge AngularJS. Kan du beskrive det paradigmeskift, der er nødvendigt? Her er et par spørgsmål, der kan hjælpe dig med at formulere et svar:
Jeg er ikke ude efter en detaljeret sammenligning mellem jQuery
og AngularJS
.
I jQuery designer du en side, og derefter gør du den dynamisk. Dette skyldes, at jQuery blev designet til augmentation og er vokset utroligt meget fra denne enkle præmis. Men i AngularJS skal du starte helt fra bunden med din arkitektur i tankerne. I stedet for at starte med at tænke "Jeg har dette stykke af DOM'en, og jeg vil få det til at gøre X", skal du starte med hvad du vil opnå, derefter gå i gang med at designe din applikation, og til sidst gå i gang med at designe dit view.
På samme måde skal du ikke starte med tanken om, at jQuery gør X, Y og Z, så jeg vil bare tilføje AngularJS oveni til modeller og controllere. Dette er virkelig fristende, når du lige er startet, og derfor anbefaler jeg altid, at nye AngularJS-udviklere slet ikke bruger jQuery, i det mindste indtil de vænner sig til at gøre tingene på Angular Way".
Jeg har set mange udviklere her og på mailinglisten skabe disse udførlige løsninger med jQuery plugins på 150 eller 200 linjer kode, som de så limer ind i AngularJS med en samling callbacks og $apply
s, der er forvirrende og indviklede; men de får det til sidst til at virke! Problemet er, at i de fleste tilfælde kunne det jQuery-plugin omskrives i AngularJS i en brøkdel af koden, hvor alt pludselig bliver forståeligt og ligetil.
Bundlinjen er denne: når du løser problemet, så tænk først "i AngularJS"; hvis du ikke kan'tænke på en løsning, så spørg fællesskabet; hvis der efter alt dette ikke er nogen nem løsning, så skal du være velkommen til at gribe efter jQuery. Men lad ikke jQuery blive en krykke, ellers vil du aldrig mestre AngularJS.
Først skal du vide, at single-page applications er applikationer. De'er ikke websider. Så vi er nødt til at tænke som en server-side udvikler uden at tænke som en klient-side udvikler. Vi er nødt til at tænke over, hvordan vi kan opdele vores applikation i individuelle, udvidelige, testbare komponenter. Så hvordan gør man det så? Hvordan tænker man "i AngularJS"? Her er nogle generelle principper, sammenholdt med jQuery.
I jQuery ændrer vi programmæssigt visningen. Vi kunne have en dropdown-menu defineret som en ul
på følgende måde:
<ul class="main-menu">
<li class="active">
<a href="#/home">Home</a>
</li>
<li>
<a href="#/menu1">Menu 1</a>
<ul>
<li><a href="#/sm1">Submenu 1</a></li>
<li><a href="#/sm2">Submenu 2</a></li>
<li><a href="#/sm3">Submenu 3</a></li>
</ul>
</li>
<li>
<a href="#/home">Menu 2</a>
</li>
</ul>
I jQuery, i vores applikationslogik, ville vi aktivere den med noget i stil med:
$('.main-menu').dropdownMenu();
Når vi bare ser på visningen, er det ikke umiddelbart indlysende, at der er nogen funktionalitet her. For små applikationer er det fint. Men for ikke-trivielle applikationer bliver tingene hurtigt forvirrende og svære at vedligeholde.
I AngularJS er viewet imidlertid den officielle registrering af viewbaseret funktionalitet. Vores ul
-deklaration ville se sådan ud i stedet:
<ul class="main-menu" dropdown-menu>
...
</ul>
Disse to gør det samme, men i AngularJS-versionen ved enhver, der kigger på skabelonen, hvad der skal ske. Når et nyt medlem af udviklingsholdet kommer om bord, kan hun se på dette og vide at der er et direktiv kaldet dropdownMenu
der virker på det; hun behøver ikke at intuitionere det rigtige svar eller gennemgå noget kode. Visningen fortalte os, hvad der skulle ske. Meget renere.
Udviklere, der er nye i AngularJS, stiller ofte et spørgsmål som: Hvordan finder jeg alle links af en bestemt type og tilføjer et direktiv på dem. Udvikleren bliver altid forbløffet, når vi svarer: Det gør du ikke. Men grunden til at du ikke gør det er, at det er som halvt jQuery, halvt AngularJS og ikke godt. Problemet her er, at udvikleren forsøger at "gøre jQuery" i forbindelse med AngularJS. Det kommer aldrig til at fungere godt. Visningen er den officielle registrering. Uden for et direktiv (mere om dette nedenfor) ændrer du aldrig, aldrig, aldrig, aldrig DOM'en. Og direktiver anvendes i visningen*, så hensigten er klar.
Husk: du skal ikke designe og derefter markere. Du skal først lave arkitekter og derefter designe.
Dette er langt en af de mest fantastiske funktioner i AngularJS og fjerner i høj grad behovet for at foretage den slags DOM-manipulationer, som jeg nævnte i det foregående afsnit. AngularJS opdaterer automatisk dit view, så du ikke behøver at gøre det! I jQuery reagerer vi på begivenheder og opdaterer derefter indholdet. Noget i stil med:
$.ajax({
url: '/myEndpoint.json',
success: function ( data, status ) {
$('ul#log').append('<li>Data Received!</li>');
}
});
For en visning, der ser sådan ud:
<ul class="messages" id="log">
</ul>
Bortset fra at blande bekymringer har vi også de samme problemer med at signalere hensigt, som jeg nævnte før. Men endnu vigtigere er det, at vi manuelt skulle referere og opdatere en DOM-knude. Og hvis vi ønsker at slette en logpost, skal vi også kode mod DOM for det. Hvordan tester vi logikken bortset fra DOM? Og hvad hvis vi ønsker at ændre præsentationen? Det er lidt rodet og en smule skrøbeligt. Men i AngularJS kan vi gøre dette:
$http( '/myEndpoint.json' ).then( function ( response ) {
$scope.log.push( { msg: 'Data Received!' } );
});
Og vores visning kan se sådan her ud:
<ul class="messages">
<li ng-repeat="entry in log">{{ entry.msg }}</li>
</ul>
Men for den sags skyld kunne vores view se sådan ud:
<div class="messages">
<div class="alert" ng-repeat="entry in log">
{{ entry.msg }}
</div>
</div>
Og i stedet for at bruge en uordnet liste bruger vi nu Bootstrap-advarselsbokse. Og vi behøvede aldrig at ændre controllerkoden! Men endnu vigtigere er det, at uanset hvor eller hvordan loggen bliver opdateret, vil visningen også ændre sig. Automatisk. Neat!
Selv om jeg ikke viste det her, er databindingen tovejs. Så disse logmeddelelser kan også redigeres i visningen bare ved at gøre dette: <input ng-model="entry.msg" />
. Og der var stor jubel.
I jQuery er DOM'en lidt ligesom modellen. Men i AngularJS har vi et separat modellag, som vi kan administrere på den måde, vi ønsker, helt uafhængigt af visningen. Dette hjælper til ovenstående databinding, opretholder separation of concerns og introducerer langt større testbarhed. Andre svar har nævnt dette punkt, så jeg lader det blive ved det.
Og alt ovenstående er forbundet med dette overordnede tema: Hold dine bekymringer adskilt. Dit view fungerer som den officielle registrering af, hvad der skal ske (for det meste); din model repræsenterer dine data; du har et servicelag til at udføre genanvendelige opgaver; du foretager DOM-manipulation og udvider dit view med direktiver; og du limer det hele sammen med controllere. Dette blev også nævnt i andre svar, og det eneste, jeg vil tilføje, vedrører testbarhed, som jeg diskuterer i et andet afsnit nedenfor.
Til at hjælpe os med adskillelse af bekymringer er dependency injection (DI). Hvis du kommer fra et server-side sprog (fra Java til PHP) er du sikkert allerede bekendt med dette koncept, men hvis du kommer fra jQuery og er en klientside fyr, kan dette koncept virke alt fra fjollet til overflødigt og hipster. Men det er det ikke. :-) Fra et bredt perspektiv betyder DI, at du kan deklarere komponenter meget frit og så fra enhver anden komponent, bare bede om en instans af den, og det vil blive bevilget. Du behøver ikke at vide noget om indlæsningsrækkefølge, eller filplaceringer eller noget lignende. Styrken er måske ikke umiddelbart synlig, men jeg vil give et enkelt (almindeligt) eksempel: testning. Lad os sige, at vi i vores applikation har brug for en tjeneste, der implementerer server-side storage via et REST API og, afhængigt af applikationens tilstand, også lokal storage. Når vi kører test på vores controllere, ønsker vi ikke at skulle kommunikere med serveren - vi tester trods alt controlleren. Vi kan bare tilføje en mock service med samme navn som vores oprindelige komponent, og injectoren vil sikre, at vores controller automatisk får den falske service - vores controller kender ikke og behøver ikke kende forskellen. Apropos test...
Dette er egentlig en del af afsnit 3 om arkitektur, men det'er så vigtigt, at jeg'sætter det som sit eget top-niveau afsnit. Ud af alle de mange jQuery-plugins, du har set, brugt eller skrevet, hvor mange af dem havde en tilhørende testsuite? Ikke ret mange, fordi jQuery ikke er særlig velegnet til det. Men AngularJS er det. I jQuery er den eneste måde at teste på ofte at oprette komponenten uafhængigt med en prøve/demoside, som vores tests kan udføre DOM-manipulationer imod. Så så er vi nødt til at udvikle en komponent separat og så integrere den i vores applikation. Hvor besværligt! Så meget af tiden, når vi udvikler med jQuery, vælger vi iterativ i stedet for testdreven udvikling. Og hvem kan bebrejde os det? Men fordi vi har adskillelse af bekymringer, kan vi lave testdrevet udvikling iterativt i AngularJS! Lad os f.eks. sige, at vi ønsker et superenkelt direktiv til at angive i vores menu, hvad vores aktuelle rute er. Vi kan erklære, hvad vi ønsker i visningen af vores applikation:
<a href="/hello" when-active>Hello</a>
Okay, nu kan vi skrive en test for det ikke-eksisterende when-active
-direktiv:
it( 'should add "active" when the route changes', inject(function() {
var elm = $compile( '<a href="/hello" when-active>Hello</a>' )( $scope );
$location.path('/not-matching');
expect( elm.hasClass('active') ).toBeFalsey();
$location.path( '/hello' );
expect( elm.hasClass('active') ).toBeTruthy();
}));
Og når vi kører vores test, kan vi bekræfte, at den fejler. Først nu skal vi oprette vores direktiv:
.directive( 'whenActive', function ( $location ) {
return {
scope: true,
link: function ( scope, element, attrs ) {
scope.$on( '$routeChangeSuccess', function () {
if ( $location.path() == element.attr( 'href' ) ) {
element.addClass( 'active' );
}
else {
element.removeClass( 'active' );
}
});
}
};
});
Vores test består nu og vores menu udfører som ønsket. Vores udvikling er både iterativ og testdrevet. Wicked-cool.
Du'vil ofte høre "kun lave DOM-manipulation i et direktiv". Det er en nødvendighed. Behandl det med behørig respekt!
Men lad's dykke lidt dybere ned...
Nogle direktiver dekorerer bare det, der allerede er i visningen (tænk ngClass
) og laver derfor nogle gange DOM-manipulation med det samme og er så stort set færdige. Men hvis et direktiv er som en "widget" og har en skabelon, bør det også respektere separation of concerns. Det vil sige, at skabelonen også bør forblive stort set uafhængig af dens implementering i link- og controllerfunktionerne.
AngularJS leveres med et helt sæt værktøjer, der gør dette meget nemt; med ngClass
kan vi dynamisk opdatere klassen; ngModel
giver mulighed for tovejs databinding; ngShow
og ngHide
viser eller skjuler et element programmatisk; og mange flere - inklusive dem, vi selv skriver. Med andre ord kan vi gøre alle mulige fantastiske ting uden DOM-manipulation. Jo mindre DOM-manipulation, jo lettere er det at teste direktiverne, jo lettere er det at style dem, jo lettere er det at ændre dem i fremtiden, og jo mere genanvendelige og distribuerbare er de.
Jeg ser mange udviklere, der er nye i AngularJS, der bruger direktiver som et sted at smide en masse jQuery. Med andre ord tænker de "siden jeg ikke kan lave DOM-manipulation i controlleren, vil jeg tage den kode og lægge den i et direktiv". Selv om det helt sikkert er meget bedre, er det ofte still wrong.
Tænk på den logger vi programmerede i afsnit 3. Selv hvis vi sætter det i et direktiv, vil vi still gøre det på den "Angular Way". Det still doesn't tage nogen DOM manipulation! Der er masser af gange, hvor DOM-manipulation er nødvendig, men det'er en meget sjældnere end du tror! Før du foretager DOM-manipulation nogle steder i din applikation, skal du spørge dig selv, om du virkelig har brug for det. Der er måske en bedre måde.
Her er et hurtigt eksempel, der viser det mønster, jeg ser oftest. Vi vil have en knap, der kan skiftes til at skifte. (Bemærk: dette eksempel er lidt konstrueret og en skosh verbose for at repræsentere mere komplicerede tilfælde, der løses på nøjagtig samme måde).
.directive( 'myDirective', function () {
return {
template: '<a class="btn">Toggle me!</a>',
link: function ( scope, element, attrs ) {
var on = false;
$(element).click( function () {
on = !on;
$(element).toggleClass('active', on);
});
}
};
});
Der er et par ting galt med dette:
angular.element
og vores komponent vil stadig fungere, når den sættes ind i et projekt, der ikke har jQuery.angular.element
) altid bruge jQuery, hvis det blev indlæst, selv hvis jQuery var påkrævet for at dette direktiv skulle virke! Så vi behøver ikke at bruge $
- vi kan bare bruge angular.element
.$
- element
, der overdrages til link
-funktionen, ville allerede være et jQuery-element!.directive( 'myDirective', function () {
return {
scope: true,
template: '<a class="btn" ng-class="{active: on}" ng-click="toggle()">Toggle me!</a>',
link: function ( scope, element, attrs ) {
scope.on = false;
scope.toggle = function () {
scope.on = !scope.on;
};
}
};
});
Igen, skabelon-tingene er i skabelonen, så du (eller dine brugere) kan nemt udskifte den til en, der opfylder enhver nødvendig stil, og logikken behøvede aldrig at blive rørt. Genanvendelighed - boom!
Og der er stadig alle de andre fordele, som f.eks. test - det er nemt! Uanset hvad der er i skabelonen, røres der aldrig ved direktivets interne API, så refaktorering er let. Du kan ændre skabelonen lige så meget, som du vil, uden at røre ved direktivet. Og uanset hvad du ændrer, består dine tests stadig.
w00t!
Så hvis direktiver ikke bare er samlinger af jQuery-lignende funktioner, hvad er de så? Direktiver er faktisk udvidelser af HTML. Hvis HTML ikke gør noget, som du har brug for, skriver du et direktiv, som gør det for dig, og bruger det så som om det var en del af HTML.
Sagt på en anden måde: Hvis AngularJS ikke gør noget ud af boksen, så tænk på, hvordan holdet ville gøre det, så det passer lige ind med ngClick
, ngClass
osv.
Don't even use jQuery. Don't even include it. Det vil holde dig tilbage. Og når du kommer til et problem, som du tror du allerede ved hvordan du kan løse i jQuery, så prøv at tænke på hvordan du kan gøre det inden for AngularJS' rammer, før du griber efter $
. Hvis du ikke ved det, så spørg! 19 ud af 20 gange behøver den bedste måde at gøre det på ikke jQuery, og at forsøge at løse det med jQuery resulterer i mere arbejde for dig.
I jQuery bruges selectors til at finde DOM-elementer og derefter binde/registrere event handlers til dem. Når en begivenhed udløses, udføres denne (imperative) kode for at opdatere/ændre DOM'en.
I AngularJS skal man tænke på views i stedet for DOM-elementer. Views er (deklarativ) HTML, der indeholder AngularJS direktiver. Directives opstiller event handlers bag kulisserne for os og giver os dynamisk databinding. Selectors bruges sjældent, så behovet for ID'er (og nogle typer af klasser) er stærkt reduceret. Views er bundet til modeller (via scopes). Views er en projektion af modellen. Begivenheder ændrer modeller (dvs. data, scope-egenskaber), og de visninger, der projicerer disse modeller, opdateres "automatisk."
I AngularJS skal du tænke på modeller i stedet for jQuery-selekterede DOM-elementer, der indeholder dine data. Tænk på visninger som projektioner af disse modeller i stedet for at registrere callbacks til at manipulere det, brugeren ser.
jQuery anvender unobtrusive JavaScript - adfærd (JavaScript) er adskilt fra struktur (HTML).
AngularJS anvender controllere og direktiver (som hver især kan have deres egen controller og/eller kompilerings- og linkfunktioner) til at fjerne adfærd fra visningen/strukturen (HTML). Angular har også services og filtre til at hjælpe med at adskille/organisere din applikation.
Se også https://stackoverflow.com/a/14346528/215945
En metode til at designe en AngularJS-applikation:
Du kan gøre meget med jQuery uden at vide, hvordan JavaScript prototypisk arv fungerer. Når du udvikler AngularJS-applikationer, undgår du nogle almindelige faldgruber, hvis du har en god forståelse af JavaScript-arvning. Anbefalet læsning: https://stackoverflow.com/questions/14049480/what-are-the-nuances-of-scope-prototypal-prototypical-inheritance-in-angularjs
Kan du beskrive det paradigmeskift, der er nødvendigt?
Imperativ vs. deklarativ
Med jQuery fortæller du DOM, hvad der skal ske, trin for trin. Med AngularJS beskriver du, hvilke resultater du ønsker, men ikke hvordan du skal gøre det. Mere om dette her. Tjek også Mark Rajcok's svar.
Hvordan arkitekturer og designer jeg klientside-webapps anderledes?
AngularJS er et helt klientside framework, der bruger MVC mønsteret (se deres grafiske repræsentation). Det fokuserer i høj grad på adskillelse af bekymringer.
Hvad er den største forskel? Hvad skal jeg holde op med at gøre/bruge; hvad skal jeg begynde at gøre/bruge i stedet?
jQuery er et bibliotek
AngularJS er et smukt klientside framework, der er meget testbart, og som kombinerer tonsvis af fede ting som MVC, dependency injection, data binding og meget mere.
Det fokuserer på separation of concerns og testning (unit testing og end-to-end-testning), hvilket letter testdreven udvikling.
Den bedste måde at starte på er at gennemgå deres fantastiske tutorial. Du kan gennemgå trinene på et par timer; men hvis du ønsker at beherske koncepterne bag kulisserne, inkluderer de et utal af referencer til yderligere læsning.
Er der nogen server-side overvejelser/begrænsninger?
Du kan bruge det på eksisterende applikationer, hvor du allerede bruger ren jQuery. Hvis du imidlertid ønsker at udnytte AngularJS-funktionerne fuldt ud, kan du overveje at kode serversiden ved hjælp af en RESTful tilgang.
Hvis du gør det, kan du udnytte deres resource factory, som skaber en abstraktion af din serverside RESTful API og gør server-side kald (get, save, delete osv.) utrolig nemt.