Vytvoření jednostránkové aplikace bez rámce

Myšlenkou jednostránkových aplikací (SPA) je vytvořit plynulé procházení, jako je tomu v nativních desktopových aplikacích. Veškerý potřebný kód pro stránku se načte pouze jednou a její obsah se dynamicky mění pomocí JavaScriptu. Pokud je vše provedeno správně, stránka by se nikdy neměla znovu načítat, pokud ji uživatel neobnoví ručně.

Existuje mnoho frameworků pro jednostránkové aplikace. Nejdřív jsme měli Backbone, pak Angular a teď React. Neustále se učit a znovu učit věci dá hodně práce (nemluvě o nutnosti podporovat starý kód, který jste napsali v dávno zapomenutém frameworku). V některých situacích, například když nápad na aplikaci není příliš složitý, ve skutečnosti není tak těžké vytvořit aplikaci s jednou stránkou bez použití externích rámců. Zde je návod, jak to udělat.

Nápad

Nebudeme používat framework, ale budeme používat dvě knihovny - jQuery pro manipulaci s DOM a zpracování událostí a řídítka pro šablony. Můžete je snadno vynechat, pokud si přejete být ještě minimálnější, ale použijeme je pro zvýšení produktivity, které poskytují. Budou tu dlouho poté, co bude zapomenuta kyčelní klientská struktura dne.

Aplikace, kterou budeme vytvářet, načítá produktová data ze souboru JSON a zobrazuje je vykreslením mřížky produktů pomocí řídítek. Po úvodním načtení zůstane naše aplikace na stejné adrese URL a bude naslouchat změnám hash část s změnou hash událost. Pro navigaci v aplikaci jednoduše změníme hash. To má další výhodu, že historie prohlížeče bude fungovat bez dalšího úsilí z naší strany.

Nastavení

Jak vidíte, v naší složce projektu toho moc není. Máme běžné nastavení webové aplikace – soubory HTML, JavaScript a CSS, doplněné o product.json obsahující data o produktech v našem obchodě a složku s obrázky produktů.

Produkty JSON

Soubor .json se používá k ukládání dat o každém produktu pro naše SPA. Tento soubor lze snadno nahradit skriptem na straně serveru pro načítání dat ze skutečné databáze.

products.json

[
  {
    "id": 1,
    "name": "Sony Xperia Z3",
    "price": 899,
    "specs": {
      "manufacturer": "Sony",
      "storage": 16,
      "os": "Android",
      "camera": 15
    },
    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam tristique ipsum in efficitur pharetra. Maecenas luctus ante in neque maximus, sed viverra sem posuere. Vestibulum lectus nisi, laoreet vel suscipit nec, feugiat at odio. Etiam eget tellus arcu.",
    "rating": 4,
    "image": {
      "small": "/images/sony-xperia-z3.jpg",
      "large": "/images/sony-xperia-z3-large.jpg"
    }
  },
  {
    "id": 2,
    "name": "Iphone 6",
    "price": 899,
    "specs": {
      "manufacturer": "Apple",
      "storage": 16,
      "os": "iOS",
      "camera": 8
    },
    "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam tristique ipsum in efficitur pharetra. Maecenas luctus ante in neque maximus, sed viverra sem posuere. Vestibulum lectus nisi, laoreet vel suscipit nec, feugiat at odio. Etiam eget tellus arcu.",
    "rating": 4,
    "image": {
      "small": "/images/iphone6.jpg",
      "large": "/images/iphone6-large.jpg"
    }
  }
]

HTML

V našem html souboru máme několik divů sdílejících stejnou třídu "stránku". To jsou různé stránky (nebo jak se jim říká ve státech SPA), které může naše aplikace zobrazit. Při načítání stránky jsou však všechny tyto skryté pomocí CSS a k jejich zobrazení je třeba JavaScript. Myšlenka je taková, že najednou může být viditelná pouze jedna stránka a náš skript je ten, kdo rozhodne, která to je.

index.html

<div class="main-content">

    <div class="all-products page">

        <h3>Our products</h3>

        <div class="filters">
            <form>
                Checkboxes here
            </form>
        </div>

    <ul class="products-list">
      <script id="products-template" type="x-handlebars-template">​
        {{#each this}}
          <li data-index="{{id}}">
            <a href="#" class="product-photo"><img src="{{image.small}}" height="130" alt="{{name}}"/></a>
            <h2><a href="#"> {{name}} </a></h2>
            <ul class="product-description">
              <li><span>Manufacturer: </span>{{specs.manufacturer}}</li>
              <li><span>Storage: </span>{{specs.storage}} GB</li>
              <li><span>OS: </span>{{specs.os}}</li>
              <li><span>Camera: </span>{{specs.camera}} Mpx</li>
            </ul>
            <button>Buy Now!</button>
            <p class="product-price">{{price}}$</p>
            <div class="highlight"></div>
          </li>
        {{/each}}
      </script>

    </ul>

    </div>

    <div class="single-product page">

        <div class="overlay"></div>

        <div class="preview-large">
            <h3>Single product view</h3>
            <img src=""/>
            <p></p>

            <span class="close">&times;</span>
        </div>

    </div>

    <div class="error page">
        <h3>Sorry, something went wrong :(</h3>
    </div>

</div>

Máme tři stránky:všechny produkty (záznam produktu), jeden produkt (stránka jednotlivého produktu) a chyba .

všechny produkty stránka se skládá z nadpisu, formuláře obsahujícího zaškrtávací políčka pro filtrování a tagu

    s třídou "products-list". Tento seznam je generován pomocí řídítek pomocí dat uložených v products.json, přičemž pro každou položku v json se vytváří
  • . Zde je výsledek:

    Jeden produkt se používá k zobrazení informací pouze o jednom produktu. Při načítání stránky je prázdný a skrytý. Když je dosaženo příslušné hash adresy, vyplní se produktovými daty a zobrazí se.

    Chybová stránka obsahuje pouze chybovou zprávu, která vás informuje, když jste dosáhli chybné adresy.

    Kód JavaScript

    Nejprve si udělejme rychlý náhled funkcí a toho, co dělají.

    script.js

    $(function () {
    
        checkboxes.click(function () {
            // The checkboxes in our app serve the purpose of filters.
            // Here on every click we add or remove filtering criteria from a filters object.
    
            // Then we call this function which writes the filtering criteria in the url hash.
            createQueryHash(filters);
        });
    
        $.getJSON( "products.json", function( data ) {
            // Get data about our products from products.json.
    
            // Call a function that will turn that data into HTML.
            generateAllProductsHTML(data);
    
            // Manually trigger a hashchange to start the app.
            $(window).trigger('hashchange');
        });
    
        $(window).on('hashchange', function(){
            // On every hash change the render function is called with the new hash.
            // This is how the navigation of our app happens.
            render(decodeURI(window.location.hash));
        });
    
        function render(url) {
            // This function decides what type of page to show 
            // depending on the current url hash value.
        }
    
        function generateAllProductsHTML(data){
            // Uses Handlebars to create a list of products using the provided data.
            // This function is called only once on page load.
        }
    
        function renderProductsPage(data){
            // Hides and shows products in the All Products Page depending on the data it recieves.
        }
    
        function renderSingleProductPage(index, data){
            // Shows the Single Product Page with appropriate data.
        }
    
        function renderFilterResults(filters, products){
            // Crates an object with filtered products and passes it to renderProductsPage.
            renderProductsPage(results);
        }
    
        function renderErrorPage(){
            // Shows the error page.
        }
    
        function createQueryHash(filters){
            // Get the filters object, turn it into a string and write it into the hash.
        }
    
    });
    

    Pamatujte, že koncept SPA spočívá v tom, že během běhu aplikace neprobíhá žádná zátěž. To je důvod, proč po úvodním načtení stránky chceme zůstat na stejné stránce, kde již server stáhl vše, co potřebujeme.

    Stále však chceme mít možnost v aplikaci někam jít a například zkopírovat url a poslat ji známému. Pokud nikdy nezměníme adresu aplikace, dostanou aplikaci jen tak, jak vypadá na začátku, ne to, co jste s nimi chtěli sdílet. Abychom tento problém vyřešili, zapíšeme informace o stavu aplikace do adresy URL jako #hash. Hodnoty hash nezpůsobují opětovné načtení stránky a jsou snadno dostupné a manipulovatelné.

    Při každé změně hash nazýváme toto:

    function render(url) {
    
            // Get the keyword from the url.
            var temp = url.split('/')[0];
    
            // Hide whatever page is currently shown.
            $('.main-content .page').removeClass('visible');
    
            var map = {
    
                // The Homepage.
                '': function() {
    
                    // Clear the filters object, uncheck all checkboxes, show all the products
                    filters = {};
                    checkboxes.prop('checked',false);
    
                    renderProductsPage(products);
                },
    
                // Single Products page.
                '#product': function() {
    
                    // Get the index of which product we want to show and call the appropriate function.
                    var index = url.split('#product/')[1].trim();
    
                    renderSingleProductPage(index, products);
                },
    
                // Page with filtered products
                '#filter': function() {
    
                    // Grab the string after the '#filter/' keyword. Call the filtering function.
                    url = url.split('#filter/')[1].trim();
    
                    // Try and parse the filters object from the query string.
                    try {
                        filters = JSON.parse(url);
                    }
                    // If it isn't a valid json, go back to homepage ( the rest of the code won't be executed ).
                    catch(err) {
                        window.location.hash = '#';
                    }
    
                    renderFilterResults(filters, products);
                }
    
            };
    
            // Execute the needed function depending on the url keyword (stored in temp).
            if(map[temp]){
                map[temp]();
            }
            // If the keyword isn't listed in the above - render the error page.
            else {
                renderErrorPage();
            }
    
        }
    

    Tato funkce bere v úvahu počáteční řetězec našeho hashe, rozhoduje, jakou stránku je třeba zobrazit, a volá příslušné funkce.

    Pokud je například hash '#filter/{"storage":["16"],"camera":["5"]}', naše kódové slovo je '#filter'. Nyní funkce render ví, že chceme vidět stránku se seznamem filtrovaných produktů a naviguje nás na ni. Zbytek hashe bude analyzován do objektu a zobrazí se stránka s filtrovanými produkty, čímž se změní stav aplikace.

    Toto je voláno pouze jednou při spuštění a přeměňuje náš JSON na skutečný obsah HTML5 pomocí řídítek.

    function generateAllProductsHTML(data){
    
        var list = $('.all-products .products-list');
    
        var theTemplateScript = $("#products-template").html();
        //Compile the template​
        var theTemplate = Handlebars.compile (theTemplateScript);
        list.append (theTemplate(data));
    
        // Each products has a data-index attribute.
        // On click change the url hash to open up a preview for this product only.
        // Remember: every hashchange triggers the render function.
        list.find('li').on('click', function (e) {
          e.preventDefault();
    
          var productIndex = $(this).data('index');
    
          window.location.hash = 'product/' + productIndex;
        })
      }
    

    Tato funkce přijme objekt obsahující pouze ty produkty, které chceme zobrazit, a zobrazí je.

    function renderProductsPage(data){
    
        var page = $('.all-products'),
          allProducts = $('.all-products .products-list > li');
    
        // Hide all the products in the products list.
        allProducts.addClass('hidden');
    
        // Iterate over all of the products.
        // If their ID is somewhere in the data object remove the hidden class to reveal them.
        allProducts.each(function () {
    
          var that = $(this);
    
          data.forEach(function (item) {
            if(that.data('index') == item.id){
              that.removeClass('hidden');
            }
          });
        });
    
        // Show the page itself.
        // (the render function hides all pages so we need to show the one we want).
        page.addClass('visible');
    
      }
    

    Zobrazí stránku s náhledem jednoho produktu:

    function renderSingleProductPage(index, data){
    
        var page = $('.single-product'),
          container = $('.preview-large');
    
        // Find the wanted product by iterating the data object and searching for the chosen index.
        if(data.length){
          data.forEach(function (item) {
            if(item.id == index){
              // Populate '.preview-large' with the chosen product's data.
              container.find('h3').text(item.name);
              container.find('img').attr('src', item.image.large);
              container.find('p').text(item.description);
            }
          });
        }
    
        // Show the page.
        page.addClass('visible');
    
      }
    

    Vezme všechny produkty, vyfiltruje je na základě našeho dotazu a vrátí objekt s výsledky.

    function renderFilterResults(filters, products){
    
          // This array contains all the possible filter criteria.
        var criteria = ['manufacturer','storage','os','camera'],
          results = [],
          isFiltered = false;
    
        // Uncheck all the checkboxes.
        // We will be checking them again one by one.
        checkboxes.prop('checked', false);
    
        criteria.forEach(function (c) {
    
          // Check if each of the possible filter criteria is actually in the filters object.
          if(filters[c] && filters[c].length){
    
            // After we've filtered the products once, we want to keep filtering them.
            // That's why we make the object we search in (products) to equal the one with the results.
            // Then the results array is cleared, so it can be filled with the newly filtered data.
            if(isFiltered){
              products = results;
              results = [];
            }
    
            // In these nested 'for loops' we will iterate over the filters and the products
            // and check if they contain the same values (the ones we are filtering by).
    
            // Iterate over the entries inside filters.criteria (remember each criteria contains an array).
            filters[c].forEach(function (filter) {
    
              // Iterate over the products.
              products.forEach(function (item){
    
                // If the product has the same specification value as the one in the filter
                // push it inside the results array and mark the isFiltered flag true.
    
                if(typeof item.specs[c] == 'number'){
                  if(item.specs[c] == filter){
                    results.push(item);
                    isFiltered = true;
                  }
                }
    
                if(typeof item.specs[c] == 'string'){
                  if(item.specs[c].toLowerCase().indexOf(filter) != -1){
                    results.push(item);
                    isFiltered = true;
                  }
                }
    
              });
    
              // Here we can make the checkboxes representing the filters true,
              // keeping the app up to date.
              if(c && filter){
                $('input[name='+c+'][value='+filter+']').prop('checked',true);
              }
            });
          }
    
        });
    
        // Call the renderProductsPage.
        // As it's argument give the object with filtered products.
        renderProductsPage(results);
      }
    

    Zobrazuje chybový stav:

    function renderErrorPage(){
        var page = $('.error');
        page.addClass('visible');
      }
    

    Zvětší objekt filtrů a zapíše jej do hash.

    function createQueryHash(filters){
    
        // Here we check if filters isn't empty.
        if(!$.isEmptyObject(filters)){
          // Stringify the object via JSON.stringify and write it after the '#filter' keyword.
          window.location.hash = '#filter/' + JSON.stringify(filters);
        }
        else{
          // If it's empty change the hash to '#' (the homepage).
          window.location.hash = '#';
        }
    
      }
    

    Závěr

    Jednostránkové aplikace jsou perfektní, když chcete, aby váš projekt působil dynamičtěji a plynuleji, a pomocí několika chytrých návrhů můžete svým návštěvníkům nabídnout uhlazený a příjemný zážitek.


No