ES6+ w przeglądarce – jak używać?

JavaScript bardzo szybko się rozwija. Na tyle szybko, że raz w roku Ecma International publikuje nową specyfikację języka rozszerzającą możliwości JavaScript o nowe funkcje. Zdarza się również, że stare funkcje przestają być wspierane lub zostają zastąpione. Nowa specyfikacja przed publikacją poddawana jest dokładnej analizie przez komitet składający się między innymi z przedstawicieli największych przeglądarek internetowych. Gdy nowy standard zostaje opublikowany, niemal natychmiast rozpoczyna się praca nad obsługą nowych funkcji w przeglądarkach. Niestety wersji przeglądarek jest wiele i większość z nich nie obsługuje nowych funkcjonalności. Są również takie (jak np. Internet Explorer), które nie są już wspierane przez twórców, a mimo wszystko są używane przez użytkowników. Rodzi się zatem pytanie – Jak używać nowego standardu języka JavaScript (ES6 +) aby nasz kod działał na wszystkich używanych przez użytkownikach przeglądarkach?

W czasach gdy najczęściej wykorzystujemy Reacta, Angulara oraz Vue.js nie musimy się tym za bardzo przejmować – ich domyślne konfiguracje zawierają wszystko, co jest potrzebne do użycia nowego standardu. Załóżmy jednak przez chwile, że musimy sami zatroszczyć się o kompilacje naszego kodu. Na potrzeby tego wpisu napiszemy prosty skrypt w czystym JavaScripcie (Vanilla.js), który użyjemy w pliku HTML.

Stworzyłem prosty kalkulator potrafiący dodawać, odejmować, mnożyć oraz dzielić. Kod został napisany z wykorzystaniem elementów standardu ES6 (jak np. import, export). Dodatkowo utworzyłem plik index.js wykonujący obliczenie za pomocą przygotowanego kalkulatora.

const sum = (a, b) => a+b;
const substract = (a, b) => a-b;
const multiply = (a, b) => a*b;
const divide = (a, b) =>  a/b;

const Calculator = (type = null, a, b) => {
    if(!a || !b)  new Error('Missing numbers');
    switch(type){
        case 'sum':
            return sum(a, b);
        case 'substract':
            return substract(a, b);
        case 'multiply':
            return multiply(a, b);
        case 'divide':
            return divide(a, b);
        default:
            throw new Error('Invalid Math type');
    }
};

export default Calculator;
import Calculator from "./Calculator";
Calculator('sum', 1, 2);

Spróbujmy uruchomić naszą implementację. Stwórzmy podstawowy plik index.html i wewnątrz znacznika <body> (na jego końcu) dodajmy znacznik <script> w którym użyjemy nasz kalkulator (plik index.js). Spodziewamy się, że po otwarciu strony w przeglądarce kod JavaScript się wykona i w konsoli deweloperskiej ujrzymy wynik. Tymczasem konsola wskazuje nam błąd: import użyty jest poza modułem.

Jeszcze do niedawna niemożliwym było przeprowadzenie importu zależności między plikami JavaScript. Wraz z rozwojem języka programiści zaproponowali koncepcję w której pojedynczy plik traktowany jest jako moduł – nie zawiera wspólnych cech z innym modułem (jest enkapsulowany), lecz umożliwia eksportowanie własnych funkcji do użytku poza nim. Do najpopularniejszych ekosystemów modułowych zaliczamy CommonJS oraz ES Modules.

Ekosystem modułowy nie jest chętnie akceptowany przez przeglądarki, gdyż każdy z projektów proponujących własne rozwiązanie różni się od siebie. W standardzie ES6 zaproponowane zostały ES Modules, które zostały przyjęte jako standard w przeglądarkach.

Wróćmy jednak do naszego problemu. System ES Modules pozwala na eksport oraz import funkcji między modułami przy użyciu przyjaznego zapisu. Dodatkowo stworzonych zostało kilka typów zarówno eksportu jak i importu dzięki którym programiści mają większe możliwości w zarządzaniu swoimi modułami. Ten system, podobnie jak inne, nie jest wspierany przez wszystkie przeglądarki.

// Przykładowy import
import {dependencyName} from '../path/to/dependency';

// Przykładowy eksport
export default MyComponent;

Obecnie istnieje możliwość bezpośrednio użycia podejścia modułowego bezpośrednio w pliku HTML. Skrypt należy zadeklarować z atrybutem type ustawionym jako module. Nie jest to jednak rozwiązanie wspierane przez starsze przeglądarki. Nadal poleca się korzystać z kompilatorów i bundlerów z powodów optymalizacyjnych tego podejścia.

Przedstawiony powyżej problem dotyczy wsparcia nowych elementów standardu ES6. Załóżmy, że dokonaliśmy tłumaczenia kodu na standard rozumiany przez przeglądarki. To jednak nie rozwiązuje problemu bo mimo, że kod jest już zrozumiały to nadal pozostaje problem użycia kodu z innego pliku JavaScript. Okazuje się, że do rozwiązania mamy dwa problemy, a nie jak pierwotnie zakładaliśmy jeden.

Do rozwiązania naszych problemów skorzystamy z dwóch rzeczy. Pierwsza z nich to kompilator Babel, który przepisze napisany przez nas (w nowym standardzie) kod zgodnie z naszą konfiguracją. Najczęściej transpilacja jest przeprowadzona do standardu ES5. Tak przetranspilowany kod złączymy w jeden plik przy pomocy Webpacka.

Uwaga! Dalsza część wpisu zakłada, że posiadasz zainstalowane środowisko Node’js wraz z menadżerem do zarządzania zależnościami (np. npm, lub yarn).

Babel (ES6 -> ES5)

Nasz kompilator działa z wieloma narzędziami – systemami do budowania projektów, narzędzi do testowania, frameworkami i nie tylko. Z racji, że tworzymy czysty projekt to wykorzystamy oficjalnego klienta Babela. Oprócz tego potrzebna nam będzie paczka główna.

npm install --save-dev @babel/core @babel/cli

Aby ułatwić sobie używanie nowego narzędzia dodamy nowe skrypty w naszym pliku package.json. Pierwszy skrypt, build zczyta pliki z folderu src, uruchomi transpilację i zapisze je w katalogu dist.

"scripts": {
  ...
  "build": "babel src -d dist",
  ...
}

Ostatnią rzeczą którą musimy wykonać jest dodanie pliku konfiguracyjnego .babelrc w głównym katalogu projektu. To właśnie w nim musimy powiedzieć kompilatorowi do jakiej wersji języka JavaScript chcemy aby nasz kod został przetranspilowany. Interesuje nas wsparcie dla standardu ES5+ zatem najłatwiej będzie wykorzystać gotowy preset, który automatycznie da nam to, co potrzebujemy.

Preset to gotowy zestaw rozszerzeń potrzebnych do poprawnej transpilacji kodu do określonego standardu. Zestaw preset-env zawiera wszystkie potrzebne zależności aby używać najnowszego standardu EcmaScript nie martwiąc się o nic więcej.

npm install --save-dev @babel/preset-env

W naszym nowo utworzonym pliku .babelrc musimy zainicjalizować użycie zainstalowanego presetu. Oczywiście możemy wszystko konfigurować samodzielnie i instalować każdy plugin z osobna. Zachęcam Was do eksperymentowania z konfiguracją na własną rękę w oparciu o oficjalną dokumentację.

{
  "presets": ["@babel/preset-env"]
}

Teraz wystarczy, że wykonamy skrypt od zbudowania naszego projektu (npm run build), a następnie podmienimy ścieżkę skryptu w naszym pliku index.html na index.js z katalogu dist. Warto zapoznać się z zawartością pliku Calculator.js po transpilacji aby zobaczyć w jaki sposób kod został przepisany.

Gdy uruchomimy naszą stronę w konsoli zobaczymy kolejny błąd (o którym krótko napisałem już wyżej) dotyczący braku definicji metody require. Nasz kod faktycznie został przetłumaczony, lecz nadal występuje w osobnych plikach, które odwołują się do siebie za pośrednictwem metody require(). Jako ciekawostkę dodam, że gdybyśmy uruchomili nasz index.js w środowisku Node’js to kod wykonałby się prawidłowo. Naszym celem jest połączyć wynikowy kod w jeden plik.

Babel domyślnie tłumaczy kod do systemu modułowego CommonJS, który odwołuje się do innych plików za pośrednictwem metody require(). CommonJS wykorzystywany jest również w implementacji Node.js.

Webpack

Nasze zadanie jest proste – przetranspilowane pliki scalamy w pojedynczy plik. Aby to osiągnąć musimy skorzystać z tzw. bundlerów – narzędzi, które poddają wskazane pliki transformacji (np. minifikacji) i scalają w jeden plik wynikowy. Do najpopularniejszych zaliczamy Webpack, Parcel oraz RollUp.

W tym wpisie skorzystamy z Webpacka. Zacznijmy standardowo od instalacji potrzebnej zależności.

npm install --save-dev webpack webpack-cli

Webpack to narzędzie o bardzo wielu możliwościach. Dzięki jego elastyczności można transformować pliki różnych rozszerzeń. Skrypty można poddać minifikacji, dodać im mapy, a nawet usunąć nieużywany kod. Podobnie jest ze stylami – dostępnych jest wiele loaderów, które automatycznie poprawiają nasz kod. To tylko kropla w morzu zatem zachęcam Was do zapoznania się z dokumentacją.

Loader to skrypt, który dokonuje transformacji na wskazanych plikach. Każda zasada zdefiniowana w konfiguracji może używać kilku loaderów w celu wykonania różnych transformacji na plikach. Warto również napomnieć, że loader może być modyfikowany przy pomocy opcji,

Wiemy już czym są loadery zatem intuicja powinna podpowiadać nam, że do rozwiązania naszego problemu również będziemy jeden z nich potrzebować. Skorzystajmy z faktu, że transpilacja jest wykonywana przez Babela i zainstalujmy specjalny loader dopełniający to co już zrobiliśmy. Zainstalujmy babel-loader.

npm install --save-dev babel-loader

Mamy wszystko co potrzebujemy. Dodajmy w głównym katalogu projektu plik konfiguracyjny Webpacka i nazwijmy go webpack.config.js. W obiekcie konfiguracyjnym musimy zdefiniować zasadę, która będzie dotyczyła plików o rozszerzeniu .js. Oprócz wskazania rozszerzenia musimy zadeklarować loader, który chcemy użyć – w naszym przypadku będzie to babel-loader. Dodajmy jeszcze jedną rzecz – powiedzmy Webpackowi by nie brał pod uwagę plików z katalogu zależności node_modules. Cała zawartość konfiguracji webpack.config.js znajduje się poniżej.

module.exports = {
    module: {
        rules: [
            { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" }
        ]
    }
};

Możesz przypadkiem zastanawiać się czym jest ten dziwny zapis /\.js$/ (bierze wszystkie pliki z rozszerzeniem .js) lub /node_modules/. Jest to wyrażenie regularne, które składa się z łańcucha symboli. Łańcuch może posiadać również symbole specjalne. W naszym przypadku używamy kilku symboli specjalnych:

/ – inicjuje wyrażenie regularne. Wszystko co znajduje się wewnątrz dwóch takich symboli jest traktowane jako łańcuch symboli.
\ – poprzedza znak specjalny, który chcemy by nie był traktowany wyjątkowo, tylko jako zwykły znak
$ – oznacza, że ciąg znaków znajdujący się przed znakiem $ powinien kończyć łańcuch (czyli rozszerzenie .js powinno być na końcu)

Temat wyrażeń regularnych jest bardzo obszerny. Zachęcam do poczytania sobie więcej na własną rękę.

Zdefiniujmy jeszcze konfigurację wyjścia. Do tego użyjemy pola output w którym podamy ścieżkę do zapisu wynikowego pliku oraz jego nazwę. Nie definiujemy katalogu w którym znajdują się pliki źródłowe, gdyż domyślnie ustawiony jest katalog src.

module.exports = {
    output: {
        publicPath: "/dist/",
        filename: "scripts.js"
    },
    module: {
       ...
    }
};

Nasza podstawowa konfiguracja jest gotowa. Ponieważ zdecydowaliśmy się korzystać z bundlera to nie jest nam już potrzebna zależność @babel/cli. Usuńmy ją.

npm uninstall @babel/cli

Przydałoby się jeszcze zmienić skrypt uruchamiający budowanie naszego projektu w package.json.

 "scripts": {
    ...
    "build": "webpack",
    ...
  },

Zauważmy, że w folderze dist powstał pojedynczy plik index.js, który zaimportowany w pliku HTML powinien w konsoli deweloperskiej zwrócić wartość (jeśli implementacja była niezmieniana) równą 3.

Gratulacje! Problemy, które napotkaliśmy zostały rozwiązane, a kod napisany w standardzie ES6 jest w pełni działający w przeglądarce.

Jeśli zdarzyło się, że Twój wynik różni się od mojego to w pierwszym kroku zachęcam Cię do prześledzenia wpisu jeszcze raz, a gdy uznasz, że nie widzisz błędu skorzystaj z gotowej konfiguracji z repozytorium.

Radek

Radek

Front-End Software Engineer w poznańskim Allegro. Powiązany z programowaniem od przeszło 15 lat. Zainteresowany zapewnianiem jakości oraz optymalizacją aplikacji webowych. Miłośnik pieszych wycieczek górskich. Po godzinach lubi pograć na Nintendo Switch.