Hacer una aplicación de una sola página sin un marco

La idea detrás de las aplicaciones de una sola página (SPA) es crear una experiencia de navegación fluida como la que se encuentra en las aplicaciones de escritorio nativas. Todo el código necesario para la página se carga solo una vez y su contenido se cambia dinámicamente a través de JavaScript. Si todo se hace correctamente, la página nunca debería volver a cargarse, a menos que el usuario la actualice manualmente.

Existen muchos marcos para aplicaciones de una sola página. Primero tuvimos Backbone, luego Angular, ahora React. Se necesita mucho trabajo para aprender y volver a aprender cosas constantemente (sin mencionar tener que admitir el código antiguo que ha escrito en un marco olvidado hace mucho tiempo). En algunas situaciones, como cuando la idea de su aplicación no es demasiado compleja, en realidad no es tan difícil crear una aplicación de una sola página sin usar marcos externos. Aquí está cómo hacerlo.

La Idea

No usaremos un marco, pero lo haremos estar usando dos bibliotecas - jQuery para manipulación de DOM y manejo de eventos, y Handlebars para plantillas. Puede omitirlos fácilmente si desea que sean aún más mínimos, pero los usaremos por las ganancias de productividad que brindan. Estarán aquí mucho después de que se olvide el marco moderno del lado del cliente del día.

La aplicación que crearemos obtiene datos de productos de un archivo JSON y los muestra mediante la representación de una cuadrícula de productos con Handlebars. Después de la carga inicial, nuestra aplicación permanecerá en la misma URL y escuchará los cambios en el hash separarse del hashchange evento. Para navegar por la aplicación, simplemente cambiaremos el hash. Esto tiene el beneficio adicional de que el historial del navegador funcionará sin ningún esfuerzo adicional de nuestra parte.

La configuración

Como puede ver, no hay mucho en nuestra carpeta de proyectos. Tenemos la configuración habitual de la aplicación web:archivos HTML, JavaScript y CSS, acompañados de un archivo products.json que contiene datos sobre los productos de nuestra tienda y una carpeta con imágenes de los productos.

Los Productos JSON

El archivo .json se utiliza para almacenar datos sobre cada producto de nuestro SPA. Este archivo se puede reemplazar fácilmente por un script del lado del servidor para obtener datos de una base de datos real.

productos.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"
    }
  }
]

El HTML

En nuestro archivo html tenemos varios divs que comparten la misma "página" de clase. Esas son las diferentes páginas (o como se les llama en SPA - estados) que nuestra aplicación puede mostrar. Sin embargo, en la carga de la página, todos estos están ocultos a través de CSS y necesitan JavaScript para mostrarlos. La idea es que solo se pueda ver una página a la vez y nuestro script sea el que decida cuál es.

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>

Tenemos tres páginas:todos los productos (la lista de productos), producto único (la página del producto individual) y error .

Los todos los productos La página consta de un título, un formulario que contiene casillas de verificación para filtrar y una etiqueta

    con la clase "lista de productos". Esta lista se genera con handlebars usando los datos almacenados en products.json, creando un
  • para cada entrada en el json. Aquí está el resultado:

    Producto único se utiliza para mostrar información sobre un solo producto. Está vacío y oculto al cargar la página. Cuando se alcanza la dirección hash adecuada, se completa con los datos del producto y se muestra.

    La página de error consta solo de un mensaje de error para avisarle cuando ha llegado a una dirección incorrecta.

    El Código JavaScript

    Primero, hagamos una vista previa rápida de las funciones y lo que hacen.

    secuencia de comandos.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.
        }
    
    });
    

    Recuerde que el concepto de SPA es no tener ninguna carga mientras se ejecuta la aplicación. Es por eso que después de la carga de la página inicial, queremos permanecer en la misma página, donde el servidor ya ha obtenido todo lo que necesitamos.

    Sin embargo, todavía queremos poder ir a algún lugar de la aplicación y, por ejemplo, copiar la URL y enviársela a un amigo. Si nunca cambiamos la dirección de la aplicación, obtendrán la aplicación como se ve al principio, no como usted quería compartir con ellos. Para resolver este problema, escribimos información sobre el estado de la aplicación en la url como #hash. Los hashes no hacen que la página se vuelva a cargar y son fácilmente accesibles y manipulables.

    En cada cambio hash lo llamamos:

    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();
            }
    
        }
    

    Esta función tiene en cuenta la cadena inicial de nuestro hash, decide qué página debe mostrarse y llama a las funciones correspondientes.

    Por ejemplo, si el hash es '#filtro/{"almacenamiento":["16"],"cámara":["5"]}', nuestra palabra clave es '#filtro'. Ahora la función de representación sabe que queremos ver una página con la lista de productos filtrados y nos llevará a ella. El resto del hash se analizará en un objeto y se mostrará una página con los productos filtrados, cambiando el estado de la aplicación.

    Esto se llama solo una vez al inicio y convierte nuestro JSON en contenido HTML5 real a través de handlebars.

    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;
        })
      }
    

    Esta función recibe un objeto que contiene solo aquellos productos que queremos mostrar y los muestra.

    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');
    
      }
    

    Muestra la página de vista previa de un solo producto:

    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');
    
      }
    

    Toma todos los productos, los filtra según nuestra consulta y devuelve un objeto con los resultados.

    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);
      }
    

    Muestra el estado de error:

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

    Stringifica el objeto de filtros y lo escribe en el 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 = '#';
        }
    
      }
    

    Conclusión

    Las aplicaciones de una sola página son perfectas cuando desea darle a su proyecto una sensación más dinámica y fluida, y con la ayuda de algunas opciones de diseño inteligente puede ofrecer a sus visitantes una experiencia agradable y refinada.


No