Crear un carrito de compras Javascript del lado del cliente

📅 14/01/2025 👤 Julio Fuente 📂 programar
  • Clase magistral de tipografía, con Elliot Jay Stocks
  • Accesibilidad para diseñadores, con Stéphanie Walter

  • Índice
    1. Almacenamiento de sesiones: un recordatorio rápido
    2. Consideraciones de Seguridad
    3. Nuestro proyecto de muestra: Bodega
    4. Estructura HTML
      1. índice.html
      2. carrito.html
      3. pago.html
      4. pedido.html
    5. Código JavaScript
      1. Estructura del objeto
      2. Propiedades del objeto
      3. Métodos de objetos
      4. Métodos privados (ayudantes)

    En esta serie de artículos, Gabriele Romanato cubrirá en profundidad una implementación práctica del almacenamiento de sesiones mediante la creación de un carrito de compras de comercio electrónico completo con el sessionStorageobjeto y jQuery. Recuerde que, en estos artículos, no encontrará una nueva técnica para reemplazar las técnicas existentes del lado del servidor, sino simplemente una prueba del concepto de almacenamiento de sesiones.

    El almacenamiento de sesiones es una nueva característica introducida por la especificación " Almacenamiento web " del W3C. Es compatible con Internet Explorer 8+, Firefox, Chrome, Safari y Opera Desktop (para obtener una lista completa, consulte "¿ Puedo usarlo ?"). En esta serie de artículos, cubriremos en profundidad una implementación práctica del almacenamiento de sesiones mediante la creación de un carrito de compras de comercio electrónico completo con el sessionStorageobjeto y jQuery.

    Tenga en cuenta que, en estos artículos, no voy a proponer una nueva técnica para reemplazar las técnicas existentes del lado del servidor, sino simplemente una prueba del concepto de almacenamiento de sesiones.

    Usamos sesiones para almacenar datos y compartirlos en varias páginas. Por lo general, un usuario elegiría un producto y guardaríamos el nombre del producto junto con la cantidad y el precio elegidos.

    Luego, el usuario rellenaría un formulario con su información personal y lo guardaríamos en la sesión actual antes de finalizar el proceso, que suele ser la página de pago y la posterior redirección a la pasarela de pago (por ejemplo, PayPal). ).

    ¿Cómo se construyen los carritos de compras? PHP, por ejemplo, utiliza frecuentemente matrices asociativas para crear la estructura básica de un carrito de compras. Las matrices asociativas permiten a los desarrolladores web PHP mantener estructurados y organizados los datos de la sesión.

    Las sesiones de JavaScript funcionan de manera diferente. Generalmente, una sesión caduca cuando el usuario cierra su navegador (pero tenga en cuenta que el concepto de “cerrar un navegador” no está claro en dispositivos móviles). Cuando caduca una sesión, se eliminan todos los datos almacenados en el almacenamiento de sesión de un navegador web. No es necesario inicializar explícitamente una sesión porque en JavaScript una sesión toma la forma de un sessionStorageobjeto global y siempre está presente. Depende de nosotros escribir datos en la sesión actual.

    Los datos de la sesión vienen en forma de pares clave-valor y el valor de cada clave puede contener solo cadenas. Para escribir datos, podemos usar el sessionStorage.setItem( name, value )método:

    sessionStorage.setItem( "total", 120 );

    En este caso, la clave nombrada totalahora contiene el valor 120como una cadena, aunque hemos usado un número entero en nuestra llamada al .setItem()método. Este valor estará disponible hasta que caduque la sesión, a menos que usemos sessionStorage.removeItem( “total” )para eliminar la clave nombrada o llamemos sessionStorage.clear()para eliminar por completo todas las claves y valores del almacenamiento de la sesión.

    Tenga en cuenta que cuando una clave no existe en el almacenamiento de la sesión, su valor siempre es null. Luego, cuando eliminamos una clave del almacenamiento de la sesión e intentamos nuevamente obtener su valor, simplemente obtendremos null.

    Como habrás adivinado, nuestra clave ahora siempre está disponible, incluso mientras el usuario navega por las páginas de nuestro sitio web. Para obtener su valor simplemente escribimos lo siguiente:

    var total = sessionStorage.getItem( "total" );console.log( total ); // '120', a string

    También podemos actualizar su valor usando sessionStorage.setItem()nuevamente con un nuevo valor:

    var total = parseInt( sessionStorage.getItem( "total" ) );var quantity = 2;var updatedTotal = total * quantity;sessionStorage.setItem( "total", updatedTotal ); // '240', a string

    Ahora, la clave nombrada totaltiene un valor de 240con nuestra última actualización. ¿Por qué llamamos parseInt()? Esta es una técnica sencilla para convertir una cadena numérica en un número verdadero, asegurando que nuestro cálculo será consistente. Recuerde que todos los valores en el almacenamiento de la sesión son cadenas y nuestros cálculos deben realizarse únicamente entre números.

    ¡Pero espera! ¿Qué pasa con los objetos? Los objetos se pueden almacenar en el almacenamiento de sesión convirtiéndolos primero en cadenas JSON (con JSON.stringify()) y luego nuevamente en objetos JavaScript (con JSON.parse()):

    var cart = {    item: "Product 1",    price: 35.50,    qty: 2};var jsonStr = JSON.stringify( cart );sessionStorage.setItem( "cart", jsonStr );// now the cart is {"item":"Product 1","price":35.50,"qty":2}var cartValue = sessionStorage.getItem( "cart" );var cartObj = JSON.parse( cartValue );// original object

    Para actualizar nuestro objeto, simplemente lo ampliamos y luego repetimos el procedimiento anterior.

    La seguridad es importante. Si leemos las notas de seguridad de la especificación del W3C, sabremos de los riesgos de seguridad incluso de una tecnología del lado del cliente como el almacenamiento web.

    El documento técnico del Equipo de Preparación para Emergencias Informáticas de EE. UU. sobre seguridad de sitios web (PDF) establece claramente:

    "Cada organización comunitaria, corporación, empresa o agencia gubernamental depende de un sitio web externo para proporcionar información sobre ellos mismos, anunciar un evento o vender un producto o servicio. En consecuencia, los sitios web públicos suelen ser los vectores de ataque más específicos para actividad maliciosa."

    Incluso si una sesión del navegador finaliza cuando el navegador está cerrado, aún pueden producirse ataques maliciosos, especialmente si el navegador se ha visto comprometido por ciertos exploits. Además, los sitios web comprometidos a menudo pueden utilizarse para difundir malware dirigido a navegadores concretos.

    Por este motivo, asegúrese de que su sitio web sea seguro antes de confiar en cualquier técnica para almacenar datos en el navegador. Mantener un sitio web seguro está más allá del alcance de este artículo, pero simplemente siguiendo las mejores prácticas de seguridad, debería poder beneficiarse del almacenamiento web sin preocuparse demasiado por sus implicaciones de seguridad.

    Nuestro proyecto de muestra es una tienda online que vende vino. Es un sitio web de comercio electrónico sencillo cuya única complicación está en cómo se calculan los gastos de envío.

    En definitiva, los vinos se venden en paquetes de seis botellas. Esto significa que la cantidad total de botellas vendidas debe ser siempre múltiplo de seis. Los gastos de envío se calculan, entonces, en función de la cantidad total de botellas vendidas.

    Nuestra tienda dependerá de PayPal, por lo que tendremos que crear una cuenta comercial en PayPal Sandbox para probar nuestro código.

    El usuario podrá agregar y quitar productos de su carrito de compras, actualizar el carrito, cambiar la cantidad de cada producto y vaciar el carrito. Deben completar un formulario con su información de contacto, especificando si su dirección de facturación es la misma que su dirección de envío.

    Antes de ser redirigido a PayPal, el usuario verá una página de resumen con sus datos personales, su carrito y el precio total del carrito más los gastos de envío.

    Después de completar su compra, el usuario deberá ser redirigido nuevamente a nuestro sitio web. Este es el único paso del proceso que no podemos manejar solo con JavaScript . PayPal enviará varios datos a través de una solicitud HTTP que debe procesarse con un lenguaje del lado del servidor (como PHP). Si necesita más información para comenzar con este tipo de procesamiento, consulte el tutorial de PayPal.

    Nuestro proyecto se compone de los siguientes apartados:

    Repasaremos el marcado de este proyecto en las siguientes secciones.

    índice.html

    Los componentes principales de esta página son los formularios que permiten al usuario agregar productos a su carrito de compras.

    div data-name="Wine #1" data-price="5"    h3Wine #1/h3        peuro; 5/p        form action="cart.html" method="post"            div                label for="qty-1"Quantity/label                input type="text" name="qty-1" value="1" /            /div            pinput type="submit" value="Add to cart" //p        /form/div

    Se puede acceder a los atributos de datos utilizados aquí para almacenar nombres y precios de productos a través de jQuery utilizando los métodos .data() y $.data() .

    carrito.html

    Nuestra página de carrito de compras se compone de tres componentes: una tabla con la información del producto, un elemento que muestra el subtotal y una lista de acciones del carrito.

    form action="cart.html" method="post"    table        thead            tr                th scope="col"Item/th                th scope="col"Qty/th                th scope="col"Price/th            /tr        /thead        tbody/tbody    /table    p        strongSub Total/strong: span/span    /p    ul        li            input type="submit" name="update" value="Update Cart" /        /li        li            input type="submit" name="delete" value="Empty Cart" /        /li        li            a href="index.html"Continue Shopping/a        /li        li            a href="checkout.html"Go To Checkout/a        /li    /ul/form

    La tabla contenida en esta página está vacía y la llenaremos con datos a través de JavaScript. El elemento que muestra el subtotal funciona simplemente como marcador de posición para JavaScript. Las dos primeras acciones, "Actualizar carrito" y "Vaciar carrito", serán manejadas por JavaScript, mientras que las dos últimas acciones son simplemente enlaces a la página de lista del producto y a la página de pago, respectivamente.

    pago.html

    Esta página tiene cuatro componentes:

    table    thead        tr            th scope="col"Item/th            th scope="col"Qty/th            th scope="col"Price/th        /tr    /thead    tbody    /tbody/tablediv    p        strongShipping/strong: span/span    /p    p        strongTotal/strong: span/span    /p/divform action="order.html" method="post"    h2Your Details/h2        fieldset            legendBilling/legend                !-- Name, Email, City, Address, ZIP Code, Country (select box) --div    label for="name"Name/label    input type="text" name="name" data-type="string" data-message="This field may not be empty" //divdiv    label for="email"Email/label    input type="text" name="email" data-type="expression" data-message="Not a valid email address" //divdiv    label for="city"City/label    input type="text" name="city" data-type="string" data-message="This field may not be empty" //divdiv    label for="address"Address/label        input type="text" name="address" data-type="string" data-message="This field may not be empty" //divdiv    label for="zip"ZIP Code/label    input type="text" name="zip" data-type="string" data-message="This field may not be empty" //divdiv    label for="country"Country/label        select name="country" data-type="string" data-message="This field may not be empty"            option value=""Select/option            option value="US"USA/option            option value="IT"Italy/option        /select/div/fieldsetdivSame as Billing input type="checkbox" value=""//divfieldsetlegendShipping/legend    !-- Same fields as billing --/fieldsetpinput type="submit" value="Submit" //p/form

    Los atributos de datos se utilizan aquí para la validación. El data-typeatributo especifica el tipo de datos que estamos validando y data-messagecontiene el mensaje de error que se mostrará en caso de falla.

    No utilicé la validación de correo electrónico integrada en los navegadores web sólo por simplicidad, pero puedes usarla si quieres.

    pedido.html

    Esta página final contiene un breve resumen del pedido del usuario, sus detalles y el formulario de PayPal.

    h1Your Order/h1table    thead        tr            th scope="col"Item/th            th scope="col"Qty/th            th scope="col"Price/th        /tr    /thead    tbody    /tbody/tablediv    p        strongShipping/strong: span/span    /p    p        strongTotal/strong: span/span    /p/divdiv    h2Your Data/h2        div/div/divform action="" method="post"    input type="hidden" name="cmd" value="_cart" /    input type="hidden" name="upload" value="1" /    input type="hidden" name="business" value="" /    input type="hidden" name="currency_code" value="" /    input type="submit" value="Pay with PayPal" //form

    El formulario de PayPal y otros elementos de esta página están inicialmente vacíos, excepto aquellos campos que no necesitan generarse dinámicamente.

    El diseño CSS de este proyecto no tendrá ninguna influencia real en el objetivo que queremos lograr. Incluso si deshabilitamos CSS por completo, el proyecto continuaría funcionando gracias a la fuerte relación entre la estructura del HTML y el comportamiento de JavaScript.

    Usaremos un enfoque orientado a objetos debido a la complejidad de nuestros objetivos. Nuestro objeto se basará en un patrón de construcción simple y utilizará métodos públicos y privados.

    Estructura del objeto

    Nuestro objeto tiene una estructura muy simple. La función constructora inicializa el elemento de nivel superior que envuelve toda la estructura de nuestro DOM e invoca el método de inicialización.

    (function( $ ) {    $.Shop = function( element ) {        this.$element = $( element ); // top-level element        this.init();    };    $.Shop.prototype = {        init: function() {            // initializes properties and methods        }    };    $(function() {        var shop = new $.Shop( "#site" ); // object's instance    });})( jQuery );

    La instancia del objeto se crea cuando el DOM está listo. Podemos probar que todo ha funcionado bien de la siguiente manera:

    $(function() {    var shop = new $.Shop( "#site" );    console.log( shop.$element );});

    Esto genera lo siguiente:

    x.fn.x.init[1]    0: div#site    context: document    length: 1    selector: "#site"

    Ahora que sabemos que se ha creado una instancia de nuestro objeto correctamente, podemos definir sus propiedades.

    Propiedades del objeto

    Las propiedades de nuestro objeto se dividen en dos categorías: primero, las propiedades para manejar cálculos, formularios y validación, y segundo, las referencias a elementos HTML.

    $.Shop.prototype = {    init: function() {        // Properties            this.cartPrefix = "winery-"; // prefix string to be prepended to the cart's name in session storage            this.cartName = this.cartPrefix + "cart"; // cart's name in session storage            this.shippingRates = this.cartPrefix + "shipping-rates"; // shipping rates key in session storage            this.total = this.cartPrefix + "total"; // total key in the session storage            this.storage = sessionStorage; // shortcut to sessionStorage object            this.$formAddToCart = this.$element.find( "form.add-to-cart" ); // forms for adding items to the cart            this.$formCart = this.$element.find( "#shopping-cart" ); // Shopping cart form            this.$checkoutCart = this.$element.find( "#checkout-cart" ); // checkout form cart            this.$checkoutOrderForm = this.$element.find( "#checkout-order-form" ); // checkout user details form            this.$shipping = this.$element.find( "#sshipping" ); // element that displays the shipping rates            this.$subTotal = this.$element.find( "#stotal" ); // element that displays the subtotal charges            this.$shoppingCartActions = this.$element.find( "#shopping-cart-actions" ); // cart actions links            this.$updateCartBtn = this.$shoppingCartActions.find( "#update-cart" ); // update cart button            this.$emptyCartBtn = this.$shoppingCartActions.find( "#empty-cart" ); // empty cart button            this.$userDetails = this.$element.find( "#user-details-content" ); // element that displays the user's information            this.$paypalForm = this.$element.find( "#paypal-form" ); // PayPal form            this.currency = "euro;"; // HTML entity of the currency to be displayed in layout            this.currencyString = "€"; // currency symbol as text string            this.paypalCurrency = "EUR"; // PayPal's currency code            this.paypalBusinessEmail = "[email protected]"; // your PayPal Business account email address            this.paypalURL = "https://www.sandbox.paypal.com/cgi-bin/webscr"; // URL of the PayPal form            // object containing patterns for form validation            this.requiredFields = {                expression: {                    value: /^([w-.]+)@((?:[w]+.)+)([a-z]){2,4}$/                },                str: {                    value: ""                }            };            // public methods invocation    }};

    Repasemos estas propiedades una por una.

    Almacenamiento y otras propiedades:

    Referencias a elementos:

    Todos los elementos tienen el prefijo del $signo, lo que significa que son objetos jQuery. Pero no todos estos elementos están disponibles en todas las páginas . Para comprobar si existe un elemento jQuery, simplemente pruebe su lengthpropiedad:

    if( $element.length ) {    // the element exists}

    Otro enfoque, que no se utiliza en nuestro proyecto, es agregar una ID o clase particular al bodyelemento y realizar acciones de forma condicional:

    var $body = $( "body" ),    page = $body.attr( "id" );    switch( page ) {        case "product-list":            // actions for handling products            break;        case "shopping-cart":            // actions for handling the shopping cart            break;        case "checkout":            // actions for handling the checkout's page            break;        default:            break;    }

    Métodos de objetos

    Las acciones de nuestro código tienen lugar en los métodos de nuestro objeto, los cuales, a su vez, se pueden dividir en métodos públicos y privados. Los métodos privados operan en segundo plano, por así decirlo, y ayudan a los métodos públicos a realizar sus tareas. Estos métodos tienen el prefijo un guión bajo y nunca se utilizan directamente.

    Mientras tanto, los métodos públicos operan directamente sobre los elementos y datos de la página y no tienen prefijos. Ya hemos visto el init()método, que simplemente inicializa propiedades y otros métodos públicos en la función constructora del objeto. Los otros métodos se explicarán a continuación.

    Métodos privados (ayudantes)

    El primer método privado, _emptyCart()simplemente vacía el almacenamiento de la sesión actual en el navegador:

    $.Shop.prototype = {    // empties session storage    _emptyCart: function() {        this.storage.clear();    }};

    Para formatear un número con un número determinado de decimales, implementamos el _formatNumber()método:

    /* Format a number by decimal places * @param num Number the number to be formatted * @param places Number the decimal places * @returns n Number the formatted number*/_formatNumber: function( num, places ) {    var n = num.toFixed( places );    return n;}

    Este método utiliza el método toFixed() del Numberobjeto de JavaScript. Su función en nuestro proyecto es formatear adecuadamente los precios.

    Debido a que no todos los precios de nuestras páginas están contenidos en atributos de datos , necesitamos un método especializado para extraer la porción numérica de una cadena de los nodos de texto. Este método se llama _extractPrice():

    /* Extract the numeric portion from a string * @param element Object the jQuery element that contains the relevant string * @returns price String the numeric string */_extractPrice: function( element ) {    var self = this;    var text = element.text();    var price = text.replace( self.currencyString, "" ).replace( " ", "" );    return price;}

    Arriba, selfhay una referencia al $.Shopobjeto, y la necesitaremos cada vez que queramos acceder a una propiedad o método de nuestro objeto sin preocuparnos mucho por el alcance.

    Puede proteger este método agregando una rutina adicional que elimine todos los espacios en blanco finales:

    var text = $.trim( element.text() );

    Tenga en cuenta que el método $.trim() de jQuery elimina todas las líneas, espacios (incluidos los espacios que no se separan) y tabulaciones nuevas desde el principio y el final de una cadena. Si estos caracteres de espacio en blanco aparecen en medio de una cadena, se conservan.

    Entonces, necesitamos dos métodos para convertir cadenas en números y números en cadenas . Esto es necesario para realizar cálculos y mostrar los resultados en nuestras páginas.

    /* Converts a numeric string into a number * @param numStr String the numeric string to be converted * @returns num Number the number, or false if the string cannot be converted */_convertString: function( numStr ) {    var num;    if( /^[-+]?[0-9]+.[0-9]+$/.test( numStr ) ) {        num = parseFloat( numStr );    } else if( /^d+$/.test( numStr ) ) {        num = parseInt( numStr );    } else {        num = Number( numStr );    }    if( !isNaN( num ) ) {        return num;    } else {        console.warn( numStr + " cannot be converted into a number" );        return false;    }},/* Converts a number to a string * @param n Number the number to be converted * @returns str String the string returned */_convertNumber: function( n ) {    var str = n.toString();    return str;}

    Arriba, _convertString()ejecuta las siguientes pruebas:

    1. ¿La cadena tiene formato decimal? Si es así, utiliza la función parseFloat() .
    2. ¿La cadena tiene un formato de número entero? Si es así, utiliza la función parseInt() .
    3. Si no se puede detectar el formato de la cadena, utiliza el constructor Number() .
    4. Si el resultado es un número (probado con la función isNaN() ), devuelve el número. De lo contrario, envía una advertencia a la consola de JavaScript y devuelve false.

    Por el contrario, _convertNumber()simplemente invoca el método toString() para convertir un número en una cadena.

    El siguiente paso es definir dos métodos para convertir un objeto JavaScript en una cadena JSON y una cadena JSON nuevamente en un objeto JavaScript:

    /* Converts a JSON string to a JavaScript object * @param str String the JSON string * @returns obj Object the JavaScript object */_toJSONObject: function( str ) {    var obj = JSON.parse( str );    return obj;},/* Converts a JavaScript object to a JSON string * @param obj Object the JavaScript object * @returns str String the JSON string */_toJSONString: function( obj ) {    var str = JSON.stringify( obj );    return str;}

    El primer método hace uso del JSON.parse()método, mientras que el último invoca el JSON.stringify()método (consulte el artículo de Mozilla Developer Network sobre " Uso de JSON nativo ").

    ¿Por qué necesitamos estos métodos? Porque nuestro carrito también almacenará la información relacionada con cada producto utilizando el siguiente formato de datos (espacios añadidos para legibilidad):

    Llave Valor
    winery-cart { "items": [ { "product": "Wine #1", "qty": 5, "price": 5 } ] }

    La winery-cartclave contiene una cadena JSON que representa una matriz de objetos (es decir, items) en la que cada objeto muestra la información relevante sobre un producto agregado por el usuario, es decir, el nombre del producto, la cantidad y el precio.

    Es bastante obvio que ahora también necesitamos un método especializado para agregar elementos a esta clave particular en el almacenamiento de sesiones:

    /* Add an object to the cart as a JSON string * @param values Object the object to be added to the cart * @returns void */_addToCart: function( values ) {    var cart = this.storage.getItem( this.cartName );    var cartObject = this._toJSONObject( cart );    var cartCopy = cartObject;    var items = cartCopy.items;    items.push( values );    this.storage.setItem( this.cartName, this._toJSONString( cartCopy ) );}

    Este método obtiene la clave del carrito del almacenamiento de la sesión, la convierte en un objeto JavaScript y agrega un nuevo objeto como una cadena JSON a la matriz del carrito. El objeto recién agregado tiene el siguiente formato:

    this._addToCart({    product: "Test",    qty: 1,    price: 2});

    Ahora, nue






    Tal vez te puede interesar:

    1. ¿Deberían abrirse los enlaces en ventanas nuevas?
    2. 24 excelentes tutoriales de AJAX
    3. 70 técnicas nuevas y útiles de AJAX y JavaScript
    4. Más de 45 excelentes recursos y repositorios de fragmentos de código