ssr приложение что это

Разработка изоморфного RealWorld приложения с SSR и Progressive Enhancement. Часть 1 — Введение и выбор стека

Весной 2017 года Eric Simons, со-основатель учебного проекта Thinkster, анонсировал проект «RealWorld» — демо приложение и спецификация к нему. Проект объявил своей целью выйти за рамки привычных «todo»-демок для более прикладного сравнения и изучения возможностей различных фреймворков и технологий, а также подходов к разработке и способов решения задач.

image loader

Введение

О проекте RealWorld

So about a year ago, I had this realization about a problem I had:

Mastering the core concepts & ideology of a new framework is unnecessarily frustrating.

You read the docs, run a contrived example in a codepen, rip apart the “todo” example app & put it back together again, get their CLI installed locally… and then you’re off to the races!

Except you’re not. Not even close. Because when you start actually trying to build out your own app, that’s when Murphy’s law hits you.

По сути RealWorld — это клон блого-социальной платформы, как Medium или Хабр, названный «Conduit», который разрабатывается энтузиастами с использованием различных frontend и backend (да-да, фуллстек) технологий по одной и той же спецификации и макетам. Любому желающему поучаствовать в проекте нужно создать новое issue в репозитории на GitHub, в котором описать желаемый технологический стек, форкнуть starter-kit проекта и начать разработку. Кроме того, разработчик имеет возможность увидеть конечный результат в действии с помощью демо-приложения, написанного Эриком на AngularJS.

Фактически, итогом каждой новой реализации должно стать точно такое же приложение, но написанное с использованием других технологий или подходов. Уже реализованы и опубликованы форки для React/Redux, Elm, Angular2, React/MobX, Svelte/Sapper и других фреймворков. На подходе также Vue, Ember и другие.

image loader

Среди backend технологий проект реализован на Node/Express, Laravel, Django, ASP.NET Core, Rails и еще куче всего.

image loader

В конце 2017 года проект подготовил сравнение 9-ти наиболее популярных frontend реализаций по 3 критериям: производительность (first meaningful paint), размер бандла(-ов) (gzip) и кол-во строк кода, которые требуются для реализации проекта (loc).

На текущий момент, RealWorld — один из лучших демо-проектов по веб-разработке. Включает в себя решения наиболее распространенных задачи на примере достаточно реального веб-приложения и актуального стека технологий. Проект имеет более 12k звезд на гитхабе и, как мне кажется, является довольно полезным и интересным. Однако, как это часто бывает, на Хабре практически нет информации о нем. Лишь парочка упоминаний в дайджестах.

О чем это я

Так вот, к чему я все это пишу. Последние несколько лет, я фактически являюсь евангелистом изоморфного (по-другому, «универсального») подхода к написанию веб-приложений. В нашей компании также практикуется подход, который мы называем «environment-agnostic apps», т.е. приложения, которые могут работать в любом окружении (точнее каком-то списке окружений) без изменений. Для нас это особенно важно, потому что компания занимается разработкой под широчайший список платформ от веба до разнообразных IoT.

«Евангелист» все же наверное слишком громкий термин, однако за это время я участвовал в немалом количестве обсуждений и конференций, а также успел несколько раз прочитать собственный доклад на эту непростую и неоднозначную тему. Кроме того, меня давно и сильно волнуют проблемы «accessibility», «progressive enhancement» и «SEO» современных веб-приложений, также приложений основанных на веб-стеке.

Я прекрасно осознаю тот факт, что изоморфный (универсальный) подход к разработке — это довольно сложная и даже холиварная тема. Множество статей написано, много копей сломано. Достаточно большое количество разработчиком не принимает этот поход, некоторые выступают как откровенные хейтеры.

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

Давайте уважать друг друга. Также отдельно прошу уважить тот труд, который я планирую вложить на пользу сообщества путем разработки данного проекта и написания подробного туториала по ходу. Прошу не воспринимать данный цикл статей как принуждение вас или кого бы то ни было к использованию данных подходов. Мир!

В рамках своего доклада я реализовал изоморфное демо-приложение в виде небольшого блога на основе одного из шаблонов Material Design Lite. Приложение прям скажем маленькое, фактически без бекенда — его наличие лишь эмулируется с помощью таких штук как JSON Placeholder.

К своему стыду, сам я узнал о проекте RealWorld лишь в конце прошлого года, но тогда не нашел достаточно времени, чтобы поучаствовать в нем. Сейчас со временем стало немного проще и я хочу попробовать реализовать RealWorld приложение (взамен текущему), отвечающее следующим характеристикам:

«Манифест» проекта:

Это немного спорный вопрос. Я много думал о том, как именно следует считать общий (изоморфный) код и пришел к выводу, что таким кодом является непосредственно код самого приложения написанный мной для его реализации. Иными словами тот код, который реализует функционал текущего проекта, т.е. проекто-зависимый код.

Та часть кода, которая является лишь основой приложения (например код веб-сервера и т.п.) и может быть без изменений использована в другом проекте, не будет учитываться в качестве кода приложения. Можно назвать этот код «starter-kit» или «boilerplate» или как угодно. В любом случае этот код не является непосредственной частью конкретной бизнес-задачи и реализует обобщённую базу, на основе которой пишется приложение.

Еще раз, кодом приложения будет считаться код написанные лично мной для реализации конкретного проекта.

Изучая примеры изоморфных приложений, можно обратить внимание что часто используются не совсем обоснованные вещи, такие как: (1) отдельные точки входа для серверного и клиентского приложения; (2) всевозможные условные операторы, проверяющие текущее окружение (аля «isServer» и т.п.) и отдельные ветки кода в рамках общих файлов; (3) серверный роутинг и фетчинг данных выполняется не так, как клиентский и т.д. и т.п.

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

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

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

Как видите, список довольно амбициозный. Тут вам и спорная «изоморфность» и всеми желанный, но такой недостижимый «progressive enhancement». И блог-платформа, работающая в стиле SPA, но при этом поддерживающая SEO. Все это на единой кодовой базе поверх существующего backend и по ТЗ, в которые нельзя внести изменения.

Кстати об этом. Для меня этот проект интересен еще потому, что многие хейтеры изоморфного подхода часто говорят, что мол для того, чтобы написать полноценное изоморфное приложение нужно каким-то специальным образом переписать бекенд. То есть ваш существующий бекенд не подойдет. Или нужно обязательно юзать в качестве бекенда NodeJS, что само по себе фу-фу-фу. Или что нужно каким-то специальным образом формулировать требования и ТЗ и еще какие-то глупости.

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

Короче говоря, для меня данный проект является еще и возможностью полностью или частично опровергнуть все это, а также повеселиться. Очень надеюсь что у меня все получится. Если вам интересна тема и хотелось бы пройти этот путь со мной по шагам — не переключайтесь!

Выбор стека

Уверен вы уже поняли, но если нет, тогда уточняю — писать будем frontend часть приложения. Для этого проект RealWorld предоставляет Frontend спецификацию, которая включает:

Пожалуй «темной лошадкой» в этом списке для многих может оказаться RactiveJS. Да, действительно, выбор фреймворка не входящего в «большую тройку» и не бьющего рекорды популярности, может показаться странным. Однако он отлично подходит для данной задачи, является один из любимейших моих инструментов и надеюсь немного разнообразит привычные будни «реакт-ангуляро-вьюшных» туториалов. К тому же, возможно тем самым мне также удастся представить новый взгляд на некоторые привычные вещи. Хотя это и не является моей целью.

Server-side

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

Данный туториал предназначен прежде всего для frontend разработчиков среднего и выше уровня. Которые знакомы с современными инструментами разработки и в курсе, что такое SPA и изоморфность.

В рамках туториала НЕ будут раскрываться вопросы установки npm-модулей, вводного знакомства с webpack, работы с командной строкой или иных базовых вещей на сегодняшний день. Исхожу из того, что для большинства читатетей рутинные операции по настройки дев-среды и работы с инструментами разработки и т.п. уже знакомы и отлажены.

Про архитектуру

Примерно вот так будет выглядеть общая архитектура приложения. Несколько ключевых моментов:

Workflow

Структура проекта

Я буду использовать совершенно привычную для себя структуру подобного проекта:

Ну и конечно же целая куча всевозможные package.json, webpack.config.js и других конфигурационных файлов, описание которых выходит за рамки туториала.

Пишем код

Основной код веб-сервера будет располагаться в файле ./server.js. Кроме самого Express я также буду использовать некоторые из его плагинов («express-middleware») для решения утилитарных задач. Предварительно все необходимые модули, естественно, необходимо прописать в package.json установить с помощью команды npm i.

Сначала подключим 3rd-party модули и сам Express:

Все используемые расширения Express совершенно обычные и не имеют отношения к изоморфности. Большая часть из них вполне себе стандартная. Вот их краткое описание и предназначение:

Устанавливаем mustache в качестве шаблонизатора для Express. Почему именно его? Просто Ractive использует mustache-синтаксис для своих шаблонов и таким образом мы добьемся единообразия.

Далее сообщаем что хотим сжимать также всю статитку и что статика располагается в папке «./dist» (туда же будут генерироваться бандлы вебпаком).

Далее финальный штрих:

Код не требующий особых пояснений. Ключевые моменты здесь в следующем:

Как мне кажется, даже чисто frontend-разработчик, не имеющий особого опыта с Express, легко разберется в столь примитивном коде. Если конечно понимает что такое «middleware» и вообще эту концепцию.

Пока данный код практически ничего не делает. Во всяком случае не делает того, что требуется сделать в рамках проекта. В следующей статье мы допишем чисто серверный код и приступим к коду самого приложения.

Спасибо за внимание и хорошего времени суток!

UPD 2: Обновил логотип RactiveJS, а также добавил пункты 9 и 10 в манифест (см. часть 3)

Источник

Server-Side Rendering с нуля до профи

image loader

Проблема

Главной проблемой Single Page приложений является то, что сервер отдает клиенту пустую HTML страницу. Её формирование происходит только после того как весь JS будет скачан (это весь ваш код, библиотеки, фреймверк). Это в большинстве случаев более 2-х мегабайт размера + задержки на обработку кода.

Даже если Google-бот умеет выполнять JS, он получает контент только спустя некоторое время, критичное для ранжирования сайта. Google-бот попросту видит пустую страницу несколько секунд! Это плохо!

Google начинает выдавать красные карты если ваш сайт рендерится более 3-х секунд. First Contentful Paint, Time to Interactive — это метрики которые будут занижены при Single Page Application. Подробнее читайте здесь.

Также есть менее продвинутые поисковые системы, которые попросту не умеют работать с JS. В них Single Page Application не будут индексироваться.

На ранжирование сайта еще влияет множество факторов, часть из них мы разберем далее в этой статье.

Рендеринг

Существует несколько путей как решить проблему пустой странички при загрузке, рассмотрим несколько из них:

Static Site Generation (SSG). Сделать пререндер сайта перед тем как его загрузить на сервер. Очень простое и эффективное решение. Отлично подходит для простых веб страничек, без взаимодействия с backend API.

Server-Side Rendering (SSR). Рендерить контент в рантайме на сервере. При таком подходе мы сможем делать запросы backend API и отдавать HTML вместе с необходимым содержимым.

Server-Side Rendering (SSR)

Рассмотрим подробнее, как работает SSR:

image loader

Из вышеописанной работы SSR приложения мы можем выделить проблемы:

Это маленькая библиотека которая способна решить проблемы асинхронной обработки запросов к данным а также синхронизации состояния с сервера на клиент. Это не “очередной убийца Next.JS”, нет! Next.JS прекрасный фреймверк имеющий множество возможностей, но чтобы его использовать вам придется почти полностью переписать ваше приложение и следовать правилам Next.JS.

Посмотрим на примере, как просто перенести обычное SPA приложение на SSR.

К примеру, у нас есть простейшее приложение с асинхронной логикой.

Данный код рендерит список выполненных задач, используя сервис jsonplaceholder для эмуляции взаимодействия с API.

Сделаем данное приложение SSR!

Шаг 1. Установка зависимостей

Для установки iSSR нужно выполнить:

Для настройки базовой билд системы установим:

Один из неочевидных моментов разработки SSR приложений является то, что некоторые API и библиотеки могут работать на клиенте но не работать на сервере. Одним из таких API является fetch. Данный метод отсутствует в nodejs где будет выполняться серверная логика нашего приложения. Для того, чтобы у нас приложение работало одинаково установим пакет:

Для сервера будем использовать express, но это не важно, можно использовать любой другой фреймверк:

Добавим модуль для сериализации состояния приложения на сервере:

Шаг 2. Настройка webpack.config.js

Шаг 3. Модификация кода

Вынесем общую логику нашего приложения в отдельный файл App.jsx. Это нужно для того, чтобы в файлах client.jsx и server.jsx осталась только логика рендеринга, ничего больше. Таким образом весь код приложения у нас будет общий.

Мы поменяли стандартный render метод React на hydrate, который работает для SSR приложений.

В коде сервера обратите внимание, что мы должны расшаривать папку с собранным SPA приложением webpack:
app.use(express.static(‘public’));
Таким образом, полученный с сервера HTML будет работать далее как обычный SPA

Шаг 4. Обработка асинхронности

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

Для обработки асинхронных операций их нужно обернуть в хук useSsrEffect из пакета @issr/core:

В server.jsx заменим стандартный renderToString на serverRender из пакета @issr/core:

Если запустить приложение сейчас, то ничего не произойдет! Мы не увидим результата выполнения асинхронной функции getTodos. Почему так происходит? Мы забыли синхронизировать состояние. Давайте исправим это.

В App.jsx заменим стандартный setState на useSsrState из пакета @issr/core:

Внесем изменения в client.jsx для синхронизации состояния переданного с сервера на клиент:

window.SSR_DATA — это объект, переданный с сервера с кешированнным состоянием, для синхронизации на клиенте.

Сделаем передачу состояние на сервере:

Обратите внимание, что функция serverRender передает не только HTML, но и состояние, которое прошло через useSsrState, мы его передаем на клиент, в качестве глобальной переменной SSR_DATA. На клиенте, данное состояние будет автоматически синхронизировано.

Шаг 5. Билд скрипты

Осталось добавить скрипты запуска в package.json:

Redux и прочие State Management библиотеки

iSSR отлично поддерживает разные state management библиотеки. В ходе работы над iSSR я заметил, что React State Management библиотеки делятся на 2 типа:

Мы не будем рассматривать этот пример так детально как предыдущий. Ознакомится с полным кодом можно по ссылке.

Redux Saga

*Для лучшего понимания происходящего, читайте предыдущую главу

Сервер запускает наше приложение через serverRender, код выполняется последовательно, выполняя все операции useSsrEffect.

Концептуально Redux работая с сагами не выполняет никаких асинхронных операций. Наша задача отправить action для старта асинхронной операции в слое Саг, отдельных от нашего react-flow. В примере по ссылке выше, в контейнере Redux мы выполняем

Это не асинхронная операция! Но iSSR понимает, что что то произошло в системе. iSSR будет идти по остальным React компонентам выполняя все useSsrEffect если таковые будут и по завершению iSSR вызовет каллбек:

Таким образом мы можем обрабатывать асинхронные операции не только на уровне с React но и на других уровнях, в данном случае мы в начале поставили на выполнение нужные нам саги, после чего в callback serverRender запустили и дождались их окончания.

Я подготовил много примеров использования iSSR, вы можете их найти по ссылке.

SSR трюки

На пути в разработке SSR приложений существуют множество проблем. Проблема асинхронных операций это только одна из них. Давайте посмотрим на другие распространенные проблемы.

HTML Meta Tags для SSR

Немаловажным аспектом в разработке SSR является использование правильных HTML meta tags. Они сообщают поисковому боту ключевую информацию на странице.
Для реализации данной задачи рекомендую использовать один из модулей:
React-Helmet-Async
React-Meta-Tags
Я подготовил несколько примеров:
React-Helmet-Async
React-Meta-Tags

Dynamic Imports

Чтобы снизить размер финального бандла приложения, принято приложение делить на части. Например dynamic imports webpack позволяет автоматически разбить приложение. Мы можем вынести отдельные страницы в чанки или блоки. При SSR мы должны уметь обрабатывать данные фрагменты приложения как одно целое. Для этого рекомендую использовать замечательный модуль @loadable

Dummies

Некоторые компоненты или фрагменты страницы можно не рендерить на сервере. Например, если у вас есть пост и комментарии, не целесообразно обрабатывать обе асинхронные операции. Данные поста более приоритетны чем комментарии к нему, именно эти данные формируют SEO нагрузку вашего приложения. По этому мы можем исключать не важные части при помощи проверок типа:

localStorage, хранение данных

NodeJS не поддерживает localStorage. Для хранения сессионных данных мы используем cookie вместо localStorage. Файлы cookie отправляются автоматически по каждому запросу. Файлы cookie имеют ограничения, например:

React Server Components

React Server Components возможно будет хорошим дополнением для SSR. Его идеей является снижение нагрузки на Bundle за счет выполнения компонент на сервере и выдачи готового JSON React дерева. Нечто подобное мы видели в Next.JS. Читайте подробнее по ссылке

Роутинг

React Router из коробки поддерживает SSR. Отличие в том, что на server используется StaticRouter с переданным текущим URL, а на клиенте Router определяет URL автоматически при помощи location API. Пример

Debugging

Дебаг на сервере может выполняться также как и любой дебаг node.js приложений через inpsect.
Для этого нужно добавить в webpack.config для nodejs приложения:

devtool: ‘source-map’
А в настройки NodemonPlugin:

Также, для улучшения работы с source map можно добавить модуль

В nodeArgs опций NodemonPlugin добавить:
‘—require=«source-map-support/register»’
Пример

Next.JS

Если вы создаете приложение с нуля, рекомендую обратить внимание на данный фреймверк. Это самое популярное решение на данный момент для создания с нуля приложений с поддержкой SSR. Из плюсов можно выделить то, что все идет из коробки (билд система, роутер). Из минусов — необходимо переписывать существующее приложение, использовать подходы Next.JS.

SEO это не только SSR!

Критерии Google бота для SEO оптимизации включают множество метрик. Рендер данных, получение первого байта и т.д. это лишь часть метрик! При SEO оптимизации приложения необходимо минимизировать вес картинок, бандла, грамотно использовать HTML теги и HTML мета теги и прочее.

Для проверки вашего сайта при SEO оптимизации можно воспользоваться:

Источник

# Руководство по серверному рендерингу Vue.js

Для этого руководства требуются следующие версии Vue и библиотек:

Если вы ранее использовали Vue 2.2 с серверным рендерингом, вы заметите, что рекомендуемая структура кода теперь немного отличается (с новой опцией runInNewContext, установленной в false ). Ваше существующее приложение по-прежнему будет работать, но лучше внесите изменения с учётом новых рекомендаций.

# Что такое серверный рендеринг (SSR)?

Vue.js — это фреймворк для создания приложений, выполняемых на клиенте (client-side). По умолчанию компоненты Vue создают и манипулируют DOM в браузере. Однако, также возможно рендерить те же компоненты в HTML-строки на сервере, отправлять их в браузер, и наконец «гидрировать» статическую разметку в полностью интерактивное приложение на клиенте.

Приложение Vue.js отрендеренное на сервере также можно считать «изоморфным» или «универсальным», в том смысле, что большая часть кода приложения является общей для сервера и клиента.

# Нужен ли вам SSR?

По сравнению с традиционным SPA (Single-Page Application), преимуществами серверного рендеринга будут:

Лучшее SEO, поскольку поисковые роботы будут видеть полностью отрендеренную страницу.

Обратите внимание, что на данный момент Google и Bing могут без проблем индексировать синхронные приложения JavaScript. Ключевое слово здесь — синхронные. Если ваше приложение запускается с индикатором загрузки, а потом догружает контент через Ajax, то поисковый робот просто не будет дожидаться окончания загрузки. Это значит, что если у вас есть асинхронный контент на страницах где SEO важен, то может потребоваться серверный рендеринг.

Лучшие показатели времени до отображения контента (time-to-content), особенно при плохом интернете или на медленных устройствах. Для разметки, отрендеренной на сервере, не требуется дожидаться пока весь JavaScript будет загружен и выполнен, поэтому ваш пользователь увидит полностью отрендеренную страницу раньше. Как правило, это приводит к лучшему пользовательскому опыту и может быть критичным для приложений, где время до отображения контента напрямую связано с коэффициентом конверсии.

Следует учитывать и некоторые компромиссы при использовании серверного рендеринга:

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

Более сложные требования по настройке и развёртыванию сборки. В отличие от полностью статичного SPA, который может быть развёрнут на любом статичном файловом сервере, приложение с серверным рендерингом требует окружения, где есть возможность запустить сервер Node.js.

Повышенная нагрузка на стороне сервера. Рендеринг полноценного приложения в Node.js очевидно более требователен к ресурсам процессора, чем простая раздача статичных файлов, поэтому если вы ожидаете большой трафик, будьте готовы к соответствующей нагрузке на сервер и используйте стратегии кэширования.

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

# SSR vs Пререндеринг

Если вы используете Webpack, то для добавления пререндеринга достаточно установить плагин prerender-spa-plugin

. Он был тщательно протестирован с приложениями Vue.

# Об этом руководстве

Это руководство ориентировано на SPA приложения с рендерингом на сервере, используя Node.js в качестве сервера. Использование серверного рендеринга Vue совместно с другими технологиями и настройками бэкэнда являются отдельной темой и кратко обсуждается в отдельном разделе.

Это руководство будет очень детальным и предполагает, что вы уже знакомы с самим Vue.js, имеете знания и опыт работы с Node.js и Webpack. Если вы предпочитаете более высокоуровневые решения, обеспечивающие работу из коробки — вам следует попробовать Nuxt.js

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

По мере прочтения руководства, будет полезным обращаться к официальному демо HackerNews

, в котором используется большинство техник, изложенных в этом руководстве.

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

Источник

Понравилась статья? Поделить с друзьями:
Добавить комментарий
  • Как сделать успешный бизнес на ритуальных услугах
  • Выездной кейтеринг в России
  • Риски бизнеса: без чего не обойтись на пути к успеху
  • ssi eeb форм фактор
  • ssi ceb форм фактор