search check home clock-o tag tags chevron-left chevron-right chevron-up chevron-down twitter facebook github rss comment comments terminal code

[JS] Leniwa inicjalizacja danych w module za pomocą JS Promises

[JS] Leniwa inicjalizacja danych w module za pomocą JS Promises

Jakiś czas temu pisałem o sposobach wykorzystania JS Promises w codziennej pracy jako webdeveloper. Celem tego artykułu jest przedstawienie praktycznego sposobu na użycie tej funkcjonalności JS.

Opis przypadku użycia

W przypadku tego wpisu zakładam, że posiadasz wystarczającą wiedzę odnośnie JS i masz przeczytany mój poprzedni wpis odnośnie JS Promises.

Wyobraźmy sobie sytuację, że chcemy utworzyć listę użytkowników na stronie. Każdy użytkownik jest osobnym obiektem na którym możemy wykonywać różnorodne działania. W momencie inicjalizacji obiektu użytkownika nie posiadamy żadnych danych o użytkowniku i zamierzamy je dopiero pobrać z zewnętrznego źródła, aby potem je użyć do wyświetlenia informacji na stronie. To wszystko brzmi prosto i fajnie, ale nie jest to trywialne zagadnienie.

Rozwiązanie problemu - podejście 1

Na początek przedstawię problem jaki może zaistnieć w takiej sytuacji, gdy będziemy chcieli od razu wyświetlić dane użytkownika na stronie.

Załóżmy, że mamy przygotowany kod HTML, który będzie wyglądał następująco:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- index.html -->
<div class="task api-1"></div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/2.4.1/lodash.min.js"></script>
<script id="user-profile" type="text/template">
<div class="user">
  <img src="<%= picture %>" alt="">
  <div class="info">
    <h3><%= name.first%> <%= name.last%></h3>
    <dl>
      <dt>Address:</dt>
      <dd><%= location.street%><br/><%= location.zip%> <%= location.city%>, <%= location.state%></dd>
      <dt>Phone:</dt>
      <dd><%= cell%></dd>
      <dt>Email:</dt>
      <dd><%= email%></dd>
    </dl>
  </div>
</div>
</script>
<script src="module-user-promise.js"></script>

Do wyświetlenia danych o jednym użytkowniku skorzystamy z funkcjonalności szablonów z biblioteki lodash.js. Jak można zauważyć, na stronie będą mogły zostać wyświetlone: zdjęcie profilu, nazwa użytkownika i jego dane.

Teraz pora na kod JS, który większość z programistów w takiej sytuacji by napisała:

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
31
32
33
34
35
36
37
// module-user-promise.js
API = window.API || {};
 
API.User = function User(gender) {
  var userObject    = {};
  var createProfile = function () {
    $('.api-1').append(_.template($('#user-profile').html(), userObject));
  };
 
  $.getJSON('http://api.randomuser.me/?gender=' + gender, function (result) {
    userObject = result.results[0].user;
  });
 
  return {
    getName : function () {
      console.log(userObject.name.first + ' ' + userObject.name.last);
    },
    getAddress : function () {
      console.log(userObject.location);
    },
    updateAddress : function (data) {
      _.assign(userObject.location, data);
 
      if ($('.user').length) {
        $('.user').remove();
        createProfile();
      }
    },
    renderProfile : function () {
      createProfile();
    }
  };
};
 
var jan = new API.User('male');
 
jan.renderProfile();

To jest niepoprawne rozwiązanie problemu. Na pierwszy rzut oka to wszystko wygląda ok, ale mamy tutaj do czynienia z problemem danych, których nie ma w momencie inicjalizacji obiektu. Jak można zauważyć, wysyłamy zapytanie do serwisu z danymi (w tym przypadku będzie to serwis: Random User Generator) i zakładamy że te dane od razu będą dostępne w momencie wywołania metod obiektu. Niestety, ze względu na asynchroniczność kodu, dane są dostarczane po inicjalizacji obiektu co powoduje problemy i kod reaguje tak, jakby tych danych nie było.

Rozwiązanie problemu - podejście 2, właściwe

Na szczęście, rozwiązanie jest proste i opiera się na obiecankach JS. Przy tworzeniu tego typu rozwiązań trzeba założyć, że danych nie ma w momencie inicjalizacji obiektu (ten przypadek rozważamy) i może ich w ogóle nie być (tego przypadku nie będziemy rozważać w tym artykule).

Kod, który rozwiąże ten problem wygląda następująco:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// module-user-promise.js
API = window.API || {};
 
API.User = function User(gender) {
  var userObject    = {};
  var defer         = $.Deferred();
  var createProfile = function () {
    $('.api-1').append(_.template($('#user-profile').html(), userObject));
  };
 
  $.getJSON('http://api.randomuser.me/?gender=' + gender, defer.resolve);
 
  defer.then(function (result) {
    userObject = result.results[0].user;
  });
 
  return {
    getName : function () {
      defer.done(function () {
        console.log(userObject.name.first + ' ' + userObject.name.last);
      });
    },
    getAddress : function () {
      defer.done(function () {
        console.log(userObject.location);
      });
    },
    updateAddress : function (data) {
      defer.done(function () {
        _.assign(userObject.location, data);
 
        if ($('.user').length) {
          $('.user').remove();
          createProfile();
        }
      });
    },
    renderProfile : function () {
      defer.done(function () {
        createProfile();
      });
    }
  };
};
 
var jan = new API.User('male');
 
jan.renderProfile();

W powyższym kodzie użyto funkcjonalności Deferred, czyli implementacji JS Promises wg pomysłu twórców biblioteki jQuery. Co ważne, na początku stworzyliśmy obiekt defer, który będzie odpowiedzialny za reagowanie na działanie kodu. To oznacza, że w momencie wysyłania zapytania do serwisu za pomocą $.getJSON() jako callback (funkcję sukcesu) ustawiamy defer.resolve, który będzie przechowywał informacje o pomyślnym przebiegu realizacji zapytania. Następnie, za pomocą defer.then(), określamy co się wydarzy w momencie otrzymania odpowiedzi z serwisu, czyli przekazujemy dane do zmiennej prywatnej obiektu User.
W metodach publicznych obiektu User korzystamy z defer.done(). Może Ciebie dziwić dlaczego korzystam z tego w każdej funkcji osobno, ale odpowiedź jest prosta. Jako twórca tego swoistego API nie mogę być pewny, kiedy dostanę odpowiedź z serwisu i czy dane będą dostępne w momencie wywołania metody.

Podsumowanie

Mam nadzieję, że powyższa propozycja rozwiązania problemu z dostepnością danych w momencie inicjalizacji obiektu będzie pomocna dla Ciebie. Uznałem, że warto się podzielić tym rozwiązaniem, bo dzięki niemu mamy o wiele lepiej rozwiązany problem związany użytkowaniem API.

Zapraszam do komentowania kodu i dzielenia się swoimi spostrzeżeniami tutaj, za pomocą komentarzy, lub na Githubie

  • Dariusz Mydlarz

    Cześć,

    artykuł fajny, dobrze wszystko pokazane. Kłuje mnie tylko w oczy używanie słowa „funkcjonalność”.
    Za każdym razem miałeś na myśli „funkcje”. Więcej na ten temat, w przyjazny sposób przeczytasz na:
    http://wittamina.pl/funkcjonalnosc-czy-funkcja/

    Pozdrawiam!

  • Dzięki wielkie za zwrócenie uwagi. Starałem się znaleźć polskie określenie dla tego czym są JS Promises i padło na funkcjonalność. Funkcja, czy ficzer jakoś do mnie nie przemawiają w tym przypadku.

  • Promises to przecież funkcjonalność. Co najwyżej ich konkretną implementację można nazwać funkcją.