Галоўная / Артыкулы / Вылікваты проблемы з переходам высоты у Vue для асінхронна адрадзіваных веб-компанентаў

Вылікваты проблемы з переходам высоты у Vue для асінхронна адрадзіваных веб-компанентаў

Дазвольце дазнаць, чаму анімацыя пераходу расширэння Vue зменшваеся з 0px да 0px паўстаноўкай Web Components, і як ResizeObserver адкладае анімацыю пакуль не будзе вялічыны.

1144 слоў

Компанент для розгортання/згортання, які можна викорыстоўваць па-разу, створаны на базе <Transition> у Vue, зазвычай виміроўвае высоту элемента ў хуке enter і анімавае його з нуля да гэтага значэння. Гэта добра працуе для стандартных элементаў і звычных компанентаў Vue, але можа моўчкі не працаваць, калі дзецявы элемент — это дынамічна завантажаны Web Component: панель з’яўляецца без жадной анімацыі. У гэтым кярыянте пояснюецца проблема часавага падбору, якая стоіць за гэтым некарэтным робачым станам, чаму польсінг ёсць толькі частковым рашэнням, і як викорыстоўваць ResizeObserver, каб пераход запускаўся роўна тады, калі контэнт мае рэальныя размеры.

Чаму анімацыя праходзіць з 0px на 0px

Падазроўваючы, што паведамленняя пра аблыканніх выкарыстоўваюцца элементам упавядомлення, які реалізаваны як спецыяльны элемент і загорнуты ў пераход розгортання:

<transition-expand>
  <notification-wrapper v-if="showError" />
</transition-expand>

Калі showError прымае значэнне true, Vue дадаўае <notification-wrapper> у DOM і негайна вызывае функцыю enter пераходу. Для компаненту Vue гэта нічога не стварае, таму што яго маркап вэлічынюецца ўжо тады. Аднак багато Web Components апдаюцца асінхронна: таг падае ў DOM як порожняя, невядомая елемент, і толькі пазней, калі яго вызначэнне завантажыцца і його shadow DOM будзе адрасаваны, ён прабывае контэнт і высоту.

Рэальная парада здарэнняў выглядае так:

v-if becomes true
        ↓
Vue inserts the element
        ↓
Transition enter() runs
        ↓
Height is still 0px
        ↓
Web Component finishes rendering
        ↓
Actual height becomes 160px

У трэцім кроку пераход выконвае свою вызначэнную меру, калі елемент яшчэ ў форме порожней шкляпы. Ён вярна анімаваецца з 0px да вымеранага 0px, а чырвоны праз момант компанент адрасаваецца ў свою пачатковую высоту 160px без жаднага пераходу. Для пользователя выглядае так, нібы анімазія ўжо не выканалася.

Альтернатыўны спосаб зьвяртання к пыткам і яго выкалікаты

Вочывае рашэньне — чакаць, пакуль элемент не паведаміць свою высоту, перш чым яе вимерваць. Цэлкам можна адсакаваць за дапамою ціклу, який пераконтваецца аднае за раз на кожны кадр:

while (element.scrollHeight === 0) {
  await new Promise(requestAnimationFrame)
}

Гэты спосаб дзейнае. Кожная ітерацыя чакае наступны кадр анімасці і знову пераконтваецца, чы не застаецца scrollHeight равным нулю. Недзеяючы момент у тым, што цікл паўтарае запиты да лэйауту на кожны кадр, пакуль чыго-небудзь не змяніцца, і якщо контэнт ніколі не павышае сваю высоту, напрыклад, таму што компонент не можа быць завантажаны, цікл будзе працаваць так далей, калі толькі элемент існуе. Кроме таго, гэта ператварае enter у асінхронную функцыю, што ускладняе розумэнне ўсіго механізму. Браузер вже мае механізм паведамлення самэй для такой ситуацыі, таму няма прычыны запытваць яго пра тое ж самае шасцудзе на секунду.

Разьеднанне вимервання і анімасці

Перш чым перайсці на варыянт з абзэрватарам, корисна незначная пераструктурацыя коду для пераходу. У стандартнай рэалізацыі функцыя enter() адначасова вимервае элемент і запускае анімацыю. Адныя як толькі вимеровка можа знадобіцца чакаць, тым лепш выдзеліць этыя два заведамення.

Сам код анімацыі застаецца такім жа, як і раней; ён проста пераводзіцца у дапаможную функцыю пад назвай animateEnter(). Новая функцыя enter() тады мае адну задачу — вяршыць выбір часу запуску:

  • Калі элемент вже можна вимерваць, негайна вызваць animateEnter().
  • Інакш, чакаць пакі змогы вимерваць яго, а потым вызваць animateEnter().

Эта переработка не змінюе працэўкі для стандартных ситуацый, а таксама дае логіке чакання чыстае, аўтаномнае месца.

Чаканне на рэальную высоту з ResizeObserver

ResizeObserver паведамляе, калі зменяецца размер элемента, і гэта самэўсёльва той сігнал, які патрэбен тут. Пасля выкарыстоўвання анімазіі новы метод enter() стае корачы:

function enter(element: HTMLElement) {
  if (element.scrollHeight > 0) {
    animateEnter(element)
    return
  }
  const observer = new ResizeObserver(() => {
    if (element.scrollHeight === 0)
      return
    observer.disconnect()
    requestAnimationFrame(() => {
      animateEnter(element)
    })
  })
  observer.observe(element)
}

Разберім усё па порядку. Швайны шлях працюе з натыўнымі элементамі і вяліканутымі компонентамі: якщо scrollHeight ў паазытным значэнні, анімазія запускаецца негайна, як і раней. У працоўным разе да элемента прыкладзаецца абсерватар. Яго функцыя-адаптавальнік зноў пераканалівае высоту і вяртаецца раней, калі яна ўсё ў значэнні нуля; гэты захад важны, таму што абсерватар таксама запускаецца ў момент пачатку спостэрэння, калі элемент можа ўсё ўжо быць порожнім. Калі з’яўляецца рэальная высота, абсерватар сам сабе ад’єктуецца, таму ён не будзе продаваць сігналы праз наступныя змены размера, а анімазія запускаецца на наступны кадр анімазіі, даўаючы браузеру час адладзіць лейаут прычым пераходу.

Змянілася толькі момент, калі запускаецца анімацыя. Сама анімацыя застала недастрыжанай.

Спецыяльныя ситуацыі, якія трэба адрабатаваць у прыменні

Залежна ад таго, як викорыстоўваецца компанент, варта рассмотрзіць калькі ситуацый:

  • Якщо элемент знімаецца або хаваецца раней, чым у яго з’яўляецца высота, назіральнік застаецца прыяўленым. Яго ад’ём у момент завершэння пераходу аб анулювання запобегае тым, каб назіральнік застаўся прыяўленым.
  • Назіральнік реагуе на размер элемента, які ён стежыць. Якщо ваш пункт beforeEnter задае элементу значэнне height: 0, пераканайцеся, што элемент у такім стане можа фактычна зменіць размер, або ж стежыце за внутрашнім контэнтам заместа таго.
  • Калі пераход выконваецца абсалютна ўсё за дапамою хуків JavaScript, Vue чакае, што вы падасте сигнал завершэння через абяранне done, якое пасылаецца да enter. Убедзіцеся, што гэты сигнал все ўсё надаецца пасля затрыманага запуску.
  • Чаму падход з абзэрватаром є болей надзеяным

    Параўняючы з методам польнінгу, версія ResizeObserver мае калькі прынад:

    • Няма циклу па кожным кадру для перагляду лэй아уту.
    • Ён справляецца з Web Components, які апдэйтуюцца асінхронна.
    • Ён таксама справляецца з компанентамі Vue, які завантажаюцца з запазджэнням.
    • Ён справляецца з адпраўкамі та іншым кантэнтам, размеры якога зменяюцца пасля першага атрыбутавання.
    • Пераход застаецца універсальным, без веды пра тое, што ён абгружае.

    Гэты пасляпэўны момант ёсць найценнейшы. Компоненту expand не трэба ведаць, чыі ўрадзік — это натыўны div, компонент Vue чы спецыяльны элемент; ён чакае, пакуль абгортаваны контэнт не падае ненульовы размер, а потым запускае анімацыю.

    Заключэнне

    Перадавальны падход «зважыць, а потым анімаваць» застаецца цалкам падходяным для большасці прыемоў Vue, дзе контэнт вужо распакаваны на момент выконання enter. Корэктаванне стае важлівым, калі адрасаванне ўрадзіка ёсць асінхронным і лейаут не готавы на момент, калі Vue вставляе элемент. Абщая наука для перадаўальных UI-прымітываў — не выкладвацца на тое, калі будзе готавы лейаут: уместа таго трэба реагаваць на сябеўны сігнал браузера. Цэе невялікая змена ў кодзе, якая значна павышае надзейныцю, а кожны такі крайні случай у рэальной інтеграцыі робіць компонент моцнейшым для кожнага прыему, які ўжо будзе яго викорыстоўваць.