На днях столкнулись с такой проблемой. Сгенерированный на стороне сервера код отказывался гидратироваться в Safari.
Гидратация относится к процессу на стороне клиента, в течение которого Vue берёт статический HTML, отправленный сервером, и превращает его в динамический DOM, который может реагировать на изменения данных на стороне клиента. Подробнее тут.
«Прод» просто падал, а dev-версия сообщала, что имеются расхождения в dom. А так как dev-версия не падает при попытке гидратации, а только сообщает об этом в консоли, ошибка была неочевидна и пока мы ее нашли, прошло довольно много времени.
Очень интересная стратегия от Vue – подождать продакшена и там упасть!
Полторы сотни компонентов разной сложности задачу не упрощали. В итоге удалось увидеть проблему, найти подходящее устройство и подружить его с консолью разработчика.

В итоге выяснилось, что падает наше приложение при подключении компонента футера. И когда нашли нужную строку, просто не поверили глазам. Ожидали все, что угодно, только не этого.

Оказалось, что когда удаляешь номер телефона, гидратация проходила без проблем. Когда начали копаться, выяснилось, что Safari после получения html-верстки подставлял рядом с телефоном тег a, который вызывал, собственно, приложение набора номера.

Естественно, когда начиналась гидратация, dom-а пришедшей с сервера страницы и вновь построенного виртуального не совпадали. Приложение падало без объявления войны.

Решилась это проблема тоже довольно неожиданно. До сего момента телефон мы вставляли обычным образом:

<div>8 (800) 111 2 333</div>

Решением проблемы стал биндинг v-text:

<div v-text=”8 (800) 111 2 333”></div>

У меня есть теория на этот счет. Если кто-то сможет подтвердить ее или опровергнуть (предложив новую), буду очень признателен. Как я понимаю, после того, как Safari получил документ, Vue строит виртуальный dom и сравнивает его с этим документом и пока он этот документ гидратирует, Safari занимается своим тёмным делом и меняет телефон на ссылку. Когда доходит дело до этого поля, Vue с помощью v-text снова заменяет содержимое нашего дива на нужное нам. В итоге на момент сравнения dom-а совпадают, полет нормальный.

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


  1. aamonster
    21.11.2018 12:11

    Я правильно понял, что ваше решение ломает «кликабельность» имеющихся на страничке номеров телефонов? Или вы их делаете кликабельными самостоятельно, и «искуственный интеллект» вам не нужен?


    1. HAGer2000 Автор
      21.11.2018 13:20

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

      По идее, если ссылки сами по себе уже содержат указание на кликабельность телефона, вряд ли сафари будет в это дело вмешиваться. Другое вопрос, это, как бы, вообще не его дело, кликабельны телефоны или нет. Хочешь сделать такой функционал, решай вопрос на уровне DOM-свойств и событий, зачем подменять html?


      1. aamonster
        21.11.2018 15:31

        Погодите. Как, где и зачем Safari подменяет html? Если я правильно понял, оно его просто парсит чуть иначе, чем «гидратация» — т.е. отличия именно на уровне DOM. Вроде в пределах допустимого (более того: аналогичный пример прописан в руководстве по данной вами ссылке).

        Но решение vue.js, конечно, феерическое: сделать обход этой ситуации в дебаге — это додуматься надо. Всю жизнь принято делать в дебаге assert — чтобы fail fast и всё такое, а если уж делается обход для кривого случая — в релизе он 100% обязан использоваться. Дебаг может (и должен) упасть раньше релиза, но не упасть там, где релиз сломается — большой косяк.


        1. HAGer2000 Автор
          21.11.2018 15:42

          т.е. отличия именно на уровне DOM

          ну как… у нас есть dom-узел, у него есть свойства, которые появляются при парсинке html-разметки. То есть, в разметке мы прописываем тегу атрибуты (attributes), которые в итоге отражаются в свойства (properties) dom-узла. Если нам надо навесить обработчик события на какой-то элемент, в данном случае номер телефона, то нам надо работать именно с dom-элементом. Я об этом.

          Сафари поступает иначе. Он получает html. Во вкладке Network видно, какой был ответ от сервера. Затем меняет текстовую ноду с телефоном на ссылку с href=«tel: тра-та-та» затем парсит это в dom-ноду с соответствующими свойствами. То есть, можно было бы (если я нигде не ошибся) повесить обработчик на эту ноду на уровне dom-properties не меняя состава dom-дерева. Тут, я правда, не уверен, при сравнении во время гидратации, не будет ли расхождением разные свойства этой самой ноды…


          1. aamonster
            21.11.2018 15:53

            Ну так пример с tbody — ровно то же самое. Не было никакого tbody в html, но браузер его втыкает, потому что считает, что так будет лучше =).
            Очень плохо иметь два независимых парсера html.

            А насчёт расхождения при гидратации — возможно, и это будет ещё фееричнее: дебаг и релиз будут просто работать на разном DOM.


            1. HAGer2000 Автор
              21.11.2018 16:05

              Возможно и то же самое. Но лично для меня такая подстава с номером телефона была неожиданной


              1. aamonster
                21.11.2018 16:18

                Естественно, неожиданной
                А теперь время для стука в дно: на десктопных браузерах есть энное количество расширений, правящих DOM. И в идеале бы с ними не конфликтовать...


    1. Zavtramen
      21.11.2018 13:25
      +1

      Так телефоны надо через href=«tel:» делать вроде.


      1. aamonster
        21.11.2018 15:25

        Я так понял, на сайтах очень часто такой URI не прописан, и Сафари умничает — чтобы в написанный на сайте телефон всегда можно было кликнуть.


  1. staticlab
    21.11.2018 12:14
    +1

    <meta name="format-detection" content="telephone=no">

    не спасёт отца русской демократиии?


    1. a_e_tsvetkov
      21.11.2018 12:38
      +1

      А завтра они решат адреса в ссылки на карте превращать. Что тогда делать?


      1. staticlab
        21.11.2018 12:42

        Думаю, что тогда появится аналогичный мета-тег :)


        1. ZaEzzz
          21.11.2018 12:45

          Обязательно появится. Огорчают только несколько моментов:
          1 — Head уже превращается в свалку.
          2 — Обычно о таких случаях узнаешь при появлении проблемы, а не заранее.


        1. a_e_tsvetkov
          21.11.2018 13:01

          Слава богу я не вебдевелопер :)


    1. ZaEzzz
      21.11.2018 12:41

      Думаю, спасет.
      Мы тоже недавно наткнулись на необходимость использования этого метатега — при отображении статистики некоторые цифры превращались в кликабельные телефонные номера.

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


      1. HAGer2000 Автор
        21.11.2018 13:26

        На мой взгляд, в текущей реализации этого функционала в Сафари, это вряд ли возможно. Он меняет дом, меняет атрибуты, создает новые ноды в дереве. При таком раскладе гидратация невозможна.

        Ну либо эту часть сайта генерить только на клиенте, чтобы сервер вообще не знал о наличии телефонов


        1. ahmpro
          21.11.2018 13:42

          А нельзя просто исключить этот кусок через no-ssr? Так ли критично, чтобы он гидрировал? :)


          1. HAGer2000 Автор
            21.11.2018 13:50

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

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


      1. mayorovp
        21.11.2018 18:23

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


  1. k12th
    21.11.2018 13:07
    +2

    А как насчет <a href="tel:88001112333">8 (800) 111 2 333</a>?


  1. Fragster
    21.11.2018 16:33

    В документации прямо так и написано:

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

    Но вообще если apple так делает, то, вероятно, надо завести issue, чтобы vue изменил это поведение (не падал, а перерендерил компонент просто).


  1. gli
    22.11.2018 11:56

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


  1. CyberAP
    22.11.2018 13:09

    То что вы описали это ошибка Vue. Гидратация должна всегда быть failsafe. Safai конечно тоже молодец что без спроса меняет DOM, но такие вещи не должны ломать всё приложение.


    1. HAGer2000 Автор
      22.11.2018 15:10

      полностью согласен
      правда слабо представляю себе, как это на практике починить


      1. staticlab
        22.11.2018 15:47

        Навскидку — обернуть гидратацию в try... catch, а в catch материться в консоль, чистить рутовый элемент и рендерить заново без участия сервера.


        1. HAGer2000 Автор
          22.11.2018 16:15

          Видимо, примерно так сделано в dev-сборке. В проде они намерено отключили такую возможность


          1. staticlab
            22.11.2018 16:22
            +2

            И это свинство с их стороны. Хотя бы потому, что при тесте на телефонах консоль с ошибками не видно (если не подключать к компьютеру), а при автотестах всё пройдёт, потому что не падает. React, например, просто ругается во всех случаях.


            1. HAGer2000 Автор
              22.11.2018 16:23

              да