Опыт разработки SPA на VueJS + Nuxt

    Наша компания занимается преимущественно разработкой интернет-магазинов и мы хотим поделиться своим опытом разработки проекта на связке 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 месяцев. Особых проблем с ним не замечено, а скорость работы сайта впечатляет и выгодно выделяет его на фоне других магазинов.
    Поделиться публикацией

    Комментарии 22

      0
      Расскажите как у вас организована работа со store и где живёт бизнес-логика.
      Контролы напрямую вызвают action'ы store или работа идёт через промежуточные сервисы?
      В моём текущем проекте вся логика лежит в модулях store, но кода стало так много, что всё это крайне тяжело сопровождать. Люди соверуют выносить логику в отдельные js-классы, но тогда появляется вопрос как им работать с state. В общем, хочется узнать, как бороться с лапшекодом в vuex store
        0
        В общем, хочется узнать, как бороться с лапшекодом в 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
        }
        
          0
          Ясно, ваш вариант: выносить логику в js-модули и прокидывать state во все методы внешних сервисов. Попробую делать так.
        0
        Показали бы сайт
          0
          Похоже что этот: drops.by
            0
            По-моему этот: drops[dot]by
              0
              Все правильно написали)
                0

                offtop


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

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

                    1) Билдить в одно место, а потом использовать подмену. Т.е. запускаем скрипт деплоя, он собирает бандлы в папку version-1.2.3, после чего симлинк папки lastest меняется на version-1.2.3. Или просто переименовываем папку version-1.2.3. Т.е. все эти манипуляции делаются уже после билда и они очень быстры.
                    2) (не очень хороший способ, но им пользуются тоже). Добавляем готовые бандлы в гит, а на продакшене вообще не собираем ничего.
                      0
                      Спасибо! Теперь мне точно не отвертется от этой задачи)
                  0
                  Благодарю за статью. Мой внутренний ценитель интересных тем доволен.
                    0
                    Глядя на этот сайт даже сложно поверить, что бандл весит меньше 500 КБ. Очень крутой результат и статья интересная. Я делал сервис, spa на vue, и бандл весил 1,5 мб.
                      –1
                      как решен вопрос с сео? нет ли проблем для индексации такого сайта?
                        +1
                        Абсолютно никаких проблем нет. Есть же Nuxt для SSR.
                        Мои догадки. Для яндекс видит сайт как обычный html сайт и ходит по ссылками, google мне кажется загружает первую страницу, а дальше уже ходит по JS, гугл вроде как понимает SPA сайты даже без SSR
                          +1
                          Уточнение: google хоть и умеет в js, тем не менее новые страницы открывает всегда по ссылке, так же как и yandex. Так же всегда остается проблема социальных сетей, которые ничего не знают про js, но, как правильно замечено, Nuxt с легкостью справляется со всеми seo вопросами.
                        0
                        За размер бандла побеспокоились, а про размер картинок видимо забыли? В итоге особой разницы не заметно, все равно главная страница грузит 5мб
                          0
                          Да, спасибо за замечание. Сейчас поправим.
                          Это вот тоже нормально на тему, что все гонятся за скоростью, а какой-нибудь такой мелочью можно все труды убить.
                          Кстати на тему ресайза картинок тоже у себя в блоге писали: надеюсь интересно) websecret.by/blog/optimizaciya-izobrazhenij-na-krupnyh-proektah

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

                        Самое читаемое