Transpilacja kodu ECMAScript 6 za pomocą Babel JS i Browserify

Artykuł dodany: 15 listopada 2015. Ostatnia modyfikacja: 17 listopada 2015.

Stopień trudności (1 - dla początkujących, 5 - dla ekspertów): 1

ECMAScript 6 edycji (znany również jako ES.next albo Harmony) jest standardem, którego specyfikację implementują inne języki w tym głównie JavaScript. Pierwszy szkic opublikowany został w lipcu 2011 roku, w sierpniu 2014 rozwój został zamrożony i skupiono się na poprawkach. 17 lipca 2015 roku ogłoszono oficjalne wydane nowego standardu. Osoby zajmujące się programowaniem webowym, w szczególności client-side, mają na pewno w świadomości jak ciężko przebiega proces implementacji standardów w samych przeglądarkach. Mijają długie miesiące o ile nie lata, zanim dana funkcja zaczyna być poprawnie obsługiwana we wszystkich browserach, a i tak pojawiają się błędy lub drobne różnice w działaniu. Aby jednak przyśpieszyć cały proces możliwe jest przetworzenie kodu pisanego przez programistę według najnowszego standardu, na kod pośredni – taki, który w danej chwili zrozumie większość urządzeń. Tym właśnie zajmuje się Babel. Jest transpilatorem kodu (code transpiler). Zawiera zbiór reguł które umożliwiają transformację jednego języka w drugi. W wersji eksperymentalnej możliwe jest nawet korzystanie z niektórych funkcji ES v7, zatem możliwości są nieograniczone. Na potrzeby artykułu zakładam, że wiesz Czytelniku czym jest menedżer pakietów npm i potrafisz z niego w podstawowym zakresie korzystać. Naszym celem będzie:

  • - przekształcenie kodu ES6 w rozumiany przez współczesne przeglądarki JavaScript
  • - wykonanie skryptów automatyzujących bez używania narzędzi typu Gulp czy Grunt
  • - minifikacja kodu wyjściowego

Przygotowanie środowiska do pracy

Aby rozpocząć pracę z naszym projektem musisz mieć dostęp do Node.js oraz npm. W zależności od systemu operacyjnego z którego korzystasz, instaluje się je z paczek lub źródeł i na potrzeby artykułu powinieneś mieć je wcześniej zainstalowane i działające. Nowy projekt inicjujemy poleceniem

> npm init

W naszym katalogu powstanie plik package.json który może mieć następującą postać:

{
  "name": "es6-example",
  "version": "1.0.0",
  "description": "Przykłady użycia ES6 w przeglądarce",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "es6",
    "transpiler",
    "babel"
  ],
  "author": "Wwwgo.pl",
  "license": "GPL-3.0"
}

W tym momencie możemy podążyć kilkoma ścieżkami instalacji o których można przeczytać w dokumentacji Babel. Najprostszą będzie oczywiście przetworzenie pliku napisanego w ES6 na stary kod ES5 bezpośrednio z konsoli.

> npm i babel-cli babel-preset-es2015
> mkdir dist src
> ./node_modules/babel-cli/bin/babel.js --presets es2015 --watch src/ --out-dir dist/

Możemy również dodać zależność babel-cli globalnie dzięki czemu wystarczy uruchomić samo polecenie “babel”:

> npm i -g babel-cli
> babel --presets es2015 --watch src/ --out-dir dist/

Utworzyliśmy dwa nowe katalogi: src będzie zawierał pliki źródłowe, dist wynik kompilacji kodu. Opcja —watch uruchamia kompilator w trybie obserwacji – wszystkie zmiany wprowadzone w katalogu źródłowym zostaną automatycznie przetworzone na kod wynikowy. Opcja —presets es2015 uruchamia zbiór wszystkich podstawowych reguł dzięki którym nasz kod jest przetwarzany. Jeżeli potrzebujemy tylko konkretnej funkcjonalności możemy tu podać pojedyncze reguły. Przykładowo “transform-es2015-classes” doda nam wyłącznie obsługę klas. Pora przetestować działanie. Utwórzmy nowy plik:

src/person.js [ECMAScript 6]

class Person {
    constructor() {
        return 'Adam';
    }
}

export default Person;

Po zapisaniu pliku zostanie on przetworzony przez Babel, w folderze dist pojawi się plik person.js o zawartości:

dist/person.js

'use strict';

Object.defineProperty(exports, "__esModule", {
    value: true
});

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Person = function Person() {
    _classCallCheck(this, Person);

    return 'Adam';
};

exports.default = Person;

W nowoczesnym JavaScript bardzo często projektuje się aplikację w postaci modułów. Dąży się do tego, aby konkretny moduł wykonywał tylko określone zadanie. Ułatwia to zarządzanie, likwiduje problem zasięgu zmiennych, ale też wspomaga znacząco pracę grupową. Bardzo popularnym projektem do obsługi modułów jest RequireJS. Umożliwia on asynchroniczne wczytywanie modułów bezpośrednio w przeglądarce i jest bardzo duża szansa, że w dotychczasowej pracy miałeś z nim Czytelniku kontakt. Tak wyeksportowany moduł moglibyśmy teraz załączyć do RequireJS i uruchomić w przeglądarce. Jednak standardów modułów jest kilka, który wybrać? Tak naprawdę to kwestia upodobania, narzuconych w firmie pracodawcy standardów.

Moduły. AMD? CommonJS? UMD?

Aby w pełni zrozumieć działanie modułów zachęcam wszystkich do przeczytania książki inżyniera Addy Osmani. Natomiast ja przedstawię wersję bardzo skróconą.

Cała koncepcja asynchronicznego wczytywania modułów jest odpowiedzią na brak takowej funkcji bezpośrednio w JS. Oczywiście pojawia się ona w ES6 w postaci wyrażenia import ale, jak widać po treści artykułu, nie możemy jeszcze z niej spokojnie korzystać. Obecnie istnieje kilka typów standardów, każdy z wygenerowanych w danym standardzie plików możemy załączyć poprzez zewnętrzny projekt (w języku angielskim nazywają się one “script loaders”). Najpopularniejszy jest wymieniony wcześniej RequireJS ale są też np.: HeadJS, yepnope.js, LABjs, ded/script.js czy SystemJS. Oprócz samego wczytywania plików umożliwiają one też dodatkowo minifikację, tworzenie pakietów, eksport zmiennych do globalnej przestrzeni. Różnią się rozmiarem, dostępnymi funkcjami i podejściem. Jest ich naprawdę dużo i każdy może wybrać ten, który mu najbardziej odpowiada.

AMD

Definicja modułu AMD wygląda następująco:

define(['modA', 'modB', function(modA, modB) {
    return {
        test: modA.foo() + modB.bar();
    }
});

Moduły A oraz B są zewnętrznymi plikami które zostaną wczytane przed uruchomieniem ciała funkcji. Sama funkcja zostanie uruchomiona natychmiast, kiedy zależności będą spełnione. Pytanie teraz, jak nasz przykładowy plik person.js skompilować jako moduł AMD? Wracamy do konsoli:

npm install babel-plugin-transform-es2015-modules-amd
babel --presets es2015 --watch src/ --out-dir dist/ --plugins transform-es2015-modules-amd --modules amd

Jeżeli teraz spojrzymy na zawartość pliku person.js będzie się ona prezentować następująco:

dist/person.js

'use strict';

define(['exports'], function (exports) {
    Object.defineProperty(exports, "__esModule", {
        value: true
    });

    function _classCallCheck(instance, Constructor) {
        if (!(instance instanceof Constructor)) {
            throw new TypeError("Cannot call a class as a function");
        }
    }

    var Person = function Person() {
        _classCallCheck(this, Person);

        return 'Adam';
    };

    exports.default = Person;
});

CommonJS

Definicja CommonJS wygląda zgoła odmiennie:

var modA = require('modA');
var modB = require('modB');

exports = {
    test: modA.foo() + modB.bar()
};

Funkcja require() odpowiada za wczytywanie zależności, “exports” eksportuje wartości na zewnątrz danego modułu, czyli może zawierać przykładowo zbiór funkcji widocznych jako publiczne. Podobnie jak w przypadku AMD możemy zażyczyć sobie eksportu naszego pliku person.js do postaci CommonJS:

npm install babel-plugin-transform-es2015-modules-commonjs
babel --presets es2015 --watch src/ --out-dir dist/ --plugins transform-es2015-modules-commonjs --modules commonjs

dist/person.js

'use strict';

Object.defineProperty(exports, "__esModule", {
    value: true
});

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Person = function Person() {
    _classCallCheck(this, Person);

    return 'Adam';
};

exports.default = Person;

Simplified CommonJS wrapping

Istnieje również połączenie obu metod, które powinno bardzo przypaść do gustu programistom lubiącym wzorzec modułu (module pattern). Po angielsku nosi nazwę “simplified CommonJS wrapper” i nawet nie staram się tłumaczyć wyrażenia na polski.

define(function(require, exports, module){
    var modA = require('modA');
    var modB = require('modB');

    // lub module.exports
    exports = {
        test: modA.foo() + modB.bar()
    };
});

Do funkcji przekazywane są 3 argumenty (opcjonalnie ale kolejność jest ważna). Podejście takie przydaje się bardzo, kiedy potrzebujemy wczytać bardzo dużą liczbę modułów. W przypadku AMD ich dołączanie do funkcji sprawia, że cały kod staje się mało przejrzysty. Ułatwia nam pracę gdy posiadamy już kod CommonJS i chcemy go tylko skopiować.

UMD (Universal Module Definition)

UMD jest połączeniem wszystkich metod tak aby kod działał wszędzie, z każdym typem loadera, nieważne czy stosujemy AMD, czy CommonJS. Powstał aby ujednolicić kod i pogodzić obie grupy programistów. Instalujemy go poleceniem:

npm install babel-plugin-transform-es2015-modules-umd
babel --presets es2015 --watch src/ --out-dir dist/ --plugins transform-es2015-modules-umd --modules umd

Plik wynikowy oczywiście będzie największy:

dist/person.js

'use strict';

(function (global, factory) {
    if (typeof define === "function" && define.amd) {
        define(['exports'], factory);
    } else if (typeof exports !== "undefined") {
        factory(exports);
    } else {
        var mod = {
            exports: {}
        };
        factory(mod.exports);
        global.person = mod.exports;
    }
})(this, function (exports) {
    Object.defineProperty(exports, "__esModule", {
        value: true
    });

    function _classCallCheck(instance, Constructor) {
        if (!(instance instanceof Constructor)) {
            throw new TypeError("Cannot call a class as a function");
        }
    }

    var Person = function Person() {
        _classCallCheck(this, Person);

        return 'Adam';
    };

    exports.default = Person;
});

Teraz, kiedy przedstawiłem kilka różnych definicji modułów nie masz wrażenia Czytelniku że jest ich po prostu za dużo? Owszem, każdy z nich może się w pewnym przypadku przydać, dobrze wiedzieć jak wyeksportować nasze pliki ES6 do konkretnego modułu ale czy nie wolałbyś podejścia jakie prezentuje serwer Node.js? Bez owijania kodu w definicję modułu, z automatycznym parsowaniem i transpilowaniem przez Babel.

Browserify na pomoc modułom

Browserify to bardzo ciekawy projekt który umożliwia pracę na plikach w identyczny sposób jak ma to miejsce w serwerze Node.js. Umożliwia nam wyeliminowanie pośrednika w postaci RequireJS czy innego “script loadera” a co więcej, uzyskujemy dzięki niemu dostęp do natywnych modułów Node.js. To jeszcze nie koniec, ponieważ plik wynikowy może być poddany przeróżnym transformacjom, w tym przekształceniu przez Babel. Browserify warto zainstalować globalnie:

> npm install -g browserify

Spośród wielu dostępnych opcji warto zwrócić uwagę na następujące:

--outfile, -o     Zapisuje wynik do podanego pliku
--transform, -t   Dokonuje transformacji na plikach
--global-transform=MODULE, -g MODULE Dokonuje transformacji globalnej, po zakończeniu innych transformacji

Sprawdźmy czy nasz plik zostanie przetworzony. Utwórzmy nowy plik:

src/start.js

var path = require('path');
console.log(path.normalize('/foo/bar//baz/asdf/quux/..'));

Pora teraz uruchomić Browserify:

browserify src/start.js -o dist/start.js

Jeżeli wszystko przebiegło prawidłowo w katalogu “dist/” powinniśmy mieć nasz przekształcony plik start.js. Jak widać jest dość duży, zostały dołączone wszystkie niezbędne funkcje i moduły, w tym moduł Node.js – path. Przechodzimy nareszcie do uruchomienia pliku w przeglądarce. W katalogu głównym projektu możemy stworzyć przykładowy plik index.html:

index.html

<!DOCTYPE html>
<html>
<head> 
    <meta charset="UTF-8"/>
    <title>Przykład użycia Babel JS w przeglądarce</title>
    <script src="dist/start.js"></script>

</head>
<body>
    <p>Przykładowa treść.</p>
</body>
</html>  

Pamiętaj czytelniku uruchomić konsolę w przeglądarce. Pojawi się w niej wynik funkcji path.normalize czyli:

/foo/bar/baz/asdf

Plik wynikowy ma u mnie 8.7kB, podczas gdy sam zminifikowany RequireJS – 15.2kB. Spora oszczędność ale nierówna walka. Nasz plik wynikowy też powinien być zmniejszony. Dokonamy tego aplikując transformację globalną, tranformatą “uglifyify”. Większość projektów bazujących na Browserify kończy się na “ify”.

Uglifyify

> npm i -g uglifyify
> browserify -g uglifyify src/start.js -o dist/start.js

Nasz plik zmalał do 4.1kB, praktycznie zerowym wysiłkiem, choć przy dużej liczbie dołączanych plików minifikacja może trochę zająć. Sugeruję wykonywać ją na finalnym pliku który chcemy wgrać na produkcję.

Watchify

No dobrze, uruchamianie Browserify po każdej zmianie w pliku JS jest odrobinę męczące. Przydałby się odpowiednik opcji —watch z Babel. Na pomoc przychodzi pakiet “watchify”.

> npm i -g watchify
> watchify -v -g uglifyify src/start.js -o dist/start.js

Opcja -v, —verbose wyświetla informacje o zapisanym pliku:

4218 bytes written to dist/start.js (0.26 seconds)
4219 bytes written to dist/start.js (0.03 seconds)

Proces obserwuje nasze pliki i po okresie czasu ustalonym w konfiguracji (domyślnie 600ms) od każdej zmiany, kompiluje aplikując transformacje. Automatycznie pomijany jest katalog “node_modules”. Dostępne są wszystkie opcje które posiada Browserify.

Browserify i Babel

Teraz brakuje nam już tylko jednej rzeczy do przekształcenia ES6 i uważny Czytelnik z pewnością domyślił się już, że chodzi o “babelify”.

> npm install --save-dev babelify

Zmodyfikujmy teraz nasze pliki źródłowe aby korzystały z funkcji ECMAScript 6.

src/person.js

export default class Person {
    constructor(val) {
	      this.personName = val;
    }
    get name() {
	      return this.personName;
    }
    set name(val) {
	      this.personName = val;
    }
}

src/start.js

import {normalize} from 'path'
import Person from './person'

console.log(normalize('/foo/bar//baz/asdf/quux/..'));

let p = new Person('Adam');
alert('A jego imię to ' + p.name);

p.name = 'Ewa';

console.log('Jej z kolei ' + p.name);
> watchify -v -t [ babelify --presets [ es2015 ] ] -g uglifyify src/start.js -o dist/start.js

Jeżeli teraz uruchomimy nasz index.html w przeglądarce, powinien pojawić nam się alert, a w konsoli powinny być zalogowane dwie wartości. W katalogu dist/start.js jest zminifikowany, transpilowany przez Babel plik wynikowy. Warto usunąć uglifyify i popatrzeć jak nasz plik wygląda w pełnej krasie. Ewentualne błędy kompilacji logowane są też od razu w terminalu. Warto zrobić jeszcze jedną rzecz. Mianowicie istnieje możliwość uruchamiania skryptów bezpośrednio przez npm. W tym celu otwórzmy w edytorze plik package.json i dodajmy nasz dotychczasowy kod:

"scripts": {
    "watchify": "watchify -v -t [ babelify --presets [ es2015 ] ]  src/start.js -o dist/start.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
npm run watchify

Poleceniem `npm run [jeden z podkluczy]` uruchomiliśmy watchify ze wszystkimi komendami. Ma to tę zaletę, że nie trzeba pisać ręcznie komend za każdym razem gdy wracamy do projektu. W niektórych edytorach, na przykład Netbeans, jest możliwość uruchomienia tych skryptów w tle (prawy przycisk myszy na package.json -> npm Scripts -> watchify). W skryptach można wykonywać również polecenia systemowe jak kopiowanie plików, zatem w rozbudowanym projekcie wszystko mamy pod ręką, pełną automatyzację procesów.

Podsumowanie

Babel w połączeniu z Browserify są potężnymi narzędziami. Dobrze zastosowane potrafią zmienić całkowicie sposób pisania kodu. Może być on od teraz nowoczesny i przejrzysty. Kod łatwo przenosić pomiędzy projektami, transformaty znacznie ułatwiają pracę i zalecam przejrzeć jakie jeszcze oprócz przedstawionych są dostępne. Babel umożliwia też niejako w zestawie, wykorzystanie JSX i React, zatem programiści korzystający z tych rozwiązań mają bardzo ułatwione zadanie. Moim zdaniem, dopóki przeglądarki nie zaczną w pełni wspierać ECMAScript 6, Babel jest najbardziej sensownym rozwiązaniem do użytku od zaraz.

Pliki źródłowe do artykułu dostępne są również na github.com

Komentarze

Nie ma jeszcze żadnych komentarzy do wyświetlenia. Może chcesz zostać pierwszą osobą która podzieli się swoją opinią?

Dodaj komentarz

*
Nazwa zostanie wyświetlona wraz z komentarzem. Możesz też utworzyć nowe konto w serwisie, dzięki czemu uzyskasz dodatkową funkcjonalność.
*
Akceptowana jest ograniczona składnia Textile. Wszystkie tagi HTML zostaną usunięte.