Невероятно подробная и самоуверенная попытка позволить Angular и Vue мирно сосуществовать (если вы хотите рок-н-ролл)

Репозиторий Github: https://github.com/arcadeJHS/AngularVueIntegration

Иногда нужно сказать «стоп!» и решить, что пора мигрировать в более теплое и солнечное место.

Скорее всего, вы потихоньку пишете код, чтобы повзрослеть и исправить свое блестящее-счастливо-умереть-крепко-годзиллу приложение в Angular 1.x день за днем ​​(с определенным удовлетворением, почему бы и нет?).
Между тем, как сказал бы Дарвин (человек, а не ОС), виды javascript со временем эволюционируют. И, что неудивительно, однажды вы просыпаетесь и обнаруживаете, что вы и ваше существо медленно тускнеете («Я не могу выносить этот ад, который я чувствую…», знаете ли).
Причины могут быть разными: Angular 1.x скоро перестанет поддерживаться; сегодня вы действительно можете написать лучший javascript; производительность и ремонтопригодность вашего приложения могут повыситься... что угодно.
Так что здесь нет выбора, на самом деле зима близко: пора мигрировать.

Отказ от ответственности

Я не буду здесь рассказывать о причинах, по которым мы предпочитаем Vue альтернативным фреймворкам: это выходит за рамки данной статьи. Я не хочу сказать, что Angular — это ядерная зима, а Vue — свежий бриз в мягком тосканском винограднике.
Но все меняется. И сегодня все может измениться очень быстро на переднем крае разработки (здесь громко играет саундтрек к Battlestar Galactica 70-х).

Более того, я не претендую на звание эксперта ни в Angular, ни в Vue (или в javascript, при всем при этом). В дальнейшем я просто излагаю то, что я нашел возможным решением конкретной проблемы, которая у меня была.
Может быть, это может быть полезно кому-то еще, или, может быть, кто-то направит меня к лучшему решению. Итак, мы здесь.

Проблема

У нас есть огромное унаследованное одностраничное приложение, написанное на Angular 1.x пять или шесть лет назад, макет которого можно схематически представить, как на этом рисунке:

Который, если мы разобьем его на составляющие, в основном состоит из пяти компонентов:

  1. Заголовок, который содержит форму для запроса чего-либо.
  2. Оболочка для представления master-detail.
  3. Боковая панель, которая отображает результаты поиска.
  4. Контейнер для отображения информации о текущей выбранной детали.
  5. Подкомпонент внутри предыдущего для отображения дополнительных данных.

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

Никаких веб-пакетов, транспиляции или других помощников по связыванию модулей.
См. кодовую базу в теге tag-01-angular-app соответствующего репозитория.

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

"Сказал, женщина, не спеши
Все само пройдет
Все, что нам нужно, это немного терпения".

(Ганз н Роуз)

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

  • поддерживать приложение Angular в рабочем состоянии;
  • полностью переписать на Vue компонент номер 2 (обёртка master-detail) вместе с его дочерними компонентами, компонентами 3 и 4; но…
  • переработайте дочерний компонент Angular номер 5 (который пока слишком велик и сложен для рефакторинга).

Я утверждаю, что от приложения, полностью написанного на Angular 1.x, мы переходим к этому гибридному решению:

Я знаю, что вы думаете. Но это случается. И (если вы похожи на меня) здесь начинается самое интересное!

Требования

Следовательно, мы можем перечислить основные рекомендации, которые будут направлять миграцию:

  1. Поддержка компонентов Vue внутри приложения Angular.
    Другое название: компоненты Vue внутри компонентов Angular. Это дает нам возможность заменить кирпичики Angular кирпичиками Vue, избегая обрушения нашего здания.
  2. Поддержка компонентов Angular внутри компонентов Vue.
    Ака: vue внутри angular внутри vue (дох!). Подождите минутку: что? Я знаю, это звучит очень странно, но лучше царствовать в аду, чем служить в раю, верно? Ну типа. Как мы уже говорили выше, нам все еще нужно поддерживать что-то Angular внутри новой кодовой базы Vue. Тут просто без вариантов.
  3. Хранилище Vuex, легко совместно используемое между Angular и Vue.
    Мы будем постепенно внедрять Vuex как единственный источник достоверной информации для управления состоянием приложения.
  4. vue-router.
    Мы хотели бы ввести маршрутизацию на стороне клиента, чтобы упростить переключение представлений.
  5. Сборщик модулей.
    Мы будем использовать JavaScript и модули ES6+, препроцессор CSS и свяжем транспилированный код, чтобы включить его в существующее приложение. Webpack здесь на помощь.

Ну и что?

Многое предстоит сделать, так много вещей, которые нужно понять и вписать друг в друга.

"Я и мой брат Вью здесь.
Мы ехали автостопом по длинной и одинокой дороге.
Внезапно появился сияющий демон".

(Настойчивый Д)

Сюда входит ngVue.

«ngVue — это модуль Angular, который позволяет разрабатывать/использовать компоненты Vue в приложениях AngularJS».

(Репозиторий ngVue)

Круто: я действительно плохой пловец, но по крайней мере мост существует. Я могу написать компонент Vue и включить его в существующее приложение Angular. Это хорошее начало.
Angular, Vue, ngVue (и Webpack). Три мушкетера!

Освещая путь

Фильм «Чужой» учит нас, что лучший способ создать новое существо — это инкубировать его изнутри.
Чтобы избежать побочных эффектов, я хотел бы сохранить все как есть, насколько это возможно. Я хотел бы изолировать исходный код, который я собираюсь добавить, и преобразовать его в форму, которую я могу использовать в существующем.
Итак, я создаю гнездо для Vue в виде новой папки, назовем ее vueApp :

В идеале папка vueApp будет содержать все, что связано с миграцией: код Vue, временный гибридный код Vue-Angular, конфигурации Webpack и package.json, node_modules и окончательный побочный продукт, готовый к использованию.
Кроме того, я хочу разделяйте Vue и гибридный код, чтобы в будущем не было возможности удалить полезный код Angular. По той же причине я также создаю папку DEV, которая содержит макеты или все, что полезно только для сервера webpack-dev. Добавив кучу ресурсов стилей, мы, наконец, придем к готовой для разработки структуре каталогов, которая, в конце концов, будет похожа на следующую:

Обратите внимание: здесь я не буду инициализировать приложение Vue через vue-cli. Я повторно использую пользовательскую конфигурацию Webpack, которая соответствует моим потребностям. Тем не менее, все должно работать так же, если вы используете vue-cli.

См. тег tag-02-app-directory-structure (с пустыми папками и файлами).

Перво-наперво: настройка зависимостей Webpack и NPM

Давайте начнем с «эмуляции» приложения Angular, чтобы воссоздать среду для быстрых итераций разработки перед внедрением кода в реальное приложение. Конечно, это надуманный пример, который показывает, как я решил свою проблему: как указано выше, в исходном приложении Angular у меня нет поддержки со стороны Webpack (или других сборщиков модулей), и в идеале я не хочу изменять в в любом случае существующая кодовая база.
Загрузив среду разработки современными инструментами, я могу вместо этого быстро писать и тестировать новый код Vue и взаимодействие Angular-Vue через webpack-dev-server. Пожалуйста, обратитесь к tag-03-bootstrapping-dev-angular-app для подробного просмотра конфигурационных файлов Webpack и зависимостей NPM (здесь я использую Webpack 4).

Конфигурация веб-пакета

Прежде чем мы начнем, обратите внимание на несколько моментов.
Начнем с файла webpack.config.js.

Режим разработки и «библиотеки»

Сначала мы создадим наши компоненты в папке DEV, воспользовавшись преимуществами нашей тестовой среды. Таким образом, во время разработки основным входным файлом будет DEV/dev.index.js, а сгенерированный javascript будет внедрен на index.html страницу.
Когда мы перейдем к реальной производственной сборке, мы создадим кодовую базу в виде пакета javascript для включения в существующий Angular, точно так же, как мы включили бы новую библиотеку, и тогда основной точкой входа будет index.js.

Производственная сборка

Здесь мы, по сути, говорим Webpack сгенерировать три файла в финальной сборке:

  • appVueLib_VendorsDependencies.js: файл, содержащий все зависимости поставщиков (например, vue, vuex, vue-router…).
  • appVueLib_NgVueBridge.js: пакет, содержащий «гибридный» код, необходимый для временной интеграции Angular и Vue. Фактически, после завершения миграции этот код может быть полностью удален, и сгенерированный файл просто перестанет существовать. Мы поработаем с этой папкой позже.
  • appVueLib.js: «настоящее портирование», новый код, полностью написанный на Vue.

Это файлы, которые мы включим в существующее старое приложение Angular.

Angular как глобальный объект

Старое приложение уже зависит от Angular, который включен в качестве тега старого скрипта.
Следовательно, чтобы позволить новым пакетам получать доступ к вещам, определенным другим javascript на странице, избегайте дублирования в процессе сборки и предупреждений о дублировании в во время выполнения мы используем внешние Webpack. Предполагается, что зависимость angular уже присутствует в среде потребителя.
Опять же, мы воспользуемся ею позже.

пакет.json

В package.json перечислены все зависимости NPM, которые мы будем использовать сейчас и позже (например, ngVue или vuex).
Единственное, что здесь следует отметить, я буду использовать ES6 для написания углового кода, поэтому я воспользуюсь преимуществом babel-plugin-angularjs-annotate для решения проблемы внедрения зависимостей. В коде ES6 вы найдете декоратор /** @ngInject */. В терминале перейдите в папку vueApp и запустите установку:

cd code/vueApp/
npm install

Новое начало: настройка приложения для разработчиков

Теперь все детали на месте, чтобы начать настоящую работу. Начнем с папки DEV.
Создайте папку AngularAppWrapper для размещения нашего поддельного приложения Angular. В итоге это будет структура папки DEV:

Мы будем использовать ES6 для написания компонента angular (синтаксис ES6 также может облегчить перенос старой кодовой базы angular на полную переписку на Vue):

DEV/AngularAppWrapper/index.js

чей шаблон настолько прост, как:

И давайте используем его в нашем приложении для разработки Angular:

DEV/dev.index.js

Теперь из вашего терминала запустите:

npm run dev

Хороший! Простое приложение Angular, в котором можно поэкспериментировать с нашей миграцией.
Опять же, обратитесь к tag-03-bootstrapping-dev-angular-app за всем, что сделано до сих пор.

Входит в ngVue

Давайте теперь создадим и используем наш первый компонент Vue-inside-Angular. Для этого мы обратимся за помощью к ngVue (дополнительную информацию см. в официальной документации ngVue).
Я начну с определения нового модуля Angular, который будет содержать все, что связано с ngVue.

ngVueBridgeCode/ngVueComponentsModule.js

Мы просто создаем новый модуль Angular, используя в качестве зависимостей ‘ngVue’ и ‘ngVue.plugins’ (позже мы будем использовать плагины ngVue, например, с vuex). По сути, это будет пространство имен, содержащее угловой код Vue.
Хорошо, пора создать наш первый компонент Vue.

Давайте определим компонент для простой навигации по приложению. Обратите внимание, что я использую здесь папку vueCode, потому что я пишу новый компонент полностью на Vue, чтобы заменить существующий код Angular. В отличие от папок DEV и ngVueBridgeCode, которые в конечном итоге будут удалены, папка vueCode содержит реальную финальную миграцию.

vueCode/components/VueAppContainer.vue

Теперь, если вы просто включите этот новый компонент Vue в AngularAppContainer, он будет проигнорирован.

DEV/AngularAppContainer/index.html

Вы должны сказать Angular, чтобы он отображал этот компонент через ngVue.
Давайте создадим файл для «преобразования» компонентов Vue в компоненты Angular. С помощью ngVueDirectives.js мы сообщаем Angular через ngVue, что наши компоненты Vue существуют. Опять же, ngVueDirectives.js — это всего лишь временный файл моста, поэтому мы поместим его в папку ngVueBridgeCode.

ngVueBridgeCode/ngVueDirectives.js

Мы используем фабрику createVueComponent ngVue для перевода компонента Vue в директиву Angular.

В качестве первого шага мы сообщаем основному модулю angular о существовании наших angularized-vue-компонентов, поэтому внутри dev.index.js заменяем

с участием

и вуаля: ваш первый компонент Vue внутри Angular!

По сути, мы только что выполнили требования № 1 и № 5: мы можем писать новые компоненты на Vue, включать их в существующее приложение Angular и использовать современные инструменты разработки и сборки.

Назад к настоящему: связывание старых и новых приложений

"Куда мы идем, нам не нужны дороги".

(Доктор Эммет Браун, «Назад в будущее»)

Но, в общем, мы должны покинуть нашу безопасную среду разработки, взлететь и использовать новый компонент внутри реального приложения.

Добавьте в index.js необходимые зависимости:

vueApp/src/index.js

зайдите в свой терминал и запустите:

npm run build

Вы получаете папку vueApp/dist, содержащую следующие файлы:

Это именно та «библиотека», которую мы искали для улучшения нашего существующего приложения Angular.
В главном index.html включите эти файлы и используйте новую директиву Angular:

код/index.html

И не забудьте сообщить Angular, что существует новый модуль для компонентов Vue:

code/angularApp/angularApp.js

И вуаля, это просто работает:

Для справки см. tag-04-vue-component-inside-real-app.

Если вам интересно, да, вы также можете передать реквизиты:

код/index.html

code/vueApp/src/vueCode/components/VueAppContainer.vue

«Вы все еще не понимаете, с чем имеете дело, не так ли? Идеальный организм. Его структурное совершенство может сравниться только с его враждебностью».

(Эш, Чужой)

Простая маршрутизация клиентов: глобальные плагины Vue

Одной из причин, по которой мы начали этот путь, была замена компонента master-detail в приложении Angular. До сих пор мы видели, как легко использовать компонент Vue внутри Angular. Теперь давайте представим немного клиентской маршрутизации через модуль vue-router. Это даст нам возможность использовать фабрику $ngVue из ngVue.plugins и проанализировать, как определить свойства корневого экземпляра Vue.

Начнем с определения простого файла маршрутизатора.

vueCode/router.js

vueCode/components/Detail/index.vue — это простая замена существующего подробного вида.

Затем в контейнере очистите тег main и добавьте компонент router-view:

vueCode/components/VueAppContainer.vue

Обычно в приложении Vue вы передаете хранилище как свойство корневому экземпляру Vue. Что-то типа:

Но здесь, в контексте Angular/ngVue, это не сработает. Мы должны использовать $ngVueProvider на этапе настройки модуля Angular для внедрения свойства.
Опять же, я настрою его в ngVueComponentsModule, потому что там живет все, что связано с ngVue.

ngVueBridgeCode/ngVueComponentsModule.js

vue-router теперь включен, и вы можете получить к нему доступ на любом дочернем компоненте: мы только что выполнили требование № 4.

Большинство людей не понимают, что НЛО находятся на космическом туристическом маршруте. Вот почему их всегда можно увидеть в Аризоне, Шотландии и Нью-Мексико. Еще одна вещь, которую следует учитывать, это то, что все три из этих направлений являются хорошими местами для игры в гольф. Так что, возможно, есть какая-то связь между инопланетянами и гольфом.

(Элис Купер)

Совместное использование фабрик: использование сервисов Angular из Vue

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

Давайте начнем с того, что перепишем его на ES6 в ngVueBridgeCode/services, чтобы преобразовать во что-то «менее угловатое».

ngVueBridgeCode/services/searchService.js

Наш сервис представляет собой простой класс JavaScript. В будущем мы просто импортируем и будем использовать его как модуль ES в коде Vue. На данный момент мы поделимся им с экземплярами Angular и Vue, спасибо поставщикам Angular и сервису $injector.
Угловой service регистрирует конструктор службы, вызываемый с помощью new, для создания экземпляра службы. Его следует использовать (угадайте, что), когда мы определяем службу как класс.
$injector — это служба Angular, используемая для получения экземпляров объектов, определенных поставщиком. $injector.get возвращает экземпляр службы.
Экспорт экземпляра службы Angular позволяет нам импортировать и использовать его где угодно.

«Моя дорогая, здесь мы должны бежать со всех ног, чтобы оставаться на месте. И если вы хотите куда-то пойти, вы должны бежать в два раза быстрее».

(Алиса в стране чудес)

Добавьте этот код в ngVueComponentsModule.js:

ngVueBridgeCode/ngVueComponentsModule.js

и мы закончили:

  1. мы переписали сервис как класс (предыдущий фрагмент кода)
  2. создал его как сервис Angular (# 1)
  3. экспортировал экземпляр через searchService (#2).

Примечание: чтобы немного упростить, я удалил файл ngVueDirectives.js из папки ngVueBridge и переместил туда код прямо в ngVueComponentsModule (также удалите импорт внутри vueApp/src/index.js и vueApp/src/DEV/dev.index.js). Обратитесь к кодовой базе в tag-05-vue-globals.

Благодаря пункту 2 выше вы можете безопасно удалить angularApp/services/searchService.js (и тег скрипта внутри index.html). Вы можете оставить существующий код Angular нетронутым, и все будет продолжать работать (помните npm run build).
Двигайтесь дальше и перенесите также компоненты «detail» и «searchResults». Здесь мы едва ли можем имитировать существующий код без особых усилий.

vueCode/components/SearchResults/index.vue

HTML-шаблон абсолютно такой же (единственное отличие состоит в использовании системы маршрутизации). Вы также можете просто скопировать и вставить код css из style.css.
И волшебство:

мы импортируем и используем ранее созданный экземпляр searchService.

В основном vueCode/components/Detail/index.vue работает точно так же, как SearchResults (см. репозиторий).
Завершите рефакторинг, упростив контейнер:

vueCode/components/VueAppContainer.vue

добавьте компонент в index.html и пересоберите:

код/index.html

Мы только что удвоили (и почти полностью мигрировали) наш дорогой старый код Angular:

Запуск поиска (компонент Angular) теперь активирует компоненты Vue. Вы можете безопасно удалить весь связанный мертвый код Angular.
Круто! Мы только что перевели на Vue большую часть нашего приложения.
Подробности см. в tag-05-vue-globals.

Централизованный магазин: Vuex

В некотором смысле мы используем searchService как своего рода централизованное хранилище для управления всеми нашими потребностями в поиске и маршрутизации. Мы можем сделать лучше, заменив часть или полностью убрав его, и представить Vuex как более продвинутый, производительный и предсказуемый менеджер состояний.
Давайте начнем с добавления базового файла хранилища.

vueCode/store.js

"Путешествие в тысячу миль должно начинаться с первого шага".

(Лао-цзы)

Однажды мы позволим магазину управлять всем. Теперь нам нужен только один-единственный маленький шаг (для мужчины).
Нет, мы не отправляем нашу заявку на Луну (хотя иногда и хотелось бы). Мы хотим перенести в магазин только код, отвечающий за получение и сохранение результатов поиска.

Что касается vue-router ранее, мы должны добавить хранилище в глобальные свойства.

ngVueBridgeCode/ngVueComponentsModule.js

Но, постойте: и что теперь? Сервис Angular и Vuex — это разные миры, как они могут взаимодействовать?

"То, что мы имеем здесь, — это неспособность общаться".

(Капитан, Хладнокровный Люк)

Ну, Vuex — это просто объект JavaScript, который хранит данные, не так ли?
Признаюсь, какое-то время я спотыкался на своем пути в никуда, отчаянно ища решение, пока не наткнулся на него, спасибо за предложение дается парой предложений в статье Как встроить Vue.js и Vuex в приложение AngularJS… подождите что?.

«В Angular есть провайдеры, которые, безусловно, являются наиболее запутанным аспектом Angular. […] Один из этих провайдеров называется «сервис», который можно использовать для создания единого хранилища, на которое можно ссылаться во всем приложении. Все, что ему нужно, это функция, которая возвращает объект. С помощью одной строки кода я могу вернуть Vuex в качестве службы Angular».

(Джонни Холлман)

Действительно интригующе! Решение критично выкопано там (в прозрачном виде). Прочтите это и прочтите еще раз; радуйся, мой маленький мозг; используйте Розеттский камень, чтобы расшифровать документацию Angular для поставщиков.
Ключами здесь являются фабричные и сервисные рецепты.

«Разработчики JavaScript часто используют пользовательские типы для написания объектно-ориентированного кода».

Да, это я.

«Рецепт Factory может создать службу любого типа, будь то примитив, объектный литерал, функция или даже экземпляр пользовательского типа».

Например:

Помните? Из store.js мы экспортируем new Vuex.Store().
И вдруг: ЭВРИКА! Добавьте одну строку кода.

ngVueBridgeCode/ngVueComponentsModule.js

Великолепно! Мы только что представили наш экземпляр магазина как сервис Angular. Спасибо, Джонни.
Чтобы использовать его, просто вставьте его в конструктор searchService.js, замените код в executeQuery и используйте его точно так же, как в Vue:

ngVueBridgeCode/services/searchService.js

Вы могли видеть потенциал? Вы можете постепенно мигрировать свои сервисы.
И вы также можете использовать его внутри своих дорогих старых простых компонентов Angular. Например, предположим, что мы хотели бы добавить счетчик результатов в заголовок:

angularApp/components/search.js

Совет: если вы внедрите сервис, переименовав его $store, вы получите что-то очень похожее на Vue.

Для меня функция resultsCount похожа на симуляцию вычисляемого свойства Vue (хорошо, только на тонну тяжелее).
Но если вы пересоберете, запустите приложение и начнете поиск... Что за хрень? Нет счета!

Как вы понимаете, здесь мы рушимся с таинственным миром цикла дайджестов Angular. Мы делаем что-то тайно от Angular. Нам явно нужно вызвать Angular и сообщить ему, что что-то изменилось, чтобы запустить дайджест. Да: $apply здесь на помощь.

"Разве небезопасно путешествовать ночью? Оставаться здесь будет гораздо менее безопасно… Там кто-нибудь есть?»

(Пинк Флойд)

ngVueBridgeCode/utilities/safeApply.js

Я оборачиваю функцию в «безопасное применение», чтобы избежать возможных ошибок «$apply уже выполняется». Но как его использовать? Далее следует одно из многих возможных решений.
Если вы переписываете свой компонент как класс ES6, вы можете просто импортировать и использовать его как модуль. Здесь мы по-прежнему имеем дело с классическим компонентом, поэтому я напишу сервис в качестве прокси для его выставления:

ngVueBridgeCode/services/utilities.js

и

ngVueBridgeCode/ngVueComponentsModule.js

Здесь не нужно ничего экспортировать, мы не будем использовать это внутри кода Vue.

angularApp/components/search.js

С практической точки зрения, мы вручную вызываем рендеринг. Мы представляем, возможно, ненужный $scope, но это небольшая цена. Теперь, если вы начнете поиск, вы получите рабочий счетчик.

Однако есть ситуации, в которых вы не можете использовать этот подход, или, может быть, вы хотите добиться чего-то более сложного, или позволить Angular и Vue обмениваться данными друг с другом.

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

Начнем с добавления кнопки в шаблон компонента:

vueCode/components/Detail/index.vue

И относительное действие в магазине:

vueCode/store.js

Если вы сейчас протестируете этот код, то легко увидите, что при нажатии на кнопку ничего не происходит. На самом деле ничего не происходит, пока вы не разбудите Angular: попробуйте нажать кнопку «Добавить результат», а затем изменить строку поиска во входных данных или снова нажать кнопку «Поиск».
Здесь нет черной магии, вы просто позволяете Angular переварить пиццу.

Есть много возможных альтернативных решений.
Например, пока я изучал проблему, я пришел к этому умному решению jQuery: Постепенный переход с AngularJS на Vue.js в Unbabel. Там автор предлагает, если вы уже включаете его, использовать jQuery в качестве моста или, лучше, в качестве шины событий, используя преимущества его методов .trigger() и .on() для запуска пользовательских событий и обмена информацией между Angular и Vue.
Конечно, возможно. Но можем ли мы воспроизвести это в более чистом виде, где чистота означает Vue как можно больше? В конце концов, было бы неплохо убрать еще одну дополнительную зависимость.
Что ж, возможно, мы сможем, благодаря возможности создания глобальной шины событий в Vue (см. официальную документацию об управлении состоянием, или глобальной шине событий здесь и здесь).

ngVueBridgeCode/utilities/vueAngularEventBus.js

Просто как есть.
Как обычно, чтобы использовать его в Angular, оберните экземпляр Vue, возвращенный выше, в фабрику:

ngVueBridgeCode/ngVueComponentsModule.js

Я помещаю его в папку ngVueBridgeCode, потому что это всего лишь временный помощник, и в идеале он будет удален после завершения миграции. Я просто удалю его и все ссылки на VueAngularEventBus.

Использовать его очень просто. Помните: мы собираемся сообщить Angular о необходимости повторного рендеринга, потому что где-то что-то изменилось. А именно: когда компонент Vue одновременно обновляет хранилище, хранилище запускает Angular, чтобы запустить новый цикл $digest.

Поэтому нам нужна ссылка на шину в магазине:

vueCode/store.js

При совершении мутации мы также отправим событие через шину. Все подписавшиеся слушатели отреагируют следующим образом:

angularApp/components/search.js

Что мы могли бы представить графически как:

Компонент перерисовывает каждый раз, когда запускается пользовательское событие «добавленный результат». Как следствие, счетчик теперь будет обновляться каждый раз, когда вы нажимаете кнопку «Добавить результат».
Не забудьте удалить прослушиватель после уничтожения компонента.

Завершая это, мы также отправили требование № 3.
См. tag-06-using-vuex о том, что мы сделали до сих пор..

“Видите это? Это моя бум-палка! Это двуствольный «Ремингтон» 12-го калибра. S-mart на вершине линейки. Вы можете найти это в отделе спортивных товаров. […] У него ложа из орехового дерева, сталь кобальтово-синего цвета и спусковой крючок».

(Эшли Дж. Уильямс, «Армия тьмы»)

Бонус №1: бесплатные компоненты Angular от Angular

Следующим шагом в миграции может быть переписывание существующих компонентов Angular в виде модулей ES6. Вы можете переместить их в свою сборку веб-пакета, вы можете написать их в более лаконичном стиле, возможно, вы изучаете ES6+ и хотите повеселиться… что угодно. Или, может быть, вас не интересует какой-либо рестайлинг (вы уже пишете компонент Angular таким образом или предпочитаете напрямую мигрировать компонент на Vue). Любой из них подходит.
На всякий случай вы можете переместить, например, angularApp/components/search.js в vueApp/src/ngVueBridgeCode/components/Search/index.js и переписать его как:

ngVueBridgeCode/components/Search/index.js

Примечание: для использования SafeApply нам больше не нужна служба утилит упаковки.

Создайте его как компонент Angular:

ngVueBridgeCode/ngVueComponentsModule.js

И удалите все файлы и код, относящиеся к исходному компоненту. Наше приложение Angular сокращается до минимума.
См. tag-07-es6-components.

Бонус №2: бесплатные компоненты Vue от сервисов Angular

Теперь у нас есть хранилище, в которое мы можем двигаться дальше и очистить searchService от ненужных частей.
Например, свойство store.currentDetail и метод selectItem(id) используются исключительно компонентом detailVue. Давайте переместим их из сервиса Angular в магазин Vuex.

Прокомментируйте (или удалите) следующие строки:

ngVueBridgeCode/services/searchService.js

И измените магазин:

vueCode/store.js

Наконец, перепишите компонент, чтобы использовать хранилище вместо службы:

vueCode/components/Detail/index.vue

Конечно, вы можете сделать то же самое со свойством store.searchResults. Наше приложение Angular сокращается до минимума.
См. tag-08-more-store.

Выводы

Как вы уже видели, как только вы поймете способ (я не утверждаю, что мой здесь лучший или единственный), чтобы облегчить совместную жизнь Angular 1.x и Vue, вы можете прибегнуть к методологии постепенной миграции вашей кодовой базы. .

«Я принадлежу воину, в котором старые пути соединились с новыми».

(Последний самурай)

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

О, еще одно…

Компоненты Angular, вложенные в компоненты Vue.

Хорошо, ты меня понял! А требование №2? Что случилось с inner-detail после того, как вы мигрировали на vue-app-container?
Я должен быть честным и признать, что мы должны быть смелыми и по-настоящему изобретательными, чтобы решить последнюю загадку.

«Это мой друг, ирландец. И ответ на ваш вопрос положительный — если вы будете сражаться за меня, вы убьете Угловатого».

(Уильям Уоллес, Храброе сердце)

Как подтвердил один из основных участников репозитория, ngVue не был разработан для обеспечения возможности рендеринга компонентов AngularJS внутри компонентов Vue. Кто-то пытался решить проблему с помощью slots, но из-за различий в рендеринге между фреймворками реализация глючит (и не рекомендуется, потому что, возможно, в будущем будет объявлено устаревшим, как указано в issue #66).
После непродолжительного обсуждения (см. issue #79 на GitHub), благодаря советам всех причастных участников (и предыдущему опыту работы с Angular injector), я преодолел проблему тем способом, о котором расскажу ниже.
Кажется, что это работает, но в какой-то мере это действительно экспериментально (у меня просто не хватает глубоких знаний по этому вопросу, и я не полностью осведомлен о возможных нежелательных побочных эффектах). Поэтому я не уверен, что действительно рекомендую это.
Во всяком случае, для меня вложение компонентов Angular внутри Vue было важным требованием; поэтому я сообщаю об этом здесь, чтобы завершить картину и дать возможное решение.

TL;DR: я создал компонент Vue, который оборачивает и компилирует компонент Angular, и спокойно прослушивает изменения в привязке области видимости.

ngVueBridgeCode/components/AngularComponent.vue

Что я мог бы примерно визуально резюмировать как:

Много интересного в нескольких строчках:

#1: injector — это объект Angular, который можно использовать для получения сервисов, а также для внедрения зависимостей (см. официальную документацию). Здесь мы обращаемся к нему для внедрения и компиляции компонента на лету, после того как приложение Angular уже загружено.

#2: здесь мы устанавливаем область действия, которую мы привяжем к шаблону компонента, расширяя новый $scope с помощью $ctrlobject, переданного реквизитом component, который в основном представляет собой объект, подобный этому:

#3: мы заменяем тег <div/> в шаблоне Vue на скомпилированный компонент Angular.

#4: как уже было видно ранее, мы запутались в цикле Angular $digest. Чтобы сообщить Angular, что что-то изменилось в объекте, связанном с его текущей областью, привязками обновления и повторной визуализацией, мы вводим watcher в реквизите component.$ctrl. Обратите внимание на параметр { deep: true } для запуска наблюдателя в случае, если у вас есть сложный вложенный объект.

#5: каждый раз, когда свойство изменяется, мы обновляем область видимости, объединяя новый объект ctrl с существующим scope.$ctrlangular.merge выполняет глубокую копию, что нам и нужно. чтобы обязательно распространять все обновления.

#6: и каждый раз, когда свойство изменяется, мы вызываем нашего старого друга SafeApply (который здесь работает как своего рода функция «рендеринга»), привязанного к обновленной области видимости, чтобы запустить $digest.

#7: this.$watch возвращает функцию, которую мы можем использовать для очистки наблюдателя при уничтожении компонента.

Учитывая то, что говорится в официальной документации для $apply, возможно, лучше переписать #5 и #6 как:

Затем вы просто используете компонент везде, где хотите внедрить компонент Angular:

vueCode/components/Detail/index.vue

innerDetail — это реквизит component, который мы представили ранее. Лучше определить его как вычисляемое свойство, чтобы правильно инициализировать его.

Обратите внимание: я обнаружил, что для полной работы компонента Angular вам необходимо определить его HTML-шаблон в отдельном файле:

Я думаю, это зависит от того, как и когда вещи анализируются и компилируются.
Например, если вы напишете:

все не будет работать полностью, и в итоге вы получите на экране неразрешенный шаблон:

Если вы сейчас пересоберете и запустите приложение, вы можете убедиться, что компонент innerDetail работает должным образом:

См. tag-09-angular-component-inside-vue.

К: Надеюсь, вы не возражаете, что я позволю себе вольность. Я старался не забрызгать грязью.
Сапер Мортон: Я не возражаю против грязи. Я возражаю против необъявленных визитов.

(Бегущий по лезвию 2049)