Skip to content
← Wszystkie wpisy
5 min czytania Dawid Skłodowski

Pierwsze kroki z AngularJS: dyrektywy i dwukierunkowe wiązanie

AngularJS sprawia, że DOM reaguje na twoje dane automatycznie. Praktyczne wprowadzenie do dwukierunkowego wiązania, dyrektyw, kontrolerów i wstrzykiwania zależności — plus uczciwe spojrzenie na cykl digest i miejsca, gdzie magia kosztuje.

Przez lata frontowy JavaScript oznaczał jQuery: wybierz element, odczytaj lub zapisz jego zawartość, podepnij zdarzenie, powtórz. Działa, ale przy czymkolwiek o realnej interaktywności staje się plątaniną ręcznych aktualizacji DOM, gdzie strona i twoje dane są zawsze o jedną pominiętą linię od niezgody. AngularJS proponuje coś fundamentalnie innego — opisz relację między danymi a DOM raz, a framework niech trzyma je w synchronizacji. Po kilku projektach z nim oto praktyczne wprowadzenie, od którego chcielibyśmy zacząć — z magią i kosztami włącznie.

Dwukierunkowe wiązanie: sztandarowa sztuczka

Funkcją, która sprawia, że ludzie się prostują, jest dwukierunkowe wiązanie danych. Deklarujesz wiązanie w markupie i od tej chwili model i widok pozostają zsynchronizowane automatycznie — w obu kierunkach, bez kodu klejącego:

<div ng-app ng-controller="GreetController">
  <input ng-model="name" placeholder="twoje imię">
  <p>Cześć, {{ name }}!</p>
</div>

Wpisuj do inputa, a akapit aktualizuje się w trakcie pisania. Zmień name w JavaScripcie, a input się aktualizuje. Nie ma $('#name').on('keyup', ...), nie ma wywołania .text(...)ng-model i interpolacja {{ name }} deklarują, że te dwie rzeczy są tą samą wartością, a Angular robi resztę. Dla formularzy, filtrów, podglądów na żywo i dashboardów usuwa to ogromną ilość harówki, z której zbudowany jest kod jQuery.

Kontrolery i scope

Dane stojące za kawałkiem UI żyją na $scope, podpiętym przez kontroler:

angular.module("app", [])
  .controller("TodoController", function ($scope) {
    $scope.todos = [
      { title: "Napisz wpis na bloga", done: false },
      { title: "Wyślij go",            done: false },
    ];

    $scope.remaining = function () {
      return $scope.todos.filter(function (t) { return !t.done; }).length;
    };

    $scope.add = function (title) {
      $scope.todos.push({ title: title, done: false });
    };
  });
<div ng-controller="TodoController">
  <p>pozostało: {{ remaining() }}</p>
  <ul>
    <li ng-repeat="todo in todos">
      <input type="checkbox" ng-model="todo.done">
      <span>{{ todo.title }}</span>
    </li>
  </ul>
  <input ng-model="newTitle">
  <button ng-click="add(newTitle)">Dodaj</button>
</div>

$scope to most między kontrolerem a widokiem: właściwości i funkcje na nim są wprost adresowalne w szablonie. ng-repeat renderuje listę i trzyma ją w synchronizacji, gdy todos się zmienia; zaznacz checkbox, a remaining() aktualizuje się natychmiast, bo checkbox jest związany z todo.done, a widok przelicza się ponownie. Zauważ, że nigdzie nie ma manipulacji DOM — opisujesz, czym UI jest jako funkcja danych, a nie kroki, by je zmienić.

Dyrektywy: uczenie HTML-a nowych sztuczek

Dyrektywy to najpotężniejsza i najbardziej wyróżniająca idea Angulara. ng-model, ng-repeat i ng-click to wszystko dyrektywy — znaczniki na elementach DOM doczepiające zachowanie. Prawdziwą mocą jest to, że możesz napisać własne, pakując kawałek UI w reużywalny element niestandardowy lub atrybut:

angular.module("app").directive("starRating", function () {
  return {
    restrict: "E",                       // użyj jako elementu: <star-rating>
    scope: { rating: "=" },              // dwukierunkowo wiąż atrybut 'rating'
    template:
      '<span ng-repeat="n in [1,2,3,4,5]" ng-click="rating = n">' +
      '  {{ n <= rating ? "★" : "☆" }}' +
      '</span>',
  };
});
<star-rating rating="review.score"></star-rating>

Teraz <star-rating> to komponent, który możesz upuścić gdziekolwiek, z własnym szablonem i rating związanym dwukierunkowo z tym, co przekażesz. To ten sam instynkt, który później napędzi web components i każdy framework komponentowy, jaki nastąpi: enkapsuluj markup, style i zachowanie w nazwaną, reużywalną jednostkę. Angular dotarł tam wcześnie, a dyrektywy są miejscem, gdzie ambicja frameworka naprawdę widać.

Wstrzykiwanie zależności i serwisy

Angular ma wbudowane wstrzykiwanie zależności, co jest niezwykłe dla frontowego JavaScriptu i naprawdę użyteczne. Prosisz o to, czego potrzebujesz, po nazwie w argumentach funkcji, a Angular to dostarcza:

angular.module("app")
  .factory("TodoApi", function ($http) {
    return {
      all:    function () { return $http.get("/todos"); },
      create: function (t) { return $http.post("/todos", t); },
    };
  })
  .controller("TodoController", function ($scope, TodoApi) {
    TodoApi.all().then(function (res) { $scope.todos = res.data; });
  });

Kontroler deklaruje, że potrzebuje $http (klienta HTTP Angulara) i twojego serwisu TodoApi, i dostaje je wstrzyknięte. Wepchnięcie komunikacji z serwerem do serwisu trzyma kontrolery cienkimi i czyni warstwę danych testowalną w izolacji — ta sama dyscyplina rozdzielenia odpowiedzialności, którą cenimy po stronie Railsów, zastosowana w przeglądarce. Dla aplikacji Rails TodoApi rozmawia z twoimi endpointami JSON, a Angular renderuje wynik — czysty podział między serwerem serwującym dane a klientem je prezentującym.

Cykl digest: skąd bierze się magia i co kosztuje

Nieodpowiedzialne byłoby uczyć Angulara bez wyjaśnienia, jak wiązanie naprawdę działa, bo tam mieszkają jego granice. Nie ma prawdziwej „magii” — jest cykl digest. Angular trzyma listę obserwatorów (jeden na wiązanie) i po każdym zdarzeniu, o którym wie (kliknięcie, odpowiedź HTTP, timeout), uruchamia pętlę, która sprawdza każdą obserwowaną wartość wobec poprzedniej i aktualizuje DOM tam, gdzie się różnią. Powtarza, aż nic się nie zmieni — to „dirty checking”.

Ma to dwie praktyczne konsekwencje, które musisz przyswoić:

  • Wydajność skaluje się z liczbą wiązań. Każde wiązanie to obserwator sprawdzany przy każdym digeście. Strona z kilkuset jest w porządku; siatka z tysiącami żywych wiązań uczyni digest wolnym, a UI szarpane. Poprawki to realne techniki, po które sięgniesz — wiązania jednorazowe ({{ ::value }} w 1.3), track by w ng-repeat i niewiązanie tego, co nie musi się zmieniać.
  • Angular zna tylko zmiany, które sam spowodował. Jeśli zmienisz dane poza światem Angulara — surowy setTimeout, callback od strony trzeciej, zdarzenie jQuery — digest się nie uruchomi, a widok zwietrzeje. Musisz powiedzieć Angularowi przez $scope.$apply(...). Pytanie „czemu mój widok się nie aktualizuje?” niemal zawsze kończy się tutaj.

Czy używać?

Dla prawdziwej aplikacji jednostronicowej — bogatej, ze stanem, z dużą ilością interaktywnego UI nad API JSON — AngularJS to duży krok naprzód względem ręcznie klepanego jQuery. Dwukierunkowe wiązanie i dyrektywy usuwają kategorię błędów i sporo kodu, struktura (kontrolery, serwisy, DI) skaluje się do prawdziwej aplikacji, a myślenie komponentowe to właściwy kierunek dla całej branży. Koszty są równie realne: cykl digest to model wydajności, który musisz szanować, framework jest opiniujący i duży, a posypanie odrobiną Angulara strony renderowanej po stronie serwera jest niezgrabne — chce posiadać swój zakątek DOM.

Nasza zasada w 2014: sięgaj po Angulara, gdy ekran jest naprawdę aplikacjopodobny, i zostań przy HTML renderowanym po stronie serwera (plus mały, celowany JavaScript), gdy to głównie treść. Użyty do tego, do czego jest zbudowany, zmienia, ile możesz komfortowo zbudować w przeglądarce — tylko wejdź w to, rozumiejąc cykl digest, bo to zrozumienie jest różnicą między Angularem czującym się jak magia a Angularem czującym się jak zagadka.