Как интерактивный контент BBC работает в AMP, приложениях и Интернете

Опубликовано: 2022-03-10
Краткий обзор ↬ Публикация контента на таком количестве носителей без больших дополнительных затрат на разработку может оказаться сложной задачей. Крис Эштон объясняет, как они подошли к этой проблеме в отделе визуальной журналистики BBC.

В команде визуальной журналистики BBC мы создаем захватывающий визуальный, увлекательный и интерактивный контент, начиная от калькуляторов и заканчивая визуализацией новых форматов повествования.

Каждое приложение само по себе представляет собой уникальную задачу, но тем более, если учесть, что нам приходится развертывать большинство проектов на разных языках. Наш контент должен работать не только на веб-сайтах BBC News и Sports, но и в их эквивалентных приложениях для iOS и Android, а также на сторонних сайтах, использующих контент BBC.

Теперь учтите, что количество новых платформ, таких как AMP, Facebook Instant Articles и Apple News, постоянно растет . Каждая платформа имеет свои ограничения и собственный механизм публикации. Создание интерактивного контента, который работает во всех этих средах, является настоящей проблемой. Я собираюсь описать, как мы подошли к этой проблеме на BBC.

Пример: Canonical против AMP

Это все немного теоретически, пока вы не увидите это в действии, так что давайте углубимся прямо в пример.

Вот статья BBC, содержащая содержание визуальной журналистики:

Скриншот страницы BBC News, содержащей материалы визуальной журналистики
Наш контент в области визуальной журналистики начинается с иллюстрации Дональда Трампа и находится внутри iframe.

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

Еще после прыжка! Продолжить чтение ниже ↓

Теперь давайте посмотрим на AMP-версию статьи:

Скриншот страницы BBC News AMP, содержащей тот же контент, что и раньше, но контент обрезан и имеет кнопку «Показать больше».
Это похоже на то же содержание, что и обычная статья, но использует другой iframe, разработанный специально для AMP.

Хотя каноническая версия и версия AMP выглядят одинаково, на самом деле это две разные конечные точки с разным поведением:

  • Каноническая версия прокручивает вас до выбранной вами страны при отправке формы.
  • Версия AMP не прокручивает вас, так как вы не можете прокручивать родительскую страницу из iframe AMP.
  • Версия AMP показывает обрезанный iframe с кнопкой «Показать больше», в зависимости от размера области просмотра и положения прокрутки. Это особенность AMP.

Помимо канонической и AMP-версий этой статьи, этот проект также был отправлен в приложение «Новости», которое является еще одной платформой со своими сложностями и ограничениями. Так как же мы поддерживаем все эти платформы?

Инструменты — это ключ

Мы не создаем наш контент с нуля. У нас есть скаффолд на основе Yeoman, который использует Node для создания стандартного проекта с помощью одной команды.

Новые проекты поставляются с готовыми Webpack, SASS, развертыванием и компонентной структурой. Интернационализация также встроена в наши проекты с использованием системы шаблонов Handlebars. Том Маслен подробно пишет об этом в своем посте «13 советов, как сделать адаптивный веб-дизайн многоязычным».

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

Встроить против автономного

В визуальной журналистике мы иногда выводим наш контент внутри iframe, чтобы он мог быть автономным «встраиванием» в статью, на который не влияют глобальные сценарии и стили. Примером этого является интерактив Дональда Трампа, встроенный в канонический пример ранее в этой статье.

С другой стороны, иногда мы выводим наш контент в виде необработанного HTML. Мы делаем это только тогда, когда у нас есть контроль над всей страницей или если нам требуется действительно отзывчивое взаимодействие при прокрутке. Давайте назовем их нашими «встроенными» и «автономными» выводами соответственно.

Давайте представим, как мы могли бы построить вопрос «Заберет ли робот вашу работу?» интерактивный как в «встроенном», так и в «автономном» формате.

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

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

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

«Автономная» версия кода может выглядеть так:

 button.on('click', (e) => { window.scrollTo(0, resultsContainer.offsetTop); });

Но если бы вы создавали это как «встроенный» вывод, вы знаете, что ваш контент находится внутри iframe, поэтому вам нужно было бы кодировать его по-другому:

 // inside the iframe button.on('click', () => { window.parent.postMessage({ name: 'scroll', offset: resultsContainer.offsetTop }, '*'); }); // inside the host page window.addEventListener('message', (event) => { if (event.data.name === 'scroll') { window.scrollTo(0, iframe.offsetTop + event.data.offset); } });

Кроме того, что, если нашему приложению нужно перейти в полноэкранный режим? Это достаточно просто, если вы находитесь на «отдельной» странице:

 document.body.className += ' fullscreen';
 .fullscreen { position: fixed; top: 0; left: 0; right: 0; bottom: 0; } 
Скриншот встроенной карты с наложением «Нажмите для взаимодействия», за которым следует скриншот карты в полноэкранном режиме после нажатия.
Мы успешно используем полноэкранные функции, чтобы максимально использовать наш модуль карты на мобильных устройствах.

Если бы мы попытались сделать это изнутри «встраивания», этот же код имел бы масштабирование контента по ширине и высоте iframe , а не по области просмотра:

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

…поэтому в дополнение к применению полноэкранного стиля внутри iframe мы должны отправить сообщение на хост-страницу, чтобы применить стиль к самому iframe:

 // iframe window.parent.postMessage({ name: 'window:toggleFullScreen' }, '*'); // host page window.addEventListener('message', function () { if (event.data.name === 'window:toggleFullScreen') { document.getElementById(iframeUid).className += ' fullscreen'; } });

Это может превратиться в много спагетти-кода, когда вы начнете поддерживать несколько платформ:

 button.on('click', (e) => { if (inStandalonePage()) { window.scrollTo(0, resultsContainer.offsetTop); } else { window.parent.postMessage({ name: 'scroll', offset: resultsContainer.offsetTop }, '*'); } });

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

Абстракция — это ключ

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

Вместо того, чтобы напрямую запрашивать DOM или собственные события браузера, теперь мы можем проксировать наш запрос через модуль- wrapper .

 import wrapper from 'wrapper'; button.on('click', () => { wrapper.scrollTo(resultsContainer.offsetTop); });

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

Диаграмма UML, показывающая, что, когда наше приложение вызывает автономный метод прокрутки оболочки, оболочка вызывает собственный метод прокрутки на главной странице.
Простая реализация 'scrollTo' автономной оболочкой

Реализация функции scrollTo в автономной оболочке очень проста: наш аргумент передается непосредственно в window.scrollTo под капотом.

Теперь давайте посмотрим на отдельную оболочку, реализующую ту же функциональность для iframe:

Диаграмма UML, показывающая, что когда наше приложение вызывает метод прокрутки встроенной оболочки, встроенная оболочка объединяет запрошенную позицию прокрутки со смещением iframe перед запуском собственного метода прокрутки на главной странице.
Расширенная реализация scrollTo с помощью встроенной оболочки

Оболочка «встраивания» принимает тот же аргумент, что и в «автономном» примере, но манипулирует значением, чтобы учитывать смещение iframe. Без этого дополнения мы бы прокручивали нашего пользователя совершенно непреднамеренно.

Шаблон обертки

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

Итак, как выглядит обертка?

Структура оболочки

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

Это упрощенный вид встроенной оболочки:

 embed-wrapper/ templates/ wrapper.hbs js/ wrapper.js scss/ wrapper.scss

Наш базовый скаффолдинг предоставляет ваш основной шаблон проекта в виде партиала Handlebars, который используется оболочкой. Например, templates/wrapper.hbs может содержать:

 <div class="bbc-news-vj-wrapper--embed"> {{>your-application}} </div>

scss/wrapper.scss содержит специфичные для оболочки стили, которые код вашего приложения не должен определять сам. Оболочка для встраивания, например, повторяет многие стили BBC News внутри iframe.

Наконец, js/wrapper.js содержит iframe-реализацию API-оболочки, подробно описанную ниже. Он поставляется в проект отдельно, а не компилируется с кодом приложения — мы помечаем wrapper как глобальную в нашем процессе сборки Webpack. Это означает, что хотя мы поставляем наше приложение на несколько платформ, мы компилируем код только один раз.

API-интерфейс оболочки

API-интерфейс оболочки абстрагирует ряд ключевых взаимодействий с браузером. Вот наиболее важные из них:

scrollTo(int)

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

getScrollPosition: int

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

onScroll(callback)

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

viewport: {height: int, width: int}

Метод для получения высоты и ширины области просмотра (поскольку это реализовано совершенно по-разному при запросе из iframe).

toggleFullScreen

В автономном режиме мы скрываем меню BBC и нижний колонтитул от просмотра и устанавливаем position: fixed для нашего контента. В приложении «Новости» вообще ничего не делаем — контент уже на весь экран. Сложный — это iframe, который основан на применении стилей как внутри, так и снаружи iframe, координируемых через postMessage.

markPageAsLoaded

Сообщите обёртке, что ваш контент загружен. Это очень важно для того, чтобы наш контент работал в приложении новостей, которое не будет пытаться отобразить наш контент пользователю, пока мы явно не сообщим приложению, что наш контент готов. Он также удаляет счетчик загрузки в веб-версиях нашего контента.

Список оберток

В будущем мы планируем создать дополнительные оболочки для крупных платформ, таких как Facebook Instant Articles и Apple News. На сегодняшний день мы создали шесть оберток:

Автономная обертка

Версия нашего контента, которая должна размещаться на отдельных страницах. Поставляется в комплекте с брендингом BBC.

Встроить оболочку

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

AMP-оболочка

Это конечная точка, которая загружается как amp-iframe на страницы AMP.

Обертка новостного приложения

Наш контент должен вызывать собственный протокол bbcvisualjournalism:// .

Основная оболочка

Содержит только HTML — никаких CSS или JavaScript нашего проекта.

JSON-оболочка

Представление нашего контента в формате JSON для совместного использования в продуктах BBC.

Обертки проводки до платформ

Чтобы наш контент появился на сайте BBC, мы предоставляем журналистам путь в пространстве имен:

 /include/[department]/[unique ID], eg /include/visual-journalism/123-quiz

Журналист вводит этот «включаемый путь» в CMS, которая сохраняет структуру статьи в базу данных. Все продукты и услуги находятся ниже этого механизма публикации. Каждая платформа отвечает за выбор нужного контента и запрос этого контента с прокси-сервера.

Возьмем тот интерактив с Дональдом Трампом, который был ранее. Здесь путь включения в CMS:

 /include/newsspec/15996-trump-tracker/english/index

Каноническая страница статьи знает, что ей нужна «встроенная» версия содержимого, поэтому она добавляет /embed к пути включения:

 /include/newsspec/15996-trump-tracker/english/index /embed

…перед запросом с прокси-сервера:

 https://news.files.bbci.co.uk/include/newsspec/15996-trump-tracker/english/index/embed

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

 /include/newsspec/15996-trump-tracker/english/index /amp

Средство визуализации AMP делает небольшое волшебство, чтобы отобразить некоторый HTML-код AMP, который ссылается на наш контент, извлекая версию /amp в виде iframe:

 <amp-iframe src="https://news.files.bbci.co.uk/include/newsspec/15996-trump-tracker/english/index/amp" width="640" height="360"> <!-- some other AMP elements here --> </amp-iframe>

Каждая поддерживаемая платформа имеет свою версию контента:

 /include/newsspec/15996-trump-tracker/english/index /amp

/include/newsspec/15996-trump-tracker/english/index /core

/include/newsspec/15996-trump-tracker/english/index /envelope

...и так далее

Это решение может масштабироваться для включения большего количества типов платформ по мере их появления.

Абстракция — это сложно

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

Особенности платформы сложно настроить абстрактно

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

Теперь, когда разметка-оболочка существует отдельно от проекта, единственным способом ее настройки будет предоставление хука в самом скаффолде. Мы можем сделать это относительно легко для кросс-платформенных функций, но раскрытие хуков для конкретных платформ нарушает абстракцию. На самом деле мы не хотим раскрывать параметр конфигурации «iframe title», который используется только одной оболочкой.

Мы могли бы назвать свойство более общим, например, title , а затем использовать это значение в качестве атрибута title iframe. Однако становится трудно отслеживать, что и где используется, и мы рискуем абстрагироваться от нашей конфигурации до такой степени, что перестанем ее понимать. По большому счету, мы стараемся, чтобы наш конфиг был как можно более компактным, устанавливая только те свойства, которые имеют глобальное использование.

Поведение компонентов может быть сложным

В Интернете наш модуль sharetools выдает кнопки общего доступа в социальных сетях, которые можно нажимать по отдельности и открывать предварительно заполненное сообщение общего доступа в новом окне.

Скриншот раздела BBC Sharetools, содержащего значки социальных сетей Twitter и Facebook.
Инструменты обмена BBC Visual Journalism представляют список вариантов публикации в социальных сетях.

В приложении «Новости» мы не хотим делиться через мобильную сеть. Если у пользователя установлено соответствующее приложение (например, Twitter), мы хотим поделиться им в самом приложении. В идеале мы хотим представить пользователю собственное меню общего доступа iOS/Android, а затем позволить ему выбрать свой вариант общего доступа, прежде чем мы откроем для него приложение с предварительно заполненным сообщением общего доступа. Мы можем вызвать собственное меню общего доступа из приложения, вызвав проприетарный протокол bbcvisualjournalism:// .

Скриншот меню «Поделиться» на Android с параметрами обмена через «Сообщения», «Bluetooth», «Копировать в буфер обмена» и т. д.
Нативное меню обмена на Android

Однако этот экран будет активирован независимо от того, нажмете ли вы «Twitter» или «Facebook» в разделе «Поделитесь своими результатами», поэтому пользователю в конечном итоге придется делать свой выбор дважды; первый раз внутри нашего контента и второй раз во всплывающем окне.

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

Скриншот кнопки «Поделиться» новостного приложения. Это единственная кнопка со следующим текстом: «Поделитесь, как вы это сделали».
Общая кнопка «Поделиться», используемая в приложении «Новости»

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

Как мы справляемся с недостающими функциями?

Сохранение абстракции — это хорошо. Наш код сообщает оболочке, что она хочет от платформы, например, «перейти в полноэкранный режим». Но что, если платформа, на которую мы поставляем, не может работать в полноэкранном режиме?

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

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

Скриншот набора гистограмм, сравнивающих область пользователя со средними показателями по стране. Каждая полоса имеет свое значение, отображаемое в виде текста справа от полосы.
Гистограмма, показывающая значения, относящиеся к моей области

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

Тот же скриншот гистограмм, что и раньше, но столбцы имеют 0&#37; ширина и значения каждой полосы фиксированы на 0&#37;. Это неправильно.
Как могла бы выглядеть гистограмма, если бы события прокрутки не пересылались

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

Тот же скриншот гистограммы, что и неправильный 0&#37; гистограммы, но на этот раз с тонкой серой накладкой и кнопкой по центру, приглашающей пользователя «Просмотреть результаты».
Вместо этого мы могли бы отобразить резервную кнопку, которая запускает анимацию при нажатии.

Планы на будущее

Мы надеемся разработать новые оболочки для таких платформ, как Apple News и Facebook Instant Articles, а также предложить всем новым платформам «базовую» версию нашего контента из коробки.

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

Работа в рамках обертки — это своего рода сдвиг парадигмы и похоже на промежуточный вариант с точки зрения долгосрочного решения. Но пока индустрия не созреет до межплатформенного стандарта, издатели будут вынуждены внедрять свои собственные решения или использовать такие инструменты, как Distro, для преобразования платформы в платформу, или вообще игнорировать целые группы своей аудитории.

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