IMA.js – Načítání a vykreslování dat

V předchozím díle jsme si zajistili data pro naši aplikaci a jejich zpracování pomocí modelů. Nyní můžeme začít s jejich zobrazováním.

Seriál: IMA.js: framework od Seznam.cz (3 díly)

  1. Co je IMA.js? Podívejme se na framework od Seznam.cz 25.2.2020
  2. IMA.js – setup aplikace a vytvoření modelů 13.3.2020
  3. IMA.js – Načítání a vykreslování dat 29.4.2020

Před přečtením tohoto dílu si prosím stáhněte zdrojové kódy aplikace z GitHubu. V textu tohoto dílu nebudeme pracovat se všemi potřebnými soubory pro zprovoznení aplikace, ale projdeme pouze ty nejdůležitější pro její fungování.

Router

Celý chod aplikace začíná zde. Pro příchozí HTTP požadavek se IMA.js snaží najít shodnou routu. Každá routa má přiřazený Controller a View, který se má v případě shody načíst a vyrenderovat. Seznam rout se nachází v konfiguračním souboru app/config/routes.js. Jednotlivé parametry routy si zde nebudeme popisovat, najdete je v dokumentaci. Doporučujeme si však dokumentaci routeru pročíst.

Pro naší aplikaci potřebujeme upravit pouze výchozí routu s názvem home. Abychom mohli vytvářet krásné SEO URL ve tvaru weather.xyz/ostrava nebo weather.xyz/ceske-budejovice přidáme do path parametr :?location. Pokud jste si četli dokumentaci, tak už víte, že : se v routě označuje parametr a ? nepovinný parametr.

// app/config/routes.js
 router
    .add('home', '/:?location', HomeController, HomeView)
    .add('filtered', '/:filter', HomeController, HomeView)
    .add(RouteNames.ERROR, '/error', ErrorController, ErrorView)
    .add(RouteNames.NOT_FOUND, '/not-found', NotFoundController, NotFoundView);

Controller

Router předává slovo Controlleru, který byl přiřazený k shodné routě. Controller tímto začíná svůj životní cyklus. Pro nás je důležitá metoda load(), ve které načteme všechna potřebná data. Tedy ne všechna, některá necháme, aby se načetla až na straně klienta.

Než ale začneme s načítáním, potřebujeme Controlleru předat ForecastService a GeoCoderService, které jsme vytvořili v předchozím díle. Uděláme to pomocí DI.

import { Router, CookieStorage } from '@ima/core';
import AbstractPageController from 'app/page/AbstractPageController';
import ForecastService from 'app/model/forecast/ForecastService';
import GeoCoderService from 'app/model/geocoder/GeoCoderService';

export default class HomeController extends AbstractController {

	static get $dependencies() {
		return [Router, CookieStorage, ForecastService, GeoCoderService, '$Settings.App.defaultLocation'];
	}

	constructor(router, cookieStorage, forecastService, geoCoderService, defaultLocation) {
		super();

		this._router = router;
		this._cookieStorage = cookieStorage;

		this._forecastService = forecastService;
		this._geoCoderService = geoCoderService;

		this._defaultLocation = defaultLocation;
	}

Jak vidíte, nepředali jsme si jen zmiňované Services, ale i něco navíc. Konkrétně:

  • Router – budeme ho potřebovat pro přesměrování při zadání špatného názvu města.
  • CookieStorage – vytáhnutí informací o městě, které si uživatel nastavil (více si o tom povíme v 5. díle).
  • Nastavení defaultLocation – jak jsme popisovali v úvodu 2. dílu, pokud nebudeme mít žádný uživatelský vstup, použijeme výchozí město. Řetězec $Settings představuje soubor app/config/settings.js. Zbytek řetězce je cesta k nastavení.
load() {
    let geoCoderPromise = Promise.resolve(this._getDefaultLocation());  // zde začneme s výchozím městem

    const { location, lat, lon } = this.params; // parametry u URL

    if (location) {
        geoCoderPromise = this._geoCoderService.geoCodeMunicipality(this.params.location); // načtení informací o městu podle parametru z URL

    } else if (!location && (typeof lat !== 'undefined' || typeof lon !== 'undefined')) {
        this._router.redirect(this._router.link('home')); // na dotazy se souřadnicemi bez názvy města neodpovídame
    }

    geoCoderPromise.then(geoLocation => {
        if (
            location && (
                typeof lat === 'undefined' ||
                typeof lon === 'undefined' ||
                lat !== geoLocation.lat ||
                lon !== geoLocation.lon
            )
        ) {
            // doplnění nebo oprava souřadnic do URL 
            this._router.redirect(this._router.link('home', { location, lat: geoLocation.lat, lon: geoLocation.lon }));
        }

        return geoLocation;
    });

    // Když známe město, můžeme načíst předpověď.
    const forecastPromise = geoCoderPromise.then(location => this._forecastService.getForecast(location.lat, location.lon));

    return {
        location: geoCoderPromise,
        forecast: forecastPromise,

        forecastDetail: null,  // detail předpovědi si necháme k načtení na klienta (ušetříme prostředky a čas na serveru)
        forecastDetailLoading: true
    };
}

Donačítání dat

Abychom nemuseli stahovat velké množství dat k vygenerování odpovědi ze serveru, můžeme odložit načítání dat na stranu klienta. Na serveru si tak stáhneme jen to nejnutnější k vytvoření smysluplné odpovědi, kterou můžou přečíst i vyhledávače.

K tomuto účelu se používá metoda Controlleru zvaná activate(). Zavolá se při oživení aplikace nebo aktivaci příslušné routy na straně klienta. Můžeme v ní tak načítat data pro první zobrazení ale i při každém vstupu na stránku.

activate() {
    const { location } = this.getState(); // získání dat ze state

    if (location) {
        this._forecastService.getDetailedForecast(location.lat, location.lon)
            .then(forecastDetail => this.setState({ forecastDetail, forecastDetailLoading: false }));

    } else {
        this.setState({ forecastDetailLoading: false });
    }
}

Meta informace

V Controlleru využijeme ještě jednu metodu – setMetaParams(). Ta nám dovoluje nastavit meta informace do záhlaví stránky. Nastavené informace se potom využívají v DocumentView (komponenta, která zajišťuje render hlavního HTML markupu).

setMetaParams(loadedResources, metaManager, router, dictionary, settings) {
    const { location, forecast } = loadedResources;

    if (!location || !forecast) {
        return;
    }

    const todayForecast = forecast.daily[0];
    const locationShortTitle = location.title.split(',')[0];

    const title = `Počasí ${locationShortTitle || location.title} - IMA.js Example`;
    const description = `${todayForecast.localDate}: ${todayForecast.summary}`;

    const url = router.getUrl();

    metaManager.setTitle(title);
    metaManager.setMetaName('description', description);

    metaManager.setMetaProperty('og:title', title);
    metaManager.setMetaProperty('og:description', description);
    metaManager.setMetaProperty('og:type', 'website');
    metaManager.setMetaProperty('og:url', url);
}

View

Načtená data jsou předávána jako props speciální React komponentě. Tato komponenta se nazývá View. Od klasické komponenty, kterou si budeme popisovat v následujícím bodě, se nijak zásadně neliší. HomeView se bude starat o zobrazení názvu města a předpovědi počasí. Každý den předpovědi bude renderovaný samostatnou komponentou, kterou pojmenujeme ForecastDay. Musíme také vyřešit stav, kdy změníme město a data se začnou načítat znovu (na straně klienta). Po určitý čas nebudeme mít dostupná žádná data a musíme vykreslit načítání. Práci si ulehčíme použitím předpřipravených komponent z balíčku IMA-UI-atoms.

  1. Nejprve si tedy nainstalujeme npm install @ima/plugin-atoms --save.
  2. app/build.js přidáme '@ima/plugin-atoms' do let vendors.common a './node_modules/@ima/plugin-atoms/dist/*.less', do let less
let less = [
  './app/assets/less/app.less',
+ './node_modules/@ima/plugin-atoms/dist/*.less',
...
let vendors = {
  common: [
+   '@ima/plugin-atoms',
import { PageContext, AbstractComponent } from '@ima/core';
import React, { Fragment } from 'react';
import { Loader } from '@ima/plugin-atoms';

import ForecastDay from 'app/component/forecastDay/ForecastDay';
import ForecastDetail from 'app/component/forecastDetail/ForecastDetail';

export default class HomeView extends extends AbstractComponent {
     static get contextType() {
        return PageContext;
    }

    render() {
        return (
            <div className="container">
                { this._renderPlaceAndForecast() }
            </div>
        );
    }

    _renderPlaceAndForecast() {
        const { forecast, location } = this.props;
        const { activeDay } = this.state;

        if (!forecast || !location) { 
            return <Loader/>; // zde ještě nemáme načtená data, zobrazíme načítání
        }

        return (
            <Fragment>
                <div className="location">
                    <h1 className="location-title">{ location.title }</h1>
                </div>
                <div className="forecast-days">
                    <ul>
                        { forecast.daily.map((day, index) => (
                            <ForecastDay
                                key = { index }
                                forecast = { day }
                                place = { forecast.place }
                                isActive = { index === activeDay }  // activeDay máme uložený ve state komponenty (view)
                                onClick = { event => this.onDayClick(event, index)}/> // viz. níže
                        ))}
                    </ul>
                </div>
                { this._renderDetailedForecast() }
            </Fragment>
        );
    }

    onDayClick(event, index) {
	event.preventDefault();

	const { forecast } = this.props;

	if (forecast.daily[index] !== undefined) {
		this.setState({ activeDay: index });
	}
    }
}

Donačtená data

Jak už víme, tak detailní předpověď počasí pro jeden den donačítáme na straně klienta. Metoda _renderDetailedForecast() v našem View bude zobrazovat tato data. Bohužel API, které využíváme vrací detailní předpověď pro všechny dny předpovědi. My si však tato zpracujeme a vykreslíme jen ta, která se týkají zvoleného dne (activeDay).

_renderDetailedForecast() {
    const { forecastDetail, forecastDetailLoading } = this.props;
    const { activeDay } = this.state;

    if (forecastDetailLoading) {
        return <Loader/>; // podobně jako v _renderPlaceAndForecast() zobrazíme Loader dokud nemáme data
    }

    return (
        <div className={this.cssClasses('forecast-detail')}>
            { forecastDetail
                .filter(day => day.dayId === activeDay) // vyfiltrujeme si data pro zvolený den
                .map((day, index) => (
                    <ForecastDetail
                        key = { activeDay + index }
                        { ...day }/>
                ))
            }
        </div>
    )
}

Komponenty

Stejně tak jako View i komponenty používají React s tím rozdílem, že nerozšiřují React.Component ale AbstractComponent z @ima/coreAbstractComponent poskytuje přístup k utilitám, které jsme si nastavili v app/config/bind.js – řádek oc.constant('$Utils').

Tyto utility jsou dostupné pod this.utils. Některé, jako například RouterEventBus nebo Dictionary mají speciální metody, které umožňují snadnější použití. Odkazy můžete vytvářet pomocí this.link('route', { param: 'value' }), vytvářet eventy přes this.fire('eventName', { data }), překládat texty metodou this.localize('string', { param }) a definovat CSS třídy pomocí this.cssClasses({ classes }).

Utilita this.cssClasses slouží k ulehčení definování CSS tříd. Můžete pomocí ní snadněji definovat třídy v závislosti na podmínce nebo nějáké hodnotě. Např.:

this.cssClasses({
    'forecast-day': true,
    'forecast-day--active': this.props.isActive
})

Argumentem může být i string (např. this.cssClasses('forecast-day')) a nemusíte tak definovat objekt tříd.

ForecastDay a ForecastDetail

Aby jsme zlepšili čitelnost HomeView oddělili jsme opakované části kódu do komponent ForecastDay a ForecastDetail. Na těchto komponentách je zvláštní pouze to, že místo AbstractComponent rozšiřují AbstractPureComponent. Jak již název napovídá, jedná se o React Pure Componenty.

Devtooly

Než se rouzloučíme, rádi bychom ještě ukázali jak je možné ladit volání funkcí v IMA.js aplikacích pomocí IMA Devtools, které jsme již zmínili v předchozím díle.

Po nainstalování IMA.js Devtools rozšíření do prohlížeče Google Chrome, je nutné toto rozšíření nejprve aktivovat. To lze provést přepnutím přepínače do správné polohy v popupu, který zobrazíme kliknutím na IMA.js ikonu v adresním řádku prohlížeče. Tento popup navíc zobrazuje, zda daná webová stránka běží na IMA.js nebo ne a jaká je případná aktuální verze, jazyk a environment aplikace.

Po jejich zapnutí je třeba stránku znovu načíst. Po načtení se do Chrome Developer Tools vloží nová záložka s názvem IMA.js, kde se nachází již zmiňované devtooly. Rozhraní těchto nástrojů se skládá ze dvou sloupců a vyhledávácího panelu. V levém sloupci jsou zobrazené chronologicky všechny události, které byly v aplikaci zachyceny. V pravém sloupci záložky ArgsPayload a Events představují:

  • Argumenty, se kterými byla funkce volána.
  • Data, které daná funkce vrátila.
  • Pole událostí, zobrazeno v takovém pořadí, v jakém do devtoolů z aplikace příjdou, ze kterých se následně vyextrahuje Payload a Args. V případě např. load metody, která vrací více promisů, se v Events nachází jednotlivé snapshoty výsledků funkce, tak jak se postupně každý promise resolvuje.

Teď se vrátíme zpět k naší aplikaci, kde si můžeme prohlédnout např. jak se vyresolvoval náš dotaz na API. Ten najdeme pod Http třídou a get metodou, kterou voláme z našeho Resource přes HTTP Agenta. Po rozkliknutí vidíme v záložce Args argumenty, které odpovídají souřadnicím, se kterými funkci voláme a v Payload potom výsledek API volání. Stejným způsobem lze zkontrolovat i jiná volání, případně si je vyfiltrovat pomocí vyhledávání, které příjímá i regulární výrazy.

Ti bystřejší si jistě všimly, u některých funkcí se v levém sloupci vedle jejich názvu zobrazil také tag ve formátu resolved [x]ms. Ten se zobrazuje pouze u promisů, kde x odpovídá času, za jak dlouho se daný promise vyresolvoval.

Závěr

V tomto díle jsme v naší ukázkové aplikaci zajistili načítání a zobrazování dat pomocí Controlleru, View a komponent. Po aplikaci CSS (less) stylů naše stránka vypadá jako na přiloženém snímku obrazovky.

V dalším díle se podíváme na jeden detailní příklad komponenty, na které si ukážeme práci s eventy, HOC a Controller Extensions.

Vojta působí v Seznamu od roku 2016, aktuálně na pozici senior programátor UI. Nejprve pracoval pro oddělení Map na projektu Jízdní řády a později přešel do oddělení obsahových služeb, kde se jako i ostatní programátoři začal podílet na vývoji IMA.js frameworku.

Komentáře: 2

Přehled komentářů

Mlocik97 Svelte/Sapper + IMA.js
Martin Jurča Re: Svelte/Sapper + IMA.js
Zdroj: https://www.zdrojak.cz/?p=23680