Наша компания занимается преимущественно разработкой интернет-магазинов и мы хотим поделиться своим опытом разработки проекта на связке VueJS + Nuxt + Laravel.

В статье пойдет речь про то, как мы решили реализовать интернет-магазин как SPA: как мы к этому пришли, трудности, легкости.

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

Почему SPA


До текущего проекта наш опыт разработки одностраничных приложений заключался только в нескольких внутренних проектах. И во многом SPA для нас оставалось в каком-то смысле темной лошадкой.

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

Выбор подхода вызвал в нашей компании достаточно жаркие споры, обе чаши весов с аргументами были наполнены и решение давалось очень сложно. Нашими разработчиками было принято решение собрать прототип нескольких страниц проекта и посмотреть, какие возникнут трудности при каждом из подходов. Этот подход помог нам с итоговым решением. Прототипы помогли показать, что управление состоянием сайта (каталог, корзина, оформление заказа и т.д.) намного более комфортно и вызывает меньше проблем именно в SPA версии. Скорость разработки и взаимодействия между верстальщиками и программистами значительно увеличилась благодаря тому, что не нужно переносить верстку, достаточно просто добавлять логику в уже готовые компоненты. Также стали более понятны проблемы с которыми мы можем столкнуться и это сподвигло к дальнейшим действиям. Перед нами стал выбор технологий.

За окном лето 2017. В twitter и на medium ни утихают споры, что все-таки лучше, vue или react. Наш офис этот тренд не обошел стороной. Разработчики так же разделились на два лагеря, каждый со своими аргументами. До этого каждый из нас уже работал с обеими технологиями.
Кому-то стал ближе jsx, кто-то предпочитает более привычный html или pug, кто-то считает что иммутабельность помогает лучше следить и управлять состоянием приложения, кому-то это кажется избыточным усложнением. С другой стороны каждый фреймворк предоставляет нам возможность создавать однофайловые компоненты и для обоих есть уже достаточно стабильные библиотеки с набором всех нам нужных функций (ssr, управление глобальным состоянием, роутинг, управление meta-данными). Для react это nextjs, а для vue — nuxtjs. Nuxt на момент выбора был еще в beta-версии, но достаточно стабилен. Т.к. процесс разработки у нас был построен таким образом, что изначально у нас идет верстка, а затем уже построение backend части и перенос сверстанных страниц во frontend, выбор фреймворка был достаточно прост. Нами был выбран vue и nuxtjs, т.к. решено было параллельно верстать сайт и запускать api. При таком подходе удобно верстать сразу компоненты и в них уже добавлять логику. Нашим верстальщикам был ближе подход создания привычного им html.

Немножко о backend


В плане серверных решений и в целом выбора технологий для построения backend мы пошли более привычным путем. Языком был выбран php, для которого мы используем фреймворк laravel. Это все крутится на nginx. В качестве решения для базы данных у нас mysql.

Начало разработки, используемые пакеты и проблемы


Nuxt предоставляет полностью удовлетворяющие нас пакеты для управления состоянием приложения (vuex) и роутинга (vue-router). Поэтому начинать собирать проект и прикручивать к компонентам логику можно было начинать сразу, а далее по мере надобности искать нужные нам пакеты. В первую очередь, конечно же, понадобилось решение для общения с backend частью. Для этого был выбран, стандартный уже для всех, axios, и обертка над ним nuxt-axios-module. Так же сразу помогаем проекту не потеряться в окружениях и запускать в каждом окружении с нужной конфигурацией — выбираем dotenv и обертку nuxt-dotenv-module. Для начала разработки этого достаточно и процесс верстки начался.

Первая пауза случилась, когда нужно было добавлять в верстку слайдер изображений. “Где мой slick-slider, я хочу jquery” было слышно из верстальщицкого конца комнаты. Быстрый обзор готовых решений выявил несколько подходящих нам слайдеров. Но практически все тянули за собой зависимость в виде jquery, которую не хотелось добавлять в готовый бандл, тем самым увеличивая его размер. Какие-то пакеты не поддерживали серверный рендеринг, что тоже было важно для нас. В итоге выбор пал на awesome-swiper, который полностью соответствовал нашим требованиям и даже чуть больше. После того как слайдер был прикручен, наши верстальщики еще долгое время оставались в недоумении. “Это и все, мне больше ничего не нужно делать? Просто указать список изображений и это работает?”

Далее встал выбор компонента для выбора дат. Тут повезло больше, т.к. обертка для любимого нашими верстальщиками flatpickr нашлась быстро. Оставалось только немного его стилизовать.

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

В некоторых формах у нас присутствуют поля для ввода телефона. Пользователю нужно помогать вводить телефон, так как вариантов формата слишком много, да и работать потом в будущем с данными введенными в едином формате проще. Поэтому нужна маска. Хотелось использовать уже привычную text-mask, и тут нам повезло, у них было уже решение для vue — vue-text-mask.

Эти пакеты покрыли практически все наши требования. Оставалось только отслеживать клики вне компонента, в чем на помог vue-click-outside. Быструю прокрутку вверх страницы мы реализовали с помощью vue-backtotop. Для работы с датами используем moment.

Итоговый размер bundle и откуда взялся 1 мегабайт


Стоит учитывать, что важным критерием при выборе пакетов являлся их вес.

В середине проекта мы решили провести анализ итогового проекта и посмотреть размеры сборки. Результаты наc мягко говоря удивили. Размер банда app.js составлял чуть большее 950kb gzip. Команда npm run analyze вывела нам красивый график с размером всех модулей, из которого мы поняли, что некоторые модули тянут за собой ненужные нам зависимости в виде jquery, lodash и т.д. От этих пакетов пришлось отказаться и найти им альтернативу. На текущий момент размер всего бандла составляет 480kb gzip.



Следите за зависимостями и периодически проверяйте размер вашего приложения.

Первоначальная загрузка страница и данные, получаемые по api


Nuxt предоставляет удобную возможность наполнить store данными на серверной стороне до загрузки клиента. Для этого используется action nuxtServerInit. У нас это выглядит так:



Так как категории и некоторые другие сущности у нас используются сразу в нескольких компонентах, нам было удобнее получить их сразу и положить в store.

Но тут возникает проблема с размером json, который вы получаете. Так как сервер отдает все полученные данные на клиент для первоначальной отрисовки, размер html может быть слишком большим. Мы с этим столкнулись, когда в категориях начали еще передавать ненужные нам на всех страницах изображения, описание и другие поля принадлежащие каждой категории. Размер json составлял более 2mb. К счастью это легко поправить, убрав ненужные поля из данных, которые отдает сервер.

Утечки памяти


Спустя некоторое время работы приложения на нашем тестовом сервере мы начали наблюдать неестественный рост потребления памяти. pm2 занимал до 90% всей памяти сервера и приложение периодически падало. На github странице nuxt уже висело несколько issue с такой же проблемой.

Проблема возникала, когда мы в методе asyncData наших страниц делали несколько реквестов.



К счастью, эту проблему разработчики nuxt достаточно быстро решили, и на текущий момент процесс потребляет около 40mb памяти.

Интересные проблемы и их решения


Статьи с компонентами
В панели управления сайтом есть возможность добавлять статьи и вставлять в содержание статьи различные блоки, например блок с товаром.



HTML, который приходит с сервера, выглядит примерно так:



$product-4 указывает на то, что на месте этого указателя должен находится компонент Product.vue с идентификатором 4. Vue нам предоставляет широкие возможности рендеринга компонента с помощью метода render. Сначала ищем все упоминания указателей на компоненты в пришедшем html и получаем по api данные, нужные для отображения этого компонента. Далее разбиваем весь html на дерево. В этом нам помогла библиотека himalaya. И затем собираем обратно html заменяя указатели на уже готовые компоненты.

… А больше сил писать статью не хватило) Статью начинали писать летом 2017 по ходу разработки проекта, а на дворе уже лето 2018, проект запущен, а статья не выпущена.
Поэтому публикуем то, что насобирали, но у нас еще много интересных тем, наблюдений.
Если будет интересно — пишите, ставьте лайки) Ну и о чем было бы интересно еще услышать, что упустили.

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

Комментарии (17)


  1. musuk
    15.06.2018 21:09

    Расскажите как у вас организована работа со store и где живёт бизнес-логика.
    Контролы напрямую вызвают action'ы store или работа идёт через промежуточные сервисы?
    В моём текущем проекте вся логика лежит в модулях store, но кода стало так много, что всё это крайне тяжело сопровождать. Люди соверуют выносить логику в отдельные js-классы, но тогда появляется вопрос как им работать с state. В общем, хочется узнать, как бороться с лапшекодом в vuex store


    1. Serg_de_Adelantado
      16.06.2018 02:52

      В общем, хочется узнать, как бороться с лапшекодом в vuex store

      Если там появляется лапшекод, значит что-то идет не так :) По идее, action должен содержать обращение к какому-то ресурсу(самый банальный вариант — HTTP запрос) и последующий вызов commit. Если там накручено много логики — возможно, её стоит вынести в отдельный модуль Vuex?
      Если все же нужно выносить логику за пределы Vuex, то это можно сделать так:
      import { myActionLogic } from '~/logic/myActionLogic'
      
      export const actions = {
        async myAction({state, commit}, payload) {
          const computedResult = myActionLogic(state, payload)
          const somethingElse = await getSomethingFromNetwork(computedResult)
          commit(commitTypes.SET_SOMETHING, somethingElse)
        }
      }
      

      и в myActionLogic:
      // Используем foo и bar из state модуля
      export const myActionLogic = ({foo, bar}, {baz}) => {
        return foo + bar + baz
      }
      


      1. musuk
        18.06.2018 10:30

        Ясно, ваш вариант: выносить логику в js-модули и прокидывать state во все методы внешних сервисов. Попробую делать так.


  1. igorgusarov
    15.06.2018 23:47

    Показали бы сайт


    1. cepro
      16.06.2018 12:39

      Похоже что этот: drops.by


    1. Glorian
      16.06.2018 12:52

      По-моему этот: drops[dot]by


    1. maximus007 Автор
      16.06.2018 12:53

      Все правильно написали)


      1. Yahweh
        16.06.2018 16:18

        offtop


        А почему вы не пересчитываете фильтры при применении фильтров? Т.е. при заходе в раздел получаем по апи фильтры, но при фильтрации товар пересчитывается, а набор фильтров остается как был, в результате применив два фильтра можно получить пустой список товаров.


  1. Golem765
    16.06.2018 00:55

    «В twitter и на medium ни утихают споры, что все-таки лучше, vue или react»
    Где то тут потеряли angular :)


    1. neurocore
      16.06.2018 09:31
      +2

      Он сам потерялся


  1. Serg_de_Adelantado
    16.06.2018 02:37

    Спасибо за статью!
    Можно подробностей о конфигурировании pm2, настройках кеширования, как проксируете запросы к nuxt, как работаете со статикой, используете ли lru-cache для компонентов?


    1. ilovetwins
      16.06.2018 12:52

      Расскажу про свой опыт.
      Back и front на одном сервере, back кешируют запросы в редис, с ключами типа '/product/123', потом оба проверяют по такому ключу. Статика отдается через nginx, Для большинства компонентов используется lru-cache. Параллельно работает несколько Nuxt инстансев, для разных подпроектов. pm2 используется по простому: pm2 start npm --name «myapp» — run myapp. Так же используем analyze, многие модули грузим через ajax по запросу пользователя. Поэтому текущий общий начальный бандл сократили примерно до 200kb в gzip. При этом функционала и кастомных контролов в проекте достаточно много. Единственное с чем до сих про не разобрался (руки не дошли) это деплой без простоев. Когда билдится новая версия, новые страницы не открываются. Около 50 секунд, для нас не критично, так как главные страницы пока отдаются не через nuxt, когда их переведем придется заморочится. Проект в продакшене около года.


      1. Spunreal
        18.06.2018 10:53

        Единственное с чем до сих про не разобрался (руки не дошли) это деплой без простоев. Когда билдится новая версия, новые страницы не открываются. Около 50 секунд, для нас не критично, так как главные страницы пока отдаются не через nuxt, когда их переведем придется заморочится.

        1) Билдить в одно место, а потом использовать подмену. Т.е. запускаем скрипт деплоя, он собирает бандлы в папку version-1.2.3, после чего симлинк папки lastest меняется на version-1.2.3. Или просто переименовываем папку version-1.2.3. Т.е. все эти манипуляции делаются уже после билда и они очень быстры.
        2) (не очень хороший способ, но им пользуются тоже). Добавляем готовые бандлы в гит, а на продакшене вообще не собираем ничего.


  1. two_cookie
    16.06.2018 12:52

    Благодарю за статью. Мой внутренний ценитель интересных тем доволен.


  1. tehnazavr
    16.06.2018 16:31

    Глядя на этот сайт даже сложно поверить, что бандл весит меньше 500 КБ. Очень крутой результат и статья интересная. Я делал сервис, spa на vue, и бандл весил 1,5 мб.


  1. alexeydg
    18.06.2018 15:06

    как решен вопрос с сео? нет ли проблем для индексации такого сайта?


    1. maximus007 Автор
      18.06.2018 15:11

      Абсолютно никаких проблем нет. Есть же Nuxt для SSR.
      Мои догадки. Для яндекс видит сайт как обычный html сайт и ходит по ссылками, google мне кажется загружает первую страницу, а дальше уже ходит по JS, гугл вроде как понимает SPA сайты даже без SSR