Шаблоны В Angularjs



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

Эта статья не преследует цели познакомить читателей с проектированием или шаблонами проектирования.

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

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

СПА .



Введение

Статья начинается с краткого обзора фреймворка AngularJS. В обзоре объясняются основные компоненты AngularJS: директивы, фильтры, контроллеры, сервисы, область действия.

Во втором разделе перечислены и описаны различные конструкции и архитектурные шаблоны, реализованные в рамках платформы.

Шаблоны сгруппированы по компонентам AngularJS, в которых они используются.

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

Последний раздел включает несколько архитектурных шаблонов, которые обычно используются в SPA, построенных на AngularJS. Краткий обзор AngularJS

Краткий обзор AngularJS

AngularJS — это веб-фреймворк JavaScript, разработанный Google. Он намерен обеспечить прочную основу для развития.

CRUD СПА.

SPA загружается только один раз и не требует перезагрузки страницы при работе с ним.

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

Поскольку большинство CRUD-приложений имеют общие характеристики и требования, AngularJS намерен предоставить их оптимальный набор «из коробки».

Вот некоторые важные особенности AngularJS:

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

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



Частичные

Частички представляют собой HTML-строки.

Они могут содержать выражения AngularJS внутри элементов или их атрибутов.

Одним из преимуществ AngularJS перед другими платформами является то, что шаблоны AngularJS не имеют промежуточного формата, который необходимо конвертировать в HTML (например, mustache.js).

Сначала каждый SPA загружает файл Index.html. В случае AngularJS этот файл содержит набор стандартных (и не очень) HTML-атрибутов, элементов и комментариев, настраивающих и запускающих приложение.

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

Пример партиалов:

  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
   

<html ng-app> <!-- Body tag augmented with ngController directive --> <body ng-controller="MyController"> <input ng-model="foo" value="bar"> <!-- Button tag with ng-click directive, and string expression 'buttonText' wrapped in "{{ }}" markup --> <button ng-click="changeFoo()">{{buttonText}}</button> <script src="angular.js"></script> </body> </html>

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

В приведенном выше примере значение атрибута ng-click означает, что методchangeFoo() будет вызываться из текущей области.



Контроллеры

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

Все внешние зависимости для контроллеров предоставляются с использованием механизма DI в AngularJS. Контроллеры также отвечают за взаимодействие моделей с партиалами путем добавления данных в область видимости.

Это можно рассматривать как модель представления.



function MyController($scope) { $scope.buttonText = 'Click me to change foo!'; $scope.foo = 42; $scope.changeFoo = function () { $scope.foo += 1; alert('Foo changed'); }; }

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

  1. Изменение «foo» путем ввода данных в поле ввода.

    Это немедленно отразит значение «foo» из-за двусторонней привязки данных.

  2. Изменение значения «foo» нажатием кнопки «Нажмите меня, чтобы изменить foo!»
Все пользовательские элементы, атрибуты, комментарии или классы могут быть директивами AngularJS (если они предопределены).



Объем

В AngularJS область видимости — это объект JavaScript, доступный для партиалов.

Область видимости может включать в себя различные свойства — примитивы, объекты или методы.

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

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

Еще одной важной характеристикой области действия в любом приложении AngularJS является то, что они связаны через механизм наследования прототипа (за исключением изолированных областей действия).

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

Наследование области показано в следующем примере:

<div ng-controller="BaseCtrl"> <div id="child" ng-controller="ChildCtrl"> <button id="parent-method" ng-click="foo()">Parent method</button> <button ng-click="bar()">Child method</button> </div> </div>



function BaseCtrl($scope) { $scope.foo = function () { alert('Base foo'); }; } function ChildCtrl($scope) { $scope.bar = function () { alert('Child bar'); }; }

Область действия ChildCtrl связана с div#child, но поскольку область действия ChildCtrl вложена в BaseCtrl, все методы из BaseCtrl доступны в ChildCtrl с использованием наследования прототипа, и поэтому метод foo будет доступен при нажатии кнопки #parent-method. .



Директивы

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

В своей простейшей форме директива имеет имя и определение функции postLink, которая включает в себя логику директивы.

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

  • шаблон
  • функция компиляции
  • функция ссылки
  • и т. д.
Директивы могут использоваться частично, например:

myModule.directive('alertButton', function () { return { template: '<button ng-transclude></button>', scope: { content: '@' }, replace: true, restrict: 'E', transclude: true, link: function (scope, el) { el.click(function () { alert(scope.content); }); } }; });



<alert-button content="42">Click me</alert-button>

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

Фильтры

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

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

Вот простой пример фильтра, который преобразует строку в верхний регистр:

myModule.filter('uppercase', function () { return function (str) { return (str || '').

toUpperCase(); }; });

Внутри партиалов фильтры можно использовать с использованием синтаксиса конвейеров Unix:

<div>{{ name | uppercase }}</div>

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

function MyCtrl(uppercaseFilter) { $scope.name = uppercaseFilter('foo'); //FOO }



Услуги

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

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



myModule.service('Developer', function () { this.name = 'Foo'; this.motherLanguage = 'JavaScript'; this.live = function () { while (true) { this.code(); } }; });

Сервисы можно добавлять к любому компоненту, поддерживающему DI (контроллеры, фильтры, директивы, другие сервисы):

function MyCtrl(developer) { var developer = new Developer(); developer.live(); }



Шаблоны AngularJS

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

Услуги



Синглтон

Синглтон — это шаблон проектирования, который ограничивает создание экземпляра класса одним объектом.

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

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

Диаграмма UML иллюстрирует шаблон Singleton:

Шаблоны в AngularJS

Когда какой-либо компонент требует зависимости, AngularJS разрешает ее, используя следующий алгоритм:

  • Берет имя зависимости и выполняет поиск в хеш-карте, определенной в лексическом замыкании.

    (вот почему он имеет частную область действия)

  • Если зависимость найдена, AngularJS передает ее как параметр компонента.

  • Если зависимость не найдена:
    • AngularJS создает его, вызывая фабричный метод или его поставщика (т. е.

      $get).

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

      Этот процесс может привести к циклической зависимости.

    • AngularJS кэширует его внутри хеш-карты, упомянутой выше.

    • AngularJS передает его в качестве параметра компоненту, для которого он указан.

Лучше взгляните на исходный код AngularJS, реализующий getService:

function getService(serviceName) { if (cache.hasOwnProperty(serviceName)) { if (cache[serviceName] === INSTANTIATING) { throw $injectorMinErr('cdep', 'Circular dependency found: {0}', path.join(' <- ')); } return cache[serviceName]; } else { try { path.unshift(serviceName); cache[serviceName] = INSTANTIATING; return cache[serviceName] = factory(serviceName); } catch (err) { if (cache[serviceName] === INSTANTIATING) { delete cache[serviceName]; } throw err; } finally { path.shift(); } } }

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

Кэш можно рассматривать как одноэлементный менеджер.

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

Таким образом, сервис на самом деле является синглтоном, но не реализуется через шаблон синглтона, что дает некоторые преимущества по сравнению со стандартной реализацией:

  • улучшает тестируемость кода
  • вы можете контролировать создание одноэлементных объектов (в этом случае МОК контейнер управляет созданием объектов с помощью ленивых вызовов.

    )

Более подробное обсуждение этой темы вы можете прочитать статья Миско Хевери в блоге Google Testing.

Фабричный метод

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

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

Другими словами, Фабрика делегирует создание объектов потомкам родительского класса.

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



Шаблоны в AngularJS

Давайте посмотрим на следующий фрагмент:

myModule.config(function ($provide) { $provide.provider('foo', function () { var baz = 42; return { //Factory method $get: function (bar) { var baz = bar.baz(); return { baz: baz }; } }; }); });

Здесь функция обратного вызова конфигурации используется для определения нового «провайдера».

«Поставщик» — это объект с методом $get. Поскольку в JavaScript нет интерфейсов и язык использует «утиную» типизацию, мы договорились вызывать фабричный метод в «провайдере» $get. У каждой службы, фильтра, директивы и контроллера есть поставщик (то есть объект, имеющий фабричный метод $get), который отвечает за создание экземпляров компонента.

Мы можем углубиться в реализацию AngularJS:

//.

createInternalInjector(instanceCache, function(servicename) { var provider = providerInjector.get(servicename + providerSuffix); return instanceInjector.invoke(provider.$get, provider, undefined, servicename); }, strictDi)); //.

function invoke(fn, self, locals, serviceName){ if (typeof locals === 'string') { serviceName = locals; locals = null; } var args = [], $inject = annotate(fn, strictDi, serviceName), length, i, key; for(i = 0, length = $inject.length; i < length; i++) { key = $inject[i]; if (typeof key !== 'string') { throw $injectorMinErr('itkn', 'Incorrect injection token! Expected service name as string, got {0}', key); } args.push(locals && locals.hasOwnProperty(key) ? locals[key] : getService(key)); } if (!fn.$inject) { // this means that we must be an array. fn = fn[length]; } return fn.apply(self, args); }

В этом примере вы можете увидеть, как на самом деле используется метод $get:

instanceInjector.invoke(provider.$get, provider, undefined, servicename)

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

Внутри метода вызова вызывается функция аннотации, которой в качестве первого аргумента также передается фабричный метод. Функция annotate разрешает все зависимости через механизм DI AngularJS (обсуждаемый выше).

После разрешения всех зависимостей вызывается фабричный метод:

fn.apply(self, args).



Если мы думаем с точки зрения описанной выше диаграммы UML, то мы можем вызвать Creator, который через фабричный метод вызовет «ConcreteCreator», который создаст «Продукт».

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

Таким образом, фреймворк влияет на макеты/шаблоны для создания новых компонентов, потому что:

  • это лучшее время для создания компонента
  • разрешить все зависимости компонентов
  • управлять количеством разрешенных экземпляров компонентов (один для службы и фильтра, но много для контроллера)


Декоратор

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

Паттерн «Декоратор» предоставляет гибкую альтернативу созданию подклассов для расширения функциональности.



Шаблоны в AngularJS

AngularJS «из коробки» предоставляет возможности расширения и/или улучшения функциональности существующих сервисов.

Используя декоратор или метод $provide, вы можете создать обертку для любого сервиса, уже определенного вами или из сторонней библиотеки:

myModule.controller('MainCtrl', function (foo) { foo.bar(); }); myModule.factory('foo', function () { return { bar: function () { console.log('I\'m bar'); }, baz: function () { console.log('I\'m baz'); } }; }); myModule.config(function ($provide) { $provide.decorator('foo', function ($delegate) { var barBackup = $delegate.bar; $delegate.bar = function () { console.log('Decorated'); barBackup.apply($delegate, arguments); }; return $delegate; }); });

В приведенном выше примере определяется новый сервис с именем «foo».

В функции обратного вызова «config» вызывается метод $provide.decorator и в качестве первого аргумента ему передается имя сервиса, который мы хотим декорировать; функция, которая фактически реализует декоратор, передается в качестве второго аргумента.

$delegate хранит ссылку на исходный сервис foo. Мы украшаем сервис, переопределив его метод bar. Фактически, декорирование — это просто расширение bar путем включения другого состояния console.log — console.log('Decorated') и последующего вызова исходного метода bar в соответствующем контексте.

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

В случаях, когда необходимы многочисленные декораторы (например, при измерении производительности методов, авторизации, регистрации и т. д.), вы можете получить много дублирующегося кода и нарушение принципа СУХОЙ .

В таких случаях целесообразно использовать аспектно-ориентированный программирование.

Фреймворк АОП для AngularJS можно найти по адресу github.com/mgechev/angular-aop.

Фасад

Шаблон «Фасад» — это шаблон структурного проектирования, который позволяет скрыть сложность системы, сводя все возможные внешние вызовы к одному объекту, который делегирует их соответствующим объектам системы.

Фасад может:

  1. Упростите использование, понимание и тестирование библиотек, поскольку у фасада есть более подходящие методы для выполнения общих задач.

  2. Сделайте библиотеку более читабельной по той же причине.

  3. Уменьшить зависимость внутренних библиотек от внешнего кода, так как большая часть кода использует фасад, это позволяет более гибко развивать систему.

  4. Оберните плохо спроектированную коллекцию API одним хорошо спроектированным (в соответствии с потребностями задачи)


Шаблоны в AngularJS

AngularJS имеет несколько фасадов.

Каждый раз, когда вы хотите предоставить высокоуровневый API для какой-либо функциональности, вы, по сути, создаете фасад. Например, давайте посмотрим, как мы можем создать POST-запрос XMLHttpRequest:

var http = new XMLHttpRequest(), url = '/example/new', params = encodeURIComponent(data); http.open("POST", url, true); http.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); http.setRequestHeader("Content-length", params.length); http.setRequestHeader("Connection", "close"); http.onreadystatechange = function () { if(http.readyState == 4 && http.status == 200) { alert(http.responseText); } } http.send(params);

Если мы хотим отправить данные с помощью сервиса AngularJS $http, мы можем:

$http({ method: 'POST', url: '/example/new', data: data }) .

then(function (response) { alert(response); });

или даже:

$http.post('/someUrl', data).

then(function (response) { alert(response); });

Второй вариант — это предварительно настроенная версия, которая создает запрос HTTP POST. Третий вариант — это абстракция высокого уровня, созданная с использованием службы $resource и построенная на основе службы $http. Мы еще раз рассмотрим этот сервис в разделах Active Record и Proxy.

Прокси

Прокси — это структурный шаблон проектирования, предоставляющий объект, который контролирует доступ к другому объекту путем перехвата всех вызовов (выполняет функцию контейнера).

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



Шаблоны в AngularJS

Мы можем выделить три типа прокси:

  • Виртуальный прокси
  • Удаленный прокси
  • Защитный прокси
В этом подразделе мы рассмотрим виртуальный прокси, реализованный в AngularJS. В приведенном ниже фрагменте мы вызываем метод get объекта $resource с именем User:

var User = $resource('/users/:id'), user = User.get({ id: 42 }); console.log(user); //{}

console.log выведет пустой объект. Так как AJAX-запрос будет выполняться асинхронно после вызова User.get, а во время вызова console.log у нас не будет пользовательских данных.

Сразу после вызова User.get выполняется GET-запрос, он возвращает пустой объект и сохраняет ссылку на него.

Мы можем думать об этом объекте как о виртуальном прокси, он будет заполнен данными, как только клиент получит ответ от сервера.

Как это работает в AngularJS? Давайте посмотрим на следующий фрагмент:

function MainCtrl($scope, $resource) { var User = $resource('/users/:id'), $scope.user = User.get({ id: 42 }); }



<span ng-bind="user.name"></span>

После выполнения приведенного выше фрагмента кода свойство пользователя объекта $scope станет пустым объектом ({}), что означает, что user.name будет неопределенным и не будет отображаться.

После того, как сервер вернет ответ на запрос GET, AngularJS заполнит этот объект данными, полученными от сервера.

Во время следующего цикла $digest AngularJS обнаружит изменения в $scope.user, и это приведет к обновлению представления.



Активная запись

Active Record — это объект, хранящий данные и поведение.

Обычно большая часть данных в этом объекте является постоянной.

Объект Active Record отвечает за связь с базой данных для создания, обновления, поиска и удаления данных.

Он может делегировать эту ответственность объектам более низкого уровня, но вызов объекта Active Record или его статических методов приведет к взаимодействию с базой данных.



Шаблоны в AngularJS

AngularJS определяет ресурс $resource. В текущей версии AngularJS (1.2+) он распространяется как отдельный модуль и не входит в ядро.

Согласно документации $resource: $resource — это фабрика для создания объектов $resource, которые позволяют взаимодействовать с источниками данных на стороне сервера RESTful. Объект $resource имеет методы, обеспечивающие высокоуровневое поведение без необходимости взаимодействия с низкоуровневой службой $http. Вот как можно использовать $resource:

var User = $resource('/users/:id'), user = new User({ name: 'foo', age : 42 }); user.$save();

Вызов $resource создает конструктор для экземпляров нашей модели.

Каждый экземпляр модели будет иметь методы, которые можно использовать для различных операций CRUD. Таким образом, мы можем использовать функцию-конструктор и ее статические методы:

User.get({ userid: userid });

Приведенный выше код немедленно вернет пустой объект и сохранит ссылку на него.

После получения и анализа ответа AngularJS заполнит объект полученными данными (см.

прокси).

Вы можете найти более подробную документацию о магии объекта $resource и AngularJS. Так, Мартин Фаулер утверждает, что: Объект Active Record должен позаботиться о связи с базой данных, чтобы создать.

$resource не реализует полный шаблон Active Record, поскольку он взаимодействует со службой RESTful, а не с базой данных.

Во всяком случае, мы можем думать об этом как об «Активной записи для взаимодействия RESTful».



Перехватывающие фильтры

Создает цепочку фильтров для выполнения простых задач обработки запросов до и после.



Шаблоны в AngularJS

В некоторых случаях вам потребуется выполнить некоторую пред-/постобработку HTTP-запросов.

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

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

Выход каждого фильтра является входом для следующего.

В AngularJS есть идея перехвата фильтров в $httpProvider. $httpProvider имеет свойство перехватчиков, которое включает список объектов.

У каждого объекта есть свойства: запрос, ответ, запросError, ответError. RequestError возникает, когда предыдущий перехватчик не удался или был отклонен, а responseError возникает, когда предыдущий перехватчик ответа вызвал исключение.

Ниже приведен базовый пример того, как можно добавить перехватчики с помощью литерала объекта:

$httpProvider.interceptors.push(function($q, dependency1, dependency2) { return { 'request': function(config) { // same as above }, 'response': function(response) { // same as above } }; });



Директивы



Композитный

Составной шаблон представляет собой структурный шаблон проектирования.

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

Целью композиции является упорядочение объектов в древовидную структуру для представления иерархии от частного к целому.



Шаблоны в AngularJS

По мнению «Банды четырех», MVC — это не что иное, как комбинация:

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

Аналогичная ситуация и в AngularJS. Представления формируются путем композиции директив и элементов DOM, на которых эти директивы построены.

Давайте посмотрим на следующий пример:

<!doctype html> <html> <head> </head> <body> <zippy title="Живой"> Zippy! </zippy> </body> </html>



myModule.directive('zippy', function () { return { restrict: 'E', template: '<div><div class="header"></div><div class="content" ng-transclude></div></div>', link: function (scope, el) { el.find('.

header').

click(function () { el.find('.

content').

toggle(); }); } } });

В этом примере создается директива, которая является компонентом пользовательского интерфейса.

Созданный компонент (с именем «zippy») имеет заголовок и содержимое.

Нажатие на его заголовок переключает видимость его содержимого.

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

Корневым компонентом является html, за которым сразу следуют подэлементы head, body и т. д. Во втором примере мы видим, что свойство директивы шаблона содержит разметку с директивой ng-transclude. Это означает, что внутри директивы «zippy» может быть еще одна директива ng-transclude, т. е.

композиция директив.

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



Устный переводчик

Интерпретатор — это поведенческий шаблон проектирования, который определяет, как определять выражения на языке.

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

Для анализа (интерпретации) используется синтаксическое дерево (пример шаблона композиции) выражений.

Теги: #angularjs #шаблоны #шаблоны #angular

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.