Prosty przykład w React od zera

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

Inspiracja


Opublikowano

w

przez

Tagi:

Komentarze

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *