Всем привет! Меня зовут Никита Жигамовский, программист в KitApp и я хочу рассказать о своем опыте построения навигации в Ionic 4: проблеме, с которой столкнулся, и ее решении.

Я занимаюсь разработкой кросс-платформенных решений для мобильных приложений с 2018 года. Раньше работал на Ionic 3-й версии, но, так как время идет, функционал развивается, решил перейти на версию поновее, да и надоедливые моменты и баги предыдущей модели в Ionic 4 вроде бы уже устранили.

Казалось бы, что может пойти не так. Наконец-то мы имеем в функционале нормальный ангуляровский роутинг, а не старый NavController со всеми его недостатками. Даже на официальном сайте Ionic в гайде о роутинге указывается, что программно совершать переход по страницам стоит с помощью методов angular/router. Но нашлось то, что заставило меня вернуться обратно к старине NavController.

Суть проблемы


Был замечен интересный баг. Предположим, у вас есть боковое меню, вы его сделали с помощью ion-split-pane. Также у вас есть отдельные страницы от меню, и вы хотите переходить с них на другие страницы, которые находятся в меню. Выполняете переход с помощью Router.navigateByUrl('/menu/...'). Далее назовем страницу меню A, а отдельную от меню страницу — B. Но есть одно НО!

Предположим, на странице A в ивенте ngOnInit срабатывает определенная логика. Вы совершаете переход на страницу B с помощью Router и замечаете, что страница меню все еще активна — она не удалилась. Соответственно, если вы перейдете обратно на страницу A — ивент ngOnInit не сработает, так как не сработал ивент ngOnDestroy этой страницы. Казалось бы, все логично. В такие моменты обычно прибегают к одному из методов жизненного цикла уже не ангуляра, а ионика — ionViewWillEnter. Он срабатывает при переходе на страницу, как только она становится активной. Вроде бы все хорошо, он идеально подходит, но тут есть определенный ряд условностей.

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

Несколько наглядных примеров:

ionViewWillEnter сработает, если у вас допустим такая структура страниц:

1) Отдельные страницы

— page1
— page2
— page3


В таком примере при переходе на каждую страницу ionViewWillEnter будет срабатывать идеально. (page1 => page2, page2 => page3 и т.д.)

2) Меню/табы

— menu
— menuPage1
— menuPage2
— menuPage3

В данном примере все будет также отлично: метод ionViewWillEnter будет срабатывать каждый раз при переходе на любую из страниц (menuPage1 => menuPage2, menuPage1 => menuPage3 и т.д.).

А вот в примере ниже все сложнее:

— menu
— menuPage1
— menuPage2
— menuPage3
— loginPage
— signupPage


Тут и начинаются проблемы стандартного ангуляровского роутинга. При переходе внутри страниц меню (menuPage1 => menuPage2 => menuPage3) — метод ionViewWillEnter будет срабатывать как обычно, точно так же и при переходе между отдельно взятыми страницами (loginPage => signupPage). Но как только мы начнем перемещаться между отдельными страницами и страницами меню (loginPage => menu/menuPage1 или menu/menuPage3 => signupPage) — ни метод ngOnInit, ни ionViewWillEnter не отрабатывает. ngOnInit не сработает, потому что страница не была разрушена, что логично. Но почему не сработал ionViewWillEnter?

Если исходить из документации, ionViewWillEnter срабатывает внутри отдельных стаков роутинга (ключевое слово «отдельных») или между отдельными страницами, или внутри меню/табов. Но никак не в смешанной структуре отдельных страниц и меню/табов. Странно, но это считается нормальным поведением. В то же время, это не совсем то поведение, которое ожидают пользователи, особенно, если учитывать название хуков жизненного цикла :).

Так как же решить эту проблему?


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

Что же делать в таком случае? Конечно же, выкинуть Router к черту и забыть о нем, потому что все-еще есть наш ненавистный ранее и такой хороший теперь NavController.

Главным отличием метода NavController.navigateRoot() является то, что после перехода на другую страницу предыдущая автоматически уничтожается! И при повторном переходе на нее — сработает как метод ngOnInit, так и ionViewWillEnter! На самом деле — это идеальное решение — без костылей и сомнительных самописных функций.

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

Подсуммируем положительные стороны:

  1. NavController удаляет предыдущую страницу со стека, соответственно, при переходе на нее обратно — она обновляется, методы ionViewWillEnter и ngOnInit срабатывают, и вы заново можете вызвать логику в них и обновить информацию на страницах, например.
  2. Забудьте о старый методах push(), setRoot() и pop(), как и о переходе по элементам класса. Ведь именно это создавало множество проблем. Теперь navCtrl имеет обновленные методы, в которые передается тот же путь, как и в методы Router-а.

Есть один нюанс, куда же без “НО” :)

Если мы добавляем обработчик события на hardware-кнопку «назад» на андроиде и в этом обработчике пытаемся куда-то перейти с помощью Router-а или navController-a, то в консоль выбивает такую ошибку: 'Navigation triggered outside Angular zone'.

Да, навигация сработает, страница откроется, но на ней не сработает ничего — ни начальная инициализация свойств, ни методы жизненного цикла. К сожалению, навигация по нажатию на кнопку «назад» срабатывает вне Angular зоны и, по сути, открывает только шаблон: без привязки переменных к шаблону, без функций, хуков, методов жизненного цикла — без ничего.

Решение очень простое, на самом деле. Мы просто явно принудительно вызываем навигацию внутри Angular зоны.

Пример:

import { Component, NgZone } from '@angular/core';
import { NavController } from '@ionic/angular';
@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html'
})
export class AppComponent {
  constructor(private navCtrl: NavController, private ngZone: NgZone){}
  this.ngZone.run(() => this.navCtrl.navigateForward('menu')).then();
  или
  this.ngZone.run(() => this.router.navigateByUrl('/menu/my-orders')).then();
}

И теперь все работает отлично!

О ngZone есть много интересных статей, советую почитать. Успехов!

Немного о методах navController’a:



  • this.navCtrl.setDirection('root') — устанавливает корневую страницу в стеке, удаляя все предыдущие.
  • this.navCtrl.navigateRoot('homePage') — аналогично navCtrl.setDirection('root') + router.navigateByUrl('homePage'), но с обязательным удалением предыдущей страницы в стеке (то, что нам и нужно).
  • this.navCtrl.navigateForward('examplePage') — аналогично router.navigateByUrl('/examplePage), но с явным указанием куда перемещаться + может удалять предыдущую страницу в стеке.
  • this.navCtrl.back() — аналогично location.back(), но с анимацией.
  • this.navCtrl.navigateBack('backPage') — аналогично navCtrl.setDirection('back') + router.navigateByUrl('backPage').

Допустим, мы находимся сейчас на menu/page1,

image

и у нас есть отдельный стак меню и нужно после перехода из menu/page1 на страницу login удалять страницу menu/page1, чтобы после повторного перехода на нее у нас срабатывала какая-то логика на ngOnInit или ionViewWillEnter. Если использовать для перехода router.navigateByUrl('login), то после него мы окажемся на странице логина, но у нас будет существовать и страница меню,



соответственно, после перехода с логина на menu/page1 не сработает ни ngOnInit, ни ionViewWillEnter.

Если использовать для перехода наш navCtrl.navigateRoot('login'), то после открытия страницы логина предыдущая страница удаляется. И методы ngOnInit и ionViewWillEnter сработают.



В этом и вся прелесть использования navController'а — ожидаемое поведение полностью соответствует текущему.

Комментарии (2)


  1. androidovshchik
    28.10.2019 20:42

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


  1. psFitz
    29.10.2019 22:00

    1. при переходе на страницу в меню использую navigateRoot
    2. При переходе на внутренние страницы использую forward
      Так работает метод pop до нужного стака, проблем нет.
    3. Не забываем про директиву routerDirection, что-бы не использовать NavController без крайней необходимости