Книга: Angular и TypeScript. Сайтостроение для профессионалов
Назад: 3. Навигация с помощью маршрутизатора Angular
Дальше: 5. Привязки, наблюдаемые объекты и каналы

4. Внедрение зависимостей

В этой главе:

знакомство с шаблоном проектирования «Внедрение зависимостей» (Depen­dency Injection, DI);

• преимущества DI;

• реализация DI в Angular;

• регистрация поставщиков объектов и использование инъекторов;

• иерархия инъекторов;

• применение DI в приложении онлайн-аукциона.

В предыдущей главе мы рассмотрели маршрутизатор, и теперь ваше приложение-аукцион знает, как перейти от представления Home (Главная страница) к почти пустому представлению Product Details (Информация о продукте). В данной главе вы продолжите работу над онлайн-аукционом, но в этот раз сконцентрируете усилия на автоматизации процесса создания объектов и сборки приложения из составных частей.

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

Мы начнем эту главу с того, что определим, какие проблемы решает DI, и рассмотрим преимущества DI как шаблона проектирования. Далее рассмотрим, как данный шаблон реализуется в Angular на примере компонента ProductComponent, который зависит от ProductService. Вы увидите, как написать внедряемый сервис и внедрить его в другой элемент.

Далее мы представим пример приложения, демонстрирующий, как Angular DI позволяет с легкостью заменять одну зависимость другой, изменяя всего одну строку кода. После этого познакомимся с более продвинутой концепцией: иерархией инъекторов.

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

4.1. Шаблоны «Внедрение зависимостей» и «Инверсия управления»

Шаблоны проектирования — рекомендации по решению некоторых распространенных задач. Заданный шаблон проектирования может быть реализован разными способами в зависимости от используемого ПО. В этом разделе мы кратко рассмотрим два шаблона проектирования: «Внедрение зависимостей» (Dependency Injection, DI) и «Инверсия управления» (Inversion of Control, IoC).

4.1.1. Шаблон «Внедрение зависимостей»

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

var product = new Product();

createShipment(product);

Функция createShipment() зависит от существования экземпляра класса Product. Другими словами, функция createShipment() имеет зависимость: Product. Но сама по себе она не знает, как создавать объекты такого типа. Вызывающий сценарий должен каким-то образом создавать и передавать (то есть внедрять) этот объект как аргумент функции. Технически, вы отвязываете место создания объекта Product от места его использования — но обе предыдущие строки кода находятся в одном сценарии, поэтому данное отвязывание нельзя назвать настоящим. При необходимости заменить тип Product на тип MockProduct понадобится внести небольшое изменение в наш простой пример.

Что если функция createShipment() имеет три зависимости (продукт, компанию-отправителя, фулфилмент-центр), и каждая из них имеет собственные зависимости? В таком случае для создания разного набора объектов для функции createShipment() потребуется внести большое количество изменений. Можно ли попросить кого-то создать экземпляры зависимостей (и их зависимостей) вместо вас?

Здесь и нужен шаблон «Внедрение зависимостей»: если объект А зависит от объекта типа Б, то объект А не будет явно создавать объект Б (в случае использования оператора new, как в предыдущем примере). Вместо этого объект Б будет внедрен из операционной среды. Объект А просто должен объявить следующее: «Мне нужен объект типа Б; может ли кто-то его мне передать?» Слово «типа» здесь самое важное. Объект А не запрашивает конкретную реализацию объекта, и его запрос будет удовлетворен, если внедряемый объект имеет тип Б.

4.1.2. Шаблон «Инверсия управления»

Шаблон «Инверсия управления» является более общим, нежели DI. Вместо того чтобы использовать в своем приложении API фреймворка (или программного контейнера), фреймворк создает и отправляет объекты, необходимые приложению. Шаблон IoC может быть реализован разными способами, а DI — это один из способов предоставления требуемых объектов. Angular играет роль контейнера IoC и может предоставлять требуемые объекты в соответствии с объявлениями, сделанными в вашем компоненте.

4.1.3. Преимущества внедрения зависимости

Прежде чем исследовать синтаксис реализации шаблона DI в Angular, рассмотрим преимущества внедрения объектов перед созданием их с помощью оператора new. Angular предлагает механизм, помогающий регистрировать и создавать экземпляры компонентов, для которых имеется зависимость. Короче говоря, DI позволяет писать слабо связанный код, а также применять его повторно и проводить более качественное тестирование.

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

Предположим, у вас имеется компонент ProductComponent, который получает подробную информацию о продукте с помощью класса ProductService.

Если вы не используете DI, то компонент ProductComponent должен знать, как создавать объекты класса ProductService. Это можно сделать несколькими способами, например, задействовать оператор new, вызвать метод getInstance() для объекта-синглтона или вызвать метод createProductService() какого-нибудь класса-фабрики. В любом из описанных случаев компонент ProductComponent становится тесно (жестко) связанным с классом ProductService.

Если вам нужно использовать компонент ProductComponent в другом приложении, которое применяет другой сервис для получения подробной информации о продукте, то вы должны модифицировать код (например, так: productService = new AnotherProductService()). DI позволяет отвязать компоненты приложения, избавив их от необходимости знать, как создавать зависимость.

Рассмотрим следующий пример компонента ProductComponent:

@Component({

  providers: [ProductService]

})

class ProductComponent {

  product: Product;

  constructor(productService: ProductService) {

    this.product = productService.getProduct();

  }

}

В приложениях вы регистрируете объекты для DI, указывая поставщики. Поставщик — это инструкция для Angular о том, как создать экземпляр объекта для последующего внедрения в целевой компонент или директиву. В предыдущем фрагменте кода строка providers:[ProductService] является сокращением строки providers:[{provide:ProductService, useClass:ProductService}].

примечание

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

Angular применяет концепцию токенов — произвольных имен, представляющих внедряемый объект. Обычно имя токена соответствует типу внедряемого объекта, соответственно, в предыдущем фрагменте кода Angular получает указание предоставить токен ProductService, задействуя класс с тем же именем. Использование объекта со свойством provide позволяет соотнести один токен с разными значениями или объектами (например, эмулировать функциональность класса ProductService, когда кто-то разрабатывает реальный класс сервиса).

примечание

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

Теперь, когда вы добавили свойство providers в аннотацию @Component компонента ProductComponent, модуль DI, предоставляемый Angular, будет знать, что он должен создать объект типа ProductService. Компонент ProductComponent не должен знать, какую именно реализацию типа ProductService будет использовать — он применит любой объект, указанный как поставщик. Ссылка на объект типа ProductService будет внедрена с помощью аргумента конструктора, нет необходимости явно создавать объект типа ProductService в компоненте ProductComponent. Просто задействуйте его как предыдущий код, который вызывает метод сервиса getProduct() экземпляра класса ProductService, созданного Angular.

Если нужно использовать один и тот же компонент ProductComponent в разных приложениях, имеющих разную реализацию типа ProductService, то измените строку providers, как показано в следующем примере:

providers: [{provide: ProductService, useClass: AnotherProductService}]

Теперь Angular будет создавать экземпляр класса AnotherProductService, но код, задействующий тип ProductService, не будет генерировать ошибки. В этом примере использование DI увеличивает возможность повторного применения компонента ProductComponent и разрушает тесное связывание с классом ProductService. Если один объект тесно связан с другим, то для использования хотя бы одного из них может потребоваться внести много изменений.

Тестируемость

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

Предположим, вам нужно добавить в приложение возможность авторизации. Можно создать компонент LoginComponent (для отрисовки полей ID и password), использующий компонент LoginService, который должен соединяться с определенным сервером авторизации и проверять привилегии пользователя. Сервер авторизации должен быть предоставлен другим отделом, но он еще не готов. Вы завершаете написание кода компонента LoginComponent, но затрудняетесь протестировать его по причинам, которые не можете контролировать, например, из-за зависимости или другого компонента, разрабатываемого другими людьми.

При тестировании часто применяются фальшивые объекты, имитирующие поведение реальных. В случае использования фреймворка DI можно создать фальшивый объект, MockLoginService, который не соединяется с сервером авторизации, но при этом в нем жестко закодированы привилегии, присвоенные пользователям, имеющим определенные комбинации идентификатора и пароля. Задействуя DI, можно написать всего одну строку, в которой MockLoginService будет внедрен в представление Login (Авторизация) приложения, что позволит не ждать готовности сервера авторизации. Далее, когда сервер будет готов, можно изменить строку providers так, чтобы Angular внедрил реальный компонент LoginService, как показано на рис. 4.1.

73176.png 

Рис. 4.1. DI при тестировании

примечание

В разделе «Практикум» главы 9 вы увидите, как выполнять модульное тестирование внедряемых сервисов.

4.2. Инъекторы и поставщики

Теперь, когда вы кратко ознакомились с шаблоном «Внедрение зависимости», перейдем к деталям реализации DI в Angular. В частности, мы рассмотрим такие концепции, как инъекторы и поставщики.

Каждый компонент может иметь объект Injector, который может внедрять объекты и примитивные значения в элемент или сервис. В любом приложении Angular имеется корневой инъектор, доступный всем его модулям. Чтобы указать инъектору, что именно внедрить, вы указываете поставщик. Инъектор внедрит объект или значение, указанное в поставщике, в конструктор компонента.

ПРИМЕЧАНИЕ

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

Поставщики позволяют соотносить пользовательские типы (или токены) с конкретными реализациями этого типа (или значениями). Можно указать поставщики либо внутри декоратора компонента @Component, либо как свойство @NgModule, что было сделано в каждом фрагменте кода, представленного до настоящего момента.

СОВЕТ

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

Вы будете использовать компонент ProductComponent и класс ProductService во всех примерах кода, показанных в этой главе. Если ваше приложение имеет класс, реализующий определенный тип (например, ProductService), то вы можете указать объект поставщика для данного класса во время предварительной загрузки модуля AppModule, например, так:

@NgModule({

  ...

  providers: [{provide:ProductService,useClass:ProductService}]

})

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

@NgModule({

  ...

  providers: [ProductService]

})

Можно указать свойство providers в аннотации @Component. Короткая нотация поставщика ProductService в @Component выглядит так:

providers:[ProductService]

Ни один экземпляр типа ProductService еще не был создан. Строка providers указывает инъектору следующее: «Когда нужно создать объект, имеющий аргумент типа ProductService, создайте экземпляр зарегистрированного класса для внедрения в этот объект».

примечание

Angular также имеет свойство viewProviders, которое задействуется, когда вы не хотите, чтобы компоненты-потомки применяли поставщики, объявленные предками. Вы увидите пример использования viewProviders в разделе 4.5.

Если нужно внедрить разные реализации определенного типа, примените более длинную нотацию:

@NgModule({

  ...

  providers: [{provide:ProductService,useClass:MockProductService}]

})

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

@Component({

  ...

  providers: [{provide:ProductService, useClass:MockProductService}]

})

Она дает инъектору следующую инструкцию: «Когда нужно внедрить объект типа ProductService в компонент, создайте экземпляр класса MockProductService».

Благодаря поставщику инъектор знает, что внедрять; теперь нужно указать, куда внедрять объект. В TypeScript все сводится к объявлению аргумента конструктора с указанием его типа. Следующая строка показывает, как внедрить объект типа ProductService в конструктор компонента:

constructor(productService: ProductService)

Внедрение: TypeScript против ES6

В TypeScript упрощен синтаксис внедрения в компонент, поскольку этот язык не требует использования аннотаций DI для аргументов конструкторов. Все, что нужно сделать — указать тип аргумента конструктора:

constructor(productService: ProductService)

Этот код работает, потому что любой компонент имеет аннотацию @Component. И, поскольку компилятор TypeScript сконфигурирован с настройкой "emitDecoratorMe­tadata": true, Angular автоматически сгенерирует все метаданные, необходимые для того, чтобы внедрить объект.

Так как вы используете SystemJS для динамической компиляции в код TypeScript, можете добавить следующую настройку компилятора TypeScript в файле systemjs.config.js:

typescriptOptions: {

    "emitDecoratorMetadata": true

}

Если вы пишете этот класс в ES6, то добавьте аннотацию @Inject и явный тип в аргументы конструктора:

constructor(@Inject(ProductService) productService)

Конструктор останется таким же независимо от того, какая конкретная реализация класса ProductService будет указана в качестве поставщика. На рис. 4.2 показана примерная схема последовательности процесса внедрения.

73189.png 

Рис. 4.2. Процесс внедрения

4.2.1. Как объявлять поставщики

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

Рассмотрим пример массива с одним элементом, в котором указан объект поставщика для токена ProductService:

[{provide:ProductService, useClass:MockProductService}]

Свойство provide позволяет соотнести токен с методом, создающим внедряемый объект. В этом примере вы даете Angular указание создать объект класса MockProductService там, где в качестве зависимости используется токен ProductService. Но создатель объекта (инъектор Angular) может применять класс, функцию фабрики, строку или специальный класс OpaqueToken для создания объекта или его внедрения.

Чтобы соотнести токен и реализацию класса, используйте объект, имеющий свойство useClass, как показано в предыдущем примере.

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

• Чтобы предоставить строку с простым внедряемым значением (например, URL сервиса), обратитесь к объекту со свойством useValue.

В следующем разделе вы будете применять свойство useClass, а также изучите простое приложение. В разделе 4.4 будет показано использование свойств useFactory и useValue.

4.3. Пример приложения, задействующего Angular DI

Теперь, когда вы увидели несколько фрагментов кода, связанных с Angular DI, создадим небольшое приложение, которое объединяет все фрагменты воедино. Мы хотим подготовить вас к использованию DI в приложении для онлайн-аукциона.

4.3.1. Внедрение сервиса продукта

Создадим простое приложение, применяющее компонент ProductComponent для отрисовки информации о продукте и сервис ProductService, который предоставляет данные о продукте. Если вы используете загружаемый код, поставляемый с книгой, то данное приложение находится в файле main-basic.ts в каталоге di_samples. В этом подразделе вы создадите приложение, генерирующее страницу, показанную на рис. 4.3.

ch04_03.tif 

Рис. 4.3. Пример приложения, использующего DI

 

Компонент ProductComponent может запросить внедрение объекта Pro­ductService путем объявления аргумента конструктора с типом:

constructor(productService: ProductService)

На рис. 4.4 показан пример приложения, которое использует эти компоненты.

73207.png 

Рис. 4.4. Внедрение ProductService в ProductComponent

Модуль AppModule предварительно загружает AppComponent, содержащий компонент, зависящий от ProductService. Обратите внимание на операторы импорта и экспорта. Определение ProductService начинается с оператора экспорта, что позволяет другим элементам получить доступ к его содержимому. Компонент ProductComponent содержит оператор импорта, который предоставляет имя класса (ProductService) и импортируемого модуля (располагается в файле product-service.ts).

Атрибут providers, определенный на уровне компонента, указывает Angular предоставить экземпляр класса ProductService по требованию. Данный класс может общаться с каким-нибудь сервером, запрашивая подробную информацию о продукте, выбранном на веб-странице. Но мы сейчас опустим эту часть и сконцентрируемся на том, как указанный сервис можно внедрить в ProductComponent. Реализуем компоненты, показанные на рис. 4.4.

Помимо index.html вы создадите следующие файлы:

файл main-basic.ts будет содержать код, необходимый для загрузки модуля AppModule, который содержит компонент AppComponent, размещающий ProductComponent;

• компонент ProductComponent будет реализован в файле product.ts;

• сервис ProductService будет реализован в файле product-service.ts.

Каждый из этих файлов довольно прост. Файл main-basic.ts, показанный в листинге 4.1, содержит код модуля и корневой компонент, который размещает компонент-потомок ProductComponent. Этот модуль импортирует и объявляет данный компонент.

Листинг 4.1. Содержимое файла main-basic.ts

import {Component} from '@angular/core';

import ProductComponent from './components/product';

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { NgModule }      from '@angular/core';

import { BrowserModule } from '@angular/platform-browser';

 

@Component({

    selector: 'app',

    template: `<h1> Basic Dependency Injection Sample</h1>

            <di-product-page></di-product-page>`

})

class AppComponent {}

 

@NgModule({

    imports:      [ BrowserModule],

    declarations: [ AppComponent, ProductComponent],

    bootstrap:    [ AppComponent ]

})

class AppModule { }

platformBrowserDynamic().bootstrapModule(AppModule);

Основываясь на теге <di-product-page>, легко догадаться, что существует элемент с селектором, имеющим это значение. Данный селектор объявлен в компоненте ProductComponent, чья зависимость (ProductService) внедрена с помощью конструктора (листинг 4.2).

Листинг 4.2. Содержимое файла product.ts

73217.png 

В листинге 4.2 имя типа совпадает с именем класса — ProductService, так что можно использовать короткую нотацию без необходимости явно соотносить свойства provide и useClass. При указании поставщиков имя (токен) внедряемого объекта отделяется от его реализации. В этом случае имя токена будет таким же, как и имя типа: ProductService. Сама реализация данного сервиса может находиться в классах ProductService, OtherProductService или где-то еще. Замена одной реализации на другую сводится к изменению строки providers.

Конструктор компонента ProductComponent вызывает метод getProduct() для сервиса и размещает ссылку на возвращенный объект типа Product в переменной класса продукта, которая применяется в шаблоне HTML. Используя двойные фигурные скобки, в листинге 4.2 можно связать свойства title, description и price класса Product.

Файл product-service.ts содержит определение двух классов: Product и ProductService (листинг 4.3).

Листинг 4.3. Содержимое файла product-service.ts

73225.png 

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

Чтобы запустить этот пример, откройте командную строку в каталоге проекта и выполните команду npm start. Live-сервер откроет окно, как было показано ранее на рис. 4.3. Экземпляр класса ProductService внедрен в компонент ProductComponent, который отрисовывает информацию о продукте, предоставленную сервером.

В следующем разделе вы увидите сервис ProductService, декорированный аннотацией @Injectable. Его можно использовать для генерации метаданных для DI, если сервис сам по себе имеет зависимости. Аннотация @Injectable здесь не нужна, поскольку в данный сервис не внедрены другие сервисы, и Angular не нужны дополнительные метаданные для его внедрения в свои компоненты.

4.3.2. Внедрение Http-сервиса

Зачастую сервису нужно сделать HTTP-запрос, чтобы получить необходимые данные. Компонент ProductComponent зависит от сервиса ProductService, внедренного с помощью механизма Angular DI. Если данному сервису нужно сделать HTTP-запрос, то он будет иметь в качестве зависимости объект Http. Для внедрения объекта Http сервису ProductService нужно будет его импортировать; аннотация @NgModule должна импортировать модуль HttpModule, в котором определены поставщики Http. Класс ProductService должен иметь конструктор для внедрения объекта Http. На рис. 4.5 показано, что компонент ProductComponent зависит от сервиса ProductService, который имеет собственную зависимость — Http.

73236.png 

Рис. 4.5. Зависимость может иметь собственную зависимость

В следующем фрагменте кода показано внедрение объекта Http в сервис ProductService, а также получение продуктов из файла products.json:

import {Http} from '@angular/';

import {Injectable} from "@angular/core";

 

@Injectable()

export class ProductService {

    constructor(private ){

      let products = ');

    }

    // здесь будет располагаться другой код приложения

}

Точкой внедрения будет являться конструктор класса, но где же объявить поставщика для внедрения объекта типа Http? Все поставщики должны внедрять разнообразные объекты Http, объявленные в модуле HttpModule. Нужно лишь добавить его в модуль AppModule, например, так:

import { HttpModule} from '@angular/';

...

@NgModule({

  imports: [

    BrowserModule,

    HttpModule

  ],

  declarations: [ AppComponent ],

  bootstrap: [ AppComponent ]

})

export class AppModule { }

примечание

В подразделе 8.3.4 вы напишете приложение, в котором будет проиллюстрирована архитектура, показанная на рис. 4.5.

Теперь, когда вы увидели способы внедрения объекта в компонент, взглянем на то, как заменить одну реализацию сервиса другой с помощью Angular DI.

4.4. Переключение внедряемых объектов — это просто

Ранее в данной главе мы указали, что шаблон DI позволяет отвязать компоненты от их зависимостей. В предыдущем разделе вы отвязали компонент ProductComponent от сервиса ProductService. Теперь симулируем другой сценарий.

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

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

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

На рис. 4.6 показано, как несколько приложений _injectors отрисовывают компоненты продукта в браузере.

ch04_06.tif 

Рис. 4.6. Отрисовка двух продуктов

Продукт iPhone 7 отрисовывается компонентом Product1Component, а продукт Samsung 7 — Product2Component. Данное приложение концентрируется на переключении сервисов продуктов с помощью Angular DI, поэтому сами сервисы мы сделали простыми. По той же причине весь код TypeScript находится в одном файле — main.ts.

Класс, играющий роль интерфейса

В приложении Б мы объясним интерфейсы TypeScript, которые представляют собой способ гарантировать: объект, переданный функции, корректен; или класс, реализующий интерфейс, следует объявленному контракту. Класс может реализовывать интерфейс с помощью ключевого слова implements, но в TypeScript все классы могут быть использованы как интерфейсы (однако мы не поощряем применение данной функциональности), поэтому ClassA может реализовывать ClassB. Даже если код изначально был написан без интерфейсов, вы все еще можете задействовать конкретный класс в качестве интерфейса.

Содержимое файла main.ts показано в листинге 4.4. Мы хотели бы обратить ваше внимание на следующую строку:

class MockProductService implements ProductService

В ней показан один класс, «реализующий» другой, как если бы последний был объявлен в качестве интерфейса.

Листинг 4.4. Содержимое файла main.ts

73256.png 

76307.png 

Если для компонента не нужна конкретная реализация ProductService, то не нужно явно объявлять поставщик для каждого элемента до тех пор, пока поставщик объявлен на уровне предка. В листинге 4.4 для компонента Product1Component не объявлены собственные поставщики, поэтому Angular найдет поставщик на уровне приложения. Но каждый элемент может переопределять объявления поставщиков, сделанные на уровне приложения или родительского компонента, как в случае с Product2Component.

Сервис ProductService становится общим токеном, который понимают оба компонента. В Product2Component явно объявляется поставщик, соотносящий MockProductService с общим пользовательским типом ProductService. Этот поставщик уровня компонента переопределит родительский поставщик. Если вы решите, что Product1Component также должен использовать MockProductService, то можете добавить строку providers в аннотацию @Component, как сделано в Product2Component.

При запуске данного приложения в браузере будут отрисованы компоненты продукта, как было показано ранее на рис. 4.6. Это все хорошо, но предположим: другая команда сказала вам, что класс ProductService (использованный в качестве поставщика на уровне приложения) не будет доступен некоторое время. Как переключиться на применение исключительно класса MockProductService в данный временной промежуток?

Для этого нужно изменить одну строку — providers в объявлении модуля:

@NgModule({

...

providers: [{provide:ProductService, useClass:MockProductService}]

...

})

С этого момента, если понадобится внедрить тип ProductService и на уровне компонента не будет указано никаких поставщиков, Angular создаст и внедрит объект MockProductService. Запуск приложения после внесения вышеозначенных правок изменит отрисовку элементов так, как показано на рис. 4.7.

ch04_07.tif 

Рис. 4.7. Отрисовка двух продуктов с помощью MockProductService

Представьте, что ваше приложение имеет десяток компонентов, использующих сервис ProductService. Если каждый из них будет создавать этот сервис с помощью оператора new или класса-фабрики, то вам придется внести десятки изменений в код. Механизм Angular DI поможет исправить применяемый сервис, изменив одну строку в объявлении поставщиков.

Поднятие и классы JavaScript

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

При объявлении нескольких классов в одном файле сервисы ProductService и MockPro­ductService должны быть объявлены до того, как будут объявлены компоненты, которые их используют. Если вы столкнетесь с ситуацией, когда объекты объявляются после точки внедрения, то рассмотрите возможность использовать функцию forwardRef() с аннотацией @Inject (см. документацию для Angular по функции forwardRef() на ).

4.4.1. Объявление поставщиков с помощью useFactory и useValue

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

В листинге 4.5, взятом из файла main-factory.ts, показано, как можно указать в качестве поставщика функцию-фабрику. Она создает объекты сервиса — ProductService либо MockProductService, — основываясь на булевом флаге.

Листинг 4.5. Указание функции-фабрики в качестве поставщика

const IS_DEV_ENVIRONMENT: boolean = true;

 

@Component({

  selector: 'product2',

  providers:[{

    provide: ProductService,

    useFactory: (isDev) => {

      if (isDev){

        return new MockProductService();

      } else{

        return new ProductService();

      }

    },

    deps: ["IS_DEV_ENVIRONMENT"]}],

  template: '{{product.title}}'

})

class Product2Component {

  product: Product;

  constructor(productService: ProductService) {

    this.product = productService.getProduct();

  }

}

Сначала вы определяете токен с произвольным именем (в нашем случае IS_DEV_ENVIRONMENT) и устанавливаете его значение равным true для извещения программы о том, что работаете в среде разработки (то есть хотите работать с фальшивым сервисом создания продуктов). Фабрика использует анонимное выражение, которое создаст объект класса MockProductService.

Конструктор компонента Product2Component имеет аргумент типа ProductSer­vice, куда будет внедрен сервис. Вы можете использовать такую фабрику и для Product1Component; изменение значения IS_DEV_ENVIRONMENT на false внедрит сервис ProductService в оба элемента.

В листинге 4.5 показано не лучшее решение по переключению сред: в нем вы обращаетесь к флагу IS_DEV_ENVIRONMENT, который объявлен за пределами компонента; это нарушает его инкапсуляцию. Вы хотите, чтобы компонент был самостоятельным, так что попробуем внедрить в него значение переменной IS_DEV_ENVIRONMENT; в данном случае вам не понадобится обращаться ко внешнему коду.

Чтобы внедрить значение, недостаточно просто объявить константу (или переменную). Нужно зарегистрировать значение IS_DEV_ENVIRONMENT в инъекторе, используя provide с useValue; это позволит использовать его как внедряемый параметр в анонимное выражение из листинга 4.5.

примечание

Свойства useFactory и useValue находятся в Angular Core. Свойство useValue представляет собой особый случай useFactory, когда фабрика представлена одним выражением и не нуждается в других зависимостях.

Для легкого переключения между средой разработки и другими средами можно указать поставщик значения для среды на корневом уровне компонента, как показано в листинге 4.6; благодаря этому фабрика сервисов будет знать, какой сервис ей создавать. Значение свойства useFactory — функция с двумя аргументами: сама функция-фабрика и ее зависимости (deps).

примечание

В листинге 4.6 и множестве других примеров кода из этой книги используются анонимные стрелочные функции (они описаны в приложении A). По сути, анонимная стрелочная функция — более короткая нотация для анонимных функций. Например, конструкция (isDev) => {…} эквивалентна вызову function(isDev) {…}.

Листинг 4.6. Указание поставщика значения среды

73282.png 

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

Напомним: поставщик соотносит токен с классом или фабрикой, чтобы известить инъектора, как именно создавать объекты. Класс или фабрика могут иметь собственные зависимости, поэтому поставщики должны указать их все. На рис. 4.8 показаны отношения между поставщиками и инъектором из листинга 4.6.

73298.png 

Рис. 4.8. Привязка фабрики к зависимостям

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

4.4.2. Использование OpaqueToken

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

Представьте, что вам нужно создать компонент, который может получать данные от разных серверов (например, dev, prod и QA). В листинге 4.7 показано, как ввести в код внедряемое значение, BackendUrl, — экземпляр класса OpaqueToken, а не строку.

Листинг 4.7. Применение OpaqueToken вместо строки

import {Component, OpaqueToken, Inject, NgModule} from '@angular/core';

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { BrowserModule } from '@angular/platform-browser';

export const BackendUrl = new OpaqueToken('BackendUrl');

 

@Component({

  selector: 'app',

  template: 'URL: {{url}}'

})

class AppComponent {

  constructor(@Inject(BackendUrl) public url: string) {}

}

 

@NgModule({

  imports:      [ BrowserModule],

  declarations: [ AppComponent],

  providers: [  {provide:BackendUrl, useValue: 'myQAserver.com'}],

  bootstrap:    [ AppComponent ]

})

class AppModule { }

platformBrowserDynamic().bootstrapModule(AppModule);

Вы оборачиваете строку BackendUrl в экземпляр класса OpaqueToken. Далее в конструкторе этого компонента вместо того, чтобы внедрять значение vague типа string, внедряете конкретный тип BACKEND_URL со значением, предоставленным при объявлении модуля.

4.5. Иерархия инъекторов

Любое Angular-приложение представляет собой дерево вложенных компонентов. Когда веб-страница загружается, Angular создает объект приложения, который имеет свой инъектор. Он, в свою очередь, создает иерархию элементов, имеющих соответственные инъекторы согласно структуре приложения. Например, вам может понадобиться выполнить определенную функцию в момент инициализации приложения:

{provide:APP_INITIALIZER, useValue: myappInit}

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

Рассмотрим следующий документ HTML, содержащий корневой компонент, представленный тегом <app>:

<html>

  <body>

    <app></app>

  </body>

</html>

Из следующего кода видно, что app является селектором компонента AppComponent, который выступает предком для компонентов <product1> и <product2>:

@Component({

  selector: 'app',

  template: `

    <product1></product1>

    <product2></product2>

  `

})

class AppComponent {}

Инъектор предка создает инъекторы для каждого потомка, поэтому есть иерархия компонентов и иерархия инъекторов. Кроме того, разметка шаблона каждого компонента может иметь собственные Shadow DOM с элементами и для каждого элемента будет присутствовать собственный инъектор. На рис. 4.9 показана иерархия инъекторов.

73311.png 

Рис. 4.9. Иерархия инъекторов

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

примечание

Angular создает дополнительный инъектор для «лениво» загружаемых модулей. Поставщики, объявленные в директиве @NgModule модуля, загружаемого «лениво», доступны только на уровне модуля, но не для всего приложения.

В приложении-примере внедряется только сервис, в нем не показывается использование инъекторов элементов. В браузере каждый экземпляр компонента может быть представлен Shadow DOM, содержащим один или несколько элементов в зависимости от того, что определено в шаблоне компонента. Каждый элемент Shadow DOM имеет объект класса ElementInjector, который следует той же иерархии «предок — потомок», что и сами элементы DOM.

Допустим, в элемент-компонент HTML <input> нужно добавить функцию автозаполнения. Для этого определите директиву следующим образом:

@Directive({

  selector: '[autocomplete]'

})

class AutoCompleter {

  constructor(element: ElementRef) {

    // Здесь будет реализована логика автозаполнения

  }

}

Квадратные скобки означают, что автозаполнение можно использовать как атрибут элемента HTML. Ссылка на этот элемент будет автоматически внедрена в конструктор класса AutoCompleter с помощью инъектора элементов.

Теперь еще раз взглянем на код из раздела 4.4. Класс Product2Component имеет поставщик MockProductService на уровне компонента. В классе Product1Component не указаны поставщики для типа ProductService, поэтому Angular выполнил следующие действия:

проверил его предка AppComponent — поставщик не найден;

• проверил AppModule и нашел строку providers:[ProductService];

• использовал инъектор уровня приложения и создал экземпляр класса Pro­ductService на уровне приложения.

В случае удаления строки providers из класса Product2Component и перезапуска приложения оно все еще будет работать, применяя инъектор уровня приложения и один экземпляр класса ProductService для обоих компонентов. Если поставщики для одного токена были указаны для предка и потомка и каждый из этих компонентов имел конструктор, который требовал представленный токеном объект, то будут созда­ны два отдельных экземпляра такого объекта: один для предка и один для потомка.

4.5.1. viewProviders

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

Рассмотрим еще один пример. Представьте, что у вас есть следующая иерархия компонентов:

<root>

  <product2>

    <luxury-product></luxury-product>

  </product2>

</root>

Компоненты AppModule и Product2Component имеют поставщики, определенные с помощью токена ProductService, но второй элемент использует особый класс, который вы должны скрыть от потомков. В этом случае можно применить свойство viewProviders для класса Product2Component; когда инъектор компонента LuxuryProductComponent не найдет поставщика, он поднимется по иерархии. В Product2Component он тоже не увидит поставщика и задействует поставщика для ProductService, определенного в компоненте RootComponent.

примечание

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

4.6. Практикум: использование DI для приложения онлайн-аукциона

В главе 3 вы добавили маршрутизацию в приложение-аукцион, и теперь оно может отрисовывать упрощенное представление Product Details (Информация о продукте). В этом упражнении вы реализуете компонент ProductDetail, который сможет показывать реальные сведения о продукте.

Главная страница аукциона показана на рис. 4.10. При нажатии одной из ссылок, например First Product или Second Product, приложение покажет довольно простое представление, которое вы видели на рис. 3.16.

ch04_10.tif 

Рис. 4.10. Главная страница аукциона

Ваша цель заключается в том, чтобы отрисовать подробную информацию о выбранном продукте; это позволит продемонстрировать механизм DI в действии. На рис. 4.11 показано, как представление Product Details (Информация о продукте) будет выглядеть по завершении этого упражнения, если выбрать ссылку First Product.

ch04_11.tif 

Рис. 4.11. Представление аукциона Product Details

СОВЕТ

В качестве стартовой точки для этого упражнения мы будем использовать приложение, разработанное в главе 3. Если хотите увидеть итоговый результат проекта, то откройте исходный код, расположенный в каталоге auction для главы 4. В противном случае скопируйте каталог auction из главы 3 в другое место, запустите команду npm install и следуйте инструкциям, представленным в данном разделе.

Теперь, когда вы узнали о поставщиках и внедрении зависимостей, кратко рассмотрим некоторые фрагменты аукциона, созданные в предыдущей главе, концентрируясь на коде, связанном с внедрением зависимостей. Сценарий из файла app.module.ts определяет поставщики сервиса на уровне приложения, как показано здесь:

@NgModule({

    ...

    providers:   [ProductService,

                 {provide: LocationStrategy, useClass: HashLocationStrategy}],

    bootstrap:   [ ApplicationComponent ]

})

export class AppModule { }

Поскольку поставщик ProductService определен в модуле, его могут применять все потомки компонента ApplicationComponent. В следующем фрагменте компонента HomeComponent (см. файл home.ts) не указываются поставщики, которые нужно использовать для внедрения ProductService с помощью конструктора — в нем задействован экземпляр класса ProductService, созданный в его предке:

@Component({

  selector: 'auction-home-page',

  styleUrls: ['/home.css'],

  template: `...`

})

export default class HomeComponent {

  products: Product[] = [];

  constructor(private productService: ProductService) {

    this.products = this.productService.getProducts();

  }

}

Сразу после создания объекта класса HomeComponent в него внедряется сервис ProductService, его метод getProducts() заполняет массив products, привязанный к представлению.

Фрагмент HTML, который отображает содержимое этого массива, использует цикл *ngFor, чтобы отобразить шаблоны <auction-product-item> для каждого элемента массива:

<div class="row">

  <div *ngFor="let product of products" class="col-sm-4 col-lg-4 col-md-4">

    <auction-product-item [product]="product"></auction-product-item>

  </div>

</div>

Шаблон элемента <auction-product-item> содержит следующую строку:

<h4><a [routerLink]="['/products', product.title]">{{ product.title }}</a>

  </h4>

Нажатие этой ссылки дает маршрутизатору команду отрисовать компонент ProductDetailComponent и предоставляет в качестве параметра маршрута значение свойства product.title. Вы можете модифицировать данный код так, чтобы передавать идентификатор продукта вместо его названия.

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

4.6.1. Изменение кода для передачи идентификатора продукта в качестве параметра

Откройте файл product-item.html и измените строку, содержащую [routerLink], чтобы она выглядела следующим образом:

<h4><a [routerLink]="['/products', product.id]">{{ product.title }}</a></h4>

Файл product-item.html содержит шаблон, используемый для отображения продуктов в представлении Home (Главная страница). Теперь после нажатия названия продукта в маршрут, сконфигурированный для пути products, будет отправляться значение идентификатора продукта.

4.6.2. Изменение компонента ProductDetailComponent

Прежде чем начнете писать код, взгляните на рис. 4.12; на нем показано отношение «предок — потомок» между компонентами аукциона. Понимание этих отношений может помочь решить, будут ли использоваться потомками некоторые инъекторы предков.

73345.png 

Рис. 4.12. Отношения «предок — потомок» в аукционе

В главе 3 вы внедрили объект типа ProductService в компонент HomeComponent, но он нужен и в компоненте ProductDetailComponent. Можно определить поставщика ProductService во время начальной загрузки приложения, чтобы сделать его доступным всем потомкам компонента ApplicationComponent. Для этого следуйте инструкциям, представленным ниже.

1. Измените конфигурацию маршрутов в файле app.module.ts следующим образом: с products/:prodTitle на products/:productId. Первые строки декоратора @NgModule должны выглядеть так (листинг 4.8).

Листинг 4.8. Изменения в файле app.module.ts

@NgModule({

    imports:[ BrowserModule,

            RouterModule.forRoot([

              {path: '', component: HomeComponent},

              {path: 'products/:productId',

                     component: ProductDetailComponent}

]) ],

Поскольку вы передаете идентификатор продукта компоненту ProductDetailCom­ponent, его код нужно изменить соответствующим образом.

2. Откройте файл product-detail.ts и измените его код так, как показано далее (листинг 4.9).

Листинг 4.9. Изменения в файле product-detail.ts

import {Component} from '@angular/core';

import { ActivatedRoute} from '@angular/router';

import {Product, Review, ProductService} from

  '../../services/product-service';

 

@Component({

  selector: 'auction-product-page',

  templateUrl: 'app/components/product-detail/product-detail.html'

})

 

export default class ProductDetailComponent {

  product: Product;

  reviews: Review[];

  constructor(route: ActivatedRoute, productService: ProductService)

  {

    let prodId: number = parseInt(route.snapshot.params['productId']);

    this.product = productService.getProductById(prodId);

    this.reviews = productService.getReviewsForProduct(this.product.id);

  }

}

Angular внедрит объект типа ProductService в компонент ProductDetailComponent. Когда создается объект класса с аналогичным названием, он вызовет метод getProductsById(). Он возвращает один продукт с идентификатором, совпадающим с переданным из представления Home (Главная страница) значением productId, с помощью аргумента конструктора типа ActivatedRoute. Так вы устанавливаете значение переменной product.

После этого конструктор вызывает метод getReviewsForProduct(), чтобы заполнить массив reviews. Вы увидите объявление данного метода, а также класс Review, далее в этом разделе.

3. Создайте файл с именем product-detail.html в каталоге product-detail, который имеет следующее содержимое (листинг 4.10).

Листинг 4.10. Содержимое файла product-detail.html

<div class="thumbnail">

    <img src="">

    <div>

        <h4 class="pull-right">{{ product.price }}</h4>

        <h4>{{ product.title }}</h4>

        <p>{{ product.description }}</p>

    </div>

    <div class="ratings">

        <p class="pull-right">{{ reviews.length }} reviews</p>

        <p><auction-stars [rating]="product.rating"></auction-stars></p>

    </div>

</div>

<div class="well" id="reviews-anchor">

    <div class="row">

        <div class="col-md-12"></div>

    </div>

    <div class="row" *ngFor="let review of reviews">

        <hr>

        <div class="col-md-12">

            <auction-stars [rating]="review.rating"></auction-stars>

            <span>{{ review.user }}</span>

            <span class="pull-right">

              {{ review.timestamp | date: 'shortDate' }}</span>

            <p>{{ review.comment }}</p>

        </div>

    </div>

</div>

В данном шаблоне HTML применяется локальная привязка для свойств переменной product. Обратите внимание на использование квардратных скобок для передачи значения рейтинга в компонент StarsComponent (представленный элементом <auction-stars>), с которым вы познакомились в главе 2. В этой версии аукциона пользователь может только просматривать отзывы; функ­циональность Leave a Review (Оставить отзыв) вы реализуете в главе 6.

Оператор канала (|) позволяет создавать фильтры, которые могут преобразовывать значения. Выражение review.timestamp | date: 'shortDate' принимает временную метку из объекта класса Review и отображает ее в формате shortDate. Вы можете найти другие форматы дат в документации к Angular, доступной на . Angular поставляется с несколькими классами, которые могут быть использованы с оператором pipe; кроме того, можно создавать собственные фильтры (об этом рассказывается в главе 5). В главе 8 вы увидите, как применять асинхронные pipe для автоматического разворачивания ответов сервера.

4. Чтобы сэкономить время, скопируйте в проект файл app/services/product-service.ts, который поставляется с кодом приложения аукциона для настоящей главы. В этом файле находятся три класса — Product, Review и ProductService, — а также жестко закодированные данные о продуктах и отзывах. В шаблоне HTML из листинга 4.10 используются следующие классы Product и Review (листинг 4.11).

Листинг 4.11. Классы Product и Review

export class Product {

  constructor(

    public id: number,

    public title: string,

    public price: number,

    public rating: number,

    public description: string,

    public categories: string[]) {

  }

}

export class Review {

  constructor(

    public id: number,

    public productId: number,

    public timestamp: Date,

    public user: string,

    public rating: number,

    public comment: string) {

  }

}

Класс ProductService показан в листинге 4.12.

Листинг 4.12. Класс ProductService

export class ProductService {

  getProducts(): Product[] {

    return products.map(p => new Product(p.id, p.title, p.price, p.rating,

      p.description, p.categories));

  }

  getProductById(productId: number): Product {

    return products.find(p => p.id === productId);

  }

  getReviewsForProduct(productId: number): Review[] {

    return reviews

      .filter(r => r.productId === productId)

      .map(r => new Review(r.id, r.productId, Date.parse(r.timestamp),

        r.user, r.rating, r.comment));

  }

}

var products = [

  {

    "id": 0,

    "title": "First Product",

    "price": 24.99,

    "rating": 4.3,

    "description": "This is a short description. Lorem ipsum dolor sit

      amet, consectetur adipiscing elit.",

    "categories": ["electronics", "hardware"]},

  {

    "id": 1,

    "title": "Second Product",

    "price": 64.99,

    "rating": 3.5,

    "description": "This is a short description. Lorem ipsum dolor sit

      amet, consectetur adipiscing elit.",

    "categories": ["books"]}];

 

var reviews = [

  {

    "id": 0,

    "productId": 0,

    "timestamp": "2014-05-20T02:17:00+00:00",

    "user": "User 1",

    "rating": 5,

    "comment": "Aenean vestibulum velit id placerat posuere. Praesent..."},

  {

    "id": 1,

    "productId": 0,

    "timestamp": "2014-05-20T02:53:00+00:00",

    "user": "User 2",

    "rating": 3,

    "comment": "Aenean vestibulum velit id placerat posuere. Praesent... "

  }];

Этот класс имеет три метода: getProducts(), возвращающий массив объектов класса Product; getProductById(), возвращающий один продукт; и getReviewsForProduct(), возвращающий массив объектов класса Review для выбранного продукта. Все данные о продуктах и обзоры жестко закодированы в массивах products и reviews соответственно. (Для краткости мы показали лишь фрагменты этих массивов.) Метод getReviewsForProduct() фильтрует массив reviews для того, чтобы найти отзывы для заданного значения productId. Затем он использует функцию map() для превращения массива элементов типа Object в новый массив объектов типа Review.

5. Запустите сервер в каталоге auction, введя команду npm start. Когда вы увидите главную страницу аукциона, нажмите название продукта, чтобы увидеть представление Product Details (Информация о продукте), показанное на рис. 4.11.

4.7. Резюме

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

Поставщики регистрируют объекты для будущего внедрения.

• Вы можете создать поставщик не только для объекта, но и для строкового значения.

• Инъекторы формируют иерархию, и если Angular не может найти поставщик для запрошенного типа на уровне компонента, то попробует найти его среди инъекторов предка.

• Значение свойства providers видно в элементах-потомках, а свойства viewPro­viders — только на уровне компонентов.

Назад: 3. Навигация с помощью маршрутизатора Angular
Дальше: 5. Привязки, наблюдаемые объекты и каналы

32
32
Alex
32
fagmefs
kamagra 100 mg on line
lasix other names
Amoxicillin 250 5ml
dragzolotoru
Ювелирные изделия, те что покупатели имеем или приобретаем именно на подарок родным несут внутри себе большое число увлекательных данных, которые реально просто изучить, когда Вы нажмет на источник новых публикаций касательно драгоценных изделий виды плетение цепочек. Интернет страничка золотых украшений ознакомит любителей из спектр полезными умений, с пособием их Вы могут хорошо ориентироваться у высококачества металлических плюс популярных изделий, к тому же дорогих породах камней плюс металлах. Онлайн сайт всякой проверенных данных о украшения ежедневно увеличивает сведения, те что смогут Вам вернее разбираться какие именно изделия совмещаются в стиле, как же требуется чистить про ювелирными предметами, и что сегодня актуально. Переходите на сайт, читайте также лайкайте подходящие вам новости или же делитесь ссылки на странички соц. сети, здесь на сайте мы будем систематически дополнять существующие библиотеку постов ювелирных украшений.
CharlesTut
импорт товаров
JacobBal
Каждый человек способен испытать испуг в разных жизненных ситуациях. Это – абсолютно обычное явление, помогающее нам спасти себе жизнь в момент угрозы жизни. Правда большинство страхов это просто напросто иррациональное переживание. Например страх летать на самолёте. С этими страхами возможно будет бороться успешно, существуют очень действенные технологии, про них узнаете в анализе ссылка на статью Также можно почувствовать страх опасности, именно это ощущение помогает спасти человеческую жизнь в угрожающих ситуациях. Соответственно страх это адекватное состояние, проблема может возникнуть только лишь если подобный страх часто сводит с ума. В этом случае надо предпринимать определенные меры, обращаться в психологический центр.
Caseybup
електро тепла підлога
Howardton
электро полы с подогревом