Как сделать корзину javascript
Корзина для интернет-магазина на фронте или Пишем модульный javascript
Идея приложения и схема работы.
Итак, интернет-магазин на фронте. Что мы хотим от него? Хотим, чтобы была возможность как минимум вывести каталог товаров, реализовать добавление их в корзину, просматривать оную корзину, менять количество товаров в ней и удалять ошибочно добавленные с пересчетом стоимости заказа. Плюс добавим хранение данных в localStorage, дабы после перезагрузки страницы наша корзина не ушла в небытие.
Функционал подробнее.
Можно посмотреть, что у нас в итоге получится, здесь. Попробуйте добавить товары в корзину, перейти в нее, менять количество и удалять товары, и увидите, как задумана работа магазина. Или можете сразу скачать исходники по этой ссылке.
Структура файлов
Небольшое отступление: я намеренно не стал вводить лишнюю логику для нашего проекта. Хорошей практикой считается разработка с подходом MVC, разделение кода на модели, контроллеры и представления и разбиение этих частей на отдельные файлы для наглядности. Можно долго спорить на тему правильности применения такого подхода для абсолютно всех проектов, но в нашем конкретном примере мы отойдем от этого постулата и весь код для управления корзиной разместим в одном файле, а уже внутри него логически отделим работу с данными от разметки.
Приступаем к разработке.
Я не буду расписывать подробно каждую строчку кода, это заняло бы слишком много места и времени. В статье рассмотрим основные моменты, а весь код можно посмотреть в исходниках по ссылке выше. Код с подробными комментариями.
Начинаем.
Создаем разметку.
На заметку: соглашения по html и css-коду.
Готовим данные и разметку для каталога
Для начала создадим файл для хранения наших товаров: data/goods.json Как видим, это обычный json-массив с четырьмя нужными нам полями. Теперь переходим к созданию каталога. Но прежде чем приступить к написанию js-кода, нам придется написать еще немного разметки для отображения отдельного товара. Мы будем использовать шаблоны html-кода библиотеки underscore для динамической генерации отдельных товаров. Если Вы не знакомы с шаблонами underscore, то у меня есть статья на эту тему. Я же не буду зацикливаться на этом, а просто приведу код шаблона, тем более, что он достаточно тривиален и не требует долгих разбирательств: Что происходит в этом коде? underscore-шаблон представляет собой обычную строку, в которую подставляются нужные данные. Это неплохой способ отделить логику и данные от представления. Вся идея шаблонов в том, что мы не знаем, каким образом получены данные, но мы знаем, как их нужно отобразить. В нашем примере мы даем на вход массив товаров goods (из файла goods.json), перебираем все товары в цикле с помощью функции each библиотеки underscore и для каждого товара выводим свою разметку, подставляя в нужные места id товара, название, картинку и цену. Обратите внимание на дата-атрибуты у кнопки «Добавить в корзину», они будут использованы в дальнейшем. Приведенный код мы поместим в тело body файла index.html. Дальше мы увидим, как связать данные и наш underscore-шаблон.
Пишем js-модуль каталога
Код файла catalog.js будет очень коротким Здесь с помощью замыкания мы объявляем переменную-модуль catalog, пишем функцию init, которая вызывает самую интересную нам функцию _render и экспортируем init наружу, разрешая при этом вызывать catalog.init() из других модулей приложения. На самом деле можно обойтись и без лишней init-функции, но лучше всегда объявлять публичную функцию инициализации во всех модулях для единообразия. При этом функция _render начинается со знака _, чем мы показываем, что эта функция частная и не должна выходить за пределы модуля. Применяя такой подход, мы уже в коде модуля видим, что используется в других модулях, а что предназначено только для внутреннего пользования. Этакая инкапсуляция кода, как в ООП.
Модуль корзины
Вероятно, Вы обратили внимание, что я экспортирую наружу не только init, но и все функции, касающиеся обработки данных. Сделано это для того, чтобы облегчить тестирование кода. Каким образом проходят тесты? Есть 2 способа: ручной и unit-тесты. При тестировании руками мы в консоли браузера вызываем функции модуля и сразу же видим результат. Например, вызвав cart.add(
UPDATED: Для интересующихся unit-тестированием опубликована статья unit-тесты на фронте или изучаем jasmine.js. В ней рассказывается, как тестировать код на примере нашей корзины с помощью популярной библиотеки jasmine.js.
Пишем функции обработки данных
Полный код работы с данными
Из интересного отмечу, что каждая функция возвращает какие-то данные: корзину целиком, добавленный товар или общую сумму. Не все возвращаемые данные используются в модуле, но все облегчают тестирование. Также видим, что в localStorage мы храним сериализованный массив товаров. В функции добавление предусмотрено условие: если мы добавляем товар, уже находящийся в корзине, то не создается дубликат товара, а увеличивается количество уже существующего. При уменьшении количества конкретного товара до нуля, оный удаляется из корзины.
Инициализация настроек. Настройки по умолчанию.
Прежде чем писать функции рендеринга и обработчиков событий, вернемся чуть назад и рассмотрим инициализацию настроек модуля. Функция _initOptions скопирует в переменную opts все настройки, переданные в модуль корзины при его создании. Сначала мы объявляем настройки по умолчанию, а затем «склеиваем» их с данными, пришедшими извне. Для небольшого приложения, как у нас, реализовывать возможность настройки модуля было не обязательно. Но это небольшое увеличение кода дает нам большую гибкость при переносе этого модуля в другой проект.
Рендер корзины и html-шаблон
Для начала создадим шаблон для отображения корзины и поместим его в секцию body файла cart.html. Здесь все знакомо по аналогичному фунционалу в каталоге. В дата-атрибуты помещаем id товаров, чтобы было понятно, с какими именно мы сейчас работаем. Атрибут data-delta показывает, увеличивать или уменьшать количество товара при клике на эту кнопку.
Функции рендеринга.
Обработчики событий.
Приближаемся к завершению.
Всего у нас будет 4 обработчика-клика: добавление в корзину, изменение количества, удаление и оформление заказа. Смотрим:
Собираем модуль корзины в одно целое
Основной код уже написан, нам осталось только написать функцию инициализации корзины и привязки обработчиков событий. Пойдем от обратного, обработчики: Думаю, здесь без особых пояснений, собираем в кучу написанные ранее функции. Иницилизация: Почему мы ввели отдельные настройки renderCartOnInit и opts.renderMenuCartOnInit? Просто потому, что на странице каталога нам нужно инициализировать корзину (мы выводим количество добавленных товаров в меню), но не нужно ее рендерить. Чтобы не усложнять логику лишними проверками, мы разделили эти опции.
Полный код корзины
Главный модуль приложения
Подводим итоги.
Итак, мы написали небольшое приложение простого интернет-магазина с каталогом и корзиной.
UPDATED: В связи с большой популярностью этой статьи и интересу к теме интернет-магазинов в целом запилена и опубликована статья-продолжение Реализация оформления заказа в интернет-магазине на клиенте и сервере. В ней рассмотрен полный цикл по сбору данных о клиенте, размещении формы заказа, отправки ajax-запроса на сервер, создание таблиц в базе данных и добавление этих самых данных, а также отправка писем с заказом. Как обычно, все с примерами. Исходники обновлены. Отправка заказа интегрирована с каталогом и корзиной, рассмотренными в этой статье. Это выглядит как цельное приложение, готовое к работе.
Еще одно обновление: готова статья про добавление способа доставки в интернет-магазин. Читайте здесь
UPDATED 2: Для тех, кому интересно, как сделать дерево с вложенными категориями для своего интернет-магазина, опубликована статья Строим дерево категорий на js, php и mysql. Там описывается интересная библиотека jstree и как достаточно просто сообразить каталог и на клиенте, и на сервере.
UPDATED 3: Для продолжающих интересоваться интернет-магазинами, начинается серия уроков на тему фильтров и сортировок в каталоге товаров. Точка входа здесь.
UPDATED 4: Еще новости по развитию магазина.
Новая статья Сравнение товаров в интернет-магазине.
Хотя главной целью была демонстрация модульного подхода при разработке на javascript и отделении логики от представления, все же модуль корзины получился вполне себе самодостаточным и независимым. При желании мы можем включать его в другие проекты. У нас есть только 2 зависимости модуля: jquery и underscore. Хотя полагаю, что люди, знакомые с обеими библиотеками, добавляют их практически в любой свой проект.
Многие моменты в нашем приложении достаточно спорны. Нет жесткого разделения логики и представления, эти функции объединены в один модуль. Также шаблоны underscore вшиты прямо в код страницы, что тоже не самая хорошая практика, нужно выносить их в отдельные файлы. Я намеренно не стал слишком усложнять структуру. В статье я рассмотрел пример создания кода с одной стороны достаточно модульного, чтобы его можно было удобно протестировать, поддерживать в дальнейшем или извлечь какие-то идеи для своих будущих приложений, но с другой стороны не настолько сложного, чтобы в нем нужно было слишком долго разбираться. В конце концов для серьезной javascript-разработки создаются библиотеки и фреймворки, и рано или поздно мы все приходим к пониманию, что их нужно знать и изучать. Backbone, Angular, Ember, React, существует их очень много и постоянно появляются новые. И чем больше мы будем изучать и узнавать различные подходы, тем шире будет наш кругозор и больше возможностей выбора.