Корзина для интернет-магазина на фронте или Пишем модульный javascript

сентябрь 6 , 2015
Следующая статья Демо Исходники

Корзина для интернет-магазина на фронтеОднажды пришла мне в голову безумная идея написать серию постов про различные подходы к организации javascript-кода. Такая мысль образовалась, когда по рабочей надобности изучал React.js и возрадовался от некоторых идей, заложенных его авторами. Захотелось потрогать его побольше, а потому как писать хеллоуворды из документации скучно, нужна была какая-то идея. Раз уж я начал вести блог на тему веб-разработки, то почему бы не создать простое, но более-менее внятное приложение с применением различных библиотек и фреймворков? И не только реакта, а любых других, до которых доберется дурной и воспаленный ум. В качестве подопытного приложения я возьму простенький интернет-магазин с каталогом и корзиной. Фишка будет в том, что код и каталога, и корзины будет написан на javascript. Корзина на фронте - не самое удачное решение для реальных проектов, но для небольших сайтов и в качестве изучения подойдет неплохо. Для изучения React понадобится сколько-то времени, поэтому для начала развлеку вас статьей, где опишу процесс создания нашего приложения без использования библиотек и фреймворков, но с использованием яваскрипт-модулей. Главная моя цель - это показать различные подходы к созданию приложений на javascript. С версткой заморачиваться сильно не буду, сверстаю на bootstrap, основной упор сделаю на javascript-код. Манипулировать DOM будем всем знакомым добрым jquery. Также подключим underscore для работы с данными и html-шаблонами. Данные для каталога загрузим из внешнего json-файла, а корзину будем хранить в localStorage. Итак, начнем...


Идея приложения и схема работы.

Итак, интернет-магазин на фронте. Что мы хотим от него? Хотим, чтобы была возможность как минимум вывести каталог товаров, реализовать добавление их в корзину, просматривать оную корзину, менять количество товаров в ней и удалять ошибочно добавленные с пересчетом стоимости заказа. Плюс добавим хранение данных в localStorage, дабы после перезагрузки страницы наша корзина не ушла в небытие.

Обращаю внимание, что это не одностраничное приложение. Мы создадим 2 html-страницы, каталог и корзина, но они будут служить только каркасом с основному коду. Основной код - это все-таки javascript.


Функционал подробнее.

Главное меню - 2 кнопки, каталог и корзина. В меню рядом с надписью "корзина" показывается выбранных количество товаров. Страница index - главная страница магазина, она же каталог. Подгружаем товары их из внешнего json-файла. Товары имеют поля: id, name, price, img. У каждого товара есть кнопка "Добавить в корзину". Список товаров храним в localStorage (id, name, count, price). Корзина - таблица с выбранными товарами. Выводим id и название товара, его количество и сумму. Под таблицей показываем общую сумму всех товаров. При изменении количества товаров и его удалении меняем все связанные данные. Каталог и корзину мы оформим в виде отдельных модулей.

Можно посмотреть, что у нас в итоге получится, здесь. Попробуйте добавить товары в корзину, перейти в нее, менять количество и удалять товары, и увидите, как задумана работа магазина. Или можете сразу скачать исходники по этой ссылке.


Структура файлов

В корне проекта разместим 2 файла: index.html (каталог) и cart.html (корзина). И несколько папок: css, там лежит bootstrap.min.css и main.css - наши стили. Папка data содержит один файл goods.json - наши товары. fonts хранит шрифты от bootstrap-иконок. img - картинки товаров и гифку loading, которую мы будем показывать посетителям сайта, пока грузится каталог и корзина. Папка js разделена на 2 подпапки: vendor и modules. vendor содержит нужные нам библиотеки: jquery и underscore, а modules - модули нашего приложения. Их всего 3: main.js отвечает за инициализацию приложения, catalog - за вывод товаров, cart - за работу корзины.

Небольшое отступление: я намеренно не стал вводить лишнюю логику для нашего проекта. Хорошей практикой считается разработка с подходом MVC, разделение кода на модели, контроллеры и представления и разбиение этих частей на отдельные файлы для наглядности. Можно долго спорить на тему правильности применения такого подхода для абсолютно всех проектов, но в нашем конкретном примере мы отойдем от этого постулата и весь код для управления корзиной разместим в одном файле, а уже внутри него логически отделим работу с данными от разметки.


Приступаем к разработке.

Я не буду расписывать подробно каждую строчку кода, это заняло бы слишком много места и времени. В статье рассмотрим основные моменты, а весь код можно посмотреть в исходниках по ссылке выше. Код с подробными комментариями.
Начинаем.


Создаем разметку.

Создадим в корне нашего проекта файлы index.html и cart.html. Каждый файл - стандартная html5-заготовка. В блоке head подключим шрифт Ubuntu с Google Fonts и 2 css-файла: bootstrap.min.css и наши собственные стили main.css.

        
        Webdevkin. Интернет-магазин. Javascript
        
        
        
    
В конце страницы, перед закрывающим тегом body, подключим скрипты в таком порядке:
        
        
        
        
        
    
Разметка для index.html
        
            

Webdevkin. Интернет-магазин. Модульный Javascript



Из интересного: мы указываем в data-page тега body название страницы и элемент с id="total-cart-count", который будет содержать количество добавленных в корзину товаров. Список товаров каталога будет выводиться в списке id="goods". При загрузке страницы в список поместим нашу гифку loading, чтобы пользователи не скучали, ожидая загрузки каталога.

Разметка для cart.html

        
            

Webdevkin. Интернет-магазин. Модульный Javascript



Артикул Название Цена Количество Сумма
Итого: 0 руб.

Здесь тоже нет ничего особенного: заготовка таблицы корзины, надпись "итого" и кнопка заказа (ее функционал мы реализовывать не будем). Содержимое корзины будет выводиться в tbody id="cart", сейчас на этом месте уже знакомая гифка.


На заметку: соглашения по html и css-коду.

В верстке я всегда стараюсь не забывать несколько моментов и рекомендую их Вам.
Первое: верстаем всегда на классах и только на классах. Никаких айдишников в css. id нужны для яваскрипт-кода - для быстрого получения доступа к элементу DOM.
И второе: если в js-коде нам нужно обращаться к множеству элементов (много кнопок добавления в корзину), то навешиваем элементам классы с префиксом "js-". И не используем эти "js-ы" в css-коде. Сначала эти соглашения кажутся излишними, но понимание плюсов быстро проходит с ростом проекта. Лучше сразу привыкать к подобному стилю, тем более, что его используют достаточно много разработчиков.


Готовим данные и разметку для каталога

Для начала создадим файл для хранения наших товаров: data/goods.json

        [
            {
                "id": "1",
                "name": "Ноутбук Lenovo",
                "price": "18000",
                "img": "img/cars/notebook_lenovo.jpg"
            },
            {
                "id": "2",
                "name": "Фотокамера Nikon",
                "price": "25000",
                "img": "img/cars/camera_nikon.jpg"
            },
            {
                "id": "3",
                "name": "Apple ipad",
                "price": "35000",
                "img": "img/cars/ipad.jpg"
            },
            {
                "id": "4",
                "name": "Samsung Galaxy",
                "price": "20000",
                "img": "img/cars/phone_galaxy.jpg"
            },
            {
                "id": "5",
                "name": "Телевизор SUPRA",
                "price": "19000",
                "img": "img/cars/tv_supra.jpg"
            }
        ]    
    
Как видим, это обычный json-массив с четырьмя нужными нам полями. Теперь переходим к созданию каталога. Но прежде чем приступить к написанию js-кода, нам придется написать еще немного разметки для отображения отдельного товара. Мы будем использовать шаблоны html-кода библиотеки underscore для динамической генерации отдельных товаров. Если Вы не знакомы с шаблонами underscore, то у меня есть статья на эту тему. Я же не буду зацикливаться на этом, а просто приведу код шаблона, тем более, что он достаточно тривиален и не требует долгих разбирательств:
        
    
Что происходит в этом коде? underscore-шаблон представляет собой обычную строку, в которую подставляются нужные данные. Это неплохой способ отделить логику и данные от представления. Вся идея шаблонов в том, что мы не знаем, каким образом получены данные, но мы знаем, как их нужно отобразить. В нашем примере мы даем на вход массив товаров goods (из файла goods.json), перебираем все товары в цикле с помощью функции each библиотеки underscore и для каждого товара выводим свою разметку, подставляя в нужные места id товара, название, картинку и цену. Обратите внимание на дата-атрибуты у кнопки "Добавить в корзину", они будут использованы в дальнейшем. Приведенный код мы поместим в тело body файла index.html. Дальше мы увидим, как связать данные и наш underscore-шаблон.

UPDATE: написал статью на тему undescrore/lodash шаблонов


Пишем js-модуль каталога

Код файла catalog.js будет очень коротким

        'use strict';
        
        // Модуль каталога
        var catalog = (function($) {
        
            // Инициализация модуля
            function init() {
                _render();
            }
        
            // Рендерим каталог
            function _render() {
                var template = _.template($('#catalog-template').html()),
                    $goods = $('#goods');
        
                $.getJSON('data/goods.json', function(data) {
                    $goods.html(template({goods: data}));
                });
            }
        
            // Экспортируем наружу
            return {
                init: init
            }
            
        })(jQuery);
    
Здесь с помощью замыкания мы объявляем переменную-модуль catalog, пишем функцию init, которая вызывает самую интересную нам функцию _render и экспортируем init наружу, разрешая при этом вызывать catalog.init() из других модулей приложения. На самом деле можно обойтись и без лишней init-функции, но лучше всегда объявлять публичную функцию инициализации во всех модулях для единообразия. При этом функция _render начинается со знака _, чем мы показываем, что эта функция частная и не должна выходить за пределы модуля. Применяя такой подход, мы уже в коде модуля видим, что используется в других модулях, а что предназначено только для внутреннего пользования. Этакая инкапсуляция кода, как в ООП.

Разберем функцию _render. Сначала мы объявляем переменную template = _.template($('#catalog-template').html()).
_.template - это функция underscore, которая рендерит html-разметку, используя шаблон html и данные для него.
$('#catalog-template').html() - содержимое шаблона, т.е. просто строка. $.getJSON получает данные из внешнего файла и передает наш массив товаров в колбэк. Выражение template({goods: data}) означает, что мы передаем функции template объект с данными и генерируем на их основе html-строку. Эта строка вставляется на страницу при помощи $goods.html(htmlString). Теперь вызов функции catalog.init() - это все, что нужно, чтобы загрузить каталог на страницу index.html


Модуль корзины

Наконец мы приступаем к самой интересной части нашего проекта - к корзине. Наш модуль будет разбит на 3 логические части:

  • 1 - логика работы с данными
  • 2 - работа с событиями DOM
  • 3 - общие функции инициализации и настройки
Вот так будет выглядеть заготовка модуля:
        'use strict';

        // Модуль корзины
        var cart = (function($) {
        
            var cartData,  // данные корзины - массив объектов
                opts = {}; // настройки модуля
        
            // Инициализация модуля
            function init(options) {
                _initOptions(options);
                // Получение данных
                // Рендер корзины
                _bindHandlers();
            }
        
            // Инициализируем настройки
            function _initOptions(options) {
                // код
            }
        
            // Навешивам события
            function _bindHandlers() {
                // код
            }
        
            // Функции работы с данными
        
            // Функции рендеринга
            
            // Функции-обработчики событий
        
            // Экспортируем наружу
            return {
                init: init,
                // функции работы с данными
            }
        
        })(jQuery);
    
Разберем нашу заготовку. Имеются 2 глобальных переменных для всего модуля: данные корзины и набор параметров. Этот набор содержит настройки модуля, такие как названия классов и дата-атрибутов, id элементов количества товаров и общей суммы заказа, классы для кнопок добавить, удалить, изменить количество и прочие. Подробнее рассмотрим ниже. Основная функция - инициализация модуля. Вне модуля будет вызываться только она, остальное скрыто в реализации корзины. Инициализируется модуль в таком порядке: назначаются опции модуля, получаем данные для корзины из localStorage, рендерим саму корзину, количество товаров и общую сумму заказа и напоследок привязываем обработчики к событиям DOM. Примечание: если Вы раньше не использовали такой подход, отделение данных от разметки, то удивитесь, насколько интереснее писать отдельно код обработки данных и после проще навешивать события на кнопочки. Дальше мы это продемонстрируем.

Вероятно, Вы обратили внимание, что я экспортирую наружу не только init, но и все функции, касающиеся обработки данных. Сделано это для того, чтобы облегчить тестирование кода. Каким образом проходят тесты? Есть 2 способа: ручной и unit-тесты. При тестировании руками мы в консоли браузера вызываем функции модуля и сразу же видим результат. Например, вызвав cart.add({id: 1, name: 'notebook', price: 30000, count: 2}), мы в консоли же можем сразу увидеть изменения и убедиться, что данные действительно добавлены (или что-то пошло не так). Мы отделили логику работы с данными от остальных функций и увидели, что так тестировать гораздо веселее, чем тыкать по кнопочкам и проверять содержимое localStorage после каждого клика. Второй способ будет напрашиваться сам собой, когда нам надоест набивать команды в консоли и мы наконец разберемся с unit-тестированием кода. Об этом я напишу в одной из ближайших статей.

UPDATED: Для интересующихся unit-тестированием опубликована статья unit-тесты на фронте или изучаем jasmine.js. В ней рассказывается, как тестировать код на примере нашей корзины с помощью популярной библиотеки jasmine.js.


Пишем функции обработки данных

Всего мы напишем 11 функций:

  • 1. updateData - обновляем данные из localStorage, записываем содержимое в переменную cartData
  • 2. getData - возвращаем данные
  • 3. saveData - сохраняем корзину в localStorage
  • 4. clearData - очищаем корзину
  • 5. getById - ищем элемент корзины по id товара
  • 6. add - добавляем товар в корзину
  • 7. remove - удаляем товар из корзины
  • 8. changeCount - меняем количество
  • 9. getCount - возвращаем число уникальных товаров корзины
  • 10. getCountAll - возвращаем число всех товаров корзины
  • 11. getSumma - возвращаем общую сумму заказа
Код всех функций достаточно простой, в несколько строк, поэтому привожу сразу все функции. При обработке используются методы underscore. Если Вы еще не знакомы с этой библиотекой, настоятельно рекомендую ее изучить. Не зря ее называют "швейцарским ножом для javascript-разработчика". Беглое изучение официальной документации (есть на русском языке) займет немного времени, а пользу принесет заметную.


Полный код работы с данными

        // Получаем данные
        function updateData() {
            cartData = JSON.parse(localStorage.getItem('cart')) || [];
            return cartData;
        }
    
        // Возвращаем данные
        function getData() {
            return cartData;
        }
    
        // Сохраняем данные в localStorage
        function saveData() {
            localStorage.setItem('cart', JSON.stringify(cartData));
            return cartData;
        }
    
        // Очищаем данные
        function clearData() {
            cartData = [];
            saveData();
            return cartData;
        }
    
        // Поиск объекта в коллекции cartData по id
        function getById(id) {
            return _.findWhere(cartData, {id: id});
        }
    
        // Добавление товара в коллекцию
        function add(item) {
            var oldItem;
            updateData();
            oldItem = getById(item.id);
            if(!oldItem) {
                cartData.push(item);
            } else {
                oldItem.count = oldItem.count + item.count;
            }
            saveData();
            return item;
        }
    
        // Удаление товара из коллекции
        function remove(id) {
            updateData();
            cartData = _.reject(cartData, function(item) {
                return item.id === id;
            });
            saveData();
            return cartData;
        }
    
        // Изменение количества товара в коллекции
        function changeCount(id, delta) {
            var item;
            updateData();
            item = getById(id);
            if(item) {
                item.count = item.count + delta;
                if (item.count < 1) {
                    remove(id);
                }
                saveData();
            }
            return getById(id) || {};
        }
    
        // Возвращаем количество товаров (количество видов товаров в корзине)
        function getCount() {
            return _.size(cartData);
        }
    
        // Возвращаем общее количество товаров 
        function getCountAll() {
            return _.reduce(cartData, function(sum, item) {return sum + item.count}, 0);
        }
    
        // Возвращаем общую сумму
        function getSumma() {
            return _.reduce(cartData, function(sum, item) {return sum + item.count * item.price}, 0);
        }    
    

Из интересного отмечу, что каждая функция возвращает какие-то данные: корзину целиком, добавленный товар или общую сумму. Не все возвращаемые данные используются в модуле, но все облегчают тестирование. Также видим, что в localStorage мы храним сериализованный массив товаров. В функции добавление предусмотрено условие: если мы добавляем товар, уже находящийся в корзине, то не создается дубликат товара, а увеличивается количество уже существующего. При уменьшении количества конкретного товара до нуля, оный удаляется из корзины.


Инициализация настроек. Настройки по умолчанию.

Прежде чем писать функции рендеринга и обработчиков событий, вернемся чуть назад и рассмотрим инициализацию настроек модуля. Функция _initOptions скопирует в переменную opts все настройки, переданные в модуль корзины при его создании.

        // Инициализируем настройки
        function _initOptions(options) {
            var defaultOptions = {
                renderCartOnInit: true,                     // рендерить ли корзину при инициализации
                renderMenuCartOnInit: true,                 // рендерить ли количество товаров в меню при инициализации
                elAddToCart: '.js-add-to-cart',             // селектор для кнопки добавления в корзину
                attrId: 'data-id',                          // дата-атрибут для id товара
                attrName: 'data-name',                      // дата-атрибут для названия товара
                attrPrice: 'data-price',                    // дата-атрибут для цены товара
                attrDelta: 'data-delta',                    // дата-атрибут, показывающий, на сколько нужно изменить количество товара в корзине (-1 и 1)
                elCart: '#cart',                            // селектор для содержимого корзины
                elTotalCartCount: '#total-cart-count',      // селектор для количества товаров в корзине
                elTotalCartSumma: '#total-cart-summa',      // селектор для общей суммы заказа
                elCartItem: '.js-cart-item',                // селектор для отдельного пункта корзины
                elCartCount: '.js-count',                   // селектор для количества отдельного товара
                elCartSumma: '.js-summa',                   // селектор для суммы отдельного товара
                elChangeCount: '.js-change-count',          // селектор для кнопок изменения количества
                elRemoveFromCart: '.js-remove-from-cart',   // селектор для кнопки удаления из корзины
                elOrder: '#order'                           // селектор для кнопки оформления заказа            
            }
            _.defaults(options || {}, defaultOptions);
            opts = _.clone(options);
        }
    
Сначала мы объявляем настройки по умолчанию, а затем "склеиваем" их с данными, пришедшими извне. Для небольшого приложения, как у нас, реализовывать возможность настройки модуля было не обязательно. Но это небольшое увеличение кода дает нам большую гибкость при переносе этого модуля в другой проект.


Рендер корзины и html-шаблон

Для начала создадим шаблон для отображения корзины и поместим его в секцию body файла cart.html.

        
    
Здесь все знакомо по аналогичному фунционалу в каталоге. В дата-атрибуты помещаем id товаров, чтобы было понятно, с какими именно мы сейчас работаем. Атрибут data-delta показывает, увеличивать или уменьшать количество товара при клике на эту кнопку.


Функции рендеринга.

        // Рендерим корзину
        function renderCart() {
            var template = _.template($('#cart-template').html()),
                data = {
                    goods: cartData
                };
            $(opts.elCart).html(template(data));
            renderTotalCartSumma();
        }
    
        // Рендерим количество товаров в меню
        function renderMenuCart() {
            var countAll = getCountAll();
            $(opts.elTotalCartCount).html(countAll !== 0 ? countAll : '');
        }
    
        // Рендерим общую сумму товаров
        function renderTotalCartSumma() {
            $(opts.elTotalCartSumma).html(getSumma());            
        }
    
Операции с DOM традиционно тяжелые, поэтому рендерить корзину мы будем целиком, а вот при генерации количества и суммы - точечно обращаться к элементам.


Обработчики событий.

Приближаемся к завершению...

Напишем вспомогательную функцию получения элемента корзины по его id. Она пригодится нам при изменении количества и удалении товаров

        // Поиск продукта в корзине по id
        function findCartElemById(id) {
            return $(opts.elCartItem + '[' + opts.attrId + '="'+id+'"]');
        }
    
Выглядит строка страшно, но смысл сводится к поиску такого элемента $('.js-cart-item[data-id="5"]').

Всего у нас будет 4 обработчика-клика: добавление в корзину, изменение количества, удаление и оформление заказа. Смотрим:

        // Добавление в корзину
        function _onClickAddBtn() {
            $('body').on('click', opts.elAddToCart, function(e) {
                var $this = $(this);
                add({
                    id: +$this.attr(opts.attrId),
                    name: $this.attr(opts.attrName),
                    price: +$this.attr(opts.attrPrice),
                    count: 1
                });    
                renderMenuCart();
                alert('Товар добавлен в корзину');
            });
        }
    
        // Меняем количество товаров в корзине
        function _onClickChangeCountInCart() {
            $('body').on('click', opts.elChangeCount, function(e) {
                var $this = $(this),
                    id = +$this.attr(opts.attrId),
                    delta = +$this.attr(opts.attrDelta),
                    $cartElem = findCartElemById(id),
                    cartItem = changeCount(id, delta);
                if (cartItem.count) {
                    $cartElem.find(opts.elCartCount).html(cartItem.count);
                    $cartElem.find(opts.elCartSumma).html(cartItem.count * cartItem.price);
                } else {
                    $cartElem.remove();
                }
                renderMenuCart();
                renderTotalCartSumma();
            });
        }
    
        // Удаляем товар из корзины
        function _onClickRemoveFromCart() {
            $('body').on('click', opts.elRemoveFromCart, function(e) {
                if(!confirm('Удалить товар из корзины?')) return false;
                var $this = $(this),
                    id = +$this.attr(opts.attrId),
                    $cartElem = findCartElemById(id);
                remove(id);
                $cartElem.remove();
                renderMenuCart();
                renderTotalCartSumma();
            });
        }
    
        // Оформляем заказ 
        function _onClickOrder() {
            $('body').on('click', opts.elOrder, function(e) {
                // Предоставлю написать код для оформления заказа и отправки данных на сервер самим
                alert('Оформляем заказ :-)');
            });
        }
    

На заметку. Код, касающийся рендеринга и навешивания событий, можно было изрядно уменьшить, применив такой нехитрый трюк. Объединяем код отрисовки корзины, количества и суммы в одну функцию render и вызываем ее после любых манипуляций с корзиной. То есть для любого события схема такая: вызвали нужный метод обработки данных - отрендерили ВСЕ. Тогда мы избавляемся от проверок вроде "а не удалился ли товар из корзины после уменьшения его количества" и вызова нескольких функций рендера вместо одной. Для небольших проектов это оправдано, но когда Ваше приложение будет содержать кучу виджетов, сложную верстку и данные будут поступать не только по клику пользователей, а еще и в режиме онлайн от разных источников, перерисовка всего содержимого страницы негативно скажется на производительности браузера. Поэтому даже в нашем простом случае я выбрал чуть усложненную, но более правильную схему.


Собираем модуль корзины в одно целое

Основной код уже написан, нам осталось только написать функцию инициализации корзины и привязки обработчиков событий. Пойдем от обратного, обработчики:

        // Навешивам события
        function _bindHandlers() {
            _onClickAddBtn();
            _onClickChangeCountInCart();
            _onClickRemoveFromCart();
            _onClickOrder();
        }
    
Думаю, здесь без особых пояснений, собираем в кучу написанные ранее функции. Иницилизация:
        // Инициализация модуля
        function init(options) {
            _initOptions(options);
            updateData();
            if (opts.renderCartOnInit) {
                renderCart();
            }
            if (opts.renderMenuCartOnInit) {
                renderMenuCart();
            }
            _bindHandlers();
        }
    
Почему мы ввели отдельные настройки renderCartOnInit и opts.renderMenuCartOnInit? Просто потому, что на странице каталога нам нужно инициализировать корзину (мы выводим количество добавленных товаров в меню), но не нужно ее рендерить. Чтобы не усложнять логику лишними проверками, мы разделили эти опции.


Полный код корзины

Код всей корзины мы уже рассмотрели в статье, но чтобы уложить это еще раз в голове, вот ссылка на файл cart.js - модуль целиком.


Главный модуль приложения

Вероятно, даже самый усидчивый читатель порядком устал от чтения букв, поэтому спешу завершить мой трактат.
Файл main.js - главный модуль, запуск приложения, царь-модуль.

        'use strict';
        
        // Модуль приложения
        var app = (function($) {
        
            var $body = $('body'),
                page = $body.data('page'),
                options = {
                    elAddToCart: '.js-add-to-cart',
                    attrId: 'data-id',
                    attrName: 'data-name',
                    attrPrice: 'data-price',
                    attrDelta: 'data-delta',
                    elCart: '#cart',
                    elTotalCartCount: '#total-cart-count',
                    elTotalCartSumma: '#total-cart-summa',
                    elCartItem: '.js-cart-item',
                    elCartCount: '.js-count',
                    elCartSumma: '.js-summa',
                    elChangeCount: '.js-change-count',
                    elRemoveFromCart: '.js-remove-from-cart',
                    elOrder: '#order'
                },
                optionsCatalog = _.extend({
                    renderCartOnInit: false,
                    renderMenuCartOnInit: true
                }, options),
                optionsCart = _.extend({
                    renderCartOnInit: true,
                    renderMenuCartOnInit: true
                }, options);
        
            function init() {
                if (page === 'catalog') {
                    catalog.init();
                    cart.init(optionsCatalog);
                }
                if (page === 'cart') {
                    cart.init(optionsCart);
                }
            }
            
            return {
                init: init
            }    
        
        })(jQuery);
        
        jQuery(document).ready(app.init);
    
Идея такова: объявляем главный модуль, настройки приложения (в нашем случае дублируют опции по умолчанию), определяем текущую страницу и настройки модуля cart для текущей страницы и, наконец, инициализируем нужные модули. Приводит все это добро в действие волшебная строчка jQuery(document).ready(app.init);


Подводим итоги.

Итак, мы написали небольшое приложение простого интернет-магазина с каталогом и корзиной.

UPDATED: В связи с большой популярностью этой статьи и интересу к теме интернет-магазинов в целом запилена и опубликована статья-продолжение Реализация оформления заказа в интернет-магазине на клиенте и сервере. В ней рассмотрен полный цикл по сбору данных о клиенте, размещении формы заказа, отправки ajax-запроса на сервер, создание таблиц в базе данных и добавление этих самых данных, а также отправка писем с заказом. Как обычно, все с примерами. Исходники обновлены. Отправка заказа интегрирована с каталогом и корзиной, рассмотренными в этой статье. Это выглядит как цельное приложение, готовое к работе.
Еще одно обновление: готова статья про добавление способа доставки в интернет-магазин. Читайте здесь

UPDATED 2: Для тех, кому интересно, как сделать дерево с вложенными категориями для своего интернет-магазина, опубликована статья Строим дерево категорий на js, php и mysql. Там описывается интересная библиотека jstree и как достаточно просто сообразить каталог и на клиенте, и на сервере.

UPDATED 3: Для продолжающих интересоваться интернет-магазинами, начинается серия уроков на тему фильтров и сортировок в каталоге товаров. Точка входа здесь.

UPDATED 4: Еще новости по развитию магазина.
Новая статья Сравнение товаров в интернет-магазине.

Еще раз нужные ссылки:

Хотя главной целью была демонстрация модульного подхода при разработке на javascript и отделении логики от представления, все же модуль корзины получился вполне себе самодостаточным и независимым. При желании мы можем включать его в другие проекты. У нас есть только 2 зависимости модуля: jquery и underscore. Хотя полагаю, что люди, знакомые с обеими библиотеками, добавляют их практически в любой свой проект.

Многие моменты в нашем приложении достаточно спорны. Нет жесткого разделения логики и представления, эти функции объединены в один модуль. Также шаблоны underscore вшиты прямо в код страницы, что тоже не самая хорошая практика, нужно выносить их в отдельные файлы. Я намеренно не стал слишком усложнять структуру. В статье я рассмотрел пример создания кода с одной стороны достаточно модульного, чтобы его можно было удобно протестировать, поддерживать в дальнейшем или извлечь какие-то идеи для своих будущих приложений, но с другой стороны не настолько сложного, чтобы в нем нужно было слишком долго разбираться. В конце концов для серьезной javascript-разработки создаются библиотеки и фреймворки, и рано или поздно мы все приходим к пониманию, что их нужно знать и изучать. Backbone, Angular, Ember, React, существует их очень много и постоянно появляются новые. И чем больше мы будем изучать и узнавать различные подходы, тем шире будет наш кругозор и больше возможностей выбора.

Все об интернет-магазинах

Следующая статья Демо Исходники
Заходите в группу в контакте - https://vk.com/webdevkin
Анонсы статей, обсуждения интернет-магазинов, vue, фронтенда, php, гита.
Истории из жизни айти и обсуждение кода.
Как Вам статья? Оцените!