От автора: мы надеемся, вам понравилась предыдущая часть этой серии, насыщенная мемами, где мы рассказали историю нашего веб-приложения и почему AngularJS пришлось полностью переработать в Angular, чтобы он оставался конкурентоспособным. Из-за значительных изменений для упомянутого рерайта мы знали, что есть работа специально для нас, поэтому в течение нескольких месяцев мы планировали и медленно конвертировали код приложения в соответствие с новыми шаблонами, которые применялись в Angular. Это помогло снизить риск во время обновления и в качестве положительного побочного эффекта улучшило производительность и тестируемость. Эта статья, вторая часть серии, будет посвящена изменениям высокого уровня, которые мы сделали до редактирования 500 тыс. строк кода. Мы рассмотрим обновления от ES5 до TypeScript, интеграцию приложения, обработку динамических шаблонов, взлома событий и подготовку Protractor для обеспечения надежности обновления. В общем, говорим мы сегодня о том, как подготовиться к Angular update.
От JavaScript (ES5) до TypeScript
Исходный код Angular построен не просто с использованием TypeScript –разработчикам настоятельно рекомендуют для создания приложений использовать фреймворк Angular. Как вы, вероятно, можете сказать по названию, он дает типизацию обычного JavaScript, чтобы помочь разработчикам увидеть ошибки во время процесса пересылки. В приведенном ниже примере описывается, как это будет работать.
function customToString(value: number) { return String(number); } let a: string = '123'; customToString(a); // This line would throw a typescript compiler error because the function is expecting a number not a string
Не убедили? Рассмотрите эти другие более убедительные причины, чтобы сделать переключатель:
Любой тип переключателя языка программирования требует приложения огромных усилий, но, к счастью, в отличие от других препроцессоров JS, таких как CoffeeScript или Dart, TypeScript полностью необязателен, поэтому вам не нужно использовать печать, если вы этого не хотите. Это означает, что вам не нужно преобразовывать всю вашу базу кода в TypeScript за один раз, потому что вы можете технически преобразовать все ваши .js-файлы и предоставить им расширение .ts без изменения чего-либо еще, и он все равно сможет скомпилировать (при условии, что у вас есть правильные инструменты для трансляции). Вы можете изменить каждый файл .js один за другим – это более простая задача.
Почти все документы и примеры кода Angular написаны на языке TypeScript. Если бы мы решили продолжить ES5, это значительно снизило бы нашу производительность. Мало того, что наши разработчики должны изучить новую структуру, так им ещё необходимо выяснить, как преобразовать те же примеры кода в ES5.
За последние несколько лет инструментарий вокруг TypeScript значительно улучшился:
… .. Языковые сервисы Angular улавливают множество ошибок шаблона прежде, чем вы начинаете выполнять компиляцию в досрочном режиме (AOT).
… .. TSLint — это мощный инструмент для создания шрифтов TypeScript, который обеспечивает определенные стандарты кодирования (это невозможно с JSHint). TSLint также принимает пользовательские плагины, поэтому, если вам нужно обеспечить соблюдение нового правила, например, без изолированных модульных тестов с «fdescribe» или «fit», это становится легко достижимым.
… .. Импорт TypeScript избавляет нас от утомительных задач, таких как запись операторов импорта в верхней части файла.
… .. Visual Studio Code и TypeScript построены и поддерживаются Microsoft, как пара из IDE и языка программирования — это чудесное совпадение. Он также способен отслеживать определения на нескольких уровнях, поэтому навигационный исходный код приложения интуитивно более понятен.
Если вы решили конвертировать в TypeScript, имейте в виду эти распространенные ошибки:
Ограничьте использование любого типа. Это простой костыль, на который можно положиться, когда вы исправляете ошибки компилятора TypeScript. Тем не менее, это повредит в долгосрочной перспективе, потому что он не сможет уловить ошибки ввода на раннем этапе. С таким же успехом можно писать ES5.
Не пытайтесь конвертировать все сразу. Лучше начинать с самого низкого уровня и двигаться вверх. Наша структура приложений AngularJS имеет модели, которые импортируются в сервисы, которые затем импортируются в компоненты (см. Диаграмму ниже).
Если вы начнете с моделей (которые можно считать основой приложения), вы воспользуетесь преимуществами ввода, когда продолжите конвертацию.
Компоненты и односторонняя привязка
До того, как React был выпущен, AngularJS следовал шаблону Model-View-Controller. Вы можете напрямую привязываться к переменным в директиве и передавать и обновлять их с помощью двусторонней привязки. Похоже на магию, поскольку это было почти мгновенно, и упростило управление состояниями приложений, но это также и непредсказуемо (об этом позже).
Директивы AngularJS и двусторонняя привязка данных http://plnkr.co/edit/kADe706q2aW7xwqOnYZB?p=preview
Если вы нажмете на демонстрационную ссылку выше, то увидите небольшое веб-приложение, которое отобразит «Erica» в поле ввода и в div. Если затем ввести в поле ввода, вы увидите, что переменная имени обновляется почти мгновенно. Что еще более важно, это изменение начинается с дочерней директивы <name-input> — и из-за двусторонней привязки это изменение распространяется по всему приложению.
Если вы откроете инструменты для разработчиков и более подробно рассмотрите исходный код в script.js, то увидите, что мы привязываемся непосредственно к двунаправленному name в <name-input>. Все компоненты наблюдают за своими объектами name и логируют их, как только видят изменение. Вы увидите, что любое изменение, которое вы делаете в поле ввода, показывает, что сначала вводится <name-input>, но его родственный <name-display> фактически обновляется перед его родителем <name-container>, который может привести к возможным условиям гонки.
Теперь Angular последовал по стопам React, поощряя идею о том, что все является компонентом. Страница может считаться родительским компонентом, в котором находится состояние приложения, и потоки, которые передаются дочерним компонентам через односторонние привязки данных. Чтобы обновить это состояние, родительский компонент передал бы привязку ввода обратного вызова к дочерним элементам, потому что управлять состоянием можно только в одном направлении. Посмотрите Plunker ниже, как пример с тем же именем, но с односторонней привязкой данных.
Обратитесь к ссылке на Plunker, представленной здесь. http://plnkr.co/edit/PMMCWi89Gv7rqaHYMQSt?p=preview
Если вы снова откроете инструменты для разработчиков и посмотрите исходный код в script.js, то увидите, что мы изменили несколько вещей:
Мы преобразовали предыдущие директивы в компоненты, у которых есть другой более простой синтаксис.
Из родительского
Если вы сейчас посмотрите на журналы консоли, мы фактически используем жизненный цикл $onChanges, который поставляется с каждым компонентом. Как вы можете увидеть, сначала мы манипулируем родителем, и этот поток просачивается к детям. Это довольно предсказуемо, что родитель впервые видит изменение, а впоследствии изменение видят и дети.
До: Директива ES5 с двусторонней привязкой данных
angular.module('erica.test') .directive('nameInput', function() { return { restrict: 'E', scope: { name: '=' }, template: '<div>' + '<input type="text" ng-model="name">' + '</div>', controller: function($scope) { $scope.$watch('name', function() { console.log('----'); console.log('child <name-input> has name: ' + $scope.name); }); } }; });
После: Компонент TypeScript с односторонней привязкой данных
export class NameInputComponent implements ng.IComponentOptions { public template = '<div>' + '<input type="text" ng-model="$ctrl.name" ng-change="$ctrl.onChange()">' + '</div>'; public bindings = { name: '<', onNameChange: '&' }; public controller = NameInputController; } class NameInputController { public name: string; public onNameChange: Function; constructor() {} public $onChanges = function(changes) { if (!changes.name.isFirstChange()) { console.log('child <name-input> has name: ' + this.name); } } public onChange() { this.onNameChange({ name: this.name }); } }
Динамические шаблоны
Одной из особенностей, которые мы широко использовали в AngularJS, была функция шаблона, которая позволила нам динамическую загрузку в разные HTML-материалы на основе атрибутов. Это было особенно полезно для нашего веб-приложения, потому что мы постоянно проводили различные эксперименты A / B. Отдельный, однофункциональный HTML-файл проще в обслуживании, чем один HTML-файл, замусоренный операторами ng-if.
Динамические функции шаблонов в AngularJS
import * as newTpl from './new-template.html'; import * as defaultTpl from './default-template.html'; export class DynamicTemplateComponent { constructor() { this.template.$inject = [ '$attrs' ]; } public template = ($attrs: any): string => { let templateMap = { newTemplate: newTpl, default: defaultTpl }; return templateMap[$attrs.template] ? templateMap[$attrs.template] : templateMap.default; } }
Увы, Angular больше не имеет этой функции. Но для достижения такого же результата есть два способа обхода:
Используйте ng-переключатель — переключите шаблон для загрузки. С ES6 мы легко достигаем этого, импортируя шаблоны как переменные и предоставляя их как часть строки шаблона (см. Ниже фрагмент кода). Имейте в виду, что для этого требуется, чтобы родительский контейнер был включен.
Создайте отдельный компонент для каждого уникального файла шаблона, но используйте тот же базовый контроллер, чтобы уменьшить повторение кода.
Использование ng-переключателя шаблонов
import * as newTpl from './new-template.html'; import * as defaultTpl from './default-template.html'; const template = ` <div ng-switch="::$ctrl.templateKey"> <div ng-switch-when="newTemplate"> ${newTpl} </div> <div ng-switch-default> ${defaultTpl} </div> </div>`; export class DynamicTemplateComponent { public bindings: any = { templateKey: '<?' }; public template: string = template; }
Отдельные компоненты с одним базовым контроллером
class BaseController { // This would be shared controller logic between NewTemplateComponent and DefaultTemplateComponent } import * as newTpl from './new-template.html'; export class NewTemplateComponent { public template: string = newTpl; public controller = BaseController; } import * as defaultTpl from './default-template.html'; export class DefaultTemplateComponent { public template: string = defaultTpl; public controller = BaseController; }
Довольно событий
AngularJS предоставил разработчикам возможность публиковать и подписываться на пользовательские события, используя $emit, $on и $broadcast. Это позволило им общаться через приложение или отправлять данные туда-сюда от родительского и дочернего компонентов. Так как Angular поощряет одностороннюю архитектуру потока данных (см. Раздел «Компоненты и односторонняя привязка» выше), эта функция будет анти-шаблоном, поскольку родители могут передавать данные своим детям через привязки ввода, а дети могут передать данные обратно родителям через обратные вызовы.
Мы живем в реальном мире, и ваша команда может просто не успевать перепроектировать все, чтобы следовать этому образцу. Хорошей новостью является то, что вы можете создать свою собственную реализацию менеджера событий, и в Angular она будет работать отлично. Плохая новость заключается в том, что он не поддерживает обход дерева компонентов.
$broadcast посылает сигнал события вниз к дочерним компонентам.
$emit отправляет сигнал события вверх к родительским компонентам.
$on — это подписка на сигнал события.
Даже если ваше приложение имеет среднюю сложность, с помощью этих сигналов оно может помешать работе, поскольку должно проходить через дерево компонентов и, по сути, повторять сигнал на каждом уровне. Более эффективный способ сделать это — подписаться ( $on ) или опубликовать ($emit) на $rootScope, что означает, что ему нужно будет запустить сигнал только один раз. Если вы уже внесли это изменение, то реализация EventsManager будет работать отлично. Подробнее ищите ниже.
interface IEventMap { [name: string]: Array<(data?: any) => void>; } export class EventsManager { public static $inject = [ '$timeout' ]; private eventMap: IEventMap; constructor(private $timeout: ng.ITimeoutService) { this.eventMap = {}; } public subscribe = (name: string, func: (...args: any[]) => void): () => void => { if (!this.eventMap.hasOwnProperty(name)) { this.eventMap[name] = []; } let currentIndex = this.eventMap[name].length; this.eventMap[name].push(func); return () => { delete this.eventMap[name][currentIndex]; }; } public publish = (name: string, data?: any): void => { if (this.eventMap.hasOwnProperty(name)) { for (let callback of this.eventMap[name]) { if (callback) { this.$timeout(() => { callback(data); }); } } } } }
Обновления Protractor
Модульные тесты — отличный способ проверить отдельные компоненты или службы, но для проверки пользовательских потоков мы в значительной степени полагаемся на сквозные тесты (E2E) с использованием Protractor. Тесты E2E великолепны, потому что они фактически взаимодействуют с веб-приложением так же, как и обычный пользователь, но они могут быть ненадёжными. Существует несколько причин, по которым тест может потерпеть неудачу, включая, среди прочего, — проблемы с данными об окружающей среде, проблемы с синхронизацией, нечувствительность селена, различия браузера, непреднамеренное ручное прерывание и многое другое. Мы создали настраиваемую инструментальную и принудительную структуру тестового кода, чтобы обойти некоторые из этих проблем, но отложим эту тему на другой раз, если вам интересно. (Оставьте нам комментарий, если вы хотите узнать об этом больше.) В конечном итоге наши комплекты E2E дали нам уверенность в продвижении к выходу на производство.
Для автоматического выполнения тестов на Angular, нам пришлось обновлять наши привязки, чтобы не полагаться на привязки, специфичные для AngularJS.
By.model — ищет ng-модель в DOM.
By.binding — ищет ng-bind в DOM.
By.repeater — ищет ng-repeat в DOM.
Мы обошли это, создав специальный атрибут с уникальным префиксом «at» на тех же элементах и соответствующим образом обновив наши селекторы. Мы решили сопоставлять атрибут над классом CSS, потому что обнаружили, что было слишком легко забыть обновлять объекты страницы Protractor. Это особенно актуально, если у вас есть команда, посвященная дизайну и CSS, где основное внимание уделяется рефакторингу, чтобы уменьшить размер приложения CSS.
var oldMenuItemBindings = element.all(by.repeater('item in order.items track by item.id')); var newMenuItemBindings = element.all(by.css('[at-menu-item]')); var badMenuItemBindings = element.all(by.css('.menuItemClass')); // Easy to inadvertently not update this. For example, developers can accidentally remove this CSS class if they see that this class has no styling or they can rename it to something else.
Это все? (Сарказм)
Это самые большие изменения, с которыми нам пришлось столкнуться, прежде чем пытаться выполнить обновление Angular. У каждой компании будет свой набор проблем, но, надеюсь, этот пост поможет вам сделать огромный шаг в направлении новой структуры. Следующий пост в нашей четырехчастной серии будет посвящен сценарию конверсии и тому, как он автоматически преобразует 90% исходного кода.
Автор: tsaibot
Источник: https://bytes.grubhub.com/
Редакция: Команда webformyself.