Создание сайтов в Белгороде. Как создать форму загрузки файлов с помощью Express и DropzoneJS

 
 

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

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

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

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

Как всегда, вы можете найти код для этого руководства в нашем репозитории GitHub.

Эта статья была обновлена ​​в 2020 году. Чтобы узнать больше о Node.js, прочитайте Node.js Web Development — Fourth Edition.

Представляем DropzoneJS

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

Однако DropzoneJS — это не просто виджет на основе перетаскивания. Щелчок по виджету запускает более традиционный диалог выбора файла.

Вот анимация виджета в действии:

Виджет DropzoneJS в действии

Как вариант, взгляните на этот самый минималистичный из примеров.

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

Функции

Подводя итог некоторым функциям и характеристикам плагина, DropzoneJS:

можно использовать с jQuery или без него

имеет поддержку перетаскивания

генерирует эскизы изображений

поддерживает несколько загрузок, опционально параллельно

включает индикатор выполнения

полностью тематический

включает расширяемую поддержку проверки файлов

доступен как модуль AMD или модуль RequireJS

занимает около 43 КБ при минимизации и 13 КБ при сжатии gzip.

Поддержка браузера

Взято из официальной документации, поддержка браузера выглядит следующим образом:

Хром 7+

Фаерфокс 4+

IE 10+

Opera 12+ (версия 12 для macOS отключена, потому что их API глючит)

Сафари 6+

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

Подготовка

Самый простой способ начать работу с DropzoneJS — включить последнюю версию из CDN. На момент написания это версия 5.5.1.

Кроме того, вы можете скачать последний релиз со страницы проекта на GitLab. Существует также сторонний пакет, обеспечивающий поддержку ReactJS.

Затем убедитесь, что вы включили в свою страницу как основной файл JavaScript, так и стили CSS. Например:

<! DOCTYPE html>

 

 

 

<link

rel="stylesheet"

href="https://cdnjs.cloudflare.com/ajax/libs/dropzone/5.5.1/min/dropzone.min.css">

 

 

 

 

 

Обратите внимание, что проект предоставляет два файла CSS — basic.cssфайл с минимальным стилем и более обширный dropzone.cssфайл. Также доступны уменьшенные версии dropzone.cssи.dropzone.js

Основное использование

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

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

 

Вы можете инициализировать его, просто добавив dropzoneкласс. Например:

 

 

 

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

Dropzone.options.WIDGET_ID = {

//

};

Чтобы получить идентификатор виджета для настройки параметров, возьмите идентификатор, который вы определили в своем HTML, и приведите его к верблюжьему регистру. Например, upload-widgetстановится uploadWidget:

Dropzone.options.uploadWidget = {

//

};

Вы также можете создать экземпляр программно:

const uploader = new Dropzone ('#upload-widget’, options) ;

Далее мы рассмотрим некоторые из доступных параметров конфигурации.

Основные параметры конфигурации

Параметр urlопределяет цель формы загрузки и является единственным обязательным параметром. Тем не менее, если вы прикрепляете его к элементу формы, он просто использует actionатрибут формы, и в этом случае вам даже не нужно указывать это.

Параметр methodустанавливает метод HTTP, и, опять же, он возьмет его из элемента формы, если вы используете этот подход, или просто по умолчанию будет использоваться значение POST, что подходит для большинства сценариев.

Опция paramNameиспользуется для установки имени параметра для загружаемого файла. Если вы используете элемент формы загрузки файла, он будет соответствовать nameатрибуту. Если вы его не включаете, по умолчанию используется file.

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

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

Это основные параметры, но давайте теперь рассмотрим некоторые из более продвинутых параметров.

Обеспечение максимального размера файла

Свойство maxFilesizeопределяет максимальный размер файла в мегабайтах. По умолчанию это размер 1000 байт, но с помощью этого filesizeBaseсвойства вы можете установить другое значение, например 1024 байта. Возможно, вам придется настроить это, чтобы гарантировать, что ваш клиентский и серверный код вычисляют любые ограничения точно таким же образом.

Ограничение определенными типами файлов

Этот acceptedFilesпараметр можно использовать для ограничения типа файла, который вы хотите принять. Это должно быть в виде списка типов MIME, разделенных запятыми, хотя вы также можете использовать подстановочные знаки.

Например, чтобы принимать только изображения:

acceptedFiles: 'image/*',

Изменение размера миниатюры

По умолчанию миниатюра генерируется размером 120×120 пикселей. То есть он квадратный. Есть несколько способов изменить это поведение.

Первый заключается в использовании thumbnailWidthи/или параметров thumbnailHeightконфигурации.

Если для обоих параметров thumbnailWidthи установлено thumbnailHeightзначение null, размер эскиза вообще не изменится.

Если вы хотите полностью настроить поведение создания эскизов, вы даже можете переопределить эту resizeфункцию.

Один важный момент, касающийся изменения размера эскиза, заключается в том, что dz-imageкласс, предоставляемый пакетом, устанавливает размер эскиза в CSS, поэтому вам также необходимо изменить его соответствующим образом.

Дополнительные проверки файлов

Эта acceptопция позволяет вам обеспечить дополнительные проверки, чтобы определить, является ли файл действительным, прежде чем он будет загружен. Вы не должны использовать это для проверки количества файлов (maxFiles), типа файла (acceptedFiles) или размера файла (maxFilesize), но вы можете написать собственный код для выполнения других видов проверки.

Вы бы использовали такой acceptвариант:

accept: function (file, done) {

if (! someCheck ()) {

return done ('This is invalid!') ;

}

return done () ;

}

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

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

Отправка дополнительных заголовков

Часто вам нужно будет прикрепить дополнительные заголовки к HTTP-запросу загрузчика.

Например, один из подходов к защите от CSRF (подделка межсайтовых запросов) заключается в выводе токена в представлении, а затем ваши POST/PUT/DELETEконечные точки проверяют заголовки запроса на наличие действительного токена. Предположим, вы вывели свой токен следующим образом:

Затем вы можете добавить это в конфигурацию:

headers: {

'x-csrf-token’: document.querySelector ('meta[name=csrf-token]').getAttributeNode ('content’).value,

},

В качестве альтернативы, вот тот же пример, но с использованием jQuery:

headers: {

'x-csrf-token’: $ ('meta[name="csrf-token"]').attr ('content’)

},

Затем ваш сервер должен проверить x-csrf-tokenзаголовок, возможно, используя промежуточное ПО.

Обработка резервных вариантов

Самый простой способ реализовать запасной вариант — вставить

в форму, содержащую элементы управления вводом, задав для элемента имя класса fallback. Например:

 

 

 

 

 

 

 

 

 

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

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

Обработка ошибок

Вы можете настроить способ обработки ошибок виджетом, предоставив пользовательскую функцию с помощью errorпараметра конфигурации. Первым аргументом является файл, вторым — сообщение об ошибке, а если ошибка произошла на стороне сервера, то третьим параметром будет экземпляр XMLHttpRequest.

Как всегда, проверка на стороне клиента — это только полдела. Вы также должны выполнить проверку на сервере. Когда позже мы реализуем простой компонент на стороне сервера, мы рассмотрим ожидаемый формат ответа об ошибке, который при правильной настройке будет отображаться так же, как ошибки на стороне клиента (показано ниже).

Отображение ошибок с DropzoneJS

Переопределение сообщений и перевод

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

В частности, dictDefaultMessageиспользуется для установки текста, который появляется в середине дропзоны до того, как кто-то выберет файл для загрузки.

Вы найдете полный список настраиваемых строковых значений — все они начинаются с dict— в документации.

События

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

Есть два способа прослушать событие. Первый — создать прослушиватель в функции инициализации:

Dropzone.options.uploadWidget = {

init: function () {

this.on ('success’, function (file, resp) {

...

}) ;

},

...

};

Это альтернативный подход, который полезен, если вы решили создать экземпляр Dropzone программно:

const uploader = new Dropzone ('#upload-widget’) ;

uploader.on ('success’, function (file, resp) {

...

}) ;

Возможно, наиболее примечательным аспектом является successсобытие, которое запускается, когда файл успешно загружен. Обратный successвызов принимает два аргумента: первый — файловый объект, а второй — экземпляр XMLHttpRequest.

Другие полезные события включают addedfileи removedfile, когда файл был добавлен или удален из списка загрузки; thumbnail, который срабатывает после создания эскиза; и uploadprogress, которые вы можете использовать для реализации собственного индикатора прогресса.

Существует также множество событий, которые принимают объект события в качестве параметра и которые вы можете использовать для настройки поведения самого виджета — drop, dragstart, dragend, dragenterи dragover.dragleave

Полный список событий вы найдете в соответствующем разделе документации.

Более сложный пример проверки: размеры изображения

Ранее мы рассмотрели асинхронный accept () вариант, который можно использовать для проверки (валидации) файлов перед их загрузкой.

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

Хотя обратный вызов accept получает файловый объект, чтобы проверить размеры изображения, нам нужно подождать, пока не будет сгенерирована миниатюра, после чего размеры будут установлены для файлового объекта. Для этого нам нужно прослушать событие thumbnail.

Вот код. В этом примере перед загрузкой мы проверяем, что изображение имеет размер не менее 640×480 пикселей:

init: function () {

this.on ('thumbnail’, function (file) {

if (file.accepted≠= false) {

if (file.width < 1024 || file.height < 768) {

file.rejectDimensions () ;

}

else {

file.acceptDimensions () ;

}

}

}) ;

},

accept: function (file, done) {

file.acceptDimensions = done;

file.rejectDimensions = function () {

done ('The image must be at least 1024 by 768 pixels in size’) ;

};

},

Полный пример

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

Вот HTML для формы:

 

 

 

 

 

 

 

 

Если вы внедряете защиту CSRF, вы можете добавить что-то вроде этого в свои макеты:

 

 

Теперь JavaScript. Обратите внимание, что мы не используем jQuery!

Dropzone.options.uploadWidget = {

paramName: 'file’,

maxFilesize: 2, // MB

maxFiles: 1,

dictDefaultMessage: 'Drag an image here to upload, or click to select one’,

headers: {

'x-csrf-token’: document.querySelectorAll ('meta[name=csrf-token]')[0].getAttributeNode ('content’).value,

},

acceptedFiles: 'image/*',

init: function () {

this.on ('success’, function (file, resp) {

console.log (file) ;

console.log (resp) ;

}) ;

this.on ('thumbnail’, function (file) {

if (file.accepted≠= false) {

if (file.width < 640 || file.height < 480) {

file.rejectDimensions () ;

}

else {

file.acceptDimensions () ;

}

}

}) ;

},

accept: function (file, done) {

file.acceptDimensions = done;

file.rejectDimensions = function () {

done ('The image must be at least 640×480px’)

};

}

};

Напоминаем, что вы найдете код для этого примера в нашем репозитории GitHub.

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

Тематика

Существует несколько способов настроить внешний вид виджета, и действительно можно полностью изменить его внешний вид.

В качестве примера того, насколько настраиваемым является внешний вид, вот демонстрация виджета, настроенного так, чтобы он выглядел и работал точно так же, как виджет jQuery File Upload с использованием Bootstrap.

Очевидно, что самый простой способ изменить внешний вид виджета — использовать CSS. Виджет имеет класс, dropzoneа его составные элементы имеют классы с префиксом dz-— например, dz-clickableдля кликабельной области внутри дропзоны, dz-messageдля заголовка, dz-preview/ dz-image-previewдля обертывания превью каждого загруженного файла и так далее. Взгляните на dropzone.cssфайл для справки.

Вы также можете применить стили к состоянию наведения, то есть когда пользователь наводит указатель мыши на файл над зоной сброса, прежде чем отпустить кнопку мыши, чтобы начать загрузку. Вы можете сделать это, настроив dz-drag-hoverкласс, который автоматически добавляется плагином.

Помимо настройки CSS, вы также можете настроить HTML, который составляет предварительный просмотр, установив previewTemplateсвойство конфигурации. Вот как выглядит шаблон предварительного просмотра по умолчанию:

 

 

 

 

data-dz-thumbnail />

 

 

 

 

 

 

data-dz-size>

 

 

 

 

data-dz-name>

 

 

 

 

 

 

data-dz-uploadprogress>

 

 

 

 

data-dz-errormessage>

 

 

 

 

REMOVED FOR BREVITY

 

 

 

 

REMOVED FOR BREVITY

 

 

 

 

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

На этом мы завершаем раздел об использовании плагина DropzoneJS. Подводя итог, давайте посмотрим, как заставить его работать с кодом на стороне сервера.

Простой обработчик загрузки на стороне сервера с помощью Node.js и Express

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

Чтобы обработать сам загруженный файл, мы будем использовать Multer, пакет, который предоставляет промежуточное ПО Express, которое действительно упрощает работу. На самом деле это легко:

const upload = multer ({ dest: 'uploads/' }) ;

app.post ('/upload’, upload.single ('file’), (req, res, next) => {

// Metadata about the uploaded file can now be found in req.file

}) ;

Прежде чем мы продолжим реализацию, самый очевидный вопрос, который нужно задать при работе с плагином, таким как DropzoneJS, который делает запросы за вас за кулисами: «Какие ответы он ожидает?»

Обработка успешной загрузки

Если процесс загрузки прошел успешно, единственное требование, касающееся вашего кода на стороне сервера, — вернуть 2xxкод ответа. Содержание и формат вашего ответа полностью зависят от вас и, вероятно, будут зависеть от того, как вы его используете. Например, вы можете вернуть объект JSON, содержащий путь к загруженному файлу или путь к автоматически сгенерированному эскизу. Для целей этого примера мы просто вернем содержимое файлового объекта, то есть набор метаданных, предоставленных Multer:

return res.status (200).send (req.file) ;

Ответ будет выглядеть примерно так:

{ fieldname: 'file’,

originalname: 'myfile.jpg’,

encoding: '7bit’,

mimetype: 'image/jpeg’,

destination: 'uploads/',

filename: 'fbcc2ddbb0dd11858427d7f0bb2273f5',

path: 'uploads/fbcc2ddbb0dd11858427d7f0bb2273f5',

size: 15458 }

Обработка ошибок загрузки

Если ваш ответ в формате JSON, то есть для вашего типа ответа задано application/jsonзначение, плагин ошибок DropzoneJS по умолчанию ожидает, что ответ будет выглядеть следующим образом:

{

error: 'The error message’

}

Если вы не используете JSON, он просто использует тело ответа. Например:

return res.status (422).send ('The error message’) ;

Давайте продемонстрируем это, выполнив пару проверок загруженного файла. Мы просто продублируем проверки, которые мы выполнили на клиенте. Помните, что проверки на стороне клиента никогда не бывает достаточно.

Чтобы убедиться, что файл является изображением, мы просто проверим, что тип MIME начинается с image/. ES6 String.prototype.startsWith () идеально подходит для этого.

Вот как мы можем запустить эту проверку и, если она не пройдена, вернуть ошибку в формате, который ожидает обработчик ошибок Dropzone по умолчанию:

if (! req.file.mimetype.startsWith ('image/')) {

return res.status (422).json ({

error: 'The uploaded file must be an image’

}) ;

}

Примечание. Я использую код состояния HTTP 422, Unprocessable Entity, для ошибки проверки, но 400 Bad Request так же действителен. Действительно, все, что выходит за пределы диапазона 2xx, приведет к тому, что плагин сообщит об ошибке.

Давайте также проверим, что изображение имеет определенный размер. Пакет image-size позволяет очень просто получить размеры изображения. Вы можете использовать его асинхронно или синхронно. Мы будем использовать последний, чтобы все было просто:

const dimensions = sizeOf (req.file.path) ;

if ( (dimensions.width < 640) || (dimensions.height < 480)) {

return res.status (422).json ({

error: 'The image must be at least 640×480px’

}) ;

}

Давайте объединим все это в полное (мини) приложение:

const express = require ('express’) ;

const multer = require ('multer’) ;

const upload = multer ({ dest:'uploads/'}) ;

const sizeOf = require ('image-size’) ;

const exphbs = require ('express-handlebars’) ;

const app = express () ;

app.use (express.static (__dirname +'/public’));

app.engine ('.hbs’, exphbs ({ extname:'.hbs’}));

app.set ('view engine’,'.hbs’) ;

app.get ('/', (req, res) => {

return res.render ('index’, {layout: false}) ;

}) ;

app.post ('/upload’, upload.single ('file’), (req, res) => {

if (! req.file.mimetype.startsWith ('image/')) {

return res.status (422).json ({

error:'The uploaded file must be an image’

}) ;

}

const dimensions = sizeOf (req.file.path) ;

if ( (dimensions.width < 640) || (dimensions.height < 480)) {

return res.status (422).json ({

error:'The image must be at least 640×480px’

}) ;

}

return res.status (200).send (req.file) ;

}) ;

app.listen (8080, () => {

console.log ('Express server listening on port 8080') ;

}) ;

Примечание: для краткости этот серверный код не реализует защиту от CSRF. Возможно, вы захотите взглянуть на такой пакет, как CSURF.

Вы найдете этот код вместе с вспомогательными активами, такими как представление, в соответствующем репозитории.

А если вы хотите узнать больше о работе с формами в Node.js, прочтите «Формы, загрузка файлов и безопасность с Node.js и Express».

Резюме

DropzoneJS — это удобный, мощный и настраиваемый плагин JavaScript, который расширяет возможности управления загрузкой файлов и выполняет загрузку AJAX. В этом руководстве мы рассмотрели ряд доступных опций, событий и способы настройки плагина. Это намного больше, чем может быть описано в одном руководстве, поэтому посетите официальный сайт, если хотите узнать больше. Но, надеюсь, этого достаточно, чтобы вы начали.

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

3D-печать5GABC-анализAndroidAppleAppStoreAsusCall-центрChatGPTCRMDellDNSDrupalExcelFacebookFMCGGoogleHuaweiInstagramiPhoneLinkedInLinuxMagentoMicrosoftNvidiaOpenCartPlayStationPOS материалPPC-специалистRuTubeSamsungSEO-услугиSMMSnapchatSonyStarlinkTikTokTwitterUbuntuUp-saleViasatVPNWhatsAppWindowsWordPressXiaomiYouTubeZoomАвдеевкаАктивные продажиАкцияАлександровск ЛНРАлмазнаяАлчевскАмвросиевкаАнализ конкурентовАнализ продажАнтимерчандайзингАнтрацитАртемовскАртемовск ЛНРАссортиментная политикаБелгородБелицкоеБелозерскоеБердянскБизнес-идеи (стартапы)БрендБрянкаБукингВахрушевоВендорВидеоВикипедияВирусная рекламаВирусный маркетингВладивостокВнутренние продажиВнутренний маркетингВолгоградВолновахаВоронежГорловкаГорнякГорскоеДебальцевоДебиторкаДебиторская задолженностьДезинтермедитацияДзержинскДивизионная система управленияДизайнДимитровДирект-маркетингДисконтДистрибьюторДистрибьюцияДобропольеДокучаевскДоменДружковкаЕкатеринбургЕнакиевоЖдановкаЗапорожьеЗимогорьеЗолотоеЗоринскЗугрэсИжевскИловайскИрминоКазаньКалининградКировскКировскоеКомсомольскоеКонстантиновкаКонтент-маркетингКонтент-планКопирайтингКраматорскКрасноармейскКрасногоровкаКраснодарКраснодонКраснопартизанскКрасный ЛиманКрасный ЛучКременнаяКураховоКурскЛисичанскЛуганскЛутугиноМакеевкаМариупольМаркетингМаркетинговая информацияМаркетинговые исследованияМаркетинговый каналМаркетинг услугМаркетологМарьинкаМедиаМелекиноМелитопольМенеджментМерчандайзерМерчандайзингМиусинскМолодогвардейскМоскваМоспиноНижний НовгородНиколаевНиколаевкаНишевой маркетингНовоазовскНовогродовкаНоводружескНовосибирскНумерическая дистрибьюцияОдессаОмскОтдел маркетингаПартизанский маркетингПервомайскПеревальскПетровскоеПлата за кликПоисковая оптимизацияПопаснаяПравило ПаретоПривольеПрогнозирование продажПродвижение сайтов в ДонецкеПроизводство видеоПромоПромоушнПрямой маркетингРабота для маркетологаРабота для студентаРазработка приложенийРаспродажаРегиональные продажиРекламаРеклама на асфальтеРемаркетингРетро-бонусРибейтРитейлРовенькиРодинскоеРостов-на-ДонуРубежноеСамараСанкт-ПетербургСаратовСватовоСвердловскСветлодарскСвятогорскСевастопольСеверодонецкСеверскСедовоСейлз промоушнСелидовоСимферопольСинергияСколковоСлавянскСнежноеСоздание сайтов в ДонецкеСоледарСоциальные сетиСочиСтаробельскСтаробешевоСтахановСтимулирование сбытаСуходольскСчастьеТелемаркетингТельмановоТираспольТорговый представительТорезТрейд маркетингТрейд промоушнТюменьУглегорскУгледарУкраинскХабаровскХарцызскХерсонХостингЦелевая аудиторияЦифровой маркетингЧасов ЯрЧелябинскШахтерскЮжно-СахалинскЮнокоммунаровскЯндексЯсиноватая