chevron-left chevron-right

Piotr Nalepa Frontend, architektura i AI

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

Ta wersja
blog.piotrnalepa.pl/2014/04/17/js-leniwa-inicjalizacja-danych-w-module-za-pomoca-js-promises
Opublikowano
Autor
, Staff Software Engineer
Kategoria
JavaScript
Uwagi
Napisz do mnie · Napisz na X

Ten wpis ma 12 lat. Od tego czasu przeglądarki i narzędzia mogły się zmienić, więc zanim użyjesz kodu, sprawdź go z aktualną dokumentacją.

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.

Sekcja 1Opis 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.

Sekcja 2Rozwią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:

Przykład 1HTML
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:

Przykład 2JavaScript
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.

Sekcja 3Rozwią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:

Przykład 3JavaScript
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.

Sekcja 4Podsumowanie

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

Udostępnij
Uwagi

O autorze

Staff Software Engineer

Piszę na tym blogu od 2009 roku, a strony i aplikacje webowe tworzę zawodowo od 14 lat. Obecnie w Bishop Fox prowadzę frontend platformy Cosmos, od samego początku projektu. Wcześniej pracowałem m.in. w OLX, KUDO, Auto1 i eZ Systems. Piszę tu o tym, czego nauczyłem się w praktyce: o JavaScripcie, TypeScripcie, Reakcie, CSS-ie i architekturze frontendu. Występowałem też jako prelegent na konferencjach w Polsce i Europie. Masz pytanie albo pomysł na wpis? Daj znać!