VRath – z Reactem w parze cz. 3

W ostatnim wpisie udało mi się pokazać, że React i A-Frame potrafią ze sobą współpracować. Nadszedł czas na finalny wpis dotyczący „bratania” obu frameworków w projekcie VRath.

W poprzednim wpisie pokazałem na prostym przykładzie, że dzięki HTML-owej strukturze obiektów w A-Frame, można dość łatwo połączyć go z komponentami React’a. Wspomniany przeze mnie przykład ma jednak pewną wadę: właściwości komponentów są podane „na sztywno”, nie są przekazywane z góry. Zanim poprawię swój błąd, chciałbym jeszcze wrócić do środowiska i usprawnić kilka rzeczy.



Webpackowe poprawki

Ostatnim razem dołączyłem do projektu bibliotekę webpack-dev-server, która pozwala błyskawicznie postawić lokalny serwer, jednakże nie zwróciłem uwagi na to, że nie działa auto-przeładowanie po każdym zapisie pliku. Błąd jest spowodowany tym, że webpack domyślnie serwuje paczkę z głównego katalogu serwera czyli z /, natomiast mój plik znajduje się w katalogu /build/js. Na szczęście serwer może czerpać opcje z pliku konfiguracyjnego webpacka, a więc dołączam do webpack.config.js następujący wpis:

devServer: {
	publicPath: "/build/js/"
}

Należy pamiętać, że podana ścieżka musi się kończyć slashem „/”. Z nadmiarowych rzeczy pozbywam się również podawania ścieżki do configa webpacka, ponieważ jego nazwa jest taka jak domyślna w webpacku, tak więc parametry --config są w moim przypadku zbędne.

Instalacja ESLinta

Obecnie najpopularniejszym linterem dla JavaScriptu jest ESLint. Narzędzie to zostało stworzone przez Nicholasa C. Zakasa i dzięki prostej składni oraz systemowi wtyczek, bardzo szybko zdetronizowało takie narzędzia jak JSLint czy JSHint. ESLint służy do statycznej analizy kodu i potrafi za pomocą zdefiniowanych przez użytkownika reguł na sprawdzenie kodu pod kątem błędów leksykalnych, nieużywanych obiektów czy chociażby dobrych wzorców pisania. Przyznam szczerze, że nie wyobrażam sobie obecnie pisania aplikacji w JSie bez takiego lintera i to nie tylko pod kątem dużych projektów. ESLint po prostu oszczędza czas i zastępuje rozbudowane IDE. W projektach w których biorą udział całe zespoły programistów ESLint pozwala wyrobić pewien standard kodowania i zmusić wszystkich do trzymania się jednego stylu. Przyda się więc także i u mnie, czas na instalację:

yarn add eslint --dev

W przypadku większej ilości projektów warto zainstalować ESLinta globalnie w systemie. Po udanej instalacji pora na konfigurację, w tym celu w terminalu wpisuję:

./node_modules/.bin/eslint --init

Uruchomi to prosty konfigurator, który może poprowadzić nas za pomocą kilku pytań do określenia reguł naszego projektu. Możliwe jest również wykorzystanie rekomendowanych reguł, lub też analiza kodu pod kątem zastosowanych bibliotek i wersji JSa. ESLint jest w stanie także doinstalować brakujące pluginy na tym etapie np. w przypadku twierdzącej odpowiedzi na pytanie o korzystanie z Reacta. Wygenerowany plik będzie miał rozszerzenie w zależności od wyboru użytkownika, w moim przypadku będzie nazywał się .eslintrc.json:

{
    "env": {
        "browser": true,
        "commonjs": true,
        "es6": true,
        "node": true
    },
    "extends": "eslint:recommended",
    "parserOptions": {
        "ecmaFeatures": {
            "experimentalObjectRestSpread": true,
            "jsx": true
        },
        "sourceType": "module"
    },
    "plugins": [
        "react"
    ],
    "rules": {
        "indent": [
            "error",
            4
        ],
        "linebreak-style": [
            "error",
            "unix"
        ],
        "quotes": [
            "error",
            "double"
        ],
        "semi": [
            "error",
            "always"
        ]
    }
}

W powyższym configu znajduje się zapis: "extends": "eslint:recommended". ESLint z pomocą tej reguły rozszerzy moje „lokalne” o reguły rekomendowane przez twórców ESLinta. Podobne rekomendowane reguły zostały stworzone do Reacta, dlatego zapis ten zmieniam na:

"extends": ["eslint:recommended", "plugin:react/recommended"]

Reguły dla Reacta zostały doinstalowane w momencie instalacji ESLinta, co jest bardzo wygodne w tej konfiguracji. Pora więc sprawdzić czy mój kod przechodzi przez linter. Aby to zrobić posłużę się dodatkowym loaderem do webpacka:

yarn add eslint-loader --dev

Następnie muszę zmodyfikować regułę dla plików z rozszerzeniem .js i .jsx w webpack.config.js:

rules: [
    {
        test: /\.(js|jsx)$/,
        exclude: [/node_modules/],
        use: [
            {
                loader: "babel-loader",
                options: {
                    presets: [
                        ["env", {
                            "targets": {
                                "browsers": ["last 2 versions", "ie >= 11", "Android >= 5"]
                            }
                        }],
                        ["react"]
                    ] 
                }
            },
            {
                loader: "eslint-loader",
            }
        ]
    }
]

Ważne jest aby umieścić loadera w odpowiedniej kolejności, ponieważ Webpack czyta klauzulę use z dołu do góry. Tak więc zanim dojdzie do transpilacji babelem, pierwszy zostanie uruchomiony ESLint. I ma to oczywiście sens, ponieważ interesuje mnie jakość kodu pisanego przeze mnie, a nie zminifikowanej paczki. Teraz gdy wykonam w terminalu polecenie npm run dev najpierw odpalony zostanie ESLint. Chciałbym jednak móc uruchamiać ESLinta autonomicznie, bez całego procesu transformacji kodu, dlatego też do package.json dopisuje skrypt dla ESLinta:

"scripts": {
    "dev": "webpack",
    "production": "webpack -p",
    "server": "webpack-dev-server --open",
    "eslint": "./node_modules/.bin/eslint ./src/assets/js/**"
}

Po uruchomieniu w konsoli npm run eslint posypało się trochę errorów. Przede wszystkim związane one są z brakiem właściwości displayName oznaczającej nazwę komponentów (co jest proste do rozwiązania), ale poważniejszą rzeczą jest to, że importuję A-Frame do zmiennej a jej nie używam:

Błąd ESLinta określający brak użycia zmiennej AFrame

Na szczęście rozwiązanie jest banalnie proste, wystarczy zmienić zapis importu na taki:

import "aframe";

Skoro ESLint nie wskazuje już zadnych problemów składniowych, mogę spokojnie wrócić do komponentów.

Przekazywanie właściwości do komponentów

Wróćmy do komponentu sceny, który na chwile obecną jest komponentem głównym projektu:

const SceneComponent = () => (
    <a-scene>
        <SphereComponent />
        <CameraComponent />
    </a-scene>
);

Scena zawiera dwa komponenty do których na chwilę obecną nie są przekazywane żadne atrybuty. Przekażę zatem do komponentu kuli promień oraz kolor, a do komponentu kamery pozycję:

const SceneComponent = () => (
    <a-scene>
        <SphereComponent radius="100" color="#258bd6" />
        <CameraComponent position="0 0 500" />
    </a-scene>
);

Ok teraz należy zmodyfikować poszczególne komponenty, zacznijmy od kuli:

const Sphere = (props) => <a-sphere radius={props.radius} color={props.color}></a-sphere>;

Sphere.displayName = "Sphere";
Sphere.propTypes = {
    radius: React.PropTypes.string.isRequired,
    color: React.PropTypes.string.isRequired
};

No to jeszcze kamera:

const Camera = (props) => <a-camera position={props.position}></a-camera>;

Camera.displayName = "Camera";
Camera.propTypes = {
    position: React.PropTypes.string.isRequired
};

Czy widzicie tutaj jakiś problem? Otóż dokładając nową właściwość do komponentu A-Frame’a musimy to samo zrobić dla jego warstwy abstrakcji w Reakcie… Przydałoby się usunąć komponenty kamery i kuli, taka warstwa abstrakcji wydaje się zupełnie zbędna. Mniejszym problemem (ale uciążliwym) jest dopisywanie do propTypes każdej nowej właściwości (które tak na prawde są przecież wbudowane w encje A-Frame’a). Czy można zapisać to wszystko nieco ładniej? Na szczęście można.

Biblioteka aframe-react wkracza do akcji

Już przy wczesnych buildach A-Frame’a dostrzeżono problem przekazywania properties’ów do komponentów połączonych z Reactem. Na szczęście inżynierowie Mozilli stworzyli bibliotekę aframe-react aby zaradzić tym bolączkom (i nie tylko tym). No to instalujemy (pakiet prop-types także jest wymagany):

yarn add aframe-react prop-types --dev

Jak opisują twórcy na githubie, aframe-react jest cienką warstwą abstrakcji nad A-Frame i swego rodzaju mostem pomiędzy tym frameworkiem a Reactem. I rzeczywiście gdy się popatrzy w źródło to ta mini biblioteka ma mniej niż 200 linii kodu. Jak zatem wygląda budowanie komponentów z pomocą aframe-react? Otóż utworzono dwie abstrakcji: jedną dla sceny w postaci komponentu <Scene> i jedną dla pozostałych komponentów w postaci obiektu encji <Entity>. Aby uzyskać np. kulę lub sześcian, wykorzystywany jest atrybut primitive z określoną wartością. Najlepiej będzie pokazać całość w praktyce. Zmodyfikuję komponent sceny i w celu uproszczenia całości usunę pliki z komponentami kamery i kuli. Nowa scena wygląda następująco:

import { Scene, Entity } from "aframe-react";
import React from "react";

const SceneComponent = () => (
    <Scene>
        <Entity primitive="a-sphere" radius="100" color="#258bd6" />
        <Entity primitive="a-camera" position="0 0 500" />
    </Scene>
);

export default SceneComponent;

Używanie aframe-react jest praktycznie tak samo proste jak „czystego” A-Frame’a, a mamy dodatkowe możliwości do pracy z Reactem jak np. system zdarzeń obsługiwany za pomocą właściwości events, który łączy zdarzenia z obu blibliotek.

Na zakończenie

W następnych postach dotyczących projektu VRath skupie się bardziej na A-Frame’ie, choć zostały jeszcze pewne rzeczy w środowisku jak np. testy jednostkowe, które przydałoby się wdrożyć. Czas leci nieubłaganie, a projekt jest jeszcze goły i wesoły. No ale nie takie rzeczy robiło się na studiach przed terminem oddania pracy… ekhm 🙂