Пошаговое руководство для вашего первого приложения AngularJS
Опубликовано: 2022-03-11Что такое AngularJS?
AngularJS — это среда JavaScript MVC, разработанная Google, которая позволяет создавать хорошо структурированные, легко тестируемые и поддерживаемые интерфейсные приложения.
И почему я должен использовать это?
Если вы еще не пробовали AngularJS, вы многое упускаете. Платформа состоит из тесно интегрированного набора инструментов, который поможет вам создавать хорошо структурированные многофункциональные клиентские приложения по модульному принципу — с меньшим количеством кода и большей гибкостью.
AngularJS расширяет HTML, предоставляя директивы, которые добавляют функциональность вашей разметке и позволяют создавать мощные динамические шаблоны. Вы также можете создавать свои собственные директивы, создавая повторно используемые компоненты, которые удовлетворяют ваши потребности и абстрагируются от всей логики манипулирования DOM.
Он также реализует двустороннюю привязку данных, беспрепятственно связывая ваш HTML (представления) с вашими объектами JavaScript (моделями). Проще говоря, это означает, что любое обновление вашей модели будет немедленно отражено в вашем представлении без необходимости каких-либо манипуляций с DOM или обработки событий (например, с помощью jQuery).
Наконец, я люблю Angular за его гибкость в отношении взаимодействия с сервером. Как и большинство фреймворков JavaScript MVC, он позволяет вам работать с любой серверной технологией, если он может обслуживать ваше приложение через веб-API RESTful. Но Angular также предоставляет услуги поверх XHR, которые значительно упрощают ваш код и позволяют абстрагировать вызовы API в повторно используемые сервисы. В результате вы можете перенести свою модель и бизнес-логику во внешний интерфейс и создавать веб-приложения, не зависящие от серверной части. В этом посте мы будем делать именно это, шаг за шагом.
Итак, с чего мне начать?
Во-первых, давайте определимся с характером приложения, которое мы хотим создать. В этом руководстве мы бы предпочли не тратить слишком много времени на серверную часть, поэтому мы напишем что-нибудь на основе данных, которые легко доступны в Интернете, например, приложение для спортивных новостей!
Так как я являюсь большим поклонником автогонок и Формулы-1, я буду использовать API-сервис для автоспорта в качестве серверной части. К счастью, ребята из Ergast любезно предоставили нам бесплатный API для автоспорта.
Чтобы получить представление о том, что мы собираемся построить, взгляните на живую демонстрацию. Чтобы украсить демонстрацию и продемонстрировать некоторые шаблоны Angular, я применил тему Bootstrap из WrapBootstrap, но, поскольку эта статья не о CSS, я просто абстрагирую ее от примеров и оставлю.
Руководство по началу работы
Давайте запустим наше примерное приложение с помощью некоторого шаблона. Я рекомендую проект angular-seed, так как он не только предоставляет вам отличный скелет для начальной загрузки, но и закладывает основу для модульного тестирования с помощью Karma и Jasmine (мы не будем проводить тестирование в этой демонстрации, поэтому мы просто пока оставьте это в стороне; см. часть 2 этого руководства для получения дополнительной информации о настройке вашего проекта для модульного и сквозного тестирования).
РЕДАКТИРОВАТЬ (май 2014 г.): с тех пор, как я написал этот учебник, проект angular-seed претерпел некоторые серьезные изменения (включая добавление Bower в качестве менеджера пакетов). Если у вас есть какие-либо сомнения относительно того, как развернуть проект, взгляните на первый раздел их справочного руководства. Во второй части этого руководства Bower, среди прочих инструментов, рассматривается более подробно.
Хорошо, теперь, когда мы клонировали репозиторий и установили зависимости, скелет нашего приложения будет выглядеть так:
Теперь мы можем начать кодирование. Поскольку мы пытаемся создать спортивную ленту для гоночного чемпионата, давайте начнем с наиболее подходящего представления: таблицы чемпионата .
Учитывая, что у нас уже есть список драйверов, определенный в рамках нашей области (подождите со мной — мы доберемся туда), и игнорируя любой CSS (для удобочитаемости), наш HTML может выглядеть так:
<body ng-app="F1FeederApp" ng-controller="driversController"> <table> <thead> <tr><th colspan="4">Drivers Championship Standings</th></tr> </thead> <tbody> <tr ng-repeat="driver in driversList"> <td>{{$index + 1}}</td> <td> <img src="img/flags/{{driver.Driver.nationality}}.png" /> {{driver.Driver.givenName}} {{driver.Driver.familyName}} </td> <td>{{driver.Constructors[0].name}}</td> <td>{{driver.points}}</td> </tr> </tbody> </table> </body>
Первое, что вы заметите в этом шаблоне, это использование выражений («{{» и «}}») для возврата значений переменных. В разработке AngularJS выражения позволяют выполнять некоторые вычисления, чтобы вернуть желаемое значение. Некоторые допустимые выражения:
-
{{ 1 + 1 }}
-
{{ 946757880 | date }}
-
{{ user.name }}
По сути, выражения представляют собой фрагменты, подобные JavaScript. Но, несмотря на то, что они очень мощные, вы не должны использовать выражения для реализации какой-либо логики более высокого уровня. Для этого мы используем директивы.
Понимание основных директив
Второе, что вы заметите, это наличие ng-attributes
, которых вы не увидите в обычной разметке. Это директивы.
На высоком уровне директивы — это маркеры (такие как атрибуты, теги и имена классов), которые сообщают AngularJS, что нужно привязать заданное поведение к элементу DOM (или преобразовать его, заменить и т. д.). Давайте посмотрим на те, которые мы уже видели:
Директива
ng-app
отвечает за загрузку вашего приложения, определяя его область действия. В AngularJS у вас может быть несколько приложений на одной странице, поэтому эта директива определяет, где начинается и заканчивается каждое отдельное приложение.Директива
ng-controller
определяет, какой контроллер будет отвечать за ваше представление. В этом случае мы обозначаемdriversController
, который предоставит наш список драйверов (driversList
).Директива
ng-repeat
является одной из наиболее часто используемых и служит для определения области действия вашего шаблона при циклическом просмотре коллекций. В приведенном выше примере он реплицирует строку таблицы для каждого драйвера вdriversList
.
Добавление контроллеров
Конечно, наше представление бесполезно без контроллера. Давайте добавим driversController
в наш controllers.js:
angular.module('F1FeederApp.controllers', []). controller('driversController', function($scope) { $scope.driversList = [ { Driver: { givenName: 'Sebastian', familyName: 'Vettel' }, points: 322, nationality: "German", Constructors: [ {name: "Red Bull"} ] }, { Driver: { givenName: 'Fernando', familyName: 'Alonso' }, points: 207, nationality: "Spanish", Constructors: [ {name: "Ferrari"} ] } ]; });
Возможно, вы заметили переменную $scope
, которую мы передаем в качестве параметра контроллеру. Предполагается, что переменная $scope
связывает ваш контроллер и представления. В частности, он содержит все данные, которые будут использоваться в вашем шаблоне. Все, что вы добавите к нему (например, driversList
в приведенном выше примере), будет напрямую доступно в ваших представлениях. Пока давайте просто поработаем с фиктивным (статическим) массивом данных, который мы позже заменим нашим сервисом API.
Теперь добавьте это в app.js:
angular.module('F1FeederApp', [ 'F1FeederApp.controllers' ]);
С помощью этой строки кода мы фактически инициализируем наше приложение и регистрируем модули, от которых оно зависит. Мы вернемся к этому файлу ( app.js
) позже.
Теперь давайте соберем все вместе в index.html
:
<!DOCTYPE html> <html> <head> <title>F-1 Feeder</title> </head> <body ng-app="F1FeederApp" ng-controller="driversController"> <table> <thead> <tr><th colspan="4">Drivers Championship Standings</th></tr> </thead> <tbody> <tr ng-repeat="driver in driversList"> <td>{{$index + 1}}</td> <td> <img src="img/flags/{{driver.Driver.nationality}}.png" /> {{driver.Driver.givenName}} {{driver.Driver.familyName}} </td> <td>{{driver.Constructors[0].name}}</td> <td>{{driver.points}}</td> </tr> </tbody> </table> <script src="bower_components/angular/angular.js"></script> <script src="bower_components/angular-route/angular-route.js"></script> <script src="js/app.js"></script> <script src="js/services.js"></script> <script src="js/controllers.js"></script> </body> </html>
По модулю незначительных ошибок теперь вы можете загрузить свое приложение и проверить свой (статический) список драйверов.
Примечание. Если вам нужна помощь в отладке вашего приложения и визуализации ваших моделей и области действия в браузере, я рекомендую взглянуть на замечательный плагин Batarang для Chrome.
Загрузка данных с сервера
Поскольку мы уже знаем, как отображать данные нашего контроллера в нашем представлении, пришло время получить данные в реальном времени с сервера RESTful.
Для облегчения связи с HTTP-серверами AngularJS предоставляет сервисы $http
и $resource
. Первый — это всего лишь слой поверх XMLHttpRequest или JSONP, а второй обеспечивает более высокий уровень абстракции. Мы будем использовать $http
.
Чтобы абстрагировать вызовы нашего серверного API от контроллера, давайте создадим собственную службу, которая будет извлекать наши данные и действовать как оболочка вокруг $http
, добавив это в наш services.js
:
angular.module('F1FeederApp.services', []). factory('ergastAPIservice', function($http) { var ergastAPI = {}; ergastAPI.getDrivers = function() { return $http({ method: 'JSONP', url: 'http://ergast.com/api/f1/2013/driverStandings.json?callback=JSON_CALLBACK' }); } return ergastAPI; });
С помощью первых двух строк мы создаем новый модуль ( F1FeederApp.services
) и регистрируем службу в этом модуле ( ergastAPIservice
). Обратите внимание, что мы передаем $http
в качестве параметра этой службе. Это сообщает механизму внедрения зависимостей Angular, что наша новая служба требует (или зависит от ) службы $http
.
Аналогичным образом нам нужно сказать Angular включить наш новый модуль в наше приложение. Давайте зарегистрируем его с помощью app.js
, заменив наш существующий код на:
angular.module('F1FeederApp', [ 'F1FeederApp.controllers', 'F1FeederApp.services' ]);
Теперь все, что нам нужно сделать, это немного изменить наш controller.js
, включить ergastAPIservice
в качестве зависимости, и все готово:
angular.module('F1FeederApp.controllers', []). controller('driversController', function($scope, ergastAPIservice) { $scope.nameFilter = null; $scope.driversList = []; ergastAPIservice.getDrivers().success(function (response) { //Dig into the responde to get the relevant data $scope.driversList = response.MRData.StandingsTable.StandingsLists[0].DriverStandings; }); });
Теперь перезагрузите приложение и проверьте результат. Обратите внимание, что мы не вносили никаких изменений в наш шаблон, но мы добавили в область видимости переменную nameFilter
. Давайте воспользуемся этой переменной.

Фильтры
Здорово! У нас есть функциональный контроллер. Но он показывает только список драйверов. Давайте добавим немного функциональности, реализовав простой ввод текстового поиска, который будет фильтровать наш список. Давайте добавим следующую строку в наш index.html
прямо под <body>
:
<input type="text" ng-model="nameFilter" placeholder="Search..."/>
Теперь мы используем директиву ng-model
. Эта директива привязывает наше текстовое поле к переменной $scope.nameFilter
и следит за тем, чтобы ее значение всегда соответствовало входному значению. Теперь давайте еще раз зайдем в index.html и внесем небольшие изменения в строку, содержащую директиву ng-repeat
:
<tr ng-repeat="driver in driversList | filter: nameFilter">
Эта строка сообщает ng-repeat
, что перед выводом данных массив driversList
должен быть отфильтрован по значению, хранящемуся в nameFilter
.
На этом этапе вступает в действие двусторонняя привязка данных: каждый раз, когда значение вводится в поле поиска, Angular немедленно гарантирует, что связанный с ним $scope.nameFilter
будет обновлен новым значением. Поскольку привязка работает в обоих направлениях, в момент обновления значения nameFilter
вторая связанная с ним директива (т. е. ng-repeat
) также получает новое значение, и представление немедленно обновляется.
Перезагрузите приложение и проверьте панель поиска.
Обратите внимание, что этот фильтр будет искать ключевое слово во всех атрибутах модели, включая те, которые не использовались. Допустим, мы хотим фильтровать только по Driver.givenName
и Driver.familyName
: во-первых, мы добавляем в driversController
прямо под $scope.driversList = [];
линия:
$scope.searchFilter = function (driver) { var keyword = new RegExp($scope.nameFilter, 'i'); return !$scope.nameFilter || keyword.test(driver.Driver.givenName) || keyword.test(driver.Driver.familyName); };
Теперь вернемся к index.html
и обновим строку, содержащую директиву ng-repeat
:
<tr ng-repeat="driver in driversList | filter: searchFilter">
Перезагрузите приложение еще раз, и теперь у нас есть поиск по имени.
Маршруты
Наша следующая цель — создать страницу сведений о водителе, которая позволит нам щелкнуть по каждому водителю и увидеть сведения о его/ее карьере.
Во-первых, давайте включим службу $routeProvider
(в app.js
), которая поможет нам справиться с этими разнообразными маршрутами приложений . Затем мы добавим два таких маршрута: один для таблицы чемпионата, а другой для сведений о гонщике. Вот наш новый app.js
:
angular.module('F1FeederApp', [ 'F1FeederApp.services', 'F1FeederApp.controllers', 'ngRoute' ]). config(['$routeProvider', function($routeProvider) { $routeProvider. when("/drivers", {templateUrl: "partials/drivers.html", controller: "driversController"}). when("/drivers/:id", {templateUrl: "partials/driver.html", controller: "driverController"}). otherwise({redirectTo: '/drivers'}); }]);
С этим изменением переход по http://domain/#/drivers
загрузит driversController
и найдет частичное представление для рендеринга в partials/drivers.html
. Но ждать! У нас пока нет частичных просмотров, верно? Нам нужно будет создать их тоже.
Частичные просмотры
AngularJS позволит вам привязать маршруты к определенным контроллерам и представлениям.
Но сначала нам нужно указать Angular, где отображать эти частичные представления. Для этого мы будем использовать директиву ng-view
, изменив наш index.html
, чтобы отразить следующее:
<!DOCTYPE html> <html> <head> <title>F-1 Feeder</title> </head> <body ng-app="F1FeederApp"> <ng-view></ng-view> <script src="bower_components/angular/angular.js"></script> <script src="bower_components/angular-route/angular-route.js"></script> <script src="js/app.js"></script> <script src="js/services.js"></script> <script src="js/controllers.js"></script> </body> </html>
Теперь всякий раз, когда мы перемещаемся по маршрутам нашего приложения, Angular будет загружать связанное представление и отображать его вместо <ng-view>
. Все, что нам нужно сделать, это создать файл с именем partials/drivers.html
и поместить туда нашу HTML-таблицу чемпионата. Мы также воспользуемся этой возможностью, чтобы связать имя водителя с нашим маршрутом сведений о водителе:
<input type="text" ng-model="nameFilter" placeholder="Search..."/> <table> <thead> <tr><th colspan="4">Drivers Championship Standings</th></tr> </thead> <tbody> <tr ng-repeat="driver in driversList | filter: searchFilter"> <td>{{$index + 1}}</td> <td> <img src="img/flags/{{driver.Driver.nationality}}.png" /> <a href="#/drivers/{{driver.Driver.driverId}}"> {{driver.Driver.givenName}} {{driver.Driver.familyName}} </a> </td> <td>{{driver.Constructors[0].name}}</td> <td>{{driver.points}}</td> </tr> </tbody> </table>
Наконец, давайте решим, что мы хотим показать на странице сведений. Как насчет сводки всех соответствующих фактов о водителе (например, рождения, национальности) вместе с таблицей, содержащей его/ее последние результаты? Для этого добавим в services.js
:
angular.module('F1FeederApp.services', []) .factory('ergastAPIservice', function($http) { var ergastAPI = {}; ergastAPI.getDrivers = function() { return $http({ method: 'JSONP', url: 'http://ergast.com/api/f1/2013/driverStandings.json?callback=JSON_CALLBACK' }); } ergastAPI.getDriverDetails = function(id) { return $http({ method: 'JSONP', url: 'http://ergast.com/api/f1/2013/drivers/'+ id +'/driverStandings.json?callback=JSON_CALLBACK' }); } ergastAPI.getDriverRaces = function(id) { return $http({ method: 'JSONP', url: 'http://ergast.com/api/f1/2013/drivers/'+ id +'/results.json?callback=JSON_CALLBACK' }); } return ergastAPI; });
На этот раз мы предоставляем идентификатор водителя службе, чтобы получить информацию, относящуюся исключительно к конкретному водителю. Теперь мы модифицируем controllers.js
:
angular.module('F1FeederApp.controllers', []). /* Drivers controller */ controller('driversController', function($scope, ergastAPIservice) { $scope.nameFilter = null; $scope.driversList = []; $scope.searchFilter = function (driver) { var re = new RegExp($scope.nameFilter, 'i'); return !$scope.nameFilter || re.test(driver.Driver.givenName) || re.test(driver.Driver.familyName); }; ergastAPIservice.getDrivers().success(function (response) { //Digging into the response to get the relevant data $scope.driversList = response.MRData.StandingsTable.StandingsLists[0].DriverStandings; }); }). /* Driver controller */ controller('driverController', function($scope, $routeParams, ergastAPIservice) { $scope.id = $routeParams.id; $scope.races = []; $scope.driver = null; ergastAPIservice.getDriverDetails($scope.id).success(function (response) { $scope.driver = response.MRData.StandingsTable.StandingsLists[0].DriverStandings[0]; }); ergastAPIservice.getDriverRaces($scope.id).success(function (response) { $scope.races = response.MRData.RaceTable.Races; }); });
Здесь важно отметить, что мы только что внедрили службу $routeParams
в контроллер драйвера. Этот сервис позволит нам получить доступ к нашим параметрам URL (в данном случае для :id
) с помощью $routeParams.id
.
Теперь, когда у нас есть данные в области видимости, нам нужно только оставшееся частичное представление. Давайте создадим файл с именем partials/driver.html
и добавим:
<section> <a href="./#/drivers"><- Back to drivers list</a> <nav class="main-nav"> <div class="driver-picture"> <div class="avatar"> <img ng-show="driver" src="img/drivers/{{driver.Driver.driverId}}.png" /> <img ng-show="driver" src="img/flags/{{driver.Driver.nationality}}.png" /><br/> {{driver.Driver.givenName}} {{driver.Driver.familyName}} </div> </div> <div class="driver-status"> Country: {{driver.Driver.nationality}} <br/> Team: {{driver.Constructors[0].name}}<br/> Birth: {{driver.Driver.dateOfBirth}}<br/> <a href="{{driver.Driver.url}}" target="_blank">Biography</a> </div> </nav> <div class="main-content"> <table class="result-table"> <thead> <tr><th colspan="5">Formula 1 2013 Results</th></tr> </thead> <tbody> <tr> <td>Round</td> <td>Grand Prix</td> <td>Team</td> <td>Grid</td> <td>Race</td> </tr> <tr ng-repeat="race in races"> <td>{{race.round}}</td> <td><img src="img/flags/{{race.Circuit.Location.country}}.png" />{{race.raceName}}</td> <td>{{race.Results[0].Constructor.name}}</td> <td>{{race.Results[0].grid}}</td> <td>{{race.Results[0].position}}</td> </tr> </tbody> </table> </div> </section>
Обратите внимание, что сейчас мы правильно используем директиву ng-show
. Эта директива покажет HTML-элемент только в том случае, если предоставленное выражение true
(т. е. ни false
, ни null
). В этом случае аватар будет отображаться только после того, как объект драйвера будет загружен в область действия контроллером.
Последние штрихи
Добавьте кучу CSS и визуализируйте свою страницу. У вас должно получиться что-то вроде этого:
Теперь вы готовы запустить свое приложение и убедиться, что оба маршрута работают должным образом. Вы также можете добавить статическое меню в index.html
, чтобы улучшить возможности навигации пользователя. Возможности безграничны.
РЕДАКТИРОВАТЬ (май 2014 г.): я получил много запросов на загружаемую версию кода, который мы создаем в этом руководстве. Поэтому я решил опубликовать его здесь (без CSS). Тем не менее, я действительно не рекомендую его скачивать, так как это руководство содержит каждый шаг, необходимый для создания такого же приложения своими руками, что будет гораздо более полезным и эффективным учебным упражнением.
Заключение
На этом этапе руководства мы рассмотрели все, что вам нужно для написания простого приложения (например, кормушки для Формулы 1). Каждая из оставшихся страниц в живой демонстрации (например, таблица чемпионатов конструкторов, информация о команде, календарь) имеет ту же базовую структуру и концепции, которые мы рассмотрели здесь.
Наконец, имейте в виду, что Angular — очень мощный фреймворк, и мы едва коснулись всего, что он может предложить. Во второй части этого урока мы приведем примеры того, почему Angular выделяется среди аналогичных интерфейсных MVC-фреймворков: тестируемость. Мы рассмотрим процесс написания и запуска модульных тестов с помощью Karma, достижение непрерывной интеграции с Yeomen, Grunt и Bower, а также другие сильные стороны этого фантастического внешнего интерфейса.