Цель статьи состоит не столько в написании пошагового руководства по созданию eCommerce-модуля (для этого есть замечательный цикл постов в блоге IDeliverable), сколько в описании решения утилитарных подзадач, с которыми лично столкнулся при разработке для Orchard чего-то более интересного, чем Twitter-виджет. Надеюсь, мои изыскания окажутся полезны для тех, кто только собрался разрабатывать решения под эту CMS.
Сама по себе реализациия сколько-нибудь серьёзного решения на базе этого продукта представляла для меня определённый интерес довольно давно, но всё, как говорится, руки не доходили. И вот недавно передо мной была поставлена конкретная задача — создать простой фронтенд VirtoCommerce-магазина с использованием какой нибудь популярной CMS. Недолго думая выбрал Orchard — скачал исходники последней версии, скомпилировал, запустил и приступил к работе.
Руководства на официальном сайте, конечно, помогут вникнуть в суть Orchard и даже сделать какие-нибудь небольшие поделки, но для real-world-решения их явно недостаточно.
Итак… Для начала нужно было определить, как в Orchard создаётся контент и из чего он состоит.
Весь контент в глобальном масштабе состоит из ContentItem — отдельно взятых сущностей, обладающих собственным URL. Примером ContentItem могут служить статические страницы вида “About us” или посты блога.
Каждый ContentItem является экземпляром определённого ContentType. Другими словами: ContentType — это класс для ContentItem.
Далее, ContentPart — блоки, из которых состоит ContentType и которые можно повторно использовать для любых других типов контента. Например, список тэгов, комментарии пользователей, рейтинг — всё это ContentPart’ы.
И, наконец, ContentField — наиболее элементарная составляющая ContentType, обладающая только наименованием и значением. Например, SKU, стоимость, вес и т.д.
В качестве глобальной единицы контента решил взять стандартный ContentType — Page. В моём случае магазин должен был состоять из страницы категории товаров, страницы товара, корзины, чекаута и thank-you-page. Каждая страница должна включать список категорий (например, в виде главного меню) и мини-корзину (в виде иконки с обозначением количества позиций, добавленных в корзину).
Итак, уже можно выделить блоки (ContentPart), из которых будут состоять страницы:
Конечно, каждый из перечисленных блоков является очень большим и может быть разделён на гораздо меньшие блоки, но пока ограничусь такой структурой.
Теперь необходимо определиться с архитектурой модуля. Посмотрел, как устроены родные и сторонние модули Orchard, пришёл к следующей структуре:
Ок, структура есть. Следующая задача — обеспечить сохраняемость настроек API-клиента. Было бы удобно, чтобы в разделе Settings в админке появился соответствующий пункт, например, Webshop, содержащий поля для настроек подключения к API — URL, Id приложения, обращающегося к API, и секретного ключа. Туда же можно поместить такие настройки, как Id магазина, ISO-код валюты (по крайней мере, для демо-целей) и т.д. То есть — глобальные настройки сайта. Процесс реализации таких настроек хорошо освещён в соответствующем разделе туториала на официальном сайте Orchard.
Кстати, во всех туториалах по Orchard рассматриваются варианты с использованием ContentPartRecord — это те случаи, когда у ContentPart есть какие-то свойства и, естественно, эти свойства нужно где-то хранить. Для этого и существует ContentPartRecord, проецирующий значения свойств, хранящихся в БД Orchard, в соответствующие поля ContentPart. В моём случае ни у одного ContentPart не было каких либо свойств (данные, пришедшие из API, должны сразу отображаться на фронтенде), соответственно и ContentPartRecord было заводить излишне. Поэтому ограничился объявлением пустого ContentPart и далее сразу же использовал его в драйвере. Что такое драйвер… По сути — это почти тот же самый контроллер, но отвещающий исключительно за заполнение ViewModel соответствующего ContentPart в режиме просмотра и управления. Опять же, поскольку в админке никаких настроек для ContentPart не предусматривал, перекрывал только метод, отвечающий за формирование ViewModel для фронтенда.
Итак, все ContentPart готовы, их ViewModel заполнены и они отображаются по отдельности. Теперь необходимо наладить между ними взаимодействие. К примеру: необходимость обновления списка товаров при переходе на другую категорию. Решил эту задачу “в лоб” — в соответствующих драйверах читал параметры QueryString (slug категории/товара и номер страницы пагинации) и трансофрмировал в запрос к API e-commerce сервиса. Не самое изящное решение, конечно, да и на уровне драйверов решать проблему SEO URL не получится, но, опять же, для демо-версии вполне подойдёт. То, что требовалось передать в API методами PUT или POST, например, изменение позиций в корзине, прохождение шагов чекаута, — регулировалось соответствующими контроллерами, как и полагается в MVC-проекте.
Гораздо интереснее было решать проблему постоянных пересозданий сервисов в конструкторах контроллеров и драйверов. Окончательные сомнения в вопросе целесообразности наследования интерфейсов сервисов (да и самого API-клиента) от Orchard'овского IDependency развеял Zoltan Lehoczky — действительно, всё очень просто: наследуем интерфейсы сервисов от IDependency и затем внедряем зависимости в конструкторы необходимых контроллеров/драйверов. Сразу же видно — что от чего зависит. DI и IoC в действии, называется. По такому же принципу работает и получение глобальных настроек сайта — добавляем зависимость от IOrchardServices в конструктор контроллера/драйвера и затем читаем из WorkContext.CurrentSite необходимый ContentPart.
В принципе, на этом можно было бы остановиться, но меня не устраивало то, как ContentPart добавляются на страницу. Этот процесс, как мне кажется, становится гораздо более наглядным, если сконвертировать ContentPart в Widget. Да, виджеты предназначены для отображения небольших частей контента, но почему бы не попробовать…
Конвертация ContentPart в Widget осуществляется посредством DataMigration (не самое очевидное и элегантное решение со стороны Orchard). Этот процесс хорошо описан в соответствующем разделе документации. Отмечу, что никогда не стоит забывать о том, что каждый Widget нужно дополнительно объявить в специальном файле Placement.info. Без этой мелочи виджет никогда не отобразится на фронтенде несмотря на то, что в админке им можно манипулировать как угодно.
Осталось только привязать виджеты, отображающиеся в зоне Content к соответствующим слоям. Слой — это и есть совокупность виджетов, обладающая собственным фильтром: если, скажем, виджет, отображающий сведения о товаре, должен отображаться только на странице товара — указываем в правилах слоя условие url(“~/Product*”). Слой Default подразумевает сквозное отображение виджетов на всех страницах и переписывать его правила, думаю, не стоит. Эксперименты с этим слоем привели к тому, что в итоге больше не мог зайти в админку, точнее — попасть на страницу логина…
Таким образом методом проб и ошибок реализовал модуль, содержащий глобальные настройки API-клиента и 7 виджетов, которые можно расположить в любой зоне существующей темы. Конечно, модуль сырой и создан исключительно в демонстрационных целях, его ещё допиливать и допиливать (это и SEO URL, и более сложная страница товара, да и в целом хотелось бы реализовать eCommerce не в виде модуля с разрозненными виджетами, а в виде готового Orchard Receipt...). Как говорится, “я работаю над этим” и готов поделиться опытом, если он будет кому-нибудь интересен.
Исходники модуля можно найти в GitHub, а готовый модуль — здесь. Буду благодарен за отзывы и особенно — за рекомендации!
Сама по себе реализациия сколько-нибудь серьёзного решения на базе этого продукта представляла для меня определённый интерес довольно давно, но всё, как говорится, руки не доходили. И вот недавно передо мной была поставлена конкретная задача — создать простой фронтенд VirtoCommerce-магазина с использованием какой нибудь популярной CMS. Недолго думая выбрал Orchard — скачал исходники последней версии, скомпилировал, запустил и приступил к работе.
Руководства на официальном сайте, конечно, помогут вникнуть в суть Orchard и даже сделать какие-нибудь небольшие поделки, но для real-world-решения их явно недостаточно.
Итак… Для начала нужно было определить, как в Orchard создаётся контент и из чего он состоит.
Весь контент в глобальном масштабе состоит из ContentItem — отдельно взятых сущностей, обладающих собственным URL. Примером ContentItem могут служить статические страницы вида “About us” или посты блога.
Каждый ContentItem является экземпляром определённого ContentType. Другими словами: ContentType — это класс для ContentItem.
Далее, ContentPart — блоки, из которых состоит ContentType и которые можно повторно использовать для любых других типов контента. Например, список тэгов, комментарии пользователей, рейтинг — всё это ContentPart’ы.
И, наконец, ContentField — наиболее элементарная составляющая ContentType, обладающая только наименованием и значением. Например, SKU, стоимость, вес и т.д.
В качестве глобальной единицы контента решил взять стандартный ContentType — Page. В моём случае магазин должен был состоять из страницы категории товаров, страницы товара, корзины, чекаута и thank-you-page. Каждая страница должна включать список категорий (например, в виде главного меню) и мини-корзину (в виде иконки с обозначением количества позиций, добавленных в корзину).
Итак, уже можно выделить блоки (ContentPart), из которых будут состоять страницы:
- CategoryList (перечень категорий товаров);
- ProductList (перечень товаров);
- Product (сам товар);
- ShoppingCartPreview (мини-корзина);
- ShoppingCart (непосредственно корзина);
- Checkout;
- ThankYouPage.
Конечно, каждый из перечисленных блоков является очень большим и может быть разделён на гораздо меньшие блоки, но пока ограничусь такой структурой.
Теперь необходимо определиться с архитектурой модуля. Посмотрел, как устроены родные и сторонние модули Orchard, пришёл к следующей структуре:
- Client — содержит API-клиент;
- Controllers — понятно, контроллеры (всё-таки каждый модуль Orchard — это MVC-проект);
- Converters — конвертеры из дата-контрактов API во ViewModels и из ViewModels в дата-контракты API;
- Drivers — драйверы Orchard (подобие контроллеров, но предназначены специально для того, чтобы настраивать отображение и поведение ContentPart на фронтенде и в админке);
- Handlers — хэндлеры Orchard — прослойка между ContentPart и БД;
- Models — модели ContentParts;
- Scripts – JS-скрипты;
- Services — сервисы, обращающиеся к API-клиенту и возвращающие готовые ViewModels;
- ViewModels — те модели, которые будут впоследствие переданы на Views;
- Views – Razor-шаблоны.
Ок, структура есть. Следующая задача — обеспечить сохраняемость настроек API-клиента. Было бы удобно, чтобы в разделе Settings в админке появился соответствующий пункт, например, Webshop, содержащий поля для настроек подключения к API — URL, Id приложения, обращающегося к API, и секретного ключа. Туда же можно поместить такие настройки, как Id магазина, ISO-код валюты (по крайней мере, для демо-целей) и т.д. То есть — глобальные настройки сайта. Процесс реализации таких настроек хорошо освещён в соответствующем разделе туториала на официальном сайте Orchard.
Кстати, во всех туториалах по Orchard рассматриваются варианты с использованием ContentPartRecord — это те случаи, когда у ContentPart есть какие-то свойства и, естественно, эти свойства нужно где-то хранить. Для этого и существует ContentPartRecord, проецирующий значения свойств, хранящихся в БД Orchard, в соответствующие поля ContentPart. В моём случае ни у одного ContentPart не было каких либо свойств (данные, пришедшие из API, должны сразу отображаться на фронтенде), соответственно и ContentPartRecord было заводить излишне. Поэтому ограничился объявлением пустого ContentPart и далее сразу же использовал его в драйвере. Что такое драйвер… По сути — это почти тот же самый контроллер, но отвещающий исключительно за заполнение ViewModel соответствующего ContentPart в режиме просмотра и управления. Опять же, поскольку в админке никаких настроек для ContentPart не предусматривал, перекрывал только метод, отвечающий за формирование ViewModel для фронтенда.
Итак, все ContentPart готовы, их ViewModel заполнены и они отображаются по отдельности. Теперь необходимо наладить между ними взаимодействие. К примеру: необходимость обновления списка товаров при переходе на другую категорию. Решил эту задачу “в лоб” — в соответствующих драйверах читал параметры QueryString (slug категории/товара и номер страницы пагинации) и трансофрмировал в запрос к API e-commerce сервиса. Не самое изящное решение, конечно, да и на уровне драйверов решать проблему SEO URL не получится, но, опять же, для демо-версии вполне подойдёт. То, что требовалось передать в API методами PUT или POST, например, изменение позиций в корзине, прохождение шагов чекаута, — регулировалось соответствующими контроллерами, как и полагается в MVC-проекте.
Гораздо интереснее было решать проблему постоянных пересозданий сервисов в конструкторах контроллеров и драйверов. Окончательные сомнения в вопросе целесообразности наследования интерфейсов сервисов (да и самого API-клиента) от Orchard'овского IDependency развеял Zoltan Lehoczky — действительно, всё очень просто: наследуем интерфейсы сервисов от IDependency и затем внедряем зависимости в конструкторы необходимых контроллеров/драйверов. Сразу же видно — что от чего зависит. DI и IoC в действии, называется. По такому же принципу работает и получение глобальных настроек сайта — добавляем зависимость от IOrchardServices в конструктор контроллера/драйвера и затем читаем из WorkContext.CurrentSite необходимый ContentPart.
В принципе, на этом можно было бы остановиться, но меня не устраивало то, как ContentPart добавляются на страницу. Этот процесс, как мне кажется, становится гораздо более наглядным, если сконвертировать ContentPart в Widget. Да, виджеты предназначены для отображения небольших частей контента, но почему бы не попробовать…
Конвертация ContentPart в Widget осуществляется посредством DataMigration (не самое очевидное и элегантное решение со стороны Orchard). Этот процесс хорошо описан в соответствующем разделе документации. Отмечу, что никогда не стоит забывать о том, что каждый Widget нужно дополнительно объявить в специальном файле Placement.info. Без этой мелочи виджет никогда не отобразится на фронтенде несмотря на то, что в админке им можно манипулировать как угодно.
Осталось только привязать виджеты, отображающиеся в зоне Content к соответствующим слоям. Слой — это и есть совокупность виджетов, обладающая собственным фильтром: если, скажем, виджет, отображающий сведения о товаре, должен отображаться только на странице товара — указываем в правилах слоя условие url(“~/Product*”). Слой Default подразумевает сквозное отображение виджетов на всех страницах и переписывать его правила, думаю, не стоит. Эксперименты с этим слоем привели к тому, что в итоге больше не мог зайти в админку, точнее — попасть на страницу логина…
Таким образом методом проб и ошибок реализовал модуль, содержащий глобальные настройки API-клиента и 7 виджетов, которые можно расположить в любой зоне существующей темы. Конечно, модуль сырой и создан исключительно в демонстрационных целях, его ещё допиливать и допиливать (это и SEO URL, и более сложная страница товара, да и в целом хотелось бы реализовать eCommerce не в виде модуля с разрозненными виджетами, а в виде готового Orchard Receipt...). Как говорится, “я работаю над этим” и готов поделиться опытом, если он будет кому-нибудь интересен.
Исходники модуля можно найти в GitHub, а готовый модуль — здесь. Буду благодарен за отзывы и особенно — за рекомендации!
evgeniiFromKalinigrad
Как вы рассматриваете перспективу использования данного CMS для больших проектов и, если у вас есть опыт работы с другими CMS на Microsoft продуктах как Orchard выглядит на фоне других, было бы очень интересно услышать личное мнение автора статьи.
VirtoCommerce
Есть опыт работы с CMS Kooboo, которая также реализована на ASP.NET MVC. Orchard понравилась гораздо больше (с точки зрения разработчика): развивается несравнимо динамичнее; более продуманный подход к архитектуре самой CMS; проще найти решение возникшей проблемы. В плане использования для больших проектов — нужно проверять. Пока не вижу никаких препятствий. Возможно, в Orchard не так удобно редактировать контент, как в Kooboo, но это дело привычки.