Статьи

Написание приложений AngularJS с использованием ES6

Как многие из вас знают, ECMAScript 6 сейчас находится в черновом состоянии и, как ожидается, будет завершен в этом году. Но это уже привлекло большое внимание в сообществе, и браузеры уже начали внедрять это. У нас также есть несколько транспортеров, таких как Traceur, 6to5 и многие другие, которые преобразуют код ES6 в код, совместимый с ES5. Члены сообщества начали играть с ES6, и многие из них пишут о том, что они узнали. JavaScript-канал SitePoint также содержит большое количество статей, описывающих различные функции ES6.

С помощью ES6 можно написать любой фрагмент повседневного JavaScript-кода. Чтобы сделать это, мы должны знать о ключевых особенностях ES6 и знать, какая часть подходит и где. В этой статье мы увидим, как мы можем использовать функции ES6 для создания различных частей приложения AngularJS и загрузки их с помощью модулей ES6. Мы сделаем это путем создания простого онлайн-приложения для книжной полки и посмотрим, как оно структурировано и написано.

Как всегда, код для этого приложения можно найти в нашем репозитории GitHub .

Замечание по применению на книжной полке

Пример приложения BookShelf содержит следующие представления:

  1. Домашняя страница: показывает список активных книг. Книги могут быть помечены как прочитанные и перенесены в архив с этой страницы
  2. Страница добавления книги: добавляет новую книгу на полку, принимая название книги и имя автора. Это не позволяет дублировать заголовок
  3. Страница архива: список всех архивных книг

Настройка приложения для ES6

Поскольку мы будем использовать ES6 для написания интерфейсной части приложения, нам нужен транспортер, чтобы сделать функции ES6 понятными для всех браузеров. Мы будем использовать клиентскую библиотеку Traceur для компиляции нашего скрипта ES6 на лету и запуска в браузере. Эта библиотека доступна на беседке. В примере кода есть запись для этой библиотеки в bower.json .

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

 traceur.options.experimental = true; new traceur.WebPageTranscoder(document.location.href).run(); 

Код JavaScript приложения разделен на несколько файлов. Эти файлы загружаются в основной файл с помощью загрузчика модуля ES6. Поскольку современные браузеры не могут понять модули ES6, Traceur заполняет эту функцию для нас.

В примере кода файл bootstrap.js отвечает за загрузку основного модуля AngularJS и ручную загрузку приложения Angular. Мы не можем использовать ng-app для начальной загрузки приложения, так как модули загружаются асинхронно. Это код, содержащийся в этом файле:

 import { default as bookShelfModule} from './ES6/bookShelf.main'; angular.bootstrap(document, [bookShelfModule]); 

Здесь bookShelfModule — это имя модуля AngularJS, содержащего все части. Мы увидим содержимое файла bookShelf.main.js позже. Файл bootstrap.js загружается в файл index.html с помощью следующего тега script:

 <script type="module" src="ES6/bootstrap.js"></script> 

Определение контроллеров

Контроллеры AngularJS могут быть определены двумя способами:

  1. Контроллеры, использующие $scope
  2. Использование контроллера в качестве синтаксиса

Второй подход лучше подходит для ES6, так как мы можем определить класс и зарегистрировать его в качестве контроллера. Свойства, связанные с экземпляром класса, будут видны через псевдоним контроллера. Кроме того, контроллер как синтаксис сравнительно менее связан с $scope . Если вы не знаете, $scope будет удален из фреймворка в Angular 2, поэтому теперь мы можем научить наш мозг меньше зависеть от $scope , используя контроллер в качестве синтаксиса.

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

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

Как указано выше, домашняя страница приложения загружается и отображает список активных книг. Это зависит от службы, которая выбирает данные и помечает книгу как прочитанную или перемещает ее в архив. Мы создадим этот сервис в следующем разделе. Чтобы зависимости, введенные в конструктор контроллера, были доступны в методах экземпляра, нам нужно сохранить их в WeakMaps. Контроллер домашней страницы имеет две зависимости: служба, выполняющая операции Ajax, и $timeout (используется для отображения сообщений об успешном завершении и их скрытия через определенное время). Нам также нужен частный метод init для извлечения всех активных книг, как только загрузится контроллер. Итак, нам нужно три WeakMaps. Давайте объявим WeakMaps как константы, чтобы предотвратить случайное переназначение.

Следующий фрагмент создает эти WeakMaps и класс HomeController :

 const INIT = new WeakMap(); const SERVICE = new WeakMap(); const TIMEOUT = new WeakMap(); class HomeController{ constructor($timeout, bookShelfSvc){ SERVICE.set(this, bookShelfSvc); TIMEOUT.set(this, $timeout); INIT.set(this, () => { SERVICE.get(this).getActiveBooks().then(books => { this.books = books; }); }); INIT.get(this)(); } markBookAsRead(bookId, isBookRead){ return SERVICE.get(this).markBookRead(bookId, isBookRead) .then(() => { INIT.get(this)(); this.readSuccess = true; this.readSuccessMessage = isBookRead ? "Book marked as read." : "Book marked as unread."; TIMEOUT.get(this)(() => { this.readSuccess = false; }, 2500); }); } addToArchive(bookId){ return SERVICE.get(this).addToArchive(bookId) .then(() => { INIT.get(this)(); this.archiveSuccess = true; TIMEOUT.get(this)(() => { this.archiveSuccess = false; }, 2500); }); } } 

Приведенный выше фрагмент использует следующие функции ES6:

  1. Классы и WeakMaps, как уже упоминалось
  2. Синтаксис функции стрелки для регистрации обратных вызовов. Ссылка this внутри функций стрелки такая же, как ссылка this снаружи, которая является текущим экземпляром класса
  3. Новый синтаксис для создания метода и присоединения его к объекту без использования ключевого слова function

Давайте применим внедрение зависимостей и зарегистрируем этот класс в качестве контроллера:

 HomeController.$inject = ['$timeout', 'bookShelfSvc']; export default HomeController; 

Как видите, нет никакой разницы в том, как мы применяли внедрение зависимостей, — это то же самое, что и в ES5. Мы экспортируем класс HomeController из этого модуля.

Проверьте код AddBookController и AddBookController . Они следуют аналогичной структуре. Файл bookShelf.controllers.js импортирует эти контроллеры и регистрирует их в модуле. Это код из этого файла:

 import HomeController from './HomeController'; import AddBookController from './AddBookController'; import ArchiveController from './ArchiveController'; var moduleName='bookShelf.controllers'; angular.module(moduleName, []) .controller('bookShelf.homeController', HomeController) .controller('bookShelf.addBookController', AddBookController) .controller('bookShelf.archiveController', ArchiveController); export default moduleName; 

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

Определение услуг

«Сервис» — это перегруженный термин в целом, а также в Angular! Используются три типа услуг: поставщики , сервисы и фабрики . Из них поставщики и службы создаются как экземпляры типов, поэтому мы можем создавать для них классы. Фабрики — это функции, которые возвращают объекты. Я могу придумать два подхода к созданию фабрики:

  1. Как и в ES5, создайте функцию, которая возвращает объект
  2. Класс со статическим методом, который возвращает экземпляр того же класса. Этот класс будет содержать поля, которые должны быть выставлены из объекта фабрики

Давайте использовать второй подход для определения фабрики. Эта фабрика отвечает за взаимодействие с Express API и передачу данных на контроллеры. Фабрика зависит от службы $http Angular для выполнения операций Ajax. Поскольку это должно быть закрытое поле в классе, мы определим для него WeakMap.

Следующий фрагмент создает класс фабрики и регистрирует статический метод как фабрику:

 var moduleName='bookShelf.services'; const HTTP = new WeakMap(); class BookShelfService { constructor($http) { HTTP.set(this, $http); } getActiveBooks(){ return HTTP.get(this).get('/api/activeBooks').then(result => result.data ); } getArchivedBooks(){ return HTTP.get(this).get('/api/archivedBooks').then(result => result.data ); } markBookRead(bookId, isBookRead){ return HTTP.get(this).put(`/api/markRead/${bookId}`, {bookId: bookId, read: isBookRead}); } addToArchive(bookId){ return HTTP.get(this).put(`/api/addToArchive/${bookId}`,{}); } checkIfBookExists(title){ return HTTP.get(this).get(`/api/bookExists/${title}`).then(result => result.data ); } addBook(book){ return HTTP.get(this).post('/api/books', book); } static bookShelfFactory($http){ return new BookShelfService($http); } } BookShelfService.bookShelfFactory.$inject = ['$http']; angular.module(moduleName, []) .factory('bookShelfSvc', BookShelfService.bookShelfFactory); export default moduleName; 

Этот фрагмент использует следующие дополнительные функции ES6 (в дополнение к классам и функциям стрелок):

  1. Статический член в классе
  2. Строковые шаблоны для объединения значений переменных в строки

Определение директив

Определение директивы аналогично определению фабрики, за одним исключением — мы должны сделать экземпляр директивы доступным для последующего использования внутри функции link , поскольку функция link не вызывается в контексте объекта директивы. Это означает, что ссылка this внутри функции link не совпадает с объектом директивы. Мы можем сделать объект доступным через статическое поле.

Мы будем создавать директиву атрибута, которая проверяет название книги, введенной в текстовое поле. Он должен вызвать API, чтобы проверить, существует ли уже заголовок, и сделать недействительным поле, если заголовок найден. Для этой задачи ему нужен сервис, который мы создали в предыдущем разделе, и $q для обещаний.

Следующий фрагмент создает директиву, которую он регистрирует в модуле.

 var moduleName='bookShelf.directives'; const Q = new WeakMap(); const SERVICE = new WeakMap(); class UniqueBookTitle { constructor($q, bookShelfSvc){ this.require='ngModel'; //Properties of DDO have to be attached to the instance through this reference this.restrict='A'; Q.set(this, $q); SERVICE.set(this, bookShelfSvc); } link(scope, elem, attrs, ngModelController){ ngModelController.$asyncValidators.uniqueBookTitle = function(value){ return Q.get(UniqueBookTitle.instance)((resolve, reject) => { SERVICE.get(UniqueBookTitle.instance).checkIfBookExists(value).then( result => { if(result){ reject(); } else{ resolve(); } }); }); }; } static directiveFactory($q, bookShelfSvc){ UniqueBookTitle.instance =new UniqueBookTitle($q, bookShelfSvc); return UniqueBookTitle.instance; } } UniqueBookTitle.directiveFactory.$inject = ['$q', 'bookShelfSvc']; angular.module(moduleName, []) .directive('uniqueBookTitle', UniqueBookTitle.directiveFactory); export default moduleName; 

Здесь мы могли бы использовать API обещаний ES6, но для этого $rootScope.$apply было бы вызвать $rootScope.$apply после того, как обещание $rootScope.$apply результат. Хорошо, что API обещаний в AngularJS 1.3 поддерживает синтаксис, аналогичный обещаниям ES6 .

Определение основного модуля и блока конфигурации

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

 import { default as controllersModuleName } from './bookShelf.controllers'; import { default as servicesModuleName } from './bookShelf.services'; import { default as directivesModuleName } from './bookShelf.directives'; 

Блок config определяет маршруты для приложения. Это может быть простая функция, поскольку она не должна возвращать никакого значения.

 function config($routeProvider){ $routeProvider .when('/',{ templateUrl:'templates/home.html', controller:'bookShelf.homeController', controllerAs:'vm' }) .when('/addBook',{ templateUrl:'templates/addBook.html', controller:'bookShelf.addBookController', controllerAs:'vm' }) .when('/archive', { templateUrl:'templates/archive.html', controller:'bookShelf.archiveController', controllerAs:'vm' }) .otherwise({redirectTo:'/'}); } config.$inject = ['$routeProvider']; 

Наконец, давайте определим основной модуль и экспортируем его имя. Если вы помните, это имя используется в файле bootstrap.js для ручной загрузки.

 var moduleName = 'bookShelf'; var app = angular.module(moduleName, ['ngRoute','ngMessages', servicesModuleName, controllersModuleName, directivesModuleName]) .config(config); export default moduleName; 

Вывод

Надеюсь, это даст вам представление об использовании ES6 для написания приложений AngularJS. AngularJS 2.0 полностью написан с использованием ES6, и как веб-разработчики мы должны знать, как мы должны писать наш код в ближайшем будущем. ES6 решает многие проблемы, которые давали программистам JavaScript долгие годы, и использовать его с AngularJS очень весело!

И, пожалуйста, помните, пример кода для этого приложения можно найти в нашем репозитории GitHub .