Codebake Blog

Modułowy JS bez ES6

Od wersji ES6 w JavaScript możemy korzystać z wielu ciekawych rozwiązań rozszerzających pisane przez nas skrypty. Często są to różnego rodzaju zapożyczenia ze współczesnych, na bieżąco rozwijanych języków programowania, np C#. Zmiany w kolejnych odsłonach ECMAScript stopniowo unowocześniają ten język. Dopóki jednak te zmiany wejdą do nowej specyfikacji ES i zostaną zaimplementowane w nowych przeglądarkach, jakoś trzeba radzić sobie bez nich.

Przy okazji, że w swoim projekcie muszę dostosować skrypty do starych przeglądarek, które o ES6 nie słyszały, to był to dobry moment na zagłębienie się w czysty JS i niektóre z jego sprawdzonych rozwiązań. Dało mi to wiedzę o podstawach działania tego języka i pozwoliło głębiej go zrozumieć. Przykładem wzorca, którego używam na co dzień, jest Revealing Module, który wprowadza namiastkę modułowości i poziomowania dostępu do obiektów, gdy nie możemy użyć import-export z ES6, ani narzędzia Babel.

Implementacja

Poniżej krok po kroku stworzę od zera przykładowy funkcjonalny moduł do zastosowania i rozbudowania pod własne potrzeby w swoim projekcie. Zacznijmy od komendy:

'use strict'

Dodanie jej włącza tzw tryb ścisły. Zastosowanie tego mechanizmu pomaga w wyłapywaniu prostych, powtarzających się błędów popełnianych przez programistów. W konsoli dostaniemy nowe komunikaty, np. przy próbach nadpisania zastrzeżonych słów, użyciu niezadeklarowanych zmiennych, duplikowaniu ich nazw itp. Musimy pamiętać, że tryb ten zostanie uruchomiony tylko jeżeli powyższe wyrażenie zostanie dodane na początku pliku lub ciała wybranej funkcji. Następnie:

'use strict'
var MyModule = (function () {
  // wnętrze zamkniętego scope'a
  return {}
})()

Stosujemy tutaj Immediately-Invoked Function Expression (IIFE) – deklarację anonimowej funkcji obejmujemy w nawiasy zamieniając ją na wyrażenie funkcji. Dodajemy kolejne nawiasy, które natychmiast ją uruchomią. Zamknięty scope funkcji pomoże nam w ograniczaniu dostępu do utworzonych wewnątrz metod i propercji. Z kolei to co zwrócimy przez return w obiekcie do zmiennej MyModule będzie publicznie dostępne poza modułem.

Kolejnym krokiem będzie wypełnienie ciała funkcji metodami i propercjami:

'use strict'
var MyModule = (function () {
  // -------------------------------- private props
  var _options = {
    // wartości domyślne opcji modułu
    JobType: jobTypeEnum.Suspended,
    Title: 'default title',
  }
  var $input

  // -------------------------------- public props
  var jobTypeEnum = {
    Ready: 1,
    Suspended: 2,
  }

  // -------------------------------- private methods
  var _initInternal = function () {
    // ciało funkcji prywatnej
    $input = document.getElementById('#my-input')
  }

  // -------------------------------- public methods
  var init = function () {
    // ciało funkcji publicznej
    _initInternal()
  }

  var printTitle = function () {
    console.log(_options.Title)
  }

  return {}
})()

Wstępnie zakładamy, że nie możemy używać rozwiązań z ES6, dlatego var zastępuje tutaj let oraz const. W liniach odseparowanych komentarzami umieszczamy zmienne i metody o zakładanym poziomie dostępności. Do pól prywatnych zaliczamy wszystkie zmienne niepotrzebne poza modułem, czyli m.in. obiekt opcji, wyszukane elementy DOM, obiekty jQuery, które załadujemy w metodzie initInternal. Dla czytelności nazwy metod i zmiennych piszemy w camelCase i poprzedzamy podkreślnikiem _, a nazwy obiektów DOM i jQuery znakiem $.

Pola i metody publiczne to funkcje, obiekty i zmienne potrzebne skryptom na zewnątrz modułu. Umieścimy wśród nich zmienną zastępującą typ wyliczeniowy (enum) oraz metodę init, którą z kolei rozszerzamy do formy:

var init = function (options) {
  if (options) {
    _options = Object.assign(_options, options)
  }
  _initInternal()
}

W ten sposób prywatną zmienną \_options z domyślnymi wartościami nadpisujemy opcjami z parametru funkcji init.

Na końcu rozszerzamy obiekt zwracany przez moduł:

return {
  JobTypeEnum: jobTypeEnum,
  Title: title,
  Init: init,
  PrintTitle: printTitle,
}

Otrzymujemy API, z którego możemy już korzystać w pozostałej części aplikacji:

MyModule.Init({
  JobType: MyModule.JobTypeEnum.Ready,
  Title: 'my title',
})

MyModule.PrintTitle()

W praktyce najwygodniej jest zmienną MyModule zadeklarować w obiekcie window. Wtedy w narzędziu dev tools dowolnej przeglądarki, bez zatrzymywania skryptu w debuggerze, możemy sprawdzić, że to działa – zwrócony obiekt nie daje nam dostępu do funkcji prywatnych ani obiektu opcji, za to umożliwia korzystanie z publicznych.

Rozszerzenia

Z założenia enumy powinny być tylko do odczytu. Żeby zabezpieczyć się przed nadpisaniem ich poza modułem możemy użyć metody Object.assign() do sklonowania oryginalnego obiektu, za każdym razem, gdy odwołujemy się do opcji enuma. Może to jednak skutkować obniżeniem wydajności w niektórych wypadkach, jeżeli obiekt enuma pobieramy wielokrotnie, np. w pętli.

return {
  JobTypeEnum: Object.assign({}, _jobTypeEnum),
  Title: title,
  Init: init,
  PrintTitle: printTitle,
}

Czasami konieczna jest współpraca dwóch lub więcej modułów, by skutecznie mogły one wymieniać się danymi. Zwykle wystarczy ograniczyć się do podania zmiennej z modułem do opcji innego przez metodę Init(). Warto tez zainteresować się wzorcami takimi jak Mediator, Observer, PubSub, które mogą zarządzać i sterować wymianą danych między modułami bez uzależniania ich wzajemnie od siebie.

Podsumowanie

Powyższe rozwiązanie znacznie ułatwiło mi życie w projekcie, który wymagał użycia starszych przeglądarek. Według mnie Revealing Module jest najbardziej przydatnym wzorcem wśród znanych technik pisania modułowego skryptu w JS.

Źródła