<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Блог MEAN stack разработчика]]></title><description><![CDATA[Заметки по MongoDB, Express, Angular и Node.js]]></description><link>https://mean-dev.info/</link><image><url>https://mean-dev.info/favicon.png</url><title>Блог MEAN stack разработчика</title><link>https://mean-dev.info/</link></image><generator>Ghost 2.3</generator><lastBuildDate>Fri, 28 Apr 2023 13:07:10 GMT</lastBuildDate><atom:link href="https://mean-dev.info/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[MEAN-stack и Docker. Часть 5 - Mongo Replica Set в Docker Swarm]]></title><description><![CDATA[Пятая часть цикла, посвященного контейнеризации MEAN-stack приложений, в которой мы развернем MongoDB Replica Set из трех узлов в Docker Swarm]]></description><link>https://mean-dev.info/mean-stack-docker-part-5/</link><guid isPermaLink="false">5bdb79bfc968fc000177b1fd</guid><category><![CDATA[Docker]]></category><category><![CDATA[Angular]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[mongoDB]]></category><dc:creator><![CDATA[Павел Прудников]]></dc:creator><pubDate>Thu, 19 Jul 2018 13:52:23 GMT</pubDate><media:content url="https://mean-dev.info/content/images/2018/07/docker-5.png" medium="image"/><content:encoded><![CDATA[<img src="https://mean-dev.info/content/images/2018/07/docker-5.png" alt="MEAN-stack и Docker. Часть 5 - Mongo Replica Set в Docker Swarm"><p>Пятая часть цикла, посвященного контейнеризации MEAN-stack приложений, в которой мы развернем MongoDB Replica Set из трех узлов в Docker Swarm.</p>
<h2 id="">Содержание цикла</h2>
<ul>
<li><strong><a href="http://mean-dev.info/mean-stack-docker-part-1">Часть 1 - Dockerfile, образы, контейнеры</a></strong></li>
<li><strong><a href="http://mean-dev.info/mean-stack-docker-part-2">Часть 2 - Docker Compose</a></strong></li>
<li><strong><a href="http://mean-dev.info/mean-stack-docker-part-3">Часть 3 - Traefik reverse proxy</a></strong></li>
<li><strong><a href="http://mean-dev.info/mean-stack-docker-part-4">Часть 4 - Docker Swarm</a></strong></li>
<li><strong>Часть 5 - Mongo Replica Set в Docker Swarm</strong></li>
</ul>
<h2 id="">Введение</h2>
<p>В <a href="http://mean-dev.info/mean-stack-docker-part-4">предыдущей части</a> мы задеплоили приложение в Docker Swarm. Однако сервис базы данных у нас не был реплицирован. В данной статье исправим это досадное недоразумение и развернем 3 реплики базы данных на нодах нашего Docker-роя. Но с репликацией mongo есть небольшая сложность - нельзя просто указать в compose-файле <code>replicas: 3</code>. В случае падения одной из рабочих нод докер перенесет сервис mongo на ноду, где такой сервис уже есть. И эти сервисы начнут работать с общей <code>data/db</code>. Но это недопустимо. Поэтому предстоит провести небольшую настройку.</p>
<h2 id="">Маркируем ноды</h2>
<p>Каждой ноде укажем label с требуемым id реплики mongo. Для этого на менеджере выполним следующие команды:</p>
<pre><code>docker node update --label-add mongo.replica=0 vm0
docker node update --label-add mongo.replica=1 vm1
docker node update --label-add mongo.replica=2 vm2
</code></pre>
<h2 id="">Настроим сервисы</h2>
<p>В compose-файле вместо сервиса <code>db</code> создадим 3 других:</p>
<pre><code>services:

  # ...

  mongo0:
    image: mongo
    command:
      - &quot;--smallfiles&quot;
      - &quot;--replSet&quot;
      - &quot;rs0&quot;
    deploy:
      mode: global
      restart_policy:
        condition: on-failure
      placement:
        constraints:
          - node.labels.mongo.replica == 0
      labels:
        - traefik.enable=false
    volumes:
      - mongodata0:/data/db
    networks:
      - db-net


  mongo1:
    image: mongo
    command:
      - &quot;--smallfiles&quot;
      - &quot;--replSet&quot;
      - &quot;rs0&quot;
    deploy:
      mode: global
      restart_policy:
        condition: on-failure
      placement:
        constraints:
          - node.labels.mongo.replica == 1
      labels:
        - traefik.enable=false
    volumes:
      - mongodata1:/data/db
    networks:
      - db-net


  mongo2:
    image: mongo
    command:
      - &quot;--smallfiles&quot;
      - &quot;--replSet&quot;
      - &quot;rs0&quot;
    deploy:
      mode: global
      restart_policy:
        condition: on-failure
      placement:
        constraints:
          - node.labels.mongo.replica == 2
      labels:
        - traefik.enable=false
    volumes:
      - mongodata2:/data/db
    networks:
      - db-net
</code></pre>
<p>В командах указываем запускать mongo как replica set с именем <code>rs0</code>. В <code>deploy</code> указываем глобальный режим <code>mode: global</code> и задаем constraints-правило <code>node.labels.mongo.replica == 0</code>, т.е. запускать <code>mongo0</code> только на ноде с меткой <code>mongo.replica=0</code>.</p>
<p>У сервера изменим путь для подключения mongoose к базе данных:</p>
<pre><code>  server:
    ...
    environment:
      - &quot;db:uri=mongodb://mongo0,mongo1,mongo2/docker?replicaSet=rs0&quot;

</code></pre>
<p>В конфиге других сервисов ничего не меняется (см. <a href="http://mean-dev.info/mean-stack-docker-part-4">предыдущую часть цикла</a>).</p>
<h2 id="volumes">Volumes</h2>
<p>Для хранения баз данных создадим следующие volumes:</p>
<pre><code>volumes:
  mongodata0:
  mongodata1:
  mongodata2:
</code></pre>
<p>и используем их в сервисах <code>mongo</code>:</p>
<pre><code>  mongo0:
    ...
    volumes:
      - mongodata0:/data/db
</code></pre>
<h2 id="deploy">Deploy</h2>
<pre><code>docker stack deploy -c docker-stack.mongo-rs.yml meanstack
</code></pre>
<h2 id="replicasetmongo">Инициируем Replica Set в самой mongo</h2>
<pre><code>docker exec -it &lt;mongo_container_id&gt; sh

&gt; mongo

&gt; rs.initiate( {
   _id : &quot;rs0&quot;,
   members: [
      { _id: 0, host: &quot;mongo0:27017&quot; },
      { _id: 1, host: &quot;mongo1:27017&quot; },
      { _id: 2, host: &quot;mongo2:27017&quot; }
   ]
})
</code></pre>
<h2 id="">Результат</h2>
<p>В браузере по адресу <a href="http://visualizer.docker-example.local/">http://visualizer.docker-example.local/</a> мы должны увидеть что-то похожее:</p>
<p><img src="https://mean-dev.info/content/images/2018/07/docker-swarm-mongo-visualizer.png" alt="MEAN-stack и Docker. Часть 5 - Mongo Replica Set в Docker Swarm"></p>
<p><strong><a href="https://github.com/twoheaded/mean-docker-stack">Код проекта на GitHub &gt;&gt;</a></strong></p>
]]></content:encoded></item><item><title><![CDATA[MEAN-stack и Docker. Часть 4 - Docker Swarm]]></title><description><![CDATA[Четвертая часть цикла, посвященного контейнеризации MEAN-stack приложений, в которой приложение мы разворачиваем в кластере Docker Swarm]]></description><link>https://mean-dev.info/mean-stack-docker-part-4/</link><guid isPermaLink="false">5bdb79bfc968fc000177b1fc</guid><category><![CDATA[Docker]]></category><category><![CDATA[Angular]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[mongoDB]]></category><dc:creator><![CDATA[Павел Прудников]]></dc:creator><pubDate>Thu, 19 Jul 2018 13:39:07 GMT</pubDate><media:content url="https://mean-dev.info/content/images/2018/07/docker-4.png" medium="image"/><content:encoded><![CDATA[<img src="https://mean-dev.info/content/images/2018/07/docker-4.png" alt="MEAN-stack и Docker. Часть 4 - Docker Swarm"><p>Четвертая часть цикла, посвященного контейнеризации MEAN-stack приложений, в которой приложение мы разворачиваем в кластере Docker Swarm.</p>
<h2 id="">Содержание цикла</h2>
<ul>
<li><strong><a href="http://mean-dev.info/mean-stack-docker-part-1">Часть 1 - Dockerfile, образы, контейнеры</a></strong></li>
<li><strong><a href="http://mean-dev.info/mean-stack-docker-part-2">Часть 2 - Docker Compose</a></strong></li>
<li><strong><a href="http://mean-dev.info/mean-stack-docker-part-3">Часть 3 - Traefik reverse proxy</a></strong></li>
<li><strong>Часть 4 - Docker Swarm</strong></li>
<li><strong><a href="http://mean-dev.info/mean-stack-docker-part-5">Часть 5 - Mongo Replica Set в Docker Swarm</a></strong></li>
</ul>
<h2 id="">Введение</h2>
<p>Docker позволяет объединить в кластер группу машин, на которых он установлен. Этот кластер называется <em>Swarm</em> (рой). После этого можно выполнять все те же команды Docker, но теперь они будут выполняться в кластере <em>swarm-менеджером</em>. Помимо менеджеров в swarm могут быть и <em>воркеры (workers)</em>.  Все машины, подключенные к swarm, называются <em>нодами (nodes)</em>. Менеджеры также отвечают и за распределение сервисов между всеми нодами. Правила распределения и взаимодействия сервисов описываются в уже известном нам docker-compose YAML-файле. Совокупность всех сервисов в swarm носит название <em>stack</em>.</p>
<h2 id="">Создаем машины</h2>
<p>Для начала нужно заиметь группу машин.</p>
<p>Под виндой 3 виртуальные машины можно создать следующим образом:</p>
<pre><code>docker-machine create -d hyperv --hyperv-virtual-switch &quot;myswitch&quot; vm0
docker-machine create -d hyperv --hyperv-virtual-switch &quot;myswitch&quot; vm1
docker-machine create -d hyperv --hyperv-virtual-switch &quot;myswitch&quot; vm2
</code></pre>
<p><a href="https://docs.docker.com/machine/drivers/hyper-v/">Как настроить Hyper-V &gt;&gt;</a></p>
<p>Под Mac/Linux:</p>
<pre><code>docker-machine create --driver virtualbox vm0
</code></pre>
<p>Если хочется поиграться в условиях, приближенных к боевым, то можно поднять машины на дроплетах Digital Ocean:</p>
<pre><code>docker-machine create --driver digitalocean --digitalocean-access-token xxxxx vm0
...
</code></pre>
<p><a href="https://docs.docker.com/machine/examples/ocean/">Пример c Digital Ocean Docker Machine &gt;&gt;</a></p>
<p>Посмотреть список доступных машин:</p>
<pre><code>docker-machine ls
</code></pre>
<h2 id="swarm">Инициируем Swarm-режим</h2>
<p>Получим команды, которые настроят текущий shell для работы с демоном Docker на виртуальной машине:</p>
<pre><code>docker-machine env vm0
</code></pre>
<p>Будет выведен список команд для текущей оболочки, которые нужно выполнить. В списке машин (<code>docker-machine ls</code>) в колонке <code>ACTIVE</code> символом <code>*</code> будет отмечена активная машина. Теперь все команды <code>docker ...</code> будут выполняться на ней.</p>
<p>Для инициации swarm-режима выполняем:</p>
<pre><code>docker swarm init
</code></pre>
<p>В ответе будет указано, что текущая машина теперь менеджер. Также будет выведена команда для подключения к swarm рабочих нод.</p>
<h2 id="worker">Подключаем рабочие (worker) ноды</h2>
<p>Переключаемся на другую машину и выполняем на ней предоставленную ранее команду:</p>
<pre><code>docker-machine env vm1
...
docker swarm join --token SWMTKN-1-52qk72ooyql1zfxb0lezwmj52oipimmgln6zh3skbdokl6pp3c-179u2zzjdffcaqjni4zjst188 192.168.2.36:2377
</code></pre>
<p>Эту же процедуру проведем и с <code>vm2</code>.</p>
<h2 id="">Подключение менеджеров</h2>
<p>Для подключения к swarm других менеджеров нужно запросить соответствующий токен:</p>
<pre><code>docker swarm join-token manager
</code></pre>
<h2 id="dockerstack">Docker Stack</h2>
<p>Теперь у нас есть кластер на Docker Swarm, готовый к деплою нашего приложения. Однако приложение осталось чутка подготовить к запуску.</p>
<p>Stack - это, по сути, тот же docker-compose файл, но есть небольшие отличия. Так убираем из файла все <code>depends_on</code> - в swarm они не работают. Все сервисы запускаются параллельно и независимо. Поэтому на сервере пришлось настроить reconnect к базе данных в случае неудачи. Сервис может быть запущен до запуска базы данных, и тогда он уходит в цикл &quot;падение-запуск&quot; и, тем самым, солидно так грузит весь стек.</p>
<p>У сервисов добавляется раздел <code>deploy</code>, в котором настраиваются:</p>
<ul>
<li>restart_policy (<code>condition: on-failure</code>);</li>
<li>update_config (<code>parallelism: 2</code>);</li>
<li>число экземпляров сервиса (<code>replicas: 3</code>);</li>
<li>правила размещения сервиса (<code>placement constraints</code>, <code>preferences</code>)</li>
<li>и <a href="https://docs.docker.com/compose/compose-file/#deploy">ряд других параметров</a></li>
</ul>
<p>Также в секцию deploy необходимо перенести все <code>lebels</code>.</p>
<p>Для стека создадим новый файл:</p>
<h3 id="dockerstackyml"><code>docker-stack.yml</code></h3>
<pre><code>version: &quot;3.5&quot;

services:

  server:
    image: myusername/public:mean-docker-server
    deploy:
      replicas: 2
      restart_policy:
        condition: on-failure
      placement:
        constraints:
          - node.role == worker
      labels:
        - traefik.docker.network=traefik-net
        - traefik.frontend.rule=Host:api.docker-example.local
        - traefik.port=3000
    environment:
      - &quot;db:uri=mongodb://db/docker&quot;
    networks:
      - traefik-net
      - db-net


  client:
    image: myusername/public:mean-docker-client
    deploy:
      replicas: 2
      restart_policy:
        condition: on-failure
      placement:
        constraints:
          - node.role == worker
      labels:
        - traefik.frontend.rule=Host:docker-example.local
        - traefik.port=4000
    networks:
      - traefik-net

  db:
    image: mongo
    command:
      - &quot;--smallfiles&quot;
    deploy:
      mode: global
      restart_policy:
        condition: on-failure
      placement:
        constraints:
          - node.role == manager
      labels:
        - traefik.enable=false
    networks:
      - db-net


  reverse-proxy:
    image: traefik
    command:
      - &quot;--api&quot;
      - &quot;--entrypoints=Name:http Address::80&quot;
      - &quot;--defaultentrypoints=http&quot;
      - &quot;--docker&quot;
      # Включаем режим Swarm
      - &quot;--docker.swarmMode&quot;
      - &quot;--docker.domain=docker-example.local&quot;
      - &quot;--docker.watch&quot;
    deploy:
      mode: global
      placement:
        constraints:
          - node.role == manager
      update_config:
        parallelism: 1
        delay: 10s
      restart_policy:
        condition: on-failure
      labels:
        - traefik.frontend.rule=Host:traefik.docker-example.local
        - traefik.port=8080
    ports:
      - 80:80
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    networks:
      traefik-net:
        aliases:
          - api.docker-example.local

</code></pre>
<p>Запустим по 2 реплики &quot;server&quot; и &quot;client&quot;. В <code>placement:constraints</code> указываем, что запускать их требуется только на воркерах (<code>node.role == worker</code>).</p>
<p>Для сервисов &quot;db&quot; и &quot;reverse-proxy&quot; укажем <code>mode: global</code> (запускать по 1 сервису на каждую ноду) и <code>node.role == manager</code> в <code>constraints</code> (только на менеджерах).</p>
<h2 id="dockerswarmvisualizer">Docker Swarm Visualizer</h2>
<p>Также в сервисы добавим <a href="https://hub.docker.com/r/dockersamples/visualizer/">визуализатор работы Swarm</a>.</p>
<pre><code>services:

  ...

  visualizer:
    image: dockersamples/visualizer:stable
    deploy:
      placement:
        constraints:
          - node.role == manager
      labels:
        - traefik.frontend.rule=Host:visualizer.docker-example.local
        - traefik.port=8080
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    networks:
      - traefik-net

</code></pre>
<h2 id="hosts">Hosts</h2>
<p>Заменим/добавим наши фейковые домены:</p>
<pre><code># windows:    c:/windows/system32/drivers/etc/hosts
# ubuntu/mac: /etc/hosts

192.168.2.40       docker-example.local
192.168.2.40       api.docker-example.local 
192.168.2.40       traefik.docker-example.local
192.168.2.40       visualizer.docker-example.local
</code></pre>
<p>Здесь <code>192.168.2.40</code> - IP-адрес менеджера (vm0). Узнать его можно с помощью <code>docker-machine ls</code>.</p>
<h2 id="deploy">Deploy</h2>
<p>Для развертывания приложения переключаемся на ноду-менеджер:</p>
<pre><code>docker-machine env vm0
...
</code></pre>
<p>Создаем сеть:</p>
<pre><code>docker network create -d overlay traefik-net
</code></pre>
<p>и выполняем деплой:</p>
<pre><code>docker stack deploy -c docker-stack.yml meanstack
</code></pre>
<p>В браузере по адресу <a href="http://visualizer.docker-example.local/">http://visualizer.docker-example.local/</a> мы должны увидеть какую-то такую картину:</p>
<p><img src="https://mean-dev.info/content/images/2018/07/docker-swarm-visualizer-1.png" alt="MEAN-stack и Docker. Часть 4 - Docker Swarm"></p>
<p>В следующей части попробуем зареплицировать нашу базу данных.</p>
<p><strong><a href="https://github.com/twoheaded/mean-docker-stack">Код проекта на GitHub &gt;&gt;</a></strong></p>
<p><strong><a href="http://mean-dev.info/mean-stack-docker-part-5">Часть 5 - Mongo Replica Set в Docker Swarm &gt;&gt;</a></strong></p>
]]></content:encoded></item><item><title><![CDATA[MEAN-stack и Docker. Часть 3 - Traefik reverse proxy]]></title><description><![CDATA[Третья часть цикла, посвященного контейнеризации MEAN-stack приложений, в которой мы настроим обратный прокси-сервер (reverse proxy) и балансировщик нагрузки (load balancer) Traefik.]]></description><link>https://mean-dev.info/mean-stack-docker-part-3/</link><guid isPermaLink="false">5bdb79bfc968fc000177b1fb</guid><category><![CDATA[Docker]]></category><category><![CDATA[Angular]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[mongoDB]]></category><dc:creator><![CDATA[Павел Прудников]]></dc:creator><pubDate>Thu, 19 Jul 2018 13:19:16 GMT</pubDate><media:content url="https://mean-dev.info/content/images/2018/07/docker-3.png" medium="image"/><content:encoded><![CDATA[<img src="https://mean-dev.info/content/images/2018/07/docker-3.png" alt="MEAN-stack и Docker. Часть 3 - Traefik reverse proxy"><p>Третья часть цикла, посвященного контейнеризации MEAN-stack приложений, в которой мы настроим обратный прокси-сервер (reverse proxy) и балансировщик нагрузки (load balancer) <a href="https://docs.traefik.io/">Traefik</a>.</p>
<h2 id="">Содержание цикла</h2>
<ul>
<li><strong><a href="http://mean-dev.info/mean-stack-docker-part-1">Часть 1 - Dockerfile, образы, контейнеры</a></strong></li>
<li><strong><a href="http://mean-dev.info/mean-stack-docker-part-2">Часть 2 - Docker Compose</a></strong></li>
<li><strong>Часть 3 - Traefik reverse proxy</strong></li>
<li><strong><a href="http://mean-dev.info/mean-stack-docker-part-4">Часть 4 - Docker Swarm</a></strong></li>
<li><strong><a href="http://mean-dev.info/mean-stack-docker-part-5">Часть 5 - Mongo Replica Set в Docker Swarm</a></strong></li>
</ul>
<h2 id="">Введение</h2>
<p>В <a href="http://mean-dev.info/mean-stack-docker-part-2">предыдущей части</a> мы написали docker-compose файл, в котором настроили запуск сервисов с серверной частью приложения, клиентской частью и базой данных. Теперь же нам необходимо настроить приложение следующим образом:</p>
<ul>
<li>Angular приложение должно быть доступно только по адресу <code>docker-example.local</code>;</li>
<li>серверное api должно принимать запросы по адресу <code>api.docker-example.local</code>;</li>
<li>должен быть открыт только порт 80.</li>
</ul>
<p>Для этого нужно поднять прокси-сервер, который будет принимать все запросы на порту 80 и по хосту ретранслировать запросы на сервис <code>client</code> для <code>docker-example.local</code> и на <code>server</code> для <code>api.docker-example.local</code>.</p>
<p>Самое популярное решение для этих целей - использовать прокси-сервер <strong>nginx</strong>. Однако в этом проекте я попробую использовать <strong>Traefik</strong> - молодой реверс-прокси сервер и балансировщик нагрузки, который подает большие надежды.</p>
<p>Такой выбор был сделан по следующим причинам:</p>
<ul>
<li>хорошая интеграция с Docker, Docker Swarm, Kubernetes</li>
<li>более интуитивная конфигурация и настройка</li>
<li>HTTPS от Let's Encrypt из коробки</li>
<li>еще ряд некоторых плюшек из разряда Websocket, HTTP/2, метрики Prometheus</li>
<li>интересно попробовать</li>
</ul>
<h2 id="hosts">Hosts</h2>
<p>Сперва добавим наши фейковые домены в hosts:</p>
<pre><code># windows:      c:/windows/system32/drivers/etc/hosts
# ubuntu/mac:   /etc/hosts

127.0.0.1       docker-example.local
127.0.0.1       api.docker-example.local 

# Traefik Dashboard
127.0.0.1       traefik.docker-example.local
</code></pre>
<h2 id="traefik">Конфигурация Traefik</h2>
<p>Вся настройка Traefik производится в том же docker-compose файле.</p>
<h3 id="dockercomposeyml"><code>docker-compose.yml</code></h3>
<pre><code>services:

  # ...

  # Добавляем сервис реверс-прокси Traefik
  reverse-proxy:
    image: traefik

    # В командах производим основную настройку
    command:

      # Включаем Traefik Dashboard (порт 8080)
      - &quot;--api&quot;

      # Точка входа http на порту 80 используется по умолчанию
      - &quot;--entrypoints=Name:http Address::80&quot;
      - &quot;--defaultentrypoints=http&quot;

      # Включаем режим совместимости с Docker
      - &quot;--docker&quot;

      # Домен по умолчанию
      - &quot;--docker.domain=docker-example.local&quot;

      # Следить за изменениями докера
      - &quot;--docker.watch&quot;

    restart: always

    # Открываем только 80 порт
    ports:
      - 80:80
    
    # Даем Traefik доступ к docker.sock 
    # для отслеживания событий Docker
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

    # Подключаемся к сети для прокси
    networks:
      traefik-net:
        
        # Альтернативное имя хотса для доступа по сети traefik-net
        aliases:
          - api.docker-example.local

    # В метках сервисов прописываем правила для Traefik
    labels:

      # Запросы на хост `traefik.docker-example.local`
      - traefik.frontend.rule=Host:traefik.docker-example.local

      # перенаправлять на порт 8080 (Traefik Dashboard) этого сервиса
      - traefik.port=8080
</code></pre>
<h2 id="">Настройка сервисов</h2>
<p>Сами сервисы подключим к сети <code>traefik-net</code>, избавляемся от проброшенных портов. В labels зададим правила проксирования.</p>
<h3 id="serverservice">server service:</h3>
<pre><code>  server:
    image: myusername/public:mean-docker-server
    restart: on-failure
    depends_on:
      - db
    environment:
      - &quot;db:uri=mongodb://db/docker&quot;
    networks:

      # Подключаемся к сети 
      - traefik-net
      - db-net

    # В метках прописываем правила для Traefik
    labels:

      # Для реверс-прокси использовать сеть traefik-net
      - traefik.docker.network=traefik-net

      # Запросы на хост `api.docker-example.local`
      - traefik.frontend.rule=Host:api.docker-example.local

      # перенаправлять на порт 3000 этого сервиса
      - traefik.port=3000
</code></pre>
<h3 id="clientservice">client service:</h3>
<pre><code>  client:
    image: myusername/public:mean-docker-client
    restart: on-failure
    depends_on:
      - server
    networks:

      # Подключаемся к сети 
      - traefik-net

    labels:

      # Запросы на хост `docker-example.local`
      - traefik.frontend.rule=Host:docker-example.local

      # перенаправлять на порт 4000 этого сервиса
      - traefik.port=4000
</code></pre>
<h3 id="dbservice">db service:</h3>
<pre><code>  db:
    image: mongo
    restart: on-failure

    # К сети proxy-net не подключаемся
    # с сервером общаемся по db-net
    networks:
      - db-net

    labels:
      # В проксировании не нуждаемся
      - traefik.enable=false
</code></pre>
<h2 id="">Сети:</h2>
<pre><code>networks:
  traefik-net:
    external: true
  db-net:
</code></pre>
<p>В работе с сетями у траефика есть небольшая <a href="https://github.com/containous/traefik/issues/2348">проблема</a>.  Дело в том, что docker compose при старте создает сети и к их именам добавляет префикс с названием проекта. Т.е. в compose файле мы указываем, что необходимо создать сеть <code>traefik-net</code>. В метке сервиса server мы указываем traefik, что для прокси из двух сетей необходимо использовать <code>traefik.docker.network=traefik-net</code>. Но после старта эта сеть не будет найдена, ибо докер назвал ее <code>mean-docker-stack_traefik-net</code>.</p>
<p>Есть два выхода:</p>
<ol>
<li>Предугадывать переименование сети и указывать <code>traefik.docker.network=mean-docker-stack_traefik-net</code>;</li>
<li>Предварительно перед запуском создать external сеть <code>traefik-net</code> и использовать ее, указав, что она является <code>external</code> в docker-compose файле.</li>
</ol>
<p>В данном случае я воспользовался вариантом №2.</p>
<h2 id="httpsletsencrypttraefik">Настройка HTTPS от Let's Encrypt в Traefik</h2>
<p>Для настройки https в командах сервиса <code>reverse-proxy</code> добавим/заменим следующие строки:</p>
<pre><code>  reverse-proxy:
    
    # ...

    command:
      - &quot;--api&quot;
      - &quot;--entrypoints=Name:http Address::80 Redirect.EntryPoint:https&quot;
      - &quot;--entrypoints=Name:https Address::443 TLS&quot;
      - &quot;--defaultentrypoints=http,https&quot;
      - &quot;--acme&quot;
      - &quot;--acme.storage=/etc/traefik/acme/acme.json&quot;
      - &quot;--acme.entryPoint=https&quot;
      - &quot;--acme.httpChallenge.entryPoint=http&quot;
      - &quot;--acme.onHostRule=true&quot;
      - &quot;--acme.onDemand=false&quot;
      - &quot;--acme.email=contact@mydomain.com&quot;

</code></pre>
<p>Во время тестирования рекомендую использовать Let's Encrypt's staging server:</p>
<pre><code>      - &quot;--acme.caServer=https://acme-staging.api.letsencrypt.org/directory&quot;
</code></pre>
<p>Подробнее читать в <a href="https://docs.traefik.io/configuration/acme/">официальной документации Traefik</a>.</p>
<h2 id="">Запуск</h2>
<p>Предварительно создадим сеть <code>traefik-net</code>:</p>
<pre><code>docker network create traefik-net
</code></pre>
<p>Запускаем уже известной нам командой:</p>
<pre><code>docker-compose up
</code></pre>
<p>Рабочее приложение наблюдаем в браузере по адресу <a href="http://docker-example.local/">http://docker-example.local/</a></p>
<p>В следующей части попробуем развернуть наше приложение в кластере Docker Swarm.</p>
<p><strong><a href="https://github.com/twoheaded/mean-docker-stack">Код проекта на GitHub &gt;&gt;</a></strong></p>
<p><strong><a href="http://mean-dev.info/mean-stack-docker-part-4">Часть 4 - Docker Swarm &gt;&gt;</a></strong></p>
]]></content:encoded></item><item><title><![CDATA[MEAN-stack и Docker. Часть 2 - Docker Compose]]></title><description><![CDATA[Вторая часть цикла, посвященного контейнеризации MEAN-stack приложений, в которой мы познакомимся с  Docker Compose - инструментом для конфигурирования и запуска мультиконтейнерных приложений Docker]]></description><link>https://mean-dev.info/mean-stack-docker-part-2/</link><guid isPermaLink="false">5bdb79bfc968fc000177b1fa</guid><category><![CDATA[Docker]]></category><category><![CDATA[Angular]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[mongoDB]]></category><dc:creator><![CDATA[Павел Прудников]]></dc:creator><pubDate>Thu, 19 Jul 2018 13:12:24 GMT</pubDate><media:content url="https://mean-dev.info/content/images/2018/07/docker-2.png" medium="image"/><content:encoded><![CDATA[<img src="https://mean-dev.info/content/images/2018/07/docker-2.png" alt="MEAN-stack и Docker. Часть 2 - Docker Compose"><p>Вторая часть цикла, посвященного контейнеризации MEAN-stack приложений, в которой мы познакомимся с  Docker Compose - инструментом для конфигурирования и запуска мультиконтейнерных приложений Docker.</p>
<h2 id="">Содержание цикла</h2>
<ul>
<li><strong><a href="http://mean-dev.info/mean-stack-docker-part-1">Часть 1 - Dockerfile, образы, контейнеры</a></strong></li>
<li><strong>Часть 2 - Docker Compose</strong></li>
<li><strong><a href="http://mean-dev.info/mean-stack-docker-part-3">Часть 3 - Traefik reverse proxy</a></strong></li>
<li><strong><a href="http://mean-dev.info/mean-stack-docker-part-4">Часть 4 - Docker Swarm</a></strong></li>
<li><strong><a href="http://mean-dev.info/mean-stack-docker-part-5">Часть 5 - Mongo Replica Set в Docker Swarm</a></strong></li>
</ul>
<p>В <a href="http://mean-dev.info/mean-stack-docker-part-1">предыдущей части</a> мы создали Docker-образы сервера и клиента приложения. Теперь нам нужно поднять базу данных. Можно воспользоваться все той же командой <code>docker run</code>. Но каждый раз запускать по отдельности все сервисы одного мультиконтейнерного приложения как-то утомительно. Хочется написать один файл конфига и запускать его одной командой. Как раз для этих целей и необходим инструмент <strong>Docker Compose</strong>. Он позволяет сконфигурировать работу сервисов (читай контейнеров) приложения в YAML-файле. Создадим этот файл.</p>
<h2 id="dockercomposeyml"><code>docker-compose.yml</code></h2>
<pre><code class="language-yaml"># Версия формата compose файла
version: &quot;3&quot;

services:

  # Серверное приложение
  server:

    # Можно собрать образ из Docerfile
    # build: ./server

    # Но у нас есть репозиторий. Берем образ из него
    image: myusername/public:mean-docker-server

    # Дать доступ на порту 3000
    ports:
      - &quot;3000:3000&quot;

    # Перезапускать сервис после падения
    restart: on-failure

    # Запускать сервис только после запуска сервиса db
    depends_on:
      - db

    # Путь к mongoDB по имени сервиса (db). docker - имя базы
    environment:
      - &quot;db:uri=mongodb://db/docker&quot;

    # Подключиться к сети db-net
    networks:
      - db-net


  # Клиентское приложение
  client:
    image: myusername/public:mean-docker-client
    ports:
      - &quot;4000:4000&quot;
    restart: on-failure
    depends_on:
      - server


  # База данных
  db:
    image: mongo
    restart: on-failure
    networks:
      - db-net


# Создаем сеть
networks:
  db-net:
</code></pre>
<h2 id="">Основные команды</h2>
<h3 id="">Запуск сервисов</h3>
<pre><code>docker-compose up -d
</code></pre>
<p>Параметр <code>-d</code> указывает запустить в фоновом режиме.</p>
<h3 id="">Масштабирование сервисов</h3>
<pre><code>docker-compose up --scale server=2 --scale client=3
</code></pre>
<h3 id="">Логи</h3>
<pre><code>docker-compose logs
</code></pre>
<h3 id="">Запущенные процессы</h3>
<pre><code>docker-compose top
</code></pre>
<h3 id="">Остановка и удаление контейнеров</h3>
<pre><code>docker-compose down
</code></pre>
<h3 id="">Другие команды</h3>
<pre><code>docker-compose help
</code></pre>
<p>После запуска серверное приложение доступно на порту 3000. По сети докера оно подключается к базе данных. Однако клиентское приложение, хоть и открывается в браузере на <code>localhost:4000</code>, но не имеет доступа к серверному api. Для этого требуется настроить реверс прокси сервер, чем мы и займемся в следующей части.</p>
<p><strong><a href="https://github.com/twoheaded/mean-docker-stack">Код проекта на GitHub &gt;&gt;</a></strong></p>
<p><strong><a href="http://mean-dev.info/mean-stack-docker-part-3">Часть 3 - Traefik reverse proxy &gt;&gt;</a></strong></p>
]]></content:encoded></item><item><title><![CDATA[MEAN-stack и Docker. Часть 1 - Dockerfile, образы, контейнеры]]></title><description><![CDATA[Первая часть из цикла статей, посвященных контейнеризации MEAN-stack приложений. В данной части создадим Docker образы из приложений на NodeJs и Angular и запустим их в контейнерах]]></description><link>https://mean-dev.info/mean-stack-docker-part-1/</link><guid isPermaLink="false">5bdb79bfc968fc000177b1f9</guid><category><![CDATA[Docker]]></category><category><![CDATA[Angular]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[mongoDB]]></category><dc:creator><![CDATA[Павел Прудников]]></dc:creator><pubDate>Thu, 19 Jul 2018 12:59:10 GMT</pubDate><media:content url="https://mean-dev.info/content/images/2018/07/docker.png" medium="image"/><content:encoded><![CDATA[<img src="https://mean-dev.info/content/images/2018/07/docker.png" alt="MEAN-stack и Docker. Часть 1 - Dockerfile, образы, контейнеры"><p>Ну вот и я добрался до Docker. До этого пытался, но все урывками, и цельная картина не складывалась. Но было принято решение, что свое приложение буду разворачивать в контейнерах. Поэтому сел раскуривать Docker основательно, параллельно конспектируя с целью написать статью. И вот, когда все детали сложились в общую картину, обнаружил, что наконспектировал 30+ страниц. Боюсь, что статья с таким объемом будет тяжела для восприятия, поэтому встречайте небольшой цикл, посвященный контейнеризации MEAN-stack приложений.</p>
<p>Для начала рекомендую пройти этот короткий <a href="https://docs.docker.com/get-started/">официальный туториал Docker</a>.</p>
<h2 id="">Содержание цикла</h2>
<ul>
<li><strong>Часть 1 - Dockerfile, образы, контейнеры</strong></li>
<li><strong><a href="http://mean-dev.info/mean-stack-docker-part-2">Часть 2 - Docker Compose</a></strong></li>
<li><strong><a href="http://mean-dev.info/mean-stack-docker-part-3">Часть 3 - Traefik reverse proxy</a></strong></li>
<li><strong><a href="http://mean-dev.info/mean-stack-docker-part-4">Часть 4 - Docker Swarm</a></strong></li>
<li><strong><a href="http://mean-dev.info/mean-stack-docker-part-5">Часть 5 - Mongo Replica Set в Docker Swarm</a></strong></li>
</ul>
<h2 id="">Описание демо-проекта</h2>
<p>Демо-приложение не имеет особой ценности и предназначено только для того, чтобы показать работоспособность в целом. Оно должно принимать запросы, сохранять в базу данных своеобразный лог (имя хоста, <code>req.hostname</code> и <code>x-real-ip</code> заголовок запроса) и в ответе возвращать 10 последних сохраненных записей.</p>
<p>Структурно демо-приложение будет состоять из следующих основных частей:</p>
<ul>
<li>серверная часть</li>
<li>клиентская часть</li>
<li>база данных</li>
</ul>
<p>В дальнейшем, по мере углубления, будут добавлены сервисы reverse proxy и мониторинга.</p>
<p><strong><a href="https://github.com/twoheaded/mean-docker-stack">Код проекта на GitHub &gt;&gt;</a></strong></p>
<h3 id="">Сервер</h3>
<ul>
<li>Express.js 4.x</li>
<li>Mongoose 5.x</li>
</ul>
<p>Сгенерирован с помощью <code>express-generator</code>:</p>
<pre><code>npm install express-generator -g
express --no-view server
</code></pre>
<p>Основная логика содержится в этих строках:</p>
<pre><code>router.post('/', function (req, res, next) {

    let newData = Data({
        host: os.hostname(),
        req_host: req.hostname,
        req_ip: req.header('x-real-ip')
    });

    newData.save()
        .then((data) =&gt; {
            return Data.find().sort('-date').limit(10).exec();
        })
        .then((list) =&gt; {
            res.json(list)
        })
        .catch(err =&gt; {
            return next(err);
        });
});
</code></pre>
<h3 id="">Клиент</h3>
<ul>
<li>Angular 6.x</li>
<li>Angular Universal 6.x (SSR)</li>
</ul>
<p>Сгенерирован с помощью Angular CLI:</p>
<pre><code>npm install -g @angular/cli
ng new client
</code></pre>
<p><strong><a href="http://mean-dev.info/angular-universal">Настройка серверного рендеринга Angular &gt;&gt;</a></strong></p>
<p>Кнопка, по нажатию на которую отправляется запрос на сервер, таблица со списком последних сохраненных в БД запросов:</p>
<p><img src="https://mean-dev.info/content/images/2018/07/docker-example-ui-2.png" alt="MEAN-stack и Docker. Часть 1 - Dockerfile, образы, контейнеры"></p>
<h3 id="">База данных</h3>
<ul>
<li>MongoDB 3.6</li>
</ul>
<h2 id="dockerfile">Dockerfile</h2>
<p>Это файл, описывающий, как должна производиться сборка Docker-образа с нашим приложением. Для серверного и клиентского приложений потребуется создать два файла с названием <code>Dockerfile</code>.</p>
<h3 id="serverdockerfile"><code>/server/Dockerfile</code></h3>
<pre><code># В качестве исходного используем официальный образ  
# диспетчера процессов PM2 на базе ОС Alpine
FROM keymetrics/pm2:latest-alpine

# Создаем рабочую директорию
WORKDIR /app

# Копируем в неё файлы package.json и package-lock.json
COPY package*.json ./

# Выполняем установку пакетов npm
RUN npm install

# Копируем остальные файлы приложения
COPY . ./

# Открываем наружу порт 3000
EXPOSE 3000

# Указываем команду, выполняемую при запуске контейнера
CMD [ &quot;pm2-runtime&quot;, &quot;start&quot;, &quot;ecosystem.config.js&quot; ]
</code></pre>
<h3 id="clientdockerfile"><code>/client/Dockerfile</code></h3>
<p>Здесь воспользуемся возможностью многостадийной сборки образа Docker:</p>
<pre><code># Первая стадия сборки (назовем build)

# В качестве исходного используем образ NodeJS на базе ОС Alpine
FROM node:alpine AS build

# Создаем рабочую директорию
WORKDIR /app

# Копируем в неё файлы package.json и package-lock.json
COPY package*.json ./

# Устанавливаем переменную среды для loglevel
# и выполняем установку пакетов npm
ENV NPM_CONFIG_LOGLEVEL warn
RUN npm install

# Копируем остальные файлы приложения
COPY . ./

# Определяем переменную стадии со значением по умолчанию
# Для изменения в build указать --build-arg STAGE=development
ARG STAGE=production

# Выполняем сборку приложения
RUN $(npm bin)/ng build --configuration=$STAGE &amp;&amp; \
    $(npm bin)/ng run client:ssr:$STAGE &amp;&amp; \
    npm run webpack:server


# Вторая стадия

# Используем образ PM2
FROM keymetrics/pm2:latest-alpine AS release

# Создаем рабочую директорию
WORKDIR /app

# Копируем из build скомпилированное Angular приложение
# и файл ecosystem
COPY --from=build /app/dist ./dist
COPY --from=build /app/ecosystem.config.js ./

# Шарим порт
EXPOSE 4000

# Команда для запуска
CMD [ &quot;pm2-runtime&quot;, &quot;start&quot;, &quot;ecosystem.config.js&quot; ]
</code></pre>
<p>Многостадийная сборка позволяет значительно уменьшить размер финального образа, так как в него не переносятся установленные пакеты npm.</p>
<h3 id="pm2">PM2</h3>
<p>Для запуска и управления процессами Node.js приложения используем диспетчер <a href="https://pm2.io/doc/en/runtime/integration/docker/">PM2</a>. Создадим его конфигурационный файл:</p>
<h4 id="serverecosystemconfigjs"><code>/server/ecosystem.config.js</code></h4>
<pre><code>module.exports = {
    apps: [{
        name: &quot;server&quot;,
        script: &quot;bin/www&quot;,
        exec_mode: &quot;cluster&quot;,
        instances: &quot;max&quot;,
        instance_var: 'INSTANCE_ID',
        env: {
            NODE_ENV: &quot;development&quot;,
        },
        env_production: {
            NODE_ENV: &quot;production&quot;,
        }
    }]
};
</code></pre>
<p>Приложение запускаем в режиме кластера, а <code>instances: &quot;max&quot;</code> указываем, что необходимо запускать по ноде на каждое ядро процессора.</p>
<p>Аналогичный файл создается и для клиента, приводить его не буду.</p>
<h3 id="">Образы</h3>
<h4 id="">Сборка образов:</h4>
<pre><code>docker build -t server ./server
docker build -t client ./client
</code></pre>
<p>Параметр <code>-t server</code> задает имя и тег образа в формате <code>name:tag</code>.</p>
<h4 id="">Просмотр списка образов:</h4>
<pre><code>docker image ls
</code></pre>
<h4 id="">Удаление образов:</h4>
<pre><code>docker image rm &lt;image_id&gt;

</code></pre>
<p>При этом вводить полный id не обязательно, достаточно первых символов.</p>
<h4 id="">Отправка в репозиторий</h4>
<p>Регистрируемся на <a href="https://hub.docker.com/">Docker Hub</a> и создаем репозиторий.</p>
<p>Предположим, я зарегистрировался там как <code>myusername</code> и создал репозиторй <code>public</code>. Пересоберем наши образы следующим образом:</p>
<pre><code>docker build -t myusername/public:mean-docker-server ./server
docker build -t myusername/public:mean-docker-client ./client
</code></pre>
<p>Теперь можно отправить их:</p>
<pre><code>docker push myusername/public:mean-docker-server
docker push myusername/public:mean-docker-client
</code></pre>
<h4 id="">Вытягивание образов</h4>
<pre><code>docker pull myusername/public:mean-docker-server
</code></pre>
<h4 id="">Остальные команды по работе с образами</h4>
<pre><code>docker image help
</code></pre>
<h3 id="">Контейнеры</h3>
<p>Контейнер включает в себя:</p>
<ul>
<li>Docker-образ;</li>
<li>среду исполнения;</li>
<li>стандартный набор исполняемых команд.</li>
</ul>
<h4 id="">Запуск приложения в контейнере</h4>
<p>Для запуска приложений используем команды:</p>
<pre><code>docker run -d -p 3000:3000 myusername/public:mean-docker-server
docker run -d -p 80:4000 myusername/public:mean-docker-client

</code></pre>
<p>Если образы локально не найдены, то они будут вытянуты из репозитория.</p>
<p>Образы наших приложений будут упакованы в контейнеры и запущены той командой, что мы указали в инструкции <code>CMD</code> Dockerfile.</p>
<p>Параметром <code>-d</code> мы указываем запустить контейнер в фоновом режиме.</p>
<p>Параметр <code>-p</code> осуществляет проброс портов. Так наше клиентское приложение в контейнере доступно на порту 4000, а инструкцией <code>-p</code> мы указываем привязать его на 80 порт хоста. Таким образом наше клиентское приложение будет доступно по адресу <code>localhost:80</code>.</p>
<h4 id="">Просмотр списка контейнеров</h4>
<p>Запущенные контейнеры:</p>
<pre><code>docker container ls
docker ps
</code></pre>
<p>Все, в том числе остановленные:</p>
<pre><code>docker container ls -a
docker ps -a
</code></pre>
<h4 id="">Остановка/запуск контейнеров</h4>
<pre><code>docker container pause &lt;container_id&gt;
docker container unpause &lt;container_id&gt;

docker container stop &lt;container_id&gt;
docker container start &lt;container_id&gt;
</code></pre>
<h4 id="">Выполнение команд в контейнере</h4>
<pre><code>docker exec &lt;options&gt; &lt;container_id&gt; &lt;command&gt;
</code></pre>
<p>Например, часто используется команда, которая позволяет подключиться к командной оболочке контейнера:</p>
<pre><code>docker exec -it &lt;container_id&gt; sh
</code></pre>
<h4 id="">Логи</h4>
<pre><code>docker container logs &lt;container_id&gt; 
</code></pre>
<h4 id="">Удаление контейнера</h4>
<pre><code>docker container rm &lt;container_id&gt;
</code></pre>
<p>Работающие контейнеры нужно либо сперва остановить, либо воспользоваться <code>--force</code>:</p>
<pre><code>docker container rm &lt;container_id&gt; -f
</code></pre>
<h4 id="">Другие команды для работы с контейнерами</h4>
<pre><code>docker container help
</code></pre>
<h2 id="">Очистка</h2>
<p>Команда:</p>
<pre><code>docker system prune
</code></pre>
<p>удалит:</p>
<ul>
<li>все остановленные контейнеры</li>
<li>все неиспользуемые сети</li>
<li>все промежуточные (dangling) образы</li>
<li>весь кэш сборки</li>
</ul>
<p>Для удаления всех неиспользуемых образов добавить параметр <code>-a</code>.</p>
<p>В следующей статье читайте про работу с Docker Compose - инструментом для конфигурирования и запуска мультиконтейнерных приложений Docker.</p>
<p><strong><a href="http://mean-dev.info/mean-stack-docker-part-2">Часть 2 - Docker Compose &gt;&gt;</a></strong></p>
]]></content:encoded></item><item><title><![CDATA[Angular Universal]]></title><description><![CDATA[Настраиваем рендеринг Angular приложения на сервере. Разбираемся с ошибками, боремся с проблемами, тестируем производительность]]></description><link>https://mean-dev.info/angular-universal/</link><guid isPermaLink="false">5bdb79bfc968fc000177b1f7</guid><category><![CDATA[Angular]]></category><dc:creator><![CDATA[Павел Прудников]]></dc:creator><pubDate>Tue, 19 Jun 2018 11:35:00 GMT</pubDate><media:content url="https://mean-dev.info/content/images/2018/06/angular_universal-3.png" medium="image"/><content:encoded><![CDATA[<img src="https://mean-dev.info/content/images/2018/06/angular_universal-3.png" alt="Angular Universal"><p>Сегодня попробую запустить Angular приложение на сервере. Не буду расписывать, что такое Server side rendering (далее SSR) и для чего это нужно, а кратко опишу основные настройки и, более подробно, те проблемы, с которыми мне пришлось столкнуться. А также проведу небольшое тестирование производительности.</p>
<h2 id="ssr">Базовая настройка SSR</h2>
<p>Настройку производил по <a href="https://angular.io/guide/universal">официальному мануалу</a>, поэтому кратко и без особых комментариев. Все самое интересное начинается <a href="#httpinterceptinghandlerisnotaconstructor">после первого запуска</a>.</p>
<h3 id="">Устанавливаем нужные инструменты:</h3>
<pre><code>npm install --save @angular/platform-server @nguniversal/module-map-ngfactory-loader ts-loader @nguniversal/express-engine
</code></pre>
<h3 id="client">Client</h3>
<h4 id="srcappappmodulets"><code>src/app/app.module.ts</code>:</h4>
<pre><code>  imports: [
    BrowserModule.withServerTransition({appId: 'yuor-app-id'}),
    ...
</code></pre>
<p>Angular добавляет appId к стилям, собранным сервером, чтобы их можно было удалить при запуске клиентского приложения.</p>
<h4 id="angularjson"><code>angular.json</code>:</h4>
<pre><code>...
&quot;build&quot;: {
  &quot;builder&quot;: &quot;@angular-devkit/build-angular:browser&quot;,
  &quot;options&quot;: {
    &quot;outputPath&quot;: &quot;dist/browser&quot;,
    ...
  } 
}
...
</code></pre>
<p>Указываем собирать клиент в директории &quot;dist/browser&quot;</p>
<h3 id="server">Server</h3>
<h4 id="srcappappservermodulets"><code>src/app/app.server.module.ts</code>:</h4>
<pre><code>import {NgModule} from '@angular/core';
import {ServerModule} from '@angular/platform-server';
import {ModuleMapLoaderModule} from '@nguniversal/module-map-ngfactory-loader';
import {FlexLayoutServerModule} from '@angular/flex-layout/server';

import {AppModule} from './app.module';
import {AppComponent} from './app.component';

@NgModule({
  imports: [
    AppModule,
    ServerModule,
    ModuleMapLoaderModule,
    FlexLayoutServerModule, // Если юзаете angular/flex-layout
  ],
  providers: [
    // Add universal-only providers here
  ],
  bootstrap: [AppComponent],
})
export class AppServerModule {
}

</code></pre>
<h4 id="srcmainserverts"><code>src/main.server.ts</code>:</h4>
<pre><code>export {AppServerModule} from './app/app.server.module';
</code></pre>
<h4 id="serverts"><code>server.ts</code>:</h4>
<pre><code>import 'zone.js/dist/zone-node';
import 'reflect-metadata';

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

import * as express from 'express';
import {join} from 'path';
import {ngExpressEngine} from '@nguniversal/express-engine';
import {provideModuleMap} from '@nguniversal/module-map-ngfactory-loader';

enableProdMode();

// Express server
const app = express();

const PORT = process.env.PORT || 4000;
const DIST_FOLDER = join(process.cwd(), 'dist');

// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./dist/server/main');


app.engine('html', ngExpressEngine({
  bootstrap: AppServerModuleNgFactory,
  providers: [
    provideModuleMap(LAZY_MODULE_MAP)
  ]
}));

app.set('view engine', 'html');
app.set('views', join(DIST_FOLDER, 'browser'));

// Server static files from /browser
app.get('*.*', express.static(join(DIST_FOLDER, 'browser')));

// All regular routes use the Universal engine
app.get('*', (req, res) =&gt; {
  res.render('index', {req});
});

// Start up the Node server
app.listen(PORT, () =&gt; {
  console.log(`Node server listening on http://localhost:${PORT}`);
});

</code></pre>
<h4 id="srctsconfigserverjson"><code>src/tsconfig.server.json</code>:</h4>
<pre><code>{
  &quot;extends&quot;: &quot;../tsconfig.json&quot;,
  &quot;compilerOptions&quot;: {
    &quot;outDir&quot;: &quot;../out-tsc/app&quot;,
    &quot;baseUrl&quot;: &quot;./&quot;,
    &quot;module&quot;: &quot;commonjs&quot;,
    &quot;types&quot;: []
  },
  &quot;exclude&quot;: [
    &quot;test.ts&quot;,
    &quot;**/*.spec.ts&quot;
  ],
  &quot;angularCompilerOptions&quot;: {
    &quot;entryModule&quot;: &quot;app/app.server.module#AppServerModule&quot;
  }
}
</code></pre>
<h4 id="webpackserverconfigjs"><code>webpack.server.config.js</code>:</h4>
<pre><code>const path = require('path');
const webpack = require('webpack');

module.exports = {
  entry: {server: './server.ts'},
  resolve: {extensions: ['.js', '.ts']},
  target: 'node',
  mode: 'none',
  // this makes sure we include node_modules and other 3rd party libraries
  externals: [/node_modules/],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: '[name].js'
  },
  module: {
    rules: [{test: /\.ts$/, loader: 'ts-loader'}]
  },
  plugins: [
    // Temporary Fix for issue: https://github.com/angular/angular/issues/11580
    // for 'WARNING Critical dependency: the request of a dependency is an expression'
    new webpack.ContextReplacementPlugin(
      /(.+)?angular(\\|\/)core(.+)?/,
      path.join(__dirname, 'src'), // location of your src
      {} // a map of your routes
    ),
    new webpack.ContextReplacementPlugin(
      /(.+)?express(\\|\/)(.+)?/,
      path.join(__dirname, 'src'),
      {}
    )
  ]
};
</code></pre>
<h4 id="angularjson"><code>angular.json</code>:</h4>
<pre><code>&quot;architect&quot;: {
  ...
  &quot;server&quot;: {
    &quot;builder&quot;: &quot;@angular-devkit/build-angular:server&quot;,
    &quot;options&quot;: {
      &quot;outputPath&quot;: &quot;dist/server&quot;,
      &quot;main&quot;: &quot;src/main.server.ts&quot;,
      &quot;tsConfig&quot;: &quot;src/tsconfig.server.json&quot;
    }
  }
  ...
}
</code></pre>
<h4 id="packagejson"><code>package.json</code>:</h4>
<pre><code>&quot;scripts&quot;: {
    ...
    &quot;build:ssr&quot;: &quot;npm run build:client-and-server-bundles &amp;&amp; npm run webpack:server&quot;,
    &quot;serve:ssr&quot;: &quot;node dist/server&quot;,
    &quot;build:client-and-server-bundles&quot;: &quot;ng build --prod &amp;&amp; ng run client:server&quot;,
    &quot;webpack:server&quot;: &quot;webpack --config webpack.server.config.js --progress --colors&quot;,
    ...
}
</code></pre>
<h3 id="">Билд</h3>
<pre><code>npm run build:ssr
</code></pre>
<p>Здесь пришлось доустановить <code>webpack-cli</code>:</p>
<pre><code>npm i -D webpack-cli
</code></pre>
<p>Перезапускаем билд.</p>
<h3 id="">Сервим</h3>
<pre><code>npm run serve:ssr
</code></pre>
<p>В консоли должно появиться:</p>
<pre><code>Node server listening on http://localhost:4000
</code></pre>
<h2 id="">Открываем и преодолеваем</h2>
<h3 id="httpinterceptinghandlerisnotaconstructor"><code>ɵHttpInterceptingHandler is not a constructor</code></h3>
<p>При первом открытии страницы в  браузере я получил ошибку (<a href="https://github.com/angular/universal/issues/1018">Github issue</a>):</p>
<pre><code>TypeError: _angular_common_http__WEBPACK_IMPORTED_MODULE_5__.ɵHttpInterceptingHandler is not a constructor at zoneWrappedInterceptingHandler
</code></pre>
<p>Решается обновлением зависимостей Angular:</p>
<pre><code>ng update @angular/core
</code></pre>
<h3 id="localstorageisnotdefined"><code>localStorage is not defined</code></h3>
<p>У меня в приложении есть сервис <code>TokenService</code>, который отвечает за сохранение и извлечение аутентификационного токена в/из localStorage браузера. Но на сервере нету localStorage. Для решения этой проблемы я создал <code>TokenServerService implements TokenService</code>, в котором исключил все взаимодействие с localStorage. И запровайдил его в <code>app.server.module.ts</code>:</p>
<pre><code>  providers: [
    {provide: TokenService, useClass: TokenServerService}
  ]
</code></pre>
<h3 id="window"><code>window</code></h3>
<p>Та же беда - на сервере отсутствует.</p>
<pre><code>this.onScrollSubscription = window &amp;&amp; observableFromEvent(window, 'scroll')
      .subscribe(() =&gt; {
...
</code></pre>
<pre><code>  ngOnDestroy() {
    this.onScrollSubscription &amp;&amp; this.onScrollSubscription.unsubscribe();
  }
</code></pre>
<h3 id="document"><code>document</code></h3>
<p>Можно поступить как и с <code>window</code>, но я обернул блок кода в проверку платформы:</p>
<pre><code>  constructor(@Inject(PLATFORM_ID) private platformId: Object,
              ...) {
    this.isBrowser = isPlatformBrowser(this.platformId);
  }

  ngOnInit() {
    if(this.isBrowser){
      const stickyBlock = document.querySelector('.sticky-block');
      ...
    }
  }
  ...
</code></pre>
<h3 id="nativeelement"><code>nativeElement</code></h3>
<p>В директиву автофокуса пришлось внести изменения в части проверки места запуска приложения:</p>
<pre><code>  ngOnInit() {
    if (isPlatformBrowser(this.platformId) &amp;&amp; (this._autofocus || typeof this._autofocus === 'undefined')) {
      this.el.nativeElement.focus();
    }
  }
</code></pre>
<h3 id="ng2charts"><code>ng2-charts</code></h3>
<p>С этим модулем на сервере у меня тоже возникли проблемы:</p>
<pre><code>Error: NotYetImplemented
...
</code></pre>
<p>Причина в том, что он использует <code>nativeElement.getContext()</code>. Поэтому пришлось отказаться от рендеринга диаграмм на сервере:</p>
<pre><code>&lt;div class=&quot;mash-chart&quot; *ngIf=&quot;isBrowser&quot;&gt;
  &lt;app-mash-chart&gt;&lt;/app-mash-chart&gt;
&lt;/div&gt;
</code></pre>
<h3 id="mattooltipthis_ariadescriberremovedescriptionisnotafunction"><code>matTooltip: this._ariaDescriber.removeDescription is not a function</code> и прочие аналогичные</h3>
<p>А вот это крайне неприятные ошибки. Возникают не всегда, не везде, но ломают приложение полностью. Вместо ожидаемой страницы показывает стэк трэйс:</p>
<pre><code>ERROR TypeError: this._ariaDescriber.removeDescription is not a function
at MatTooltip.set [as message] (/usr/src/app/server/server.js:219808:33)
at updateProp (/usr/src/app/server/server.js:15266:37)
at checkAndUpdateDirectiveInline (/usr/src/app/server/server.js:15017:19)
at checkAndUpdateNodeInline (/usr/src/app/server/server.js:16326:20)
at checkAndUpdateNode (/usr/src/app/server/server.js:16288:16)
at prodCheckAndUpdateNode (/usr/src/app/server/server.js:16832:5)
at Object.Of.t.ɵvid.e [as updateDirectives] (/usr/src/app/server/server.js:127276:851229)
at Object.updateDirectives (/usr/src/app/server/server.js:16618:29)
at checkAndUpdateView (/usr/src/app/server/server.js:16270:14)
at callViewAction (/usr/src/app/server/server.js:16511:21)
</code></pre>
<p>Воспроизвести можно быстро перейдя по ссылке в момент, пока происходит загрузка стартовой страницы.<br>
Проблема известна и, вроде как, уже <a href="https://github.com/angular/material2/pull/11586">решена</a>. Но решение еще не в релизе.</p>
<p>Также есть ряд похожих ошибок: <a href="https://github.com/angular/material2/issues/11396">focusMonitor.monitor is not a function</a>, <a href="https://github.com/angular/material2/issues/11603">SSR crashes because of undefined functions</a>. Все они связаны с <a href="https://github.com/angular/angular/issues/23715">этой</a> проблемой самого Angular, и их частные решения выглядят какими-то костыльными. А пока добавляем в <code>app.server.module.ts</code>:</p>
<pre><code>import {AriaDescriber, FocusMonitor} from '@angular/cdk/a11y';
import {AutofillMonitor} from '@angular/cdk/text-field';
  ...
  providers: [
    AriaDescriber,
    FocusMonitor,
    AutofillMonitor,
    ...
</code></pre>
<h3 id="flexlayoutdisplayblockfxshowfxhide"><code>flex-layout</code> устанавливает свойство <code>display:block</code> на инлайн элементы с <code>fxShow/fxHide</code></h3>
<p>Слетает выравнивание элементов разметки, ибо <code>span</code>ы, которые я скрываю/показываю в зависимости от ширины окна браузера, внезапно рендерятся как блоки.</p>
<p><a href="https://github.com/angular/flex-layout/issues/724">Issue</a> обещают пофиксить в ближайшем релизе. Можно подождать, можно указать для них <code>display: inline</code>.</p>
<h3 id="">Мерцания</h3>
<p>После загрузки скриптов Universal удаляет пререндеренный контент и загружает вместо него клиентское приложение. Это клиентское приложение начинает обрабатывать роутинг и выполнять http запросы. Если в приложении настроены routing resolvers, то они не будут отображать содержимое компонет в <code>router-outlet</code> до тех пор, пока не получат (резолвят) ответ от сервера. Поэтому в промежуток времени между удалением пререндеренного контента и загрузкой данных с сервера в <code>router-outlet</code> будет пустота. Возникают довольно неприятные мерцания на странице.</p>
<p>Для предотвращения этой проблемы, помимо</p>
<pre><code>// app.module.ts
  imports: [
    BrowserModule.withServerTransition({appId: 'my-app-id'})
    ...
</code></pre>
<p>в файле <code>app-routing.module.ts</code> необходимо для <code>RouterModule</code> указать <code>initialNavigation: 'enabled'</code>:</p>
<pre><code>@NgModule({
  imports: [RouterModule.forRoot(routes, {
    initialNavigation: 'enabled'
  })]
})
</code></pre>
<p>В <code>dev</code> это работает вполне сносно. Однако, <a href="https://github.com/angular/angular/issues/23427">тут</a> и <a href="https://github.com/angular/universal/issues/856">тут</a> пишут, что в <code>prod</code> и с https могут быть проблемы. Требуется дополнительное тестирование.</p>
<h3 id="404500">Коды ответа (404, 500)</h3>
<p>Поисковики ругаются на неправильно настроенное отображение несуществующих страниц:</p>
<blockquote>
<p>Вероятно, на сайте некорректно настроен возврат HTTP-кода 404 Not Found, что может негативно сказаться на индексировании сайта роботом. Настройте возврат кода 404 на запрос несуществующих страниц. ©Яндекс Вэбмастер</p>
</blockquote>
<p>Настроим приложение так, чтобы отрендеренная на сервере страница 404 отдавалась со статусом 404. Для этого в компоненте страницы с ошибкой <code>error-404.component.ts</code> добавим:</p>
<pre><code>import {Component, Inject, OnInit, Optional, PLATFORM_ID} from '@angular/core';
import {RESPONSE} from '@nguniversal/express-engine/tokens';
import {isPlatformServer} from '@angular/common';

@Component({
  selector: 'app-error-404',
  templateUrl: './error-404.component.html'
})
export class Error404Component implements OnInit {

  constructor(@Inject(PLATFORM_ID) private platformId: Object,
              @Optional() @Inject(RESPONSE) private response,
              private appService: AppService) { }

  ngOnInit() {
    if (isPlatformServer(this.platformId)) {
      this.response.status(404);
    }
  }
}
</code></pre>
<p>Инжектим <code>platformId</code> и объект ответа сервера <code>response</code>. Если страница будет рендериться на сервере, то указываем <code>this.response.status(404)</code>.</p>
<p>Требуется только запровайдить этот объект <code>response</code>. Делается это в файле <code>server.ts</code>:</p>
<pre><code>import {REQUEST, RESPONSE} from '@nguniversal/express-engine/tokens';

...

app.get('*', (req, res) =&gt; {
  res.render('index', {
    req, 
    providers: [
      {
        provide: RESPONSE,
        useValue: res
      }
    ]
  });
});
</code></pre>
<p>Аналогично можно поступить и со страницей ошибки 500.</p>
<h3 id="jwt">Проблемы с JWT аутентификацией</h3>
<p>Мое приложение использует json web token аутентификацию пользователей. Если кратко, то пользователь вводит логин/пароль и сервер выдает ему токен. Этот токен сохраняется в localStorage браузера. Все последующие запросы на сервер выполняются с передачей этого токена в хэдере. Но при первом открытии страницы приложения, когда клиент еще не загружен и не может взять токен из хранилища и вставить его в хэдер, браузер выполняет запрос без токена. Universal пытается отрендерить страницу для неаутентифицированного пользователя. И, если данная страница для него не доступна и функция <code>canActivate</code> настроена на возврат страницы 404, то будет отрендерена страница 404. Таким образом получаем следующую картину:</p>
<ul>
<li>пользователь вошел в приложение, в браузере хранится токен;</li>
<li>пользователь жмет F5 на странице, доступной только вошедшим;</li>
<li>браузер запрашивает пререндер этой страницы без токена;</li>
<li>render натыкается на <code>canActivate</code> в <code>Router</code>;</li>
<li><code>canActivate</code> редиректит на страницу 404;</li>
<li>Universal отдает в браузер страницу 404;</li>
<li>пользователь видит в окне браузер страницу ошибки вместо той, которую запрашивал;</li>
<li>и только после загрузки и запуска клиентского приложения будет отрисована нужная страница.</li>
</ul>
<p>И это очень некрасиво с точки зрения UX.</p>
<p>Для решения этой проблемы нужно менять архитектуру приложения и api, использовав для хранения токена <a href="https://github.com/angular/universal-starter/issues/373">cookies</a>. Но мне этого делать ну никак не хочется, поэтому в функции <code>canActivate</code> я добавил проверку платформы и оставил редирект на 404 страницу только для браузера:</p>
<pre><code>  canActivate(route: ActivatedRouteSnapshot,
              state: RouterStateSnapshot) {

    const availableRoles = route.data.availableRoles;
    const userRole = this.userService.isLoggedIn() &amp;&amp; this.userService.getUserRole();

    if (userRole &amp;&amp; availableRoles.includes(userRole)) {
      return true;
    } else if(isPlatformBrowser(this.platformId)) {
      this.router.navigate(['/404'], { skipLocationChange: true });
      return false;
    } else {
      return false;
    }
  }
</code></pre>
<p>Universal возвращает страницу с хэдером, футером, но без основного контента. После загрузки приложения будет отрисовано содержимое защищенной страницы. Или же страница 404, если пользователь не авторизирован. С точки зрения UX это выглядит как постепенная загрузка страницы. Но с точки зрения SEO это не очень хорошо. Если требуется, чтобы защищенные страницы были проиндексированы поисковиками, то нужно все же смотреть в сторону печенек. Или же, как вариант, отлавливать user agent поисковых ботов в <code>server.ts</code>, провайдить эту инфу в <code>CanActivate</code> и давать им доступ.</p>
<h2 id="">Стресс-тесты</h2>
<p>Очень интересно было проверить, а какая же все-таки производительность у серверного рендеринга Angular? Почему-то все измеряют производительность только в миллисекундах, за которые рендерится страница. Мне же еще было интересно, а сколько же страниц одновременно может быть отрендерено без падения сервера? Для этого я воспользовался инструментом <a href="https://artillery.io/">Artillery.io</a>. Выбрал достаточно сложную страницу своего приложения (несколько запросов к API, несколько таблиц с <code>ngFor</code>, много материал компонент), которая больше всего нуждается в индексации поисковиками. В Artillery установил время теста 3 минуты и начал играть с <code>arrivalRate</code>. Начал с <code>arrivalRate = 5</code> (каждую секунду создается 5 новых виртуальных пользователей, делающих запрос выбранной страницы):</p>
<pre><code>artillery quick -d 180 -r 5 http://localhost:4000/my-hard-page

</code></pre>
<p>И вот результат:</p>
<pre><code>arrivalRate: 5:

  Scenarios launched:  900
  Scenarios completed: 900
  Requests completed:  900
  RPS sent: 4.99
  Request latency:
    min: 287.8
    max: 514.2
    median: 324.9
    p95: 413.8
    p99: 445.2
  Scenario counts:
    0: 900 (100%)
  Codes:
    200: 900
</code></pre>
<p>325 ms - ну так себе.</p>
<pre><code>arrivalRate: 7:

  Scenarios launched:  1260
  Scenarios completed: 1260
  Requests completed:  1260
  RPS sent: 6.99
  Request latency:
    min: 287.6
    max: 454.8
    median: 323.4
    p95: 382.7
    p99: 407
  Scenario counts:
    0: 1260 (100%)
  Codes:
    200: 1260
</code></pre>
<p>Похожий результат. Держимся.</p>
<pre><code>arrivalRate: 9:

  Scenarios launched:  1620
  Scenarios completed: 1620
  Requests completed:  1620
  RPS sent: 8.98
  Request latency:
    min: 289.1
    max: 802.6
    median: 332.5
    p95: 583.9
    p99: 697.2
  Scenario counts:
    0: 1620 (100%)
  Codes:
    200: 1620
</code></pre>
<p>А вот на 9 rps уже заметно, что задержка начала расти. Подымаем еще немного:</p>
<pre><code>arrivalRate: 10:

  Scenarios launched:  1800
  Scenarios completed: 1800
  Requests completed:  1800
  RPS sent: 8.09
  Request latency:
    min: 403.1
    max: 51821.2
    median: 16316.1
    p95: 47372.2
    p99: 50949.6
  Scenario counts:
    0: 1800 (100%)
  Codes:
    200: 1800
</code></pre>
<p>И вот он предел - c 10 rps Universal уже не справляется. Тесты проводил на реальном, довольно крупном приложении, на локальной машине с серьезным процессором и 16 ГБ оперативной памяти. На объективность не претендую и какие-либо выводы делать не берусь.</p>
<h2 id="settimeout"><code>setTimeout</code></h2>
<p>Как-то совсем забыл про рекомендацию избавиться от всех <code>setTimeout</code> в приложении. Избавился и провел еще одну серию тестов. В результате для <code>arrivalRate = 5</code> удалось уменьшить среднее время ответа сервера почти в 3 раза (median: 324.9 vs 113):</p>
<pre><code>arrivalRate: 5:

  Scenarios launched:  900
  Scenarios completed: 900
  Requests completed:  900
  RPS sent: 4.99
  Request latency:
    min: 93
    max: 214.2
    median: 113
    p95: 165.8
    p99: 184
  Scenario counts:
    0: 900 (100%)
  Codes:
    200: 900


</code></pre>
<p>Так что обязательно избавляемся от всех <code>setTimeout</code> в SSR!</p>
<p>Однако взять планку в 10 rps это все же не позволило. Время ответа выходит за все разумные пределы:</p>
<pre><code>arrivalRate: 10:

  Scenarios launched:  1800
  Scenarios completed: 1613
  Requests completed:  1613
  RPS sent: 7.85
  Request latency:
    min: 499.4
    max: 69183.9
    median: 39094.5
    p95: 62356.7
    p99: 65200.5
  Scenario counts:
    0: 1800 (100%)
  Codes:
    200: 1613
  Errors:
    ECONNREFUSED: 187
</code></pre>
<h2 id="">Итоги</h2>
<p>А в итоге такое неприятное ощущение, будто использую какую-то поделку, а не продукт, шестой версии, от корпорации добра, громко заявляющий:</p>
<blockquote></blockquote>
<p>DEVELOP ACROSS ALL PLATFORMS<br>
Learn one way to build applications with Angular and reuse your code and abilities to build apps for any deployment target. <a href="https://angular.io/">https://angular.io/</a></p>
<p>Слишком много надо допиливать, выравнивать, чинить и костылить. В статье описал далеко не все возникшие проблемы и сложности - и так много текста получилось.</p>
<p>Очень хотелось бы в комментариях почитать отзывы людей, успешно использующих Angular SSR &quot;в бою&quot;. Как повышаете производительность, как кэшируете, какие еще проблемы были у вас? Или может кто-то уже использует headless браузеры для этих целей?</p>
]]></content:encoded></item><item><title><![CDATA[Кладем плитку из картинок. Masonry grid gallery на Angular Material своими руками]]></title><description><![CDATA[Разбираемся, как самостоятельно разложить плитки с изображениями в стиле Masonry на Angular Material, добавляем адаптивности Flex-Layout и приправляем анимациями]]></description><link>https://mean-dev.info/masonry-grid-gallery/</link><guid isPermaLink="false">5bdb79bfc968fc000177b1f6</guid><category><![CDATA[Angular]]></category><category><![CDATA[Material]]></category><dc:creator><![CDATA[Павел Прудников]]></dc:creator><pubDate>Tue, 08 May 2018 19:46:00 GMT</pubDate><media:content url="https://mean-dev.info/content/images/2018/05/masonry-grid-header-4.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://mean-dev.info/content/images/2018/05/masonry-grid-header-4.jpg" alt="Кладем плитку из картинок. Masonry grid gallery на Angular Material своими руками"><p>Ух, давненько ничего не писал, но тут созрел повод. На моем вялотекущем проекте встала задача добавить пользователям возможность размещать и просматривать фотографии. Было решено оформить массив загруженных фоток в стиле <a href="https://www.pinterest.com/">Pinterest</a>.</p>
<p>После легкого курса гуглотерапии было усвоено следующее:</p>
<ul>
<li>данная компоновка сетки из картинок носит условное название &quot;Masonry&quot; <em>(да-да, масоны - сообщество вольных плиточников)</em>;</li>
<li>есть библиотека, которая позволяет вертикально укладывать фотки будто плитки - <a href="https://masonry.desandro.com/">Masonry</a>;</li>
<li>у нее нет официальной поддержки Angular;</li>
<li>ее адаптации сторонних разработчиков под Angular либо очень сырые, либо заброшены (сори, без примеров и ссылок, гуглите и оценивайте сами, применяйте на свой страх и риск);</li>
<li>реализация плиточной кладки на чистом CSS на момент начала 2018 года <a href="https://regisphilibert.com/blog/2017/12/pure-css-masonry-layout-with-flexbox-grid-columns-in-2018/">не возможна</a>;</li>
<li>готовые решения в статьях показывают не совсем верную реализацию (высота ряда равна высоте наибольшей плитки, плитки располагаются по колонкам и т.п.);</li>
<li>есть интересная <a href="https://medium.com/@andybarefoot/a-masonry-style-layout-using-css-grid-8c663d355ebb">статья</a>, как можно реализовать требуемый эффект на <a href="https://www.w3schools.com/css/css_grid.asp">CSS Grid</a> с помощью малой толики javascript.</li>
</ul>
<p><strong>Вывод:</strong> это нужно пилить самостоятельно и писать по этому поводу статью ;)</p>
<p>Для наглядности <a href="https://masonry-grid-gallery-final.stackblitz.io/">демо</a> того, что должно получиться в итоге.</p>
<p>Ну что ж, приступим?</p>
<h2 id="">Постановка задачи</h2>
<p>Думаю, что при просмотре <a href="https://masonry-grid-gallery-final.stackblitz.io/">демки</a>, общий принцип работы данного компонента и способ построения плиток в сетке становится сразу понятен. Но все же обобщу требования:</p>
<ul>
<li>имеется массив из картинок;</li>
<li>данные изображения требуется расположить на странице таким образом, чтобы они занимали все её свободное пространство;</li>
<li>ширина всех картинок одинакова и вычисляется исходя из ширины страницы и заданного количества колонок;</li>
<li>картинки по порядку располагаются в ряд;</li>
<li>если картинку в текущем ряду поместить невозможно, то она переносится ниже;</li>
<li>перенесенная картинка должна быть помещена в колонку с наименьшей высотой;</li>
<li>последующие картинки располагаются также в колонки с наименьшей высотой (с наибольшим свободным пространством под ними).</li>
</ul>
<p>Дополнительные требования (адаптивность/responsive design):</p>
<ul>
<li>количество колонок определяется разработчиком для различных диапазонов ширины окна браузера.</li>
</ul>
<p>Дополнительные требования (декоративные):</p>
<ul>
<li>визуальный эффект перемещения картинок при изменении размера экрана;</li>
<li>визуальный эффект при загрузке/добавлении изображения;</li>
<li>визуальный эффект приближения при наведении курсора;</li>
<li>визуальный эффект при клике.</li>
</ul>
<h2 id="">Инструменты</h2>
<p>В моем приложении, а следовательно и в примере данной статьи, используются:</p>
<ul>
<li>Angular ~5.2.10;</li>
<li>Material ~5.2.4;</li>
<li>Flex-layout ~5.0.0.</li>
</ul>
<h2 id="">Принцип</h2>
<p>Для построения сетки изображений будет использоваться компонент <a href="https://material.angular.io/components/grid-list/overview">Material Grid List</a>. Он позволяет задавать следующие параметры:</p>
<ul>
<li><code>cols</code> - количество колонок сетки;</li>
<li><code>rowHeight</code> - высота каждого ряда;</li>
<li><code>gutterSize</code> - ширина шва между плитками сетки.</li>
</ul>
<p>Для каждой конкретной плитки задаются:</p>
<ul>
<li><code>colspan</code> - количество колонок, которое будет занято плиткой;</li>
<li><code>rowspan</code> - количество рядов, занимаемых плиткой.</li>
</ul>
<p>Итак, основная идея заключается в том, что мы устанавливаем очень малую высоту ряда (<code>rowHeight=&quot;1px&quot;</code>), картинки растягиваем на всю ширину плитки (<code>style=&quot;width: 100%;&quot;</code>). Высота каждой картинки будет вычислена пропорционально ее базовым размерам. Нам остается только извлечь ее и передать в качестве значения количества рядов (<code>[rowspan]=&quot;imageHeight&quot;</code>).</p>
<p><img src="https://mean-dev.info/content/images/2018/05/Masonry-Grid.jpg" alt="Кладем плитку из картинок. Masonry grid gallery на Angular Material своими руками"></p>
<h2 id="">Реализация</h2>
<p>Наше приложение будет состоять из  трех компонентов:</p>
<ul>
<li><code>app.component</code> - главный компонент, в котором будем генерировать массив картинок и передавать его в галерею;</li>
<li><code>grid-gallery.component</code> - собственно компонент самой галереи;</li>
<li><code>grid-gallery-item.component</code> - элемент галереи, плитка, в которой будет располагаться изображение.</li>
</ul>
<p>Начнем с реализации основного функционала, а далее доработаем его в части адаптивности и декоративности.</p>
<h3 id="">Интерфейс изображения</h3>
<pre><code class="language-typescript">//app/image.model.ts

export interface Image {
  src: string,
  alt?: string
}
</code></pre>
<h3 id="">Главный компонент приложения</h3>
<pre><code class="language-typescript">//app/app.component.ts

import {Component, Input} from '@angular/core';
import {Image} from &quot;./image.model&quot;;

@Component({
  selector: 'app-component',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.scss'],
})
export class AppComponent {

  numOfImages = 10; // Требуемое количество сгенерированных изображений
  images: Image []; // Массив изображений

  constructor() {
    this.images = this.generateImagesList();
  }

  private generateImagesList(): Image[] {
    const images: Image[] = [];
    for (let i = 0; i &lt; this.numOfImages; i++){
      const image = this.generateRandomImage();
      image.alt = `#${i}`;
      images.push(image);
    }
    return images;
  }

  private generateRandomImage(): Image {
    const width = 600;
    const height = (Math.random() * (1000 - 400) + 400).toFixed();
    return {src: `https://picsum.photos/${width}/${height}/?random`};
  }

  addImage() {
    const image = this.generateRandomImage();
    image.alt = `#${this.images.length}`;
    this.images.push(image);
  }
}
</code></pre>
<p>Генерируем массив из 10 изображений. Для этого используем сервис <a href="https://picsum.photos/">picsum.photos</a>. Высота изображения устанавливается случайным образом из диапазона 400 - 1000 px, ширина - 600 px. Ну и метод для добавления изображения.</p>
<pre><code class="language-html">&lt;!-- app/app.component.html --&gt;

&lt;!-- Header --&gt;
&lt;div class=&quot;toolbar&quot; fxFlex='100%' fxLayout=&quot;row&quot; fxLayoutAlign=&quot;end center&quot;&gt;
  &lt;button mat-raised-button color=&quot;primary&quot; (click)=&quot;addImage()&quot;&gt;ADD IMAGE&lt;/button&gt;
&lt;/div&gt;

&lt;!-- Gallery --&gt;
&lt;app-grid-gallery [images]=&quot;images&quot; [cols]=&quot;4&quot; [rowHeight]=&quot;1&quot;&gt;&lt;/app-grid-gallery&gt;
</code></pre>
<p>В шаблоне будет хэдер с кнопкой для добавления картинки и компонент галереи <code>app-grid-gallery</code>. В него мы передаем наш массив с фотками, указываем требуемое количество колонок (4) и указываем высоту рядов (как писал выше - 1 px).</p>
<p>Ну и приступаем непосредственно к самой галерее.</p>
<h3 id="">Компонент галереи</h3>
<pre><code class="language-typescript">// app/grid-gallery/grid-gallery.component.ts

import {Component, Input} from '@angular/core';
import {Image} from &quot;../image.model&quot;;

@Component({
  selector: 'app-grid-gallery',
  templateUrl: './grid-gallery.component.html'
})
export class GridGalleryComponent{

  @Input() images: Image[];
  @Input() cols: number = 4; // Количество колонок
  @Input() rowHeight: number = 1; // Высота рядов, px
  @Input() gutterSize: number = 1; // Ширина шва, px
}
</code></pre>
<p>Здесь практически пусто. Определяем входные параметры компонента и указываем их дефолтные значения. В дальнейшем здесь также будем следить за шириной окна браузера и изменять количество колонок. Но для начала этого достаточно.</p>
<p>В шаблоне поинтереснее:</p>
<pre><code class="language-html">&lt;!-- app/grid-gallery/grid-gallery.component.html --&gt;

&lt;mat-grid-list [cols]=&quot;cols&quot;
               [rowHeight]=&quot;rowHeight&quot;
               [gutterSize]=&quot;gutterSize+'px'&quot;&gt;

  &lt;mat-grid-tile *ngFor=&quot;let imageItem of images&quot;
                 [rowspan]=&quot;item.rows&quot;&gt;

    &lt;app-grid-gallery-item  #item 
                            [image]=&quot;imageItem&quot; 
                            [rowHeight]=&quot;rowHeight&quot; 
                            [gutterSize]=&quot;gutterSize&quot;&gt;
    &lt;/app-grid-gallery-item&gt;

  &lt;/mat-grid-tile&gt;

&lt;/mat-grid-list&gt;
</code></pre>
<p><code>mat-grid-list</code> и <code>mat-grid-tile</code> - компоненты Material Grid List. В цикле пробегаем по массиву изображений, генерируя для каждого <code>mat-grid-tile</code>. В каждую плитку вставляем компонент <code>app-grid-gallery-item</code>. Его рассмотрим чуть ниже.</p>
<p>Тут важный момент: для каждого элемента мы создаем шаблонную переменную <code>#item</code> и привязываем свойство компонента с количеством строк к параметру <code>rowspan</code> плитки:</p>
<pre><code class="language-html">[rowspan]=&quot;item.rows&quot;
</code></pre>
<h3 id="">Компонент элемента галереи</h3>
<p>Основное назначение данного компонента - следить за высотой картинки и вычислять количество рядов, которое должна занимать плитка, исходя из заданных <code>rowHeight</code> и <code>gutterSize</code>.</p>
<pre><code class="language-typescript">// app/grid-gallery/grid-gallery-item/grid-gallery-item.component.ts

import {Component, ElementRef, Input, ViewChild} from '@angular/core';
import {Image} from &quot;../../image.model&quot;;

@Component({
  selector: 'app-grid-gallery-item',
  templateUrl: './grid-gallery-item.component.html',
  styleUrls: ['./grid-gallery-item.component.scss']
})
export class GridGalleryItemComponent {

  @Input() image: Image;
  @Input() rowHeight: number = 1;
  @Input() gutterSize: number = 1;

  @ViewChild('img') img: ElementRef;

  public rows: number = 0; // Число рядов, используемое для rowspan mat-grid-tile

  calculateRows() {
    this.rows = Math.floor(this.img.nativeElement.offsetHeight / (this.rowHeight + this.gutterSize));
  }
}
</code></pre>
<p>Здесь нужно обратить внимание на метод <code>calculateRows</code>. Он отвечает за обновление высоты нашей плитки. Округляем в меньшую сторону для того, чтобы изображение немного обрезалось,  а не оставалось пустое пространство между картинками.</p>
<pre><code class="language-html">&lt;!-- app/grid-gallery/grid-gallery-item/grid-gallery-item.component.html --&gt;

&lt;img  #img 
      [src]=&quot;image?.src&quot; 
      [alt]=&quot;image?.alt&quot;/&gt;
</code></pre>
<p>В шаблоне все просто. Используем переменную шаблона <code>#img</code> для доступа к ней из класса через <code>@ViewChild</code>. Передаем src и alt.</p>
<p>Ну и немножко стилей:</p>
<pre><code class="language-sass">// app/grid-gallery/grid-gallery-item/grid-gallery-item.component.scss

:host {
  height: 100%; 

  img {
    width: 100%;
  }
}
</code></pre>
<p>Устанавливаем высоту компонента в 100% и растягиваем картинку на всю ширину плитки.</p>
<p>Теперь нужно все это оживить. Для этого необходимо определить моменты, когда вызывать метод <code>calculateRows</code>. Во-первых, нам нужно просчитать количество строк сразу после отрисовки компонента. На ум приходит использовать хуки жизненного цикла Angular: <code>OnInit</code>, <code>AfterViewInit</code> или <code>AfterViewChecked</code>. Однако эти методы будут вызываться до того, как успеет подгрузиться изображение, а следовательно количество рядов не будет вычислено. Для решения этой задачи привяжем вызов метода <code>calculateRows</code> к событию <code>(load)</code> нашего изображения:</p>
<pre><code class="language-html">&lt;!-- app/grid-gallery/grid-gallery-item/grid-gallery-item.component.html --&gt;

&lt;img  #img 
      [src]=&quot;image?.src&quot; 
      [alt]=&quot;image?.alt&quot;
      (load)=&quot;calculateRows()&quot;/&gt;
</code></pre>
<p>Отлично! Теперь после загрузки приложения наши картинки будут выстраиваться плитками в нужном нам порядке. Но... При изменении ширины окна браузера картинки будут изменять свой размер, а количество рядов, занимаемых плиткой, пересчитано не будет. Поэтому, во-вторых, нужно добавить декоратор <code>@HostListener</code> к методу <code>calculateRows</code> для обработки события изменения размеров окна:</p>
<pre><code class="language-typescript">// app/grid-gallery/grid-gallery-item/grid-gallery-item.component.ts

...
  @HostListener('window:resize')
  calculateRows() {
  ...
  }
</code></pre>
<p>Посмотреть работу компонента и код на этой стадии можно <a href="https://stackblitz.com/edit/masonry-grid-gallery-base">здесь</a>.</p>
<p>Далее приступаем к добавлению отзывчивого дизайна.</p>
<h3 id="">Добавляем адаптивность</h3>
<p>В разделе постановки задач я привел дополнительное требование, касающееся адаптивности (англ. Adaptive Web Design). Требуется обеспечить возможность указывать для компонента галереи количество колонок в зависимости от ширины окна браузера. То есть мы хотим указать, что при открытии приложения на экранах мобильных устройств с шириной до 600 px, все наши изображения должны быть выстроены в 1 колонку, для экранов с шириной 600-960 px - в две колонки, и т.д. Здесь нам и понадобится модуль <a href="https://github.com/angular/flex-layout">Angular Flex-Layout</a>.</p>
<pre><code class="language-typescript">// app/grid-gallery/grid-gallery.component.ts

import {Component, Input, OnInit, OnDestroy} from '@angular/core';
import {MediaChange, ObservableMedia} from '@angular/flex-layout';
import {Subscription} from &quot;rxjs/Subscription&quot;;
import {Image} from &quot;../image.model&quot;;

@Component({
  selector: 'app-grid-gallery',
  templateUrl: './grid-gallery.component.html'
})
export class GridGalleryComponent implements  OnInit, OnDestroy {

  @Input() images: Image[];
  @Input() cols: number = 4;
  @Input('cols.xs') cols_xs: number = 1;
  @Input('cols.sm') cols_sm: number = 2;
  @Input('cols.md') cols_md: number = 3;
  @Input('cols.lg') cols_lg: number = 4;
  @Input('cols.xl') cols_xl: number = 6;
  @Input() rowHeight: number = 1;
  @Input() gutterSize: number = 1;

  mediaWatcher: Subscription;

  constructor(private media: ObservableMedia) {
  }

  ngOnInit(){
    this.mediaWatcher = this.media.subscribe((change: MediaChange) =&gt; {
      this.cols = this[`cols_${change.mqAlias}`];
     });
  }

  ngOnDestroy(): void {
    this.mediaWatcher.unsubscribe();
  }
}

</code></pre>
<p>Добавляем <code>@Input</code> свойства <code>cols_xs</code>, <code>cols_sm</code>, <code>cols_md</code>, <code>cols_lg</code> и <code>cols_xl</code> для компонента галереи и указываем их значения по умолчанию. Почитать про использование медиа псевдонимов (<code>aliases</code>) можно <a href="https://github.com/angular/flex-layout/wiki/Responsive-API">здесь</a>.</p>
<p>Для отслеживания события перехода от одного медиа диапазона к другому воспользуемся сервисом <code>ObservableMedia</code>. Подпишемся на <code>MediaChange</code> и из него будем вытягивать актуальный alias, а затем обновлять параметр <code>cols</code> для <code>mat-grid-list</code>.</p>
<p>Указывать количество колонок в шаблоне можно следующим образом:</p>
<pre><code class="language-html">&lt;!-- app/app.component.html --&gt;

&lt;!-- Gallery --&gt;
&lt;app-grid-gallery [images]=&quot;images&quot; 
                  cols.xs=&quot;1&quot;
                  cols.sm=&quot;2&quot;
                  cols.md=&quot;3&quot;
                  cols.lg=&quot;4&quot;
                  cols.xl=&quot;6&quot;&gt;
&lt;/app-grid-gallery&gt;
</code></pre>
<p><a href="https://stackblitz.com/edit/masonry-grid-gallery-responsive">Код приложения на текущем этапе</a>.</p>
<h3 id="">Наводим лоск</h3>
<p>Добавим немного декоративной мишуры.</p>
<p>Во-первых, обернем наши изображения в кнопки <code>mat-button</code>. Это добавит эффект волны при клике по ним.</p>
<pre><code class="language-html">&lt;!-- app/grid-gallery/grid-gallery-item/grid-gallery-item.component.html --&gt;

&lt;button mat-button&gt;
  &lt;img  #img 
        [src]=&quot;image?.src&quot; 
        [alt]=&quot;image?.alt&quot;
        (load)=&quot;calculateRows()&quot;/&gt;
&lt;/button&gt;
</code></pre>
<p>В стилях сбросим padding для кнопок и зададим эффект медленного приближения картинки при наведении мыши:</p>
<pre><code class="language-sass">// app/grid-gallery/grid-gallery-item/grid-gallery-item.component.scss

:host {
  height: 100%; 

  button {
    padding: 0;
  }

  img {
    width: 100%;
    transition: transform 90s;
    transform: scale(1) translateZ(0); 
  }

  &amp;:hover {
    img {
      transition: transform 30s;
      transform: scale(1.33) translateZ(0); 
    }
  }
}
</code></pre>
<p>Ну и напоследок добавим анимацию появления и перемещения плиток:</p>
<pre><code class="language-sass">// app/grid-gallery/grid-gallery.component.scss

mat-grid-tile {
  transition: top 0.3s, left 0.3s, height 0.3s;
}
</code></pre>
<p><a href="https://stackblitz.com/edit/masonry-grid-gallery-final">Финальная версия компонента</a>.</p>
<p>Надеюсь, что кому-нибудь это будет полезно. Буду рад вопросам, замечаниям и здоровой критике в комментариях. Спасибо.</p>
]]></content:encoded></item><item><title><![CDATA[Прилипающий футер (sticky footer) на Angular 2+ и Flex-layout]]></title><description><![CDATA[Задача старая, инструменты новые. Приклеиваем футер к полу. Теперь на Angular с его новым движком компоновки Flex-layout]]></description><link>https://mean-dev.info/sticky-footer-angular-2-flex-layout/</link><guid isPermaLink="false">5bdb79bfc968fc000177b1f5</guid><category><![CDATA[Angular]]></category><dc:creator><![CDATA[Павел Прудников]]></dc:creator><pubDate>Thu, 13 Jul 2017 22:17:59 GMT</pubDate><media:content url="http://mean-dev.info/content/images/2016/09/superhero-paper2.png" medium="image"/><content:encoded><![CDATA[<img src="http://mean-dev.info/content/images/2016/09/superhero-paper2.png" alt="Прилипающий футер (sticky footer) на Angular 2+ и Flex-layout"><p>Задача старая, инструменты новые. Приклеиваем футер к полу. Теперь на Angular с его новым движком компоновки <a href="https://github.com/angular/flex-layout">Flex-layout</a>.</p>
<p>Пусть мы имеем файл <code>app.component.ts</code> с шаблоном вида:</p>
<pre><code>  template: `
    &lt;div class=&quot;app-container&quot;&gt;
      &lt;div class=&quot;header&quot;&gt;&lt;/div&gt;
      &lt;div class=&quot;body&quot;&gt;&lt;/div&gt;      
      &lt;div class=&quot;footer&quot;&gt;&lt;/div&gt;
    &lt;/div&gt;`
</code></pre>
<p>Для начала зададим высоту 100% для родительских элементов в файле <code>style.css</code>:</p>
<pre><code>html, body, .app-container {
    height: 100%;
}
</code></pre>
<p>В шаблоне для <code>.app-container</code> зададим модель выравнивания в колонку с растягиванием по ширине:</p>
<pre><code>&lt;div class=&quot;app-container&quot; fxLayout=&quot;column&quot; fxLayoutAlign=&quot;start stretch&quot;&gt;
...
</code></pre>
<p>Между блоками <code>.body</code> и <code>.footer</code> требуется добавить вспомогательный div, который будет занимать все свободное пространство между ними, когда контента до футера не хватает.</p>
<pre><code>&lt;div class=&quot;helper&quot; fxFlex fxFlexFill&gt;&lt;/div&gt;
</code></pre>
<p>Здесь важно заметить, что требуется указать и <code>fxFlex</code>, и <code>fxFlexFill</code>, хотя из документации выглядит, что прописывать <code>fxFlex</code> не требуется. Однако без него не работает.</p>
<p>Ну и собственно все! Можно поиграться с <a href="https://embed.plnkr.co/pal27yypI1ABBzFe8ZOj/">Plunker</a>:</p>
<iframe style="width: 100%; height:600px" src="https://embed.plnkr.co/pal27yypI1ABBzFe8ZOj/" frameborder="1" allowfullscren="allowfullscren"></iframe>
<p>У этого способа есть еще одно преимущество - футер может быть динамической высоты. Т.е. не обязательно задавать фиксированную высоту для него, а она может определяться содержимым и размером окна. При этом футер все равно будет оставаться снизу и не будет перекрывать основной контент.</p>
]]></content:encoded></item><item><title><![CDATA[Кнопка для загрузки файлов в Angular 2 Material]]></title><description><![CDATA[Стилизуем html кнопку для загрузки файлов input type="file" под Angular 2 Material]]></description><link>https://mean-dev.info/knopka-dlia-zaghruzki-failov-v-angular-2-material/</link><guid isPermaLink="false">5bdb79bfc968fc000177b1f4</guid><category><![CDATA[Angular]]></category><category><![CDATA[Material]]></category><dc:creator><![CDATA[Павел Прудников]]></dc:creator><pubDate>Fri, 19 May 2017 16:45:03 GMT</pubDate><media:content url="http://mean-dev.info/content/images/2016/09/superhero-paper2.png" medium="image"/><content:encoded><![CDATA[<img src="http://mean-dev.info/content/images/2016/09/superhero-paper2.png" alt="Кнопка для загрузки файлов в Angular 2 Material"><p>Как мы знаем, для загрузки файлов в html есть соответствующий тэг:</p>
<pre><code>&lt;input type=&quot;file&quot;&gt;
</code></pre>
<p>И выглядит он довольно убого, не стильно, не модно. Как-то так:</p>
<input type="file">
<p>Если вы работаете с Angular 2 Material, и пытаетесь найти этот элемент, то смею вас огорчить. Вот ответ разработчика:</p>
<p><img src="https://mean-dev.info/content/images/2017/05/material2-not-support-input-file.png" alt="Кнопка для загрузки файлов в Angular 2 Material"></p>
<p>Ну что же, будем действовать самостоятельно. Как оказалось, запилить свою, стилизованную под material кнопку загрузки, не так то сложно. Будем отталкиваться от этого:</p>
<pre><code>    &lt;input type=&quot;file&quot; accept=&quot;image/&quot; id=&quot;file-input&quot;&gt;
    &lt;label for=&quot;file-input&quot;&gt;
        Загрузить файл
    &lt;/label&gt;
</code></pre>
<p>Выглядит на форме это так:</p>
<input type="file" accept="image/" id="file-input">
<label for="file-input">Загрузить файл</label>
<p>Здесь нужно обратить внимание на то, что по клику на <code>label</code> мы получаем необходимую нам реакцию - открытие окна для выбора файла.</p>
<p>Далее в него мы и помещаем нашу красивую кнопку, использовав стиль angular material:</p>
<pre><code>    &lt;label for=&quot;file-input&quot;&gt;
      &lt;a md-raised-button class=&quot;btn&quot;&gt;&lt;i class=&quot;material-icons&quot;&gt;file_upload&lt;/i&gt; 
        Загрузить файл
      &lt;/a&gt;
    &lt;/label&gt;
</code></pre>
<p>И осталось только скрыть уродливый html input с помощью <code>hidden=&quot;true&quot;</code>:</p>
<pre><code>    &lt;input hidden=&quot;true&quot; type=&quot;file&quot; id=&quot;file-input&quot;&gt;
    &lt;label for=&quot;file-input&quot;&gt;
      &lt;a md-raised-button class=&quot;btn&quot;&gt;&lt;i class=&quot;material-icons&quot;&gt;file_upload&lt;/i&gt; 
        Загрузить файл
      &lt;/a&gt;
    &lt;/label&gt;
</code></pre>
<p>Проверяем:<br>
<img src="https://mean-dev.info/content/images/2017/05/angular-2-upload-button-1.png" alt="Кнопка для загрузки файлов в Angular 2 Material"></p>
<p>Вот и все. Должно работать.</p>
]]></content:encoded></item><item><title><![CDATA[Angular 2 - управляем тегами title, meta description и meta robots]]></title><description><![CDATA[Задавать теги title, meta description и meta robots в Angular 2 теперь можно с помощью классов Title и Meta]]></description><link>https://mean-dev.info/angular2-title-meta/</link><guid isPermaLink="false">5bdb79bfc968fc000177b1f3</guid><category><![CDATA[Angular]]></category><dc:creator><![CDATA[Павел Прудников]]></dc:creator><pubDate>Tue, 11 Apr 2017 22:31:57 GMT</pubDate><media:content url="http://mean-dev.info/content/images/2016/09/superhero-paper2.png" medium="image"/><content:encoded><![CDATA[<img src="http://mean-dev.info/content/images/2016/09/superhero-paper2.png" alt="Angular 2 - управляем тегами title, meta description и meta robots"><p>Чтобы оптимизировать страницы нашего приложения для поисковых систем нам необходимо иметь возможность управлять такими тегами как title, meta description и meta robots. Для этого в Angular 2 появились классы <a href="https://angular.io/docs/ts/latest/api/platform-browser/index/Title-class.html">Title</a> и <a href="https://angular.io/docs/ts/latest/api/platform-browser/index/Meta-class.html">Meta</a>. На момент написания статьи данные классы в документации помечены как экспериментальные. Так что использовать на свой страх и риск.</p>
<p>Создадим сервис <code>app.service.ts</code> со следующим содержимым:</p>
<pre><code>import {Injectable} from '@angular/core';
import {Title, Meta} from '@angular/platform-browser';

@Injectable()
export class AppService {

  constructor(private titleService: Title, private metaService: Meta) {  }

  setPageTitle(title: string) {
    this.titleService.setTitle(title);
  }

  setPageDescription(description: string) {
    this.metaService.updateTag({name: 'description', content: description});
  }
    
  setMetaRobots(robots: string) {
    this.metaService.updateTag({name: 'robots', content: robots});
  }
}
</code></pre>
<p>В модуле <code>app.module.ts</code> не забываем указать его в списке провайдеров:</p>
<pre><code>import {AppService} from './app.service'

@NgModule({
  providers: [
    AppService
  ]
})
</code></pre>
<p>Теперь в других компонентах можем использовать наш сервис для задания тегов:</p>
<pre><code>import {Component, OnInit} from '@angular/core';
import {AppService} from './app.service'

@Component({
  selector: 'app-component',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {

  constructor(private appService: AppService) {
  }

  ngOnInit() {
    this.appService.setPageTitle('My app page title');
    this.appService.setPageDescription('My app page description');
    this.appService.setMetaRobots('Index, Follow');
  }    
}
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Добавляем аутентификацию в REST API на Swagger Node]]></title><description><![CDATA[Добавляем JSON Web Token (JWT) аутентификацию в RESTful API, написанный на Node.js и Swagger, с использованием passport.js и его стратегий Basic и JWT.]]></description><link>https://mean-dev.info/authentication-rest-api-swagger/</link><guid isPermaLink="false">5bdb79bfc968fc000177b1f2</guid><category><![CDATA[Node.js]]></category><category><![CDATA[Swagger]]></category><category><![CDATA[mongoDB]]></category><dc:creator><![CDATA[Павел Прудников]]></dc:creator><pubDate>Mon, 12 Dec 2016 16:13:08 GMT</pubDate><media:content url="https://mean-dev.info/content/images/2016/11/Swagger_API_back-3.png" medium="image"/><content:encoded><![CDATA[<img src="https://mean-dev.info/content/images/2016/11/Swagger_API_back-3.png" alt="Добавляем аутентификацию в REST API на Swagger Node"><p>В предыдущих статьях я описал <a href="https://mean-dev.info/restful-api-node-js-swagger/">создание RESTful API</a> для коллекции книг на Swagger Node.js и <a href="https://mean-dev.info/swagger-pagination/">добавление пагинации</a> - возможности разбивать на части большой список книг в ответе сервера. Теперь же попробуем решить следующую проблему: абсолютно любой пользователь API может как просматривать книги, так и добавлять новые, редактировать и удалять их. Я хочу сделать так, чтобы операции добавления, редактирования и удаления книг были доступны только зарегистрированным пользователям. Для этого требуется встроить в API систему аутентификации пользователей.</p>
<h2 id="">Общие данные</h2>
<h3 id="">Определения</h3>
<p><strong>Аутентификация</strong> - процедура проверки подлинности пользователя, например, путём сравнения введённого им пароля с паролем, сохранённым в базе данных пользователей.</p>
<p><strong>Авторизация</strong> - предоставление определённому пользователю прав на выполнение определённых действий.</p>
<p>Т.о. для того, чтобы авторизировать пользователя на выполнение некоторых операций, мы  должны его сперва аутентифицировать.</p>
<h3 id="">Применяемые стратегии аутентификации</h3>
<h4 id="basic">Basic</h4>
<p>Пользователь вводит логин и пароль. Введенные данные объединяются в строку вида:</p>
<pre><code>username:password
</code></pre>
<p>Данная строка кодируется в Base64:</p>
<pre><code>dXNlcm5hbWU6cGFzc3dvcmQ=
</code></pre>
<p>И передается на сервер в заголовке запроса в виде:</p>
<pre><code>Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
</code></pre>
<p>На сервере строка декодируется, находится профиль пользователя, верифицируется его пароль.</p>
<p>По этой схеме данные пользователя, по сути, передаются в открытом виде и https обязателен к применению.</p>
<h4 id="jsonwebtokenjwt">JSON Web Token (JWT)</h4>
<p>Для подтверждения своей личности пользователь должен предоставить специальный JSON Web Token. Этот токен представляет собой строку вида:</p>
<pre><code>eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwidXNlcm5hbWUiOiJKb2huIERvZSJ9.4emg-RQp4p0i9tDfjhhYDdlFEgHYalkQlbminY_0iSc
</code></pre>
<p>Эта строка состоит из трех частей, разделенных точками:</p>
<p><strong>1. Заголовок (header)</strong> - строка, закодированная в Base64, представляющая собой JSON объект:</p>
<pre><code>{
  &quot;alg&quot;: &quot;HS256&quot;,
  &quot;typ&quot;: &quot;JWT&quot;
}
</code></pre>
<p>Обычно содержит в себе сведения о типе токена и применяемом алгоритме шифрования.</p>
<p><strong>2. Полезная нагрузка (payload)</strong> - строка, закодированная в Base64, представляющая собой JSON объект. Содержит в себе некоторую полезную информацию о пользователе, например:</p>
<pre><code>{
  &quot;sub&quot;: &quot;1234567890&quot;,
  &quot;name&quot;: &quot;John Doe&quot;,
  &quot;admin&quot;: false
}
</code></pre>
<p><strong>3. Подпись</strong> - строка, вычисленная по указанному в заголовке алгоритму шифрования:</p>
<pre><code>HMACSHA256(
  base64UrlEncode(header) + &quot;.&quot; +
  base64UrlEncode(payload),
  secret)
</code></pre>
<p><code>secret</code> - секретная фраза, которую знает только сервер, генерирующий токены и проверяющий их.</p>
<p>Получив данный токен, сервер с помощью секретной фразы шифрует по заданному алгоритму заголовок и нагрузку. Затем сверяет подпись, получившуюся при шифровании, и переданную в токене. Если они совпадают, то токен считается валидным и пользователь проходит аутентификацию. Если же в нагрузку токена внесены какие-то изменения, например <code>&quot;admin&quot;: true</code>, то подписи не совпадут и аутентификация будет провалена.</p>
<p>Завладев чужим токеном, злоумышленник получает его права доступа. Поэтому необходимо использовать <code>https</code>. Также эта стратегия позволяет добавить в токен срок жизни, по истечении которого требуется обновить токен или получить новый.</p>
<h2 id="api">Проектируем API</h2>
<p>В конце файла <code>swagger.yaml</code> добавим описание наших стратегий аутентификации:</p>
<pre><code>securityDefinitions:
  Basic:
    type: basic
  JWT:
    type: apiKey
    name: Authorization
    in: header
</code></pre>
<p>Конечные точки API (операции добавления, редактирования и удаления книг) будут защищены с помощью JWT. Т.е. пользователь, который захочет добавить книгу, сможет это сделать, только предоставив в заголовке запроса токен.</p>
<p>Выдавать токены будем по маршруту <code>GET /login</code>. Для получения токена пользователь должен ввести свое имя и пароль, т.е. пройти Basic аутентификацию. Соответственно, чтобы пройти ее, пользователь предварительно должен быть зарегестрирован в нашем приложении. Регистрация новых пользователей будет производиться по маршруту <code>POST /signup</code>.</p>
<p>Добавим в <code>swagger.yaml</code> файл описания для пользователя и для токена:</p>
<pre><code>User:
  properties:
    username: 
      type: string
    password:
      type: string
      format: password
  required:
    - username
    - password
    
Token:
  properties:
    user:
      type: string
    token:
      type: string
  required:
    - user
    - token
</code></pre>
<p>Опишем маршрут для регистрации новых пользователей <code>POST /signup</code>:</p>
<pre><code>/signup:
  x-swagger-router-controller: auth-controller
  post:
    operationId: signup
    parameters:
      - name: user
        in: body
        description: New User
        required: true
        schema:
          $ref: '#/definitions/User'
    responses:
      &quot;200&quot;:
        description: User created
        schema:
          $ref: &quot;#/definitions/GeneralResponse&quot;
       # responses may fall through to errors
      default:
        description: Error
        schema:
          $ref: &quot;#/definitions/ErrorResponse&quot;
</code></pre>
<p>Маршрут для получения токена <code>GET /login</code>:</p>
<pre><code>/login:
  x-swagger-router-controller: auth-controller
  get:
    operationId: login
    security:
      - Basic: [] # Так указываем применяемую схему!
    responses:
      &quot;200&quot;:
        description: Log In
        schema:
          $ref: &quot;#/definitions/Token&quot;
       # responses may fall through to errors
      default:
        description: Error
        schema:
          $ref: &quot;#/definitions/ErrorResponse&quot;
</code></pre>
<p>И теперь в описаниях операций, которые мы хотим защитить (<code>POST /books</code>, <code>PUT /books/{id}</code> и <code>DELETE /books/{id}</code>), нужно добавить строки:</p>
<pre><code>  security:
    - JWT: []
</code></pre>
<p>С YAML-файлом закончили. Приступаем к написанию логики.</p>
<h2 id="api">Реализуем функционал аутентификации API</h2>
<p>Нам потребуется <a href="http://passportjs.org/">passport.js</a>, две стратегии аутентификации, <a href="https://github.com/jaredhanson/passport-http">passport-http</a> и <a href="https://github.com/themikenicholson/passport-jwt">passport-jwt</a>, а также <a href="https://github.com/auth0/node-jsonwebtoken">jsonwebtoken</a> - модуль для работы с JWT токенами:</p>
<pre><code>npm install passport passport-http passport-jwt jsonwebtoken -save
</code></pre>
<p>Определим mongoose схему и модель для пользователя в файле <code>api/models/user-model.js</code>:</p>
<pre><code>var crypto = require('crypto');
var mongoose = require('mongoose');
var Schema = mongoose.Schema;

// User
var User = new Schema({
    username: {
        type: String,
        unique: true,
        required: true
    },
    hashedPassword: {
        type: String,
        required: true
    },
    salt: {
        type: String,
        required: true
    },
    created: {
        type: Date,
        default: Date.now
    }
});

// Шифруем пароль
User.methods.encryptPassword = function (password) {
    return crypto.createHmac('sha1', this.salt).update(password).digest('hex');
};    

// Аксессоры для пароля
User.virtual('password')
    .set(function (password) {
        this._plainPassword = password;
        this.salt = crypto.randomBytes(32).toString('base64');
        this.hashedPassword = this.encryptPassword(password);
    })
    .get(function () {
        return this._plainPassword;
    });

// Проверка пароля
User.methods.checkPassword = function (password) {
    return this.encryptPassword(password) === this.hashedPassword;
};

// Проверка на уникальность 'username'
User.path('username').validate(function(value, done) {
    this.model('User').count({ username: value }, function(err, count) {
        if (err) {
            return done(err);
        }
        done(!count);
    });
}, 'username already exists');

var UserModel = mongoose.model('User', User);

module.exports = UserModel;
</code></pre>
<p>Пароли пользователей хранить в БД в открытом виде не будем, а будем хранить случайно сгенерированную соль и хэш пароля, полученный с помощью этой соли. При проверке будем сравнивать не сами пароли, а хэш пароля, переданного пользователем, с хэшем, хранимым в БД.</p>
<p>Создадим файл конфигурации <code>config/config.js</code>, в котором зададим секретную фразу. С ее помощью будут подписываться наши токены. Там же зададим срок жизни токенов:</p>
<pre><code>const config = {
    jwt: {
        secret: 'my_supper_extra_mega_uber_secret_phrase',
        expiresIn: '2 days'
    }
};

module.exports = config;
</code></pre>
<p>В файле <code>api/auth/passport.js</code> опишем работу наших стратегий аутентификации:</p>
<pre><code>var passport = require('passport');
var BasicStrategy = require('passport-http').BasicStrategy;
var JwtStrategy = require('passport-jwt').Strategy,
    ExtractJwt = require('passport-jwt').ExtractJwt;

var config = require('../../config/config');
var User = require('../models/user-model');

// Basic стратегия
passport.use(new BasicStrategy(
    function(username, password, done) {
        User.findOne({ username: username }, function (err, user) {
            if (err) {
                return done(err);
            }
            if (!user) {
                return done(null, false);
            }
            if (!user.checkPassword(password)) {
                return done(null, false);
            }
            return done(null, user);
        });
    }
));

// JWT стратегия
var opts = {};
opts.jwtFromRequest = ExtractJwt.fromAuthHeader();
opts.secretOrKey = config.jwt.secret;

passport.use(new JwtStrategy(opts, function(jwt_payload, done) {
    User.findOne({username: jwt_payload.username}, function(err, user) {
        if (err) {
            return done(err);
        }
        if (!user) {
            return done(null, false);
        }
        return done(null, user);
    });
}));

module.exports = passport;
</code></pre>
<p>В <code>api/helpers/security-handlers.js</code> напишем обработчики безопасности, которые будет использовать Swagger:</p>
<pre><code>var passport = require('../auth/passport');

function Basic (req, def, scopes, callback) {
    passport.authenticate('basic', {session: false}, function (err, user, info) {
        if (err) {
            callback(new Error('Authentication error'));
        } else if (!user) {
            callback(new Error('User not found'));
        } else {
            req.user = user;
            callback();
        }
    })(req, null, callback);
}

function JWT (req, def, scopes, callback) {
    passport.authenticate('jwt', {session: false}, function (err, user, info) {
        if (err) {
            callback(new Error('Authentication error'));
        } else if (!user) {
            callback(new Error('User not found'));
        } else {
            req.user = user;
            callback();
        }
    })(req, null, callback);
}

module.exports = {
    Basic: Basic,
    JWT: JWT
};
</code></pre>
<p>Эти обработчики требуется подключить в конфиге SwaggerExpress в файле <code>app.js</code>:</p>
<pre><code>var passport = require('./api/auth/passport');
var secHandlers = require ('./api/helpers/security-handlers');

app.use(passport.initialize());

...

var config = {
    appRoot: __dirname,
    swaggerSecurityHandlers: {
        Basic: secHandlers.Basic,
        JWT: secHandlers.JWT
    }
};

SwaggerExpress.create(config, function (err, swaggerExpress) {

...
</code></pre>
<p>Именно их названия мы указывали для операций в поле <code>security</code> swagger.yaml файла.</p>
<p>Осталось написать контроллеры для регистрации новых пользователей и для получения токена по логину и паролю. В файле <code>api/controllers/auth-controller.js</code> пропишем:</p>
<pre><code>var jwt = require('jsonwebtoken');

var config = require('../../config/config');
var User = require('../models/user-model');

module.exports = {
    signup: signup,
    login: login
};

// Регистрация новых пользователей
function signup(req, res) {
    var user = new User (req.swagger.params.user.value);
    user.save(function (err) {
        if (err) {
            console.error(err);
            res.status(500).json({message: err.message});
        } else {
            res.json({message: 'OK'});
        }
    })
}

// Аутентификация пользователей по логину и паролю, выдача токена
function login(req, res) {
    if (req.isAuthenticated()) {
        res.json({
            user: req.user.username,
            token: 'JWT ' + jwt.sign({username: req.user.username}, config.jwt.secret, {expiresIn: config.jwt.expiresIn})
        });
    }
}
</code></pre>
<p>Теперь можно проверять работу аутентификации.</p>
<h2 id="">Тестируем аутентификацию</h2>
<p>Запускаем mongod, выполняем <code>swagger project start</code>, <code>swagger project edit</code>. В Swagger Editor зарегистрируем нового пользователя по маршруту <code>POST /signup</code>.</p>
<p>В правой части редактора вверху появились блоки, которые позволяют аутентифицироваться:</p>
<p><img src="https://mean-dev.info/content/images/2016/12/Auth-fields.png" alt="Добавляем аутентификацию в REST API на Swagger Node"></p>
<p>Введем логин и пароль нашего тестового пользователя в Basic форму. Затем по маршруту <code>GET /login</code> получим токен для нашего пользователя:</p>
<p><img src="https://mean-dev.info/content/images/2016/12/Get-token.png" alt="Добавляем аутентификацию в REST API на Swagger Node"></p>
<p>Попробуем создать книгу по маршруту <code>POST /books</code>, который требует наличие JWT токена:</p>
<p><img src="https://mean-dev.info/content/images/2016/12/Need-JWT.png" alt="Добавляем аутентификацию в REST API на Swagger Node"></p>
<p>В ответе получим ошибку &quot;User not found&quot;.</p>
<p>А теперь аутентифицируемся по JWT, вставив в поле <code>key</code> токен, который мы получили ранее.</p>
<p><img src="https://mean-dev.info/content/images/2016/12/JWT-auth.png" alt="Добавляем аутентификацию в REST API на Swagger Node"></p>
<p>И вновь попробуем создать книгу:</p>
<p><img src="https://mean-dev.info/content/images/2016/12/JWT-auth-ok.png" alt="Добавляем аутентификацию в REST API на Swagger Node"></p>
<p>Аутентификация пройдена, новая книга успешно добавлена.</p>
<p>Ну как-то так. Весь код на <a href="https://github.com/twoheaded/swagger-node-example/tree/auth">Github</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Пагинация для RESTful API на Swagger Node]]></title><description><![CDATA[Разбиваем ответ Swagger RESTful API сервера, состоящий из большого списка элементов, на части с использованием параметров count, skip и limit.]]></description><link>https://mean-dev.info/swagger-pagination/</link><guid isPermaLink="false">5bdb79bfc968fc000177b1f1</guid><category><![CDATA[Swagger]]></category><category><![CDATA[Node.js]]></category><category><![CDATA[mongoDB]]></category><dc:creator><![CDATA[Павел Прудников]]></dc:creator><pubDate>Wed, 30 Nov 2016 16:47:00 GMT</pubDate><media:content url="https://mean-dev.info/content/images/2016/11/Swagger_API_back-3.png" medium="image"/><content:encoded><![CDATA[<img src="https://mean-dev.info/content/images/2016/11/Swagger_API_back-3.png" alt="Пагинация для RESTful API на Swagger Node"><p>В <a href="https://mean-dev.info/restful-api-node-js-swagger">предыдущей статье</a> мы написали на Swagger Node небольшой простенький API для работы с коллекцией книг. Однако там есть нюанс. Предположим, у нас собралась большая библиотека из нескольких десятков тысяч книг. Каждый раз, когда мы делаем запрос <code>GET /books</code>, наш API отдает полную коллекцию. Это отражается на скорости работы нашего приложения и количестве переданного трафика. Поэтому в этой статье попробуем организовать частичную выдачу книг.</p>
<h2 id="api">Редактируем API</h2>
<p>Для организации постраничной выдачи всех наших книг нам нужно 3 параметра:</p>
<ul>
<li><code>count</code> - общее количество книг в коллекции;</li>
<li><code>limit</code> - количество книг в одном ответе (показываемых на одной странице);</li>
<li><code>skip</code> - количество пропущенных книг от начала и до первой из текущей выдачи (количество книг на предыдущих страницах).</li>
</ul>
<p>Зная эти параметры в клиентском приложении легко можно рассчитать общее количество страниц, номер текущей страницы, номера последних страниц.</p>
<p><code>skip</code> и <code>limit</code> мы будем передавать в качестве параметров запроса. <code>count</code> должен вернуть в ответе сервер.</p>
<p>В файле <code>swagger.yaml</code> в самом конце добавим описания параметров:</p>
<pre><code>parameters:
  skipParam:
    name: skip
    in: query
    description: number of items to skip
    required: false
    type: integer
    format: int32
    minimum: 0
  limitParam:
    name: limit
    in: query
    description: max records to return
    required: false
    type: integer
    format: int32
    maximum: 100
    minimum: 0
</code></pre>
<p>Ограничим минимальное значение для <code>skip</code> и <code>limit</code> и максимальное значение для <code>limit</code>. Передавать их будем в query запроса.</p>
<p>Добавим описание схемы для ответа с постраничной выдачей книг в <code>definitions:</code>:</p>
<pre><code>Books:
  properties:
    paging:
      type: object
      properties:
        skip:
          type: integer
        limit: 
          type: integer
        count: 
          type: integer
    data:
      type: array
      items:
        $ref: '#/definitions/Book'
  required:
      - paging
      - data
</code></pre>
<p>Как видим, теперь ответ сервера должен содержать объект <code>paging</code> с параметрами пагинации и массив с книгами <code>data</code>.</p>
<p>Подправим маршрут <code>GET /books</code> следующим образом:</p>
<pre><code>get:
  description: Return a books list
  parameters:
    - $ref: &quot;#/parameters/skipParam&quot;
    - $ref: &quot;#/parameters/limitParam&quot;
  responses:
    &quot;200&quot;:
      description: Success 
      schema:
        $ref: '#/definitions/Books'
    # responses may fall through to errors
    default:
      description: Error
      schema:
        $ref: &quot;#/definitions/ErrorResponse&quot;
</code></pre>
<p>Перезапустим сервер в mock-режиме:</p>
<pre><code>swagger project start -m
</code></pre>
<p>и попробуем отправить запрос.</p>
<p><img src="https://mean-dev.info/content/images/2016/11/Swagger_editor_paging.png" alt="Пагинация для RESTful API на Swagger Node"></p>
<p>Работает. Можно браться за контроллер.</p>
<h2 id="">Модифицируем контроллер</h2>
<p>Заменим код функции <code>getAll</code> на следующий:</p>
<pre><code>function getAll(req, res) {
    var skip = req.swagger.params.skip.value ? req.swagger.params.skip.value : 0;
    var limit = req.swagger.params.limit.value ? req.swagger.params.limit.value : 10;

    Book.find().count(function (err, count) {

        Book.find().skip(skip).limit(limit).exec(function (err, books) {
            if (err) {
                throw err;
            } else {
                res.json({
                        paging: {
                            count: count,
                            skip: skip,
                            limit: limit
                        },
                        data: books
                    }
                );
            }
        });
    });
}
</code></pre>
<p>Сперва считываем параметры <code>skip</code> и <code>limit</code> и присваиваем значения по умолчанию, если они не заданы. Затем выполняем запрос в БД для определения общего количества книг <code>count</code>. А потом с заданными параметрами делаем соответствующую выборку книг из БД и отдаем ответ клиенту.</p>
<p>Убедимся, что в swagger.yaml для <code>GET /books</code> задан <code>operationId: getAll</code>. Создадим несколько книг методом <code>POST /books</code> и попробуем поиграться с параметрами пагинации.</p>
<p><img src="https://mean-dev.info/content/images/2016/11/Swagger_editor_paging_result.png" alt="Пагинация для RESTful API на Swagger Node"></p>
<p>Посмотреть код целиком можно в<br>
<a href="https://github.com/twoheaded/swagger-node-example/tree/pagination">этой</a> ветке на гитхабе.</p>
<p>Можете критиковать.</p>
]]></content:encoded></item><item><title><![CDATA[Пишем RESTful API на Node.js с использованием Swagger]]></title><description><![CDATA[Для разработки RESTful API на Node.js воспользуемся фреймворком Swagger. Swagger Editor позволяет работать с YAML-файлом и тестировать API  в самом редакторе]]></description><link>https://mean-dev.info/restful-api-node-js-swagger/</link><guid isPermaLink="false">5bdb79bfc968fc000177b1f0</guid><category><![CDATA[Node.js]]></category><category><![CDATA[mongoDB]]></category><category><![CDATA[Swagger]]></category><dc:creator><![CDATA[Павел Прудников]]></dc:creator><pubDate>Tue, 29 Nov 2016 20:23:02 GMT</pubDate><media:content url="https://mean-dev.info/content/images/2016/11/Swagger_API_back-3.png" medium="image"/><content:encoded><![CDATA[<img src="https://mean-dev.info/content/images/2016/11/Swagger_API_back-3.png" alt="Пишем RESTful API на Node.js с использованием Swagger"><p>Можно разработать просто лучший REST API, но без документации им просто никто не сможет пользоваться. <strong>Swagger</strong> - популярный фреймворк с развитой экосистемой инструментов, который помогает проектировать, писать, документировать и работать с RESTful API. Для Node.js есть модуль <a href="https://github.com/swagger-api/swagger-node">Swagger Node</a>. С его помощью можно спроектировать API, используя mock-объекты, а только потом реализовать логику, написав соответствующие контроллеры. В данной статье мы воспользуемся подобной тактикой и попробуем Swagger в действии.</p>
<h2 id="">Установка и запуск примера</h2>
<p>Для начала глобально установим swagger:</p>
<pre><code>npm install -g swagger
</code></pre>
<p>Теперь создадим новый проект. Пускай это будет коллекция книг:</p>
<pre><code>swagger project create books
</code></pre>
<p>В диалоге выбираем сервер <em>express</em>.  Swagger сгенерирует нам следующую структуру:</p>
<pre><code>    -- api  
    ---- controllers 
    ------ hello_world.js
    ---- helpers
    ---- mocks
    ---- swagger
    ------ swagger.yaml

    -- config 
    ---- default.yaml

    -- test
    ---- api
    ------ controllers
    -------- hello_world.js
    ------ helpers

    -- app.js
    -- package.json
</code></pre>
<p><code>app.js</code> - это главный файл, который запускает наш сервер.</p>
<p>В директории <code>api</code> находится все, что касается нашего API: контроллеры, вспомогательные модули, mock-объекты. Здесь следует обратить внимание на файл <code>api/swagger/swagger.yaml</code>. В этом файле собственно и находится все описание API в формате <a href="http://yaml.org/">YAML</a>.</p>
<p>В файле <code>config/default.yaml</code> находится описание конфигурации swagger.</p>
<p>В каталоге <code>test</code> располагаются тесты для нашего кода.</p>
<p>При создании нового проекта swagger сгенерировал пример обработки GET заброса по маршруту <code>/hello</code>. Поэтому мы можем видеть контроллер <code>hello_world.js</code> и тест для него. Давайте попробуем запустить этот пример в редакторе <a href="http://editor.swagger.io/">Swagger Editor</a>.</p>
<p>Запускаем сервер:</p>
<pre><code>swagger project start
</code></pre>
<p>Сервер следит за файлами и при изменении перезапускается автоматически.</p>
<p>Теперь в другой командной строке запускаем редактор:</p>
<pre><code>swagger project edit
</code></pre>
<p>Должно открыться окно браузера и там вы должны увидеть такую картину:</p>
<p><img src="https://mean-dev.info/content/images/2016/11/Swagger_editor.png" alt="Пишем RESTful API на Node.js с использованием Swagger"></p>
<p>Слева отображается собственно наш файл <code>swagger.yaml</code> с возможностью его редактирования. А вот справа находится самое интересное - красивый список маршрутов и методов. Их можно сворачивать/разворачивать и при этом сворачивается/разворачивается код в файле. Можно удобно переходить между маршрутами. По клику на методе открывается подробная информация о параметрах, принимаемых сервером, и возможных вариантах ответа. И прямо тут можно протестировать каждый метод. Нажимаем кнопку <code>Try this operation</code>, вводим имя в поле <code>name</code>, кликаем на <code>Send Request</code> и смотрим на ответ сервера:</p>
<p><img src="https://mean-dev.info/content/images/2016/11/Swagger_editor_2.png" alt="Пишем RESTful API на Node.js с использованием Swagger"></p>
<p>Теперь подробнее рассмотрим yaml-файл.</p>
<p>В самом его начале указывается формат запросов и ответов:</p>
<pre><code># формат тела запроса, который клиент может отправить (Content-Type)
consumes:
  - application/json
# формат ответа клиенту (Accepts)
produces:
  - application/json
</code></pre>
<p>Далее следует описание маршрута:</p>
<pre><code>paths:
    /hello:
        x-swagger-router-controller: hello_world 
        get:
            description: Returns 'Hello' to the caller
            # метод контроллера
            operationId: hello
            parameters:
                - name: name
                in: query
                description: The name of the person to whom to say hello
                required: false
                type: string
            responses:
                &quot;200&quot;:
                    description: Success
                    schema:
                        # ссылка на определение
                        $ref: &quot;#/definitions/HelloWorldResponse&quot;
                # могут быть и ошибки
                default:
                    description: Error
                    schema:
                        $ref: &quot;#/definitions/ErrorResponse&quot;
</code></pre>
<p>Здесь:</p>
<ul>
<li><code>x-swagger-router-controller</code> - название файла-контроллера (<code>/api/controllers/hello_world.js</code>);</li>
<li><code>operationId</code> - функция в этом контроллере, которая обрабатывает данный запрос;</li>
<li><code>parameters</code> - перечень необходимых параметров. В нашем случае только один необязательный (<code>required: false</code>) строковый (<code>type: string</code>) параметр <code>name</code>, который должен находиться в строке запроса (<code>in: query</code>);</li>
<li><code>responses</code> - список возможных ответов сервера. Для статуса &quot;200&quot; через ссылку <code>$ref</code> определена схема <code>HelloWorldResponse</code>, а для ошибки - <code>ErrorResponse</code>. Данные определения располагаются в конце файла.</li>
</ul>
<p>Еще взглянем на функцию <code>hello</code> в контроллере:</p>
<pre><code>function hello(req, res) {
    var name = req.swagger.params.name.value || 'stranger';
    var hello = util.format('Hello, %s!', name);
    res.json(hello);
}
</code></pre>
<p>Здесь стоит обратить внимание на то, что теперь у объекта <code>req</code> есть свойство <code>swagger</code> с информацией о параметрах.</p>
<p>Теперь, пожалуй, можно приступать к написанию своего API.</p>
<h2 id="mock">Включаем mock-режим</h2>
<p>Для того чтобы сосредоточиться исключительно на написании самого API и не переключаться на написание кода, можно запустить сервер в mock-режиме:</p>
<pre><code>swagger project start -m
</code></pre>
<p>Теперь сервер будет отдавать ответы, сгенерированные на основании описанных схем. Строку <code>operationId</code> можно не указывать. Так для <code>/hello</code> в этом режиме сервер вернет ответ:</p>
<pre><code>{
  &quot;message&quot;: &quot;Sample text&quot;
}
</code></pre>
<h3 id="getbooks">GET /books</h3>
<p>Данный роут должен возвращать список всех книг. По поводу пагинации и разбиения выдачи на части здесь заморачиваться не будем - это тема для <a href="https://mean-dev.info/swagger-pagination/">отдельной статьи</a>.</p>
<p>Удаляем пример с <code>/hello</code> роутом и добавляем в <code>paths</code> следующие строки:</p>
<pre><code>/books:
  x-swagger-router-controller: book-controller
  get:
    description: Return a books list
    responses:
      &quot;200&quot;:
        description: Success 
        schema:
          type: array
          items:
            $ref: '#/definitions/Book'
      default:
        description: Error
        schema:
          $ref: &quot;#/definitions/ErrorResponse&quot;
</code></pre>
<p>Здесь мы указываем, что будем использовать контроллер <code>book-controller</code>. Определяем две схемы для ответа сервера:</p>
<pre><code>definitions:
  Book:
    properties:
      id:
        type: string
      title:
        type: string
      author:
        type: string
      year:
        type: integer
    required:
      - title
      - author
      - year  
  ErrorResponse:
    properties:
      message:
        type: string
    required:
      - message
</code></pre>
<p>Эти определения располагаем в конце файла.</p>
<p>Сервер может вернуть нам либо список (<code>type: array</code>) книг со статусом &quot;200&quot;, либо ошибку с обязательным полем <code>message</code>.</p>
<p>Попробуем протестировать наше API:</p>
<p><img src="https://mean-dev.info/content/images/2016/11/Books_GET_Response.png" alt="Пишем RESTful API на Node.js с использованием Swagger"></p>
<p>Вернулся автоматически сгенерированный ответ. Для поля, тип которого указан как строковый, возвращается строка &quot;Sample text&quot;. Для целочисленного поля - число. Если данный вариант не устраивает, то в директории <code>/api/mocks</code> можно создать свой mock-контроллер <code>book-controller</code> и задать ссылку на его функцию в поле <code>operationId</code>.</p>
<h3 id="postbooks">POST /books</h3>
<p>Создадим роут для добавления новой книги. Следом за <code>get</code> добавим следующие строки:</p>
<pre><code>post:
  description: Add a new book
  parameters:
  - name: book
    description: Book properties
    in: body
    required: true
    schema:
      $ref: &quot;#/definitions/Book&quot;
  responses:
    &quot;200&quot;:
      description: Success
      schema:
        $ref: &quot;#/definitions/GeneralResponse&quot;
    default:
      description: Error
      schema:
        $ref: &quot;#/definitions/ErrorResponse&quot;
</code></pre>
<p>Также нам нужно определить форму ответа сервера в случае успешного добавления книги. В <code>definitions</code> добавим:</p>
<pre><code>GeneralResponse:
  properties:
    message:
      type: string
  required:
    - message
</code></pre>
<p>Попробуем протестировать:</p>
<p><img src="https://mean-dev.info/content/images/2016/11/Swagger_editor_post.png" alt="Пишем RESTful API на Node.js с использованием Swagger"></p>
<p>Как видим, редактор предлагает ввести параметры и указывает, какие поля обязательны для заполнения.</p>
<h3 id="getbooksid">GET /books/{id}</h3>
<p>Теперь попробуем получить одну книгу. Для этого нужно создать новый маршрут <code>/books/{id}</code>, где <code>id</code> - идентификатор книги - параметр, передаваемый в самом url запроса. В блок <code>paths</code>  добавим следующие строки:</p>
<pre><code>/books/{id}:
  x-swagger-router-controller: book-controller
  get:
    description: Return Book by id
    parameters:
    - name: id
      type: string
      in: path
      required: true
    responses:
      &quot;200&quot;:
        description: Success
        schema:
          $ref: '#/definitions/Book'
      default:
        description: Error
        schema:
          $ref: &quot;#/definitions/ErrorResponse&quot;
</code></pre>
<h3 id="putbooksid">PUT /books/{id}</h3>
<p>Данный роут используется для изменения книги с <code>id</code>, указанным в url запроса. Изменяемые параметры передаются в теле запроса.</p>
<pre><code>put:
  description: Update a book
  parameters:
  - name: id
    description: Book id
    type: string
    in: path
    required: true
  - name: book
    description: Book properties
    in: body
    required: true
    schema:
      $ref: &quot;#/definitions/Book&quot;
  responses:
    &quot;200&quot;:
      description: Success
      schema:
        $ref: &quot;#/definitions/GeneralResponse&quot;
    default:
      description: Error
      schema:
        $ref: &quot;#/definitions/ErrorResponse&quot;
</code></pre>
<h3 id="deletebooksid">DELETE /books/{id}</h3>
<p>Последняя операция - удаление по заданному <code>id</code>. Копипастим следующий код:</p>
<pre><code>delete:
  description: Delite a book by ID
  parameters:
  - name: id
    description: Book id
    type: string
    in: path
    required: true
  responses:
    &quot;200&quot;:
      description: Success
      schema:
        $ref: &quot;#/definitions/GeneralResponse&quot;
    default:
      description: Error
      schema:
        $ref: &quot;#/definitions/ErrorResponse&quot;
</code></pre>
<p>Ну вот мы и спроектировали API для нашей библиотеки. Теперь можно приступать к написанию кода.</p>
<h2 id="api">Реализуем функционал API</h2>
<p>Для хранения нашей коллекции книг будем использовать MongoDB. Убедитесь, что у вас она у вас установлена и запущена.</p>
<p>Для работы с базой данных установим Mongoose ODM:</p>
<pre><code>npm install mongoose -save
</code></pre>
<p>В файле <code>app.js</code> добавим зависимость:</p>
<pre><code>var mongoose = require('mongoose');
</code></pre>
<p>и подключимся к базе <code>books</code>:</p>
<pre><code>SwaggerExpress.create(config, function(err, swaggerExpress) {
  if (err) { throw err; }

  mongoose.connect('mongodb://localhost/books');
  ...
</code></pre>
<p>Ниже можно удалить условие проверки пути <code>/hello</code>. Для нас это больше не актуально.</p>
<h3 id="bookmodel">Book model</h3>
<p>В каталоге <code>api</code> создадим директорию <code>models</code> и разместим в ней файл с моделью книги <code>book-model.js</code>:</p>
<pre><code>var mongoose = require('mongoose');
var Schema = mongoose.Schema;

var bookSchema = new Schema({
    title:  String,
    author: String,
    year:   Number
});

var Book = mongoose.model('Book', bookSchema);

module.exports = Book;
</code></pre>
<h3 id="bookcontroller">Book controller</h3>
<p>В директории <code>/api/controllers</code> создадим файл <code>book-controller.js</code>:</p>
<pre><code>var Book = require('../models/book-model');

module.exports = {
    getAll: getAll,
    create: create,
    find: find,
    update: update,
    remove: remove
};

function getAll(req, res) {
    Book.find({}, function (err, books) {
        if (err) {
            throw err;
        } else {
            res.json(books);
        }
    });
}

function create(req, res) {
    var newBook = Book(req.swagger.params.book.value);

    newBook.save(function (err) {
        if (err) {
            throw err
        } else {
            res.json({message: 'OK'});
        }
    });
}

function find(req, res) {
    var bookId = req.swagger.params.id.value;

    Book.findById(bookId, function (err, book) {
        if (err) {
            throw err;
        } else if (!book) {
            res.status(404).json({message: 'Book not found'})
        } else {
            res.json({title: book.title, author: book.author, year: book.year});
        }
    });
}

function update(req, res) {
    var bookId = req.swagger.params.id.value;
    var newBook = req.swagger.params.book.value;

    Book.findByIdAndUpdate(bookId, newBook, function (err, book) {
        if (err) {
            throw err;
        } else {
            res.json({message: 'OK'});
        }
    });
}

function remove(req, res) {
    var bookId = req.swagger.params.id.value;

    Book.findByIdAndRemove(bookId, function (err) {
        if (err) {
            throw err;
        } else {
            res.json({message: 'OK'});
        }
    });
}
</code></pre>
<p>Осталось в редакторе swagger.yaml прописать для каждой операции свой <code>operationId</code>, соответствующий названию функции в контроллере:</p>
<pre><code>  # в /books:
    get:
      operationId: getAll

    post:
      operationId: create

  # в /books/{id}:

    get:
      operationId: find

    put:
      operationId: update

    delete:
      operationId: remove
</code></pre>
<p>и перезапустить сервер без mock-режима:</p>
<pre><code>swagger project start
</code></pre>
<p>Скриншоты работы контроллеров и результаты ответов сервера уже не буду выкладывать. И так вышло многабукоф. Можете протестировать самостоятельно. Код данного примера можно найти на <a href="https://github.com/twoheaded/swagger-node-example">github</a>. Комментарии приветствуются.</p>
]]></content:encoded></item><item><title><![CDATA[Как внедрить систему комментариев Disqus в AngularJS приложение]]></title><description><![CDATA[Настраиваем комментарии статей на страницах приложения, написанного на AngularJS, с использованием системы Disqus]]></description><link>https://mean-dev.info/angularjs-disqus/</link><guid isPermaLink="false">5bdb79bfc968fc000177b1ef</guid><category><![CDATA[AngularJS]]></category><dc:creator><![CDATA[Павел Прудников]]></dc:creator><pubDate>Thu, 24 Nov 2016 23:30:43 GMT</pubDate><media:content url="https://mean-dev.info/content/images/2016/09/angularjs2.png" medium="image"/><content:encoded><![CDATA[<img src="https://mean-dev.info/content/images/2016/09/angularjs2.png" alt="Как внедрить систему комментариев Disqus в AngularJS приложение"><p>Disqus - это простой, удобный и бесплатный сервис, который позволяет добавить возможность комментирования и обсуждения на страницы вашего сайта. В данном блоге используется этот сервис. Также его я решил внедрить и в свой проект, написанный на AngularJS. Однако простого руководства я не нашел, а при добавлении сторонних решений столкнулся с некоторыми трудностями. Об этом и попробую рассказать в данной статье.</p>
<h2 id="">Поиск готовых решений</h2>
<p>Гуглим. Находим несколько проектов на гитхабе. Некоторые отметаем из-за срока давности, некоторые из-за малого количества звезд. В итоге я остановился вот на <a href="https://github.com/michaelbromley/angularUtils/tree/master/src/directives/disqus">этом</a> решении. Инструкция по применению выглядит просто. Код тоже весьма лаконичен и представляет собой AngularJS директиву, которую в нужном месте требуется добавить в шаблон. В контроллере прописать пару строк конфига и все. Поехали!</p>
<h2 id="">Установка</h2>
<ol>
<li>Скачиваем файл <code>dirDisqus.js</code>,</li>
</ol>
<ul>
<li>
<p>либо <code>bower install angular-utils-disqus</code>,</p>
</li>
<li>
<p>либо <code>npm install angular-utils-disqus</code></p>
</li>
</ul>
<ol start="2">
<li>
<p>Добавляем этот файл в index.html:</p>
<pre><code> &lt;script src=&quot;modules/components/dirDisqus.js&quot;&gt;&lt;/script&gt;
</code></pre>
</li>
<li>
<p>Добавляем директиву <code>angularUtils.directives.dirDisqus</code> в список зависимостей приложения:</p>
<pre><code> angular.module('app', [
     ...
     'angularUtils.directives.dirDisqus',
 ])
</code></pre>
</li>
</ol>
<h2 id="">Использование</h2>
<p>В шаблоне страницы, там, где должен находиться блок с комментариями, нужно вставить директиву:</p>
<pre><code> &lt;dir-disqus config=&quot;disqusConfig&quot;&gt;&lt;/dir-disqus&gt;
</code></pre>
<p>А в соответствующем контроллере:</p>
<pre><code>$scope.disqusConfig = {
    disqus_shortname: 'Ваше Disqus shortname',
    disqus_identifier: 'Идентификатор комментариев',
    disqus_url: 'url страниц с комментариями',
    disqus_title: 'Заголовок страницы'
};
</code></pre>
<p>На сайте Disqus добавляйте свой сайт. По адресу <code>https://ВАШ_САЙТ.disqus.com/admin/settings/general/</code> в настройках смотрите ваш shortname.</p>
<p>Идентификатор комментариев - это строка, которая должна быть уникальна для данного трэда комментов. Хорошим вариантом будет, например, строка <code>'/december-2010/the-best-day-of-my-life/'</code>.</p>
<p>В качестве url используется полный адрес страницы, на которой располагается трэд: <code>'http://example.com/helloworld.html'</code>.</p>
<h2 id="">Проблемы</h2>
<p>Блок комментариев появился на странице и работает. Но в консоли выводится ошибка:</p>
<pre><code>TypeError: Cannot read property 'disqus_shortname' of undefined
</code></pre>
<p>и ссылается на строки</p>
<pre><code>if (!scope.config.disqus_shortname ||
    !scope.config.disqus_identifier ||
    !scope.config.disqus_url) {
    return;
}
</code></pre>
<p>Pull request на фикс этой ошибки <a href="https://github.com/michaelbromley/angularUtils/pull/347">висит</a> уже 8 месяцев, так что, видимо, автор забил. Поэтому    ручками. Добавляем перед этими строками условие:</p>
<pre><code>if (typeof scope.config === 'undefined'){
    return;
}
</code></pre>
<p>Для ленивых <a href="https://github.com/niklanus/angularUtils/blob/b351f5542c6bcf5e578851d3ea1906ee515b5c30/src/directives/disqus/dirDisqus.js">коммит фикса</a>.</p>
<p>Теперь все работает. Почти...</p>
<p>Еще обнаружил, что Prerender, который скармливает поисковым ботам снэпшоты страниц сайта, не отрендеривает блок комментариев. То ли Disqus не успевает загрузиться до момента снимка, то ли это из-за iframe и отсутствия скриптов в снэпшоте. Если важна индексация, и используется данный сервис, то на это нужно обратить внимание. А если найдете решение  проблемы, то обязательно поделитесь со мной в комментариях.</p>
]]></content:encoded></item><item><title><![CDATA[TypeScript. Декораторы]]></title><description><![CDATA[Декораторы в TypeScript позволяют, как добавить аннотации, так и использовать синтаксис метапрограммирования при объявлении классов и их членов]]></description><link>https://mean-dev.info/typescript-decorators/</link><guid isPermaLink="false">5bdb79bfc968fc000177b1ee</guid><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[Павел Прудников]]></dc:creator><pubDate>Thu, 03 Nov 2016 21:22:05 GMT</pubDate><media:content url="https://mean-dev.info/content/images/2016/09/typescript_bn-1.png" medium="image"/><content:encoded><![CDATA[<img src="https://mean-dev.info/content/images/2016/09/typescript_bn-1.png" alt="TypeScript. Декораторы"><p>С введением классов в TypeScript и ES6 в настоящее время существуют определенные сценарии, которые требуют дополнительных возможностей для поддержки аннотирования или модификации классов и членов класса. <strong>Декораторы</strong> позволяют, как добавить аннотации, так и использовать синтаксис метапрограммирования при объявлении классов и их членов. Декораторы предлагают удобный декларативный синтаксис для изменения формы объявления класса.</p>
<p>Декораторы еще не вошли в стандарт EcmaScript и на момент написания статьи находятся на стадии <a href="https://github.com/wycats/javascript-decorators/blob/master/README.md">stage 1 proposal</a>. Однако, несмотря на то, что они являются экспериментальной функцией и в дальнейшем возможны изменения, декораторы активно используются в Angular2. Ангуларовские <code>@NgModule</code>, <code>@Component</code>, <code>@Injectable</code> - это не что иное, как эти самые декораторы. Поэтому данную тему я решил не откладывать до момента официального вхождения в стандарт. Попробую разобраться хотя бы в том, как они работают, чтобы в дальнейшем было проще с изучением Angular2.</p>
<p>Для того чтобы включить поддержку декораторов в TS, нужно добавить опцию <code>experimentalDecorators</code> в командной строке:</p>
<pre><code>tsc --target ES5 --experimentalDecorators
</code></pre>
<p>либо в файле <code>tsconfig.json</code>:</p>
<pre><code>{
	&quot;compilerOptions&quot;: {
		&quot;target&quot;: &quot;ES5&quot;,
		&quot;experimentalDecorators&quot;: true
	}
}
</code></pre>
<h2 id="">Декоратор</h2>
<p>Декоратор представляет собой объявление вида <code>@expression</code>, где <code>expression</code> - функция, которая будет вызвана во время выполнения с информацией о декорируемом элементе. Декорировать можно классы, методы, аксессоры, свойства или параметры.</p>
<p>Например, для декоратора <code>@sealed</code> (<a href="https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Object/seal">запечатанный</a>) можно было бы написать функцию <code>sealed</code>:</p>
<pre><code>function sealed(target) {
    // запечатываем target
}
</code></pre>
<h2 id="">Фабрика декораторов</h2>
<p>Если мы хотим настроить, как декоратор применяется к элементу, мы можем написать фабрику декораторов. Это просто функция, которая может принимать некоторые параметры и возвращать декоратор.</p>
<p>Пример фабрики декораторов:</p>
<pre><code>function color(value: string) { // это фабрика
	return function (target) { // это декоратор
		// делаем что-то с  'target' и 'value'...
	}
}
</code></pre>
<h2 id="">Композиция декораторов</h2>
<p>Несколько декораторов могут быть применены к объявлению следующим образом:</p>
<ul>
<li>
<p>в одну строку:</p>
<pre><code>  @f @g x
</code></pre>
</li>
<li>
<p>в несколько строк:</p>
<pre><code>  @f
  @g
  x
</code></pre>
</li>
</ul>
<p>Несколько декораторов вместе будут работать подобно функциям в математике, т.е. так: <em>f(g(x))</em>.</p>
<h2 id="">Декораторы классов</h2>
<p>Декоратор класса объявляется непосредственно перед объявлением самого декорируемого класса. Применяется к конструктору класса и может быть использован для наблюдения, модификации или замены определения класса.</p>
<p>Выражение для декоратора класса будет вызываться во время выполнения как функция с конструктором декорируемого класса в качестве единственного аргумента.</p>
<p>Пример декоратора класса (<code>@sealed</code>) для класса <code>Greeter</code>:</p>
<pre><code>@sealed
class Greeter {
	greeting: string;
	constructor(message: string) {
		this.greeting = message;
	}
	greet() {
		return &quot;Hello, &quot; + this.greeting;
	}
}
</code></pre>
<p>Для декоратора <code>sealed</code> можем записать следующую функцию:</p>
<pre><code>function sealed(constructor: Function) {
	Object.seal(constructor);
	Object.seal(constructor.prototype);
}
</code></pre>
<p>Когда декоратор <code>@sealed</code> будет выполнен, он запечатает и конструктор, и прототип класса <code>Greeter</code>.</p>
<h2 id="">Декораторы методов</h2>
<p>Декоратор метода объявляется непосредственно перед самим методом. Декоратор применяется на дескрипторе свойства для данного метода и может быть использован для наблюдения, модификации или замены определения метода.</p>
<p>Выражение для декоратора метода будет вызвано как функция с тремя аргументами:</p>
<ol>
<li>
<p>Либо функция-конструктор класса для статического метода, либо прототип класса.</p>
</li>
<li>
<p>Название метода.</p>
</li>
<li>
<p>Дескриптор свойства для данного метода.</p>
</li>
</ol>
<blockquote>
<p>Примечание: если версия целевого скрипта ниже ES5, то дескриптор свойства будет <code>undefined</code>.</p>
</blockquote>
<p>Пример использования декоратора <code>@enumerable</code>, который включает или отключает доступность метода при перечислении свойств объекта:</p>
<pre><code>class Greeter {
	greeting: string;
	constructor(message: string) {
		this.greeting = message;
	}

	@enumerable(false)
	greet() {
		return &quot;Hello, &quot; + this.greeting;
	}
}
</code></pre>
<p>Реализация фабрики для декоратора <code>enumerable</code>:</p>
<pre><code>function enumerable(value: boolean) {
	return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
		descriptor.enumerable = value;
	};
}
</code></pre>
<h2 id="">Декораторы аксессоров</h2>
<p>Декоратор аксессора объявляется непосредственно перед объявлением самого аксессора. Декоратор применяется на дескрипторе свойства для данного аксессора и может быть использован для наблюдения, модификации или замены его определения.</p>
<p>TypeScript не позволяет применять декоратор и к геттеру, и к сеттеру. Декоратор должен быть объявлен для первого аксессора, указанного в коде. Это происходит потому, что декоратор применяется для дескриптора свойства, которое содержит и <code>get</code>, и <code>set</code>.</p>
<p>Выражение для декоратора аксессора будет вызвано как функция с тремя аргументами:</p>
<ol>
<li>
<p>Либо функция-конструктор класса для статического члена, либо прототип класса.</p>
</li>
<li>
<p>Название члена.</p>
</li>
<li>
<p>Дескриптор свойства для данного члена.</p>
</li>
</ol>
<blockquote>
<p>Примечание: если версия целевого скрипта ниже ES5, то дескриптор свойства будет <code>undefined</code>.</p>
</blockquote>
<p>Если декоратор аксессора возвращает значение, то оно будет использовано как дескриптор свойства для данного члена.</p>
<p>В следующем примере декоратор аксессоров <code>@configurable</code> применяется для членов класса  <code>Point</code>:</p>
<pre><code>class Point {
	private _x: number;
	private _y: number;
	constructor(x: number, y: number) {
		this._x = x;
		this._y = y;
	}

	@configurable(false)
	get x() { return this._x; }

	@configurable(false)
	get y() { return this._y; }
}
</code></pre>
<p>Реализация фабрики для декоратора <code>configurable</code>:</p>
<pre><code>function configurable(value: boolean) {
	return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
		descriptor.configurable = value;
	};
}
</code></pre>
<h2 id="">Декораторы свойств</h2>
<p>Все то же самое. Объявляется непосредственно перед объявлением свойства. Выражение для декоратора свойства будет вызвано как функция с двумя аргументами:</p>
<ol>
<li>
<p>Либо функция-конструктор класса для статического члена, либо прототип класса.</p>
</li>
<li>
<p>Название свойства.</p>
</li>
</ol>
<p>Если декоратор свойства возвращает значение, то оно будет использовано как дескриптор.</p>
<blockquote>
<p>Примечание: если версия целевого скрипта ниже ES5, то возвращаемое значение будет проигнорировано.</p>
</blockquote>
<p>Можно использовать декоратор для сохранения метаданных о свойстве класса:</p>
<pre><code>class Greeter {
	@format(&quot;Hello, %s&quot;)
	greeting: string;

	constructor(message: string) {
		this.greeting = message;
	}
	greet() {
		let formatString = getFormat(this, &quot;greeting&quot;);
		return formatString.replace(&quot;%s&quot;, this.greeting);
	}
}
</code></pre>
<p>Мы можем написать декоратор <code>@format</code> и функцию <code>getFormat</code> следующим образом:</p>
<pre><code>import &quot;reflect-metadata&quot;;

const formatMetadataKey = Symbol(&quot;format&quot;);

function format(formatString: string) {
	return Reflect.metadata(formatMetadataKey, formatString);
}

function getFormat(target: any, propertyKey: string) {
	return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}
</code></pre>
<p>Здесь <code>@format(&quot;Hello, %s&quot;)</code> - фабрика декораторов. Когда декоратор вызывается, он добавляет запись метаданных для свойства с использованием функции <code>Reflect.metadata</code> из библиотеки <code>reflect-metadata</code> (ниже про нее будет написано). Когда вызывается функция <code>getFormat</code>, она считывает метаданные и возвращает требуемый формат.</p>
<h2 id="">Декораторы параметров</h2>
<p>Объявляется непосредственно перед объявлением параметра. Применяется для функции-конструктора класса или для метода.</p>
<p>Выражение для декоратора параметра будет вызвано как функция с тремя аргументами:</p>
<ol>
<li>
<p>Либо функция-конструктор класса для статического члена, либо прототип класса.</p>
</li>
<li>
<p>Название члена.</p>
</li>
<li>
<p>Порядковый номер параметра в списке аргументов функции.</p>
</li>
</ol>
<p>Декораторы  применяется только для наблюдения за параметром, объявленным в методе.<br>
Значение, возвращаемое декоратором параметра, будет проигнорировано.</p>
<p>В следующем примере декоратор параметра <code>@required</code> применяется к параметру <code>name</code> функции <code>greet</code>:</p>
<pre><code>class Greeter {
	greeting: string;

	constructor(message: string) {
		this.greeting = message;
	}

	@validate
	greet(@required name: string) {
		return &quot;Hello &quot; + name + &quot;, &quot; + this.greeting;
	}
}
</code></pre>
<p>Декораторы <code>@required</code> и <code>@validate</code> можно описать следующим образом:</p>
<pre><code>import &quot;reflect-metadata&quot;;

const requiredMetadataKey = Symbol(&quot;required&quot;);

function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
	let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
	existingRequiredParameters.push(parameterIndex);
	Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}

function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor&lt;Function&gt;) {
	let method = descriptor.value;
	descriptor.value = function () {
		let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
		if (requiredParameters) {
			for (let parameterIndex of requiredParameters) {
				if (parameterIndex &gt;= arguments.length || arguments[parameterIndex] === undefined) {
					throw new Error(&quot;Missing required argument.&quot;);
				}
			}
		}

		return method.apply(this, arguments);
	}
}
</code></pre>
<p>Декоратор <code>@required</code> добавляет запись метаданных, которая помечает параметр как обязательный. Декоратор <code>@validate</code> оборачивает метод <code>greet</code> в функцию, которая предварительно выполняет валидацию аргументов. В данном примере также требуется установка дополнительной библиотеки <code>reflect-metadata</code>, о которой речь пойдет чуть ниже.</p>
<h2 id="">Метаданные</h2>
<p>В некоторых примерах была использована библиотека <code>reflect-metadata</code>, которая является полифиллом для эксперементального <a href="https://github.com/rbuckton/ReflectDecorators">metadata API</a>. Библиотека не является частью стандарта ECMAScript (JavaScript). Однако, как только декораторы войдут в стандарт, эти расширения будут предложены для принятия.</p>
<p>Установить библиотеку можно с помощью npm:</p>
<pre><code>npm i reflect-metadata --save 
</code></pre>
<p>Для того чтобы включить поддержку метаданных в TypeScript нужно добавить опцию <code>emitDecoratorMetadata</code> в консоли:</p>
<pre><code>tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata
</code></pre>
<p>либо в файле <code>tsconfig.json</code>:</p>
<pre><code>{
    &quot;compilerOptions&quot;: {
        &quot;target&quot;: &quot;ES5&quot;,
        &quot;experimentalDecorators&quot;: true,
        &quot;emitDecoratorMetadata&quot;: true
    }
}
</code></pre>
<p>Когда библиотека <code>reflect-metadata</code> импортирована, дополнительная design-time информация о типах становится доступной в рантайме.</p>
<p>Мы можем увидеть в действии это в следующем примере:</p>
<pre><code>import &quot;reflect-metadata&quot;;

class Point {
	x: number;
	y: number;
}

class Line {
	private _p0: Point;
	private _p1: Point;

	@validate
	set p0(value: Point) { this._p0 = value; }
	get p0() { return this._p0; }

	@validate
	set p1(value: Point) { this._p1 = value; }
	get p1() { return this._p1; }
}

function validate&lt;T&gt;(target: any, propertyKey: string, descriptor: TypedPropertyDescriptor&lt;T&gt;) {
	let set = descriptor.set;
	descriptor.set = function (value: T) {
		let type = Reflect.getMetadata(&quot;design:type&quot;, target, propertyKey);
		if (!(value instanceof type)) {
			throw new TypeError(&quot;Invalid type.&quot;);
		}
	}
}
</code></pre>
<p>Компилятор TypeScript внедрит информацию о типе с помощью декоратора <code>@Reflect.metadata</code>. Вот эквивалентный код класса:</p>
<pre><code>class Line {
	private _p0: Point;
	private _p1: Point;

	@validate
	@Reflect.metadata(&quot;design:type&quot;, Point)
	set p0(value: Point) { this._p0 = value; }
	get p0() { return this._p0; }

	@validate
	@Reflect.metadata(&quot;design:type&quot;, Point)
	set p1(value: Point) { this._p1 = value; }
	get p1() { return this._p1; }
}
</code></pre>
<p><em>Уф. Довольно сложная тема.  Поэтому сильно глубоко проникнуться не получилось. Получил лишь легкое понимание принципа работы декораторов. Соответственно и статья вышла обычным сухим переводом официальной <a href="http://www.typescriptlang.org/docs/handbook/decorators.html">документации</a>. Если у кого-то есть  замечания, предложения или более доступные для понимания объяснения и примеры по данной теме,  то буду рад комментариям. Спасибо за понимание.</em></p>
<p>P. S. Нашел на хабре добротную <a href="https://habrahabr.ru/company/docsvision/blog/310870/">статью про декораторы в TypeScript</a> с интересными примерами. Советую к прочтению. Да прибудет с вами Сила ;)</p>
]]></content:encoded></item></channel></rss>