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

 

Темная сторона JavaScript

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

Но неужели проблема только в отсутствии профессионалов? Ведь рано или поздно новички должны были превратиться в гуру? Да, должны, но причины лежали и в самом языке и его архитектурных проблемах. Разработку JavaScript можно сравнить с гонками «Формулы-1» — спорткары развивают реактивную скорость, но крайне ненадежны. Времени на создание языка было потрачено непростительно мало. Добавим сюда внезапную смену концепции проекта (изначально JavaScript планировался как функциональный язык) — и в итоге получили то, что получили. Местами запутанный синтаксис, непредсказуемость работы некоторых операторов, бесконечные нюансы со слабой типизацией, проблемы с областью видимости переменных и кучу других дефектов, способных нарушить психику людей, привыкших работать с нормальными языками.

Первым, кто отважился по-настоящему развернуть неповоротливый JavaScript, стал Джон Резиг. Он разглядел в гадком утенке невидимое изящество и отважился взяться за одну из самых важных на то время проблем — покончить с беспрестанным переписыванием одного и того же кода. В то время он занимался разработкой сайта компании Brand Logic и нюансы JavaScript прочувствовал на собственной шкуре.

Результатом его рвения стал первый релиз библиотеки jQuery. Она должна была не только решить проблему одноразового кода, но и добиться хоть какой-то кросс-браузерности. Не остался без внимания и синтаксис JavaScript. Библиотека позволила сократить многие монструозные конструкции и стала самым настоящим кусочком «синтаксического сахара». С каждым новым релизом библиотека совершенствовалась и вбирала в себя новые возможности. Сегодня jQuery входит в число обязательных инструментов любого веб-разработчика, и представить современный сайт без ее (или альтернатив) использования уже проблематично.

Ну а дальше началось самое интересное. jQuery показала, что JavaScript способен на многое, и, возможно, именно это послужило своеобразным пинком для многих игроков веба. К языку стали относиться более снисходительно, и его начали всячески развивать. Чего только стоит пример Google, подаривший нам быстрейший JavaScript-движок V8! Благодаря ему появилась возможность создавать тяжелые JS-приложения. Открывшиеся перспективы стали подспорьем для создания принципиально новых технологий на базе JavaScript. Достаточно назвать Node.JS, и сразу становится ясно, что сегодня JavaScript намного больше, чем скриптовый язык. Сегодня он готов взять на себя все этапы создания приложения (клиентскую и серверную часть), не прибегая к вспомогательным инструментам.

 

Они пришли с миром

На смену библиотекам вроде jQuery в сообщество JavaScript стали приходить фреймворки, реализующие популярный паттерн MVC (MVVM, MVP и так далее). Фреймворки стали приносить новую культуру разработки и альтернативные взгляды на устоявшиеся в умах девелоперов вещи. Все больше стали подниматься вопросы применения паттернов, хорошо зарекомендовавших себя в мире большого программирования, и в умах JS-разработчиков наконец-то стала укладываться аксиома: «Нельзя мешать логику с представлением».

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

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

Лежащий в основе большинства современных фреймворков паттерн проектирования MVC хорошо зарекомендовал себя и теперь стал доступен для JavaScript-мира. Конечно, особой новизны и революции в этом событии нет. Любой продвинутый разработчик может без всяких фреймворков спроектировать приложение, следуя этому паттерну. Однако «может» и «обязывают» — разные вещи. Учитывая, что в JS-сообществе на передовые методики долго все клали железный болт, то требование применять паттерн со стороны фреймворка будет весьма кстати.

Код контроллера
Код контроллера
 

AngularJS

Проект AngularJS относительно новый. Впервые он был представлен в 2009 году. За это время вышло несколько версий и вокруг продукта сколотилось плотное сообщество. Успех проекта во многом определил его родитель — компания Google. К релизам корпорации добра гики относятся с особым трепетом, особенно если речь идет об инструментарии для разработчиков. В этот раз получилось то же самое. Не буду вдаваться в технические дебри, а лучше сразу расскажу, чем зацепил проект лично меня.

 

Как изучать AngularJS

  • Официальная документация goo.gl/uLYIh. Над ее составлением потрудились хорошо, и для первого легкого заплыва ее будет более чем достаточно.
  • Бесплатный видеокурс от egghead www.egghead.io. На момент написания статьи это был, пожалуй, самый большой видеокурс по применению AngularJS. Автор освещает темы биндинга, применения контроллеров/фильтров, тестирования и многое другое. Все насчитывается 42 видеоурока.
  • Вводные уроки на Code School goo.gl/YXBGA. Несмотря на большое количество видеоуроков по различным кодерским дисциплинам, AngularJS затронут слабенько. Всего два видео, но посмотреть их однозначно стоит.
  • Wiki на проекте docs.angularjs.ru goo.gl/20lCB. Проект стартовал в конце апреля этого года и нацелен на коллективный перевод официальной документации. Пока у ребят нет полноценного сайта, поэтому готовые части перевода они выкладывают на wiki популярного хостинга проектов GitHub. На момент написания этих строк процент переведенного материала достиг отметки 22%. Вполне возможно, что к выходу журнала перевод документации будет готов.
  • Статья «Практикум AngularJS — разработка административной панели», опубликованная на Хабре goo.gl/BLSvA, goo.gl/eSpBk.
  • Официальный твиттер-аккаунт goo.gl/TyLM0. Здесь всегда можно узнать последние новости и найти ссылки на свежие мануалы.
  • Отличная статья о моделях в AngularJs goo.gl/V0U2C.

Предельная простота

С одной стороны, AngularJS имеет достаточно низкий порог вхождения по сравнению со многими подобными решениями. С другой — документация носит слегка противоречивый характер. Она вроде бы хорошо структурирована, есть примеры кода, но некоторые вещи освещены крайне слабо. С ними придется разбираться самостоятельно, изучая исходники или запрашивая комментарии от коллег по цеху. Например, понять работу scope (областей видимости) помогут комментарии Мишко Хевери (Misko Hevery) на StackOverflow и просмотр видео AngularJS: Best Practices goo.gl/UYG6Q.

Декларативный подход

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

Тестирование

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

Выражения

Чтобы что-то вывести в содержимое какого-либо объекта, тебе не требуется писать в шаблоне громоздкие конструкции в стиле или что-то в этом роде. Достаточно заключить выражение в двойные фигурные скобки, и все, данные будут обработаны. Например, результатом выполнения кода <h1> 2 + 1 = {{2+1}}</h1> будет «2 + 1 = 3».

Директивы

Директивы являются одной из ключевых возможностей Angular. Они позволяют разработчику описать поведение отдельных элементов и расширить синтаксис HTML. В состав Angular входит лишь базовый набор директив. Однако никто не мешает нам его расширить своими собственными наработками. Правильно созданные директивы могут использоваться и в других проектах. Некоторые разработчики даже практикуют выкладывание своих коллекций компонентов в публичный доступ. Ярким примером тому служит команда AngularUIgoo.gl/tauKU, которые выложили и поддерживают в актуальном состоянии около двадцати готовых к использованию компонентов.

Я неспроста сказал, что директивы — одна из ключевых возможностей. Начинающие разработчики частенько пропускают эту мысль мимо ушей и начинают мудрить по-своему — изменяют напрямую DOM. Этого делать ни в коем случае нельзя. Почему? Во-первых, так гласят священные заповеди Angular, а во-вторых, разработчик лишается вкусностей вроде многоразового использования своих наработок, простого сопровождения кода и так далее.

Scope, производительность и мифы

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

Чтобы раз и навсегда развеять мифы о производительности, разберемся с таинственной сущностью scope (область видимости). Scope — это не модель, и не нужно пытаться ее из него делать. Ничего хорошего из этого не получится. В руководстве по Angular четко сказано, что под моделью подразумевается любой массив данных, а scope может содержать ссылку на модель. Не нужно пихать все данные из модели в scope. Да, в таком контексте ими удобно манипулировать, но никакой выгоды ты не получишь, а только тормоза. Не стоит считать все сказанное банальным обходным маневром и отмазкой со стороны разработчиков Angular. Это архитектура Angular, и с ней нужно считаться. Тем более не стоит забывать о паттерне MVVM. Помимо традиционной сущности «модель», он выделяет «модель-представление», и в данном случае scope — это есть модель-представление, а значит, в ней должны быть данные, которые требуется отображать.

Получается, что наезды на производительность Angular по большей части несправедливы. Во всяком случае, в озвученных выше примерах. Хорошо, один миф разрушен. Но ведь проблемы могут случиться (теоретически) и на более скромных объемах. Некоторые разработчики утверждают, что тормоза могут проявляться, когда в scope напихано ни много ни мало 2000–3000 объектов. Как быть с этим аргументом? Частично ответ я уже дал — не нужно пихать в scope то, что не требуется отображать прямо сейчас. Вот серьезно, я не могу представить ни одной задачи, которая может потребовать вывод такого большего количества объектов на одной странице. Тут либо закралась ошибка в архитектуре приложения, либо разработчик неправильно трактует решаемую задачу.

Почему же несколько тысяч элементов могут вызывать серьезные проблемы с производительностью? Ну ей-богу, в каком веке мы живем? Неужели пара тысяч объектов могут стать серьезной проблемой для современного ПК? На самом деле все несколько сложнее, чем просто большое количество объектов. Причина кроется в работе биндинга. Во время компоновки шаблона директивы создают так называемые наблюдатели ($watch). Наблюдатели уведомляют директивы об изменении свойств, чтобы те, в свою очередь, вовремя обновили значения элементов в DOM. Эта операция производится часто, поэтому при большом количестве объектов в scope тормоза будут неизбежны.

Загрузка Angular-приложения

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

  • создастся injector (механизм, применяемый для получения экземпляров объектов, предоставляемых поставщиком, загрузки модулей и так далее), который будет использоваться для внедрения зависимостей в пределах приложения;
  • injector сформирует глобальный scope в контексте модели нашего приложения;
  • Angular начнет обрабатывать дерево DOM с элемента, в котором была объявлена директива ngApp. Во время этого процесса будут созданы все найденные биндинги и выполнены обнаруженные директивы.

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

 

Как отладить Angular-приложение?

При разработке на Angular тебе однозначно понадобится дополнительный инструмент отладки. К счастью, такой инструмент уже создан и выполнен в виде расширения для Google Chrome. Называется оно AngularJS Batarang goo.gl/jqEW7, и после установки из Google Play расширение встраивается дополнительной примочкой в Developers Tools. Batarang позволит тебе просматривать иерархию scope, их содержимое и при желании изменять значения прямо из консоли. Более подробную информацию ты сможешь получить из видео, доступного на странице расширения в Google Play.

 

ToDo-лист. Держим задачи под бдительным надзором

Я долго думал, какой лучше подобрать пример для демонстрации Angular. Скажу честно, сначала у меня была грандиозная идея сотворить матерое расширение для браузера Google Chrome, способное взаимодействовать с различными сервисами корпорации добра. Но, к сожалению, до конца отладить весь задуманный функционал у меня не вышло. Что-то постоянно не работало и глючило. В итоге я решил рассмотреть классический пример — создание ToDo-листа, а про разработку расширений для Google Chrome когда-нибудь сделаю, с позволения редактора рубрики, отдельную статью.

Для начала сформулируем задачу. Что собой представляет типичный ToDo-лист? В первую очередь это список задач, которые необходимо выполнить в определенное время. Каждая задача — это отдельный элемент, обладающий минимум двумя свойствами: наименование (описание задачи) и состояние.

ToDo-лист в работе
ToDo-лист в работе

Представление

Часть кода представления я привел в первом листинге, а полную версию ты всегда можешь взять с нашего диска. Любое Angular-приложение начинается с объявления директивы ngApp. Обрати внимание, что в коде название директив пишется через дефис, а в хелпе и в тексте статьи слитно. Для директивы ngApp можно указать дополнительный атрибут — имя модуля, который будет загружен при инициализации приложения. Пока оставим этот вопрос открытым, а в качестве имени модуля укажем todomvc. Далее я привожу описание формы с одним единственным полем. В него пользователь будет вводить новую задачу, и при нажатии кнопки «Отправить» она будет отправляться в общий список.

 

Листинг 1. Представление

 <html lang="en" ng-app="todomvc">
…
<form id="todo-form" ng-submit="addTodo()">
<input id="new-todo" placeholder="Что еще нужно сделать?" ng-model="newTodo" autofocus>
</form>
</header>

<li ng-repeat="todo in todos | filter:statusFilter" ng-class="{completed: todo.completed, editing: todo == editedTodo}">
<div class="view">

<input class="toggle" type="checkbox" ng-model="todo.completed">
<label ng-dblclick="editTodo(todo)">{{todo.title}}</label>
<button class="destroy" ng-click="removeTodo(todo)"></button>
</div>
…

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

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

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

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

Последнее, что нуждается в пояснении, — конструкция filter:statusFilter. Здесь filter — встроенная функция, позволяющая отфильтровать данные из массива todos, соответствующие значению переменной statusFilter (ее значение будет изменяться в контроллере).

Рисуем прототип будущего приложения
Рисуем прототип будущего приложения

Контроллер

Во втором листинге я привел описание контроллера — ключевой части нашего примера. Контроллер в Angular оформляется в виде обычной JavaScript-функции. Например: function MyController ().

 

Листинг 2. Контроллер

todomvc.controller('TodoCtrl', function TodoCtrl($scope, $location, todoStorage) {

var todos = $scope.todos = todoStorage.get();

    $scope.$watch('todos', function () { 
            todoStorage.put(todos);
    }, true);


    $scope.$watch('location.path()', function (path) {
        $scope.statusFilter = (path === '/active') ?
            { completed: false } : (path === '/completed') ?
            { completed: true } : null;
    });

    $scope.addTodo = function () {
        if (!$scope.newTodo.length) {
            return;
        }

        todos.push({
            title: $scope.newTodo,
            completed: false
        });

        $scope.newTodo = '';
    };

    $scope.editTodo = function (todo) {
        $scope.editedTodo = todo;
    }; …

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

Правда, при вынесении контроллера в отдельный файл мы должны как-то сообщить Angular, что в контексте нашего приложения необходимо использовать именно этот контроллер. Решением этого вопроса занимается директива ngApp. В качестве дополнительного атрибута она принимает имя модуля, который будет загружен при инициализации приложения. Под модулем в Angular подразумевается совокупность служб, директив, фильтров и различной вспомогательной информации, которая может быть использована для конфигурирования injector. Для нашего примера в качестве модуля я указываю «todomvc», а его создание описываю в файле app.js:

var todomvc = angular.module('todomvc', []);

После этого в рамках данного модуля можно описать наш контроллер, что я и делаю конструкцией:

todomvc.controller('TodoCtrl', function TodoCtrl($scope, $location, todoStorage)

Давай разберем ее на кусочки:

  • todomvc — имя модуля (вспоминаем про директиву ngApp и файл app.js);
  • controller — служба, отвечающая за создание экземпляра контроллера. В качестве параметров мы должны передать название функции с контроллером и ссылку на объект, соответствующий контроллеру. Обрати внимание, как я передаю ссылку на объект контроллера. Поскольку отдельной функции контроллера у нас нет, то я ее определяю прямо в параметре;
  • function TodoCtrl($scope, $location, todoStorage) — функция, определяющая контроллер. В качестве параметров передаем:
  • $scope. Область видимости, созданная при объявлении директивы ngController;
  • $location. Служба, предназначенная для работы с URL, введенным в адресной строке браузера;
  • todoStorage. Самописная служба, созданная для взаимодействия с локальным хранилищем.

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

С объявлением контроллера разобрались. Теперь посмотрим на его внутренности. В самой первой строчке я объявляю модель (todos), которая представляет собой обычный массив. В нем будут храниться все добавленные пользователем задачи. Чтобы получить список задач, который отображен в представлении в настоящее время, достаточно обратиться к свойству todos в scope. Однако нас должны интересовать не только текущие данные, но и ранее сохраненные в локальном хранилище, которые мы можем получить через описанную мной службу todoStorage. После этих манипуляций в модели будут абсолютно все данные.

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

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

В теле функции, которая будет вызываться при изменении содержимого модели, я определяю всего лишь один метод — todoStorage.put(todos). Он отвечает за сохранение списка задач в локальное хранилище.

Чуть ниже по тексту второго листинга ты можешь увидеть похожий трюк. Только на этот раз следить нужно за изменением адресной строки. Появление параметра active означает необходимость установки фильтра, отображающего пользователю только невыполненные задачи. Ну а если в URL присутствует параметр completed, то делаем обратную операцию — невыполненные задачи скрываем, а завершенные отображаем.

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

 

Тестируем

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

 

Листинг 3. Тест для контроллера

// Описываем набор тестов (могут быть вложенными)
// Первый параметр — название группы тестов, а второй —
// функция с тестами
describe('Тестирование контроллера', function () {

// Переменные для взаимодействия с контроллером
// и scope
var ctrl, scope;

// До выполнения теста загружаем
//  модуль todomvc  
beforeEach(module('todomvc'));

// Инициализируем контроллер
beforeEach(inject(function ($controller, $rootScope) {
    scope = $rootScope.$new();
    ctrl = $controller('TodoCtrl', {$scope: scope});
}));

// Определяем новый тест посредством функции it
// except позволяет определить ожидания, которые будут проверяться
// в тесте
it(‘значение должно быть null’, function () {
expect(scope.editedTodo).toBeNull();
});

it('модель должна быть пустой', function () {
    expect(scope.todos.length).toBe(0);
});
…

Логотип AngularJS
Логотип AngularJS
 

Братья по духу

  • Flight — новый проект от команды Twitter. Предыдущий их проект (Twitter Bootstrap) до сих пор почивает на лаврах, и, судя по всему, Flight рано или поздно к нему присоединится. Тем более что полюбить его есть за что: компонентный подход, не требует выбора определенного подхода для отображения данных, в основе лежит событийная модель (для связи между компонентами), поддержка функциональных примесей и другие полезные плюшки.
  • Backbone — фреймворк заинтересует заядлых любителей jQuery, которые устали разгребать тонны неструктурированного JS-кода. Backbone поможет навести порядок и разгрузить искусственно усложненные решения.
  • CanJS — один из самых легковесных и простых JS-фреймворков, призванных упростить разработку веб-приложений. Из коробки CanJS прекрасно уживается с популярными JS-библиотеками jQuery, Zepto, Mootols, Yui, Dojo.
  • Ember — MVC-фреймворк с низким порогом вхождения. Возможности бедней, чем у Backbone, но и разобраться новичкам с ним значительно проще. Из главных преимуществ стоит выделить наличие функционала, упрощающего рутинные операции. Для многих вещей кода писать требуется меньше, чем, скажем, для Backbone.
  • KnockoutJS — пропагандирует модель MVVM, хвастается шикарной реализацией биндингов, хорошей производительностью и нетребовательностью к сторонним библиотекам.
Официальный сайт проекта Angular
Официальный сайт проекта Angular
 

Вместо заключения

Однозначно, AngularJS получился качественным и интересным решением, которое стоит применять в своих проектах. Компания Google вновь смогла удивить нас релизом хорошо продуманного продукта. Нельзя сказать, что он получился идеальным, но с возложенными на него задачами ему справиться под силу, а это самое главное. На этом спешу откланяться и пожелать тебе взглянуть на мир JS-разработки иначе.

 

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии