React to biblioteka JavaScript, stworzona w 2013 roku i rozwijana przez Facebook. Fakt, że stoi za nią ogromna korporacja ma swoje wady i zalety, z jednej strony możemy być pewni, że pracują nad nią świetni programiści i wszystkie decyzje dotyczące jej rozwoju są podejmowane przez profesjonalistów, z drugiej jednak możemy mieć pewne opory moralne ze względu na specyficzne środki bezpieczeństwa związane z licencjonowaniem, o których niedawno było dość głośno.
Pomijając jednak sprawy polityczne, spróbujemy stworzyć od zera prostą aplikację wyświetlającą tabelkę z listą kryptowalut z Bittrex. Projekt w zasadzie nikomu do niczego nie potrzebny, ale jako pierwszy przykład z pewnością wystarczy.
Wykorzystujemy:
- React v15.6
- Express v4.15
- Babel v6.26
- Webpack v3.5
- Axios v0.16
- Bootstrap v3.3
Inicjalizacja projektu
Zaczynamy od klasycznego „npm init” i generujemy nowy plik package.json.
{ "name": "rekt", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" }
Następnie pobieramy potrzebne biblioteki:
npm install --save-dev axios babel-cli babel-core babel-loader babel-preset-env babel-preset-react bootstrap react react-dom webpack npm install --save express
Może się wydawać dziwne dodawanie Bootstrapa do devDependiences, ale dla uproszczenia skopiujemy z niego tylko zawartość katalogu dist i nie będzie nam później potrzebny (opcjonalnie można wykorzystać CDN).
Do package.json dopisujemy linijki odpowiedzialne za skrypty i pluginy babel:
{ "name": "rekt", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start": "node index.js", "dev": "DEBUG=express:* node index.js", "build": "webpack -p", "watch": "webpack --progress --watch", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "devDependencies": { "axios": "^0.16.2", "babel-cli": "^6.26.0", "babel-core": "^6.26.0", "babel-loader": "^7.1.2", "babel-preset-env": "^1.6.0", "babel-preset-react": "^6.24.1", "bootstrap": "^3.3.7", "react": "^15.6.1", "react-dom": "^15.6.1", "webpack": "^3.5.6" }, "dependencies": { "express": "^4.15.4" }, "babel": { "presets": [ "env", "react" ] } }
Tworzymy katalogi i kopiujemy Bootstrapa:
. ├── node_modules/ │ └── (bibliteki z npm) ├── package.json ├── public/ │ ├── css/ │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ ├── bootstrap.min.css │ │ ├── bootstrap.min.css.map │ │ ├── bootstrap-theme.css │ │ ├── bootstrap-theme.css.map │ │ ├── bootstrap-theme.min.css │ │ └── bootstrap-theme.min.css.map │ ├── fonts/ │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ └── js/ │ ├── bootstrap.js │ ├── bootstrap.min.js │ └── npm.js └── src/
Konfiguracja Webpack
W naszym przypadku, Webpack będzie potrzebny do przetłumaczenia przy pomocy Babel kodu ES6+JSX na klasyczny JavaScript rozumiany przez przeglądarki oraz do sklejenia wszystkich wymaganych plików w jeden bundle.js. Ostateczny webpack.config.js wygląda następująco:
var path = require('path'); require("babel-polyfill"); module.exports = { entry: ['babel-polyfill', './src/main.js'], output: { filename: 'bundle.js', path: path.resolve(__dirname, 'public') }, module: { loaders: [{ test: /\.(js|jsx)$/, exclude: /node_modules/, loader: "babel-loader" }] } };
Czas na HTML
Cała logika aplikacji będzie realizowana przez JavaScript, dlatego też wystarczy nam minimalistyczny plik HTML. Tworzymy /public/index.html:
<!DOCTYPE html> <html> <head> <title>REkT</title> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="/css/bootstrap.min.css"/> </head> <body> <noscript>(╯°□°)╯︵ ┻━┻</noscript> <div class="container-fluid"> <div class="row"> <div id="root"></div> </div> </div> <script src="/bundle.js"></script> </body> </html>
Expressem do przodu
Kierując się zasadą strzelania z armaty do wróbli, framework Express posłuży wyłącznie do serwowania statycznych plików. Tworzymy index.js:
const express = require('express'); const app = express(); app.use(express.static('public')); const listen = app.listen(process.env.PORT || 3000, () => { console.log(listen.address()); });
Akcja, reakcja
W tym momencie jesteśmy już gotowi do napisania pierwszego kodu wykorzystującego bibliotekę React. Umieścimy go w pliku /src/main.js i po uruchomieniu Webpacka i Express zobaczymy wynik w przeglądarce.
Żeby za długo nie zwlekać i jednocześnie szybko przetestować czy wszystko działa zgodnie z założeniami, zaczniemy od klasycznego „Hello world”:
import React from 'react'; import ReactDOM from 'react-dom'; class App extends React.Component { render() { return ( <p>Hello world</p> ); } } ReactDOM.render( <App />, document.getElementById('root') );
Jak to w ogóle działa? Na początku importowane są biblioteki react oraz react-dom. Później deklarujemy klasę App wywodzącą się z React.Component, której jedynym zadaniem na tę chwilę jest udostępnienie metody render zwracającej kod HTML. Na koniec, wywołujemy metodę render z ReactDOM, zlecając jej podpięcie komponentu <App> (którego działanie jest zdefiniowane właśnie w klasie App) do elementu o identyfikatorze „root” (w naszym przypadku DIV w index.html).
Umieszczanie HTML bezpośrednio w kodzie JavaScript może się na pierwszy rzut oka wydawać dziwne, ale od tego właśnie mamy Babel, który przetłumaczy go na mniej więcej coś takiego:
var App = function (_React$Component) { _inherits(App, _React$Component); function App() { _classCallCheck(this, App); return _possibleConstructorReturn(this, (App.__proto__ || Object.getPrototypeOf(App)).apply(this, arguments)); } _createClass(App, [{ key: 'render', value: function render() { return _react2.default.createElement( 'p', null, 'Hello world' ); } }]); return App; }(_react2.default.Component); _reactDom2.default.render(_react2.default.createElement(App, null), document.getElementById('root'));
Startujemy
Uruchamiamy dwa osobne terminale, przechodzimy w nich do folderu z naszym projektem i uruchamiamy w pierwszym z nich „npm run watch” a w drugim „npm run dev”.
Jeśli wszystko pójdzie z planem, w pierwszym uruchomi się Webpack i utworzy plik /public/bundle.js, po czym zacznie na bieżąco w pętli monitorować zmiany w plikach źródłowych.
Version: webpack 3.5.6 Time: 3250ms Asset Size Chunks Chunk Names bundle.js 1.05 MB 0 [emitted] [big] main [140] (webpack)/buildin/global.js 509 bytes {0} [built] [212] multi babel-polyfill ./src/main.js 40 bytes {0} [built] [415] ./src/main.js 2.39 kB {0} [built] + 534 hidden modules
W drugim uruchomi się Express w trybie DEBUG i wyświetli informację o porcie na którym nasłuchuje serwer HTTP.
{ address: '::', family: 'IPv6', port: 3000 }
Pozostaje wpisać adres https://localhost:3000 do przeglądarki i zobaczyć efekt działania aplikacji. Dla pewności, w konsoli DevTools możemy zweryfikować czy paragraf z „Hello world” podpiął się przy pomocy React do DIV:
<html> <head> <title>REkT</title> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="/css/bootstrap.min.css"> </head> <body> <noscript>(╯°□°)╯︵ ┻━┻</noscript> <div class="container-fluid"> <div class="row"> <div id="root"><p data-reactroot="">Hello world</p></div> </div> </div> <script src="/bundle.js"></script> </body> </html>
Gratulacje. Udało się nam wyświetlić dwa słowa przy pomocy pliku JS, który zajmuje aktualnie ponad 1MB w projekcie zawierającym ponad 60MB bibliotek.
I co dalej?
Zaczynamy od przebudowania /src/main.js:
import React from 'react'; import ReactDOM from 'react-dom'; import App from './app'; const url = 'https://bittrex.com/api/v1.1/public/getmarketsummaries'; ReactDOM.render( <App url={url} />, document.getElementById('root') );
Po pierwsze, wyrzuciliśmy klasę App do zewnętrznego pliku /src/app.js (zaraz go utworzymy). Po drugie, do komponentu <App> przekazujemy parametr url zawierający adres API strony Bittrex, zwracający JSON z listą kryptowalut w formacie:
{ "success": true, "message": "", "result": [{ "MarketName": "BTC-1ST", "High": 0.00008108, "Low": 0.00007205, "Volume": 1441030.45893439, "Last": 0.00007391, "BaseVolume": 110.40565565, "TimeStamp": "2017-09-10T15:09:38.907", "Bid": 0.00007359, "Ask": 0.00007468, "OpenBuyOrders": 143, "OpenSellOrders": 5537, "PrevDay": 0.00007645, "Created": "2017-06-06T01:22:35.727" }, { "MarketName": "BTC-2GIVE", "High": 0.00000143, "Low": 0.00000115, "Volume": 12007718.13364286, "Last": 0.00000120, "BaseVolume": 15.42232806, "TimeStamp": "2017-09-10T15:08:28.83", "Bid": 0.00000120, "Ask": 0.00000121, "OpenBuyOrders": 335, "OpenSellOrders": 2249, "PrevDay": 0.00000142, "Created": "2016-05-16T06:44:15.287" }, { [...] }] }
Tworzymy klasę App w /src/app.js:
import React from 'react'; import axios from 'axios'; import CoinsTable from './coinstable'; class App extends React.Component { constructor(props) { super(props); this.state = { coins: [] }; } componentDidMount() { axios.get(this.props.url) .then((res) => { if (res && res.data && res.data.result && res.data.result.length) { this.setState({ coins: res.data.result }); } }); } render() { return ( <div className="panel-body"> <CoinsTable coins={this.state.coins} /> </div> ); } } export default App;
Tutaj zaczynają się dziać ciekawe rzeczy. Po pierwsze, w konstruktorze definiujemy this.state i wrzucamy do niego pustą tablicę o nazwie coins. Będziemy w niej później przechowywać dane pobrane z API Bittrex.
Metoda componentDidMount jest automatycznie wywoływana przez React po przypięciu komponentu <App> na stronie. W naszym przypadku, jej zadaniem jest wykonanie przy pomocy biblioteki Axios zapytania AJAX i wczytanie danych z adresu przekazanego do komponentu w parametrze url (jest dostępny w obiekcie this.props). Po przejściu zgrubnej weryfikacji, dane trafiają do this.state.coins.
Metoda render zwraca DIV zawierający kolejny komponent, tym razem CoinsTable, któremu przekazana zostaje tablica danych w parametrze coins. Należy zauważyć, że ze względu na fakt że „class” jest zastrzeżonym słowem w JavaScript, zmuszeni jesteśmy wpisywać w jego miejscu „className”.
Gwoli wyjaśnienia, w nomenklaturze React obiekt this.state zawiera podręczne dane komponentu w którym się znajdujemy i które możemy zmieniać przy pomocy metody this.setState. Obiekt this.props zawiera parametry przekazane przez komponent nadrzędny i jego elementy są tylko do odczytu.
Następny krok to klasa CoinsTable w /app/coinstable.js:
import React from 'react'; import CoinsRow from './coinsrow'; class CoinsTable extends React.Component { render() { if (this.props.coins && this.props.coins.length) { const rows = []; this.props.coins.forEach((coin) => { rows.push( <CoinsRow key={coin.MarketName} marketname={coin.MarketName} high={coin.High} low={coin.Low} volume={coin.Volume} /> ); }); return ( <table className="table table-striped"> <thead> <tr> <th>MarketName</th> <th>High</th> <th>Low</th> <th>Volume</th> </tr> </thead> <tbody> {rows} </tbody> </table> ); } else { return ( <p>loading...</p> ); } } } export default CoinsTable;
W tym komponencie nie potrzebujemy przechowywać stanu, bo jego działanie polega wyłącznie na wyświetleniu tego co otrzyma na liście parametrów w obiekcie this.props.
Metoda render na początku sprawdza, czy this.props.coins zawiera jakieś dane. Jeżeli tak jest, przygotowana jest nowa tablica o nazwie rows, składająca się z listy komponentów CoinsRow, do których są przekazane wartości poszczególnych komórek każdego z wierszy. Dodatkowo, każdy z nich otrzymuje parametr o nazwie „key”, zawierający unikatowy symbol umożliwiający bibliotece React szybką identyfikację elementów wymagających odświeżenia na wypadek zmian w danych. Na koniec zwracany jest kod gotowej tabeli w HTML.
W przypadku, gdy this.props.coins jest puste, otrzymamy tylko paragraf z treścią „loading…”.
Pozostała już tylko klasa CoinsRow w /app/coinsrow.js:
import React from 'react'; class CoinsRow extends React.Component { render() { return ( <tr> <td>{this.props.marketname}</td> <td>{this.props.high.toFixed(8)}</td> <td>{this.props.low.toFixed(8)}</td> <td>{this.props.volume.toFixed(3)}</td> </tr> ); } } export default CoinsRow;
Tutaj nie ma już nawet czego wyjaśniać, komponent zwraca pojedynczy wiersz tabeli z danymi w komórkach.
Gotowe!
Stworzyliśmy prostą aplikację, pobierającą dane z zewnętrznego API i wyświetlającą je w tabeli na stronie. W rzeczywistości nawet nie dotknęliśmy czubka możliwości biblioteki React, ale jak na pierwszy raz powinno to wystarczyć.
Kolejnym krokiem mogło by być dodanie możliwości przeszukiwania tabeli oraz sortowania danych w kolumnach. Przydała by się również opcja odświeżenia danych.
Uwagi
Metoda render w komponencie <App> jest wywoływana dwukrotnie. Za pierwszym razem z pustą tabelą this.state.coins, co skutkuje wyświetleniem napisu „loading”. Zaraz po tym zdarzeniu, React uruchamia metodę componentDidMount, która asynchronicznie pobiera dane z API. W chwili, gdy dane są gotowe, aplikacja modyfikuje this.state.coins przy pomocy this.setState a to automatycznie wymusza odświeżenie komponentu i ponowne uruchomienie metody render, tym razem już z gotową tabelą.
Ostateczna struktura projektu wygląda następująco:
. ├── index.js ├── node_modules/ │ └── (biblioteki z npm) ├── package.json ├── public/ │ ├── bundle.js (generowany przez webpack) │ ├── css/ │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ ├── bootstrap.min.css │ │ ├── bootstrap.min.css.map │ │ ├── bootstrap-theme.css │ │ ├── bootstrap-theme.css.map │ │ ├── bootstrap-theme.min.css │ │ └── bootstrap-theme.min.css.map │ ├── fonts/ │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ ├── index.html │ └── js/ │ ├── bootstrap.js │ ├── bootstrap.min.js │ └── npm.js ├── src/ │ ├── app.js │ ├── coinsrow.js │ ├── coinstable.js │ └── main.js └── webpack.config.js
Dodaj komentarz