BBC 互动内容如何跨 AMP、应用程序和 Web 工作

已发表: 2022-03-10
快速总结 ↬在没有大量额外开发开销的情况下将内容发布到如此多的媒体可能很困难。 Chris Ashton 解释了他们是如何在 BBC 的视觉新闻部门解决这个问题的。

在 BBC 的视觉新闻团队中,我们制作令人兴奋的视觉、引人入胜和互动的内容,从计算器到可视化新的故事讲述格式。

每个应用程序本身都是一个独特的挑战,但当您考虑到我们必须以多种不同的语言部署大多数项目时更是如此。 我们的内容不仅可以在 BBC 新闻和体育网站上运行,而且还可以在 iOS 和 Android 上的等效应用程序以及使用 BBC 内容的第三方网站上运行。

现在考虑有越来越多的新平台,例如 AMP、Facebook Instant Articles 和 Apple News。 每个平台都有自己的局限性和专有的发布机制。 创建适用于所有这些环境的交互式内容是一项真正的挑战。 我将描述我们在 BBC 是如何解决这个问题的。

示例:Canonical 与 AMP

在您实际看到它之前,这都是理论性的,所以让我们直接研究一个示例。

这是包含视觉新闻内容的 BBC 文章:

包含视觉新闻内容的 BBC 新闻页面截图
我们的视觉新闻内容以唐纳德特朗普的插图开始,并位于 iframe 中

这是文章的规范版本,即默认版本,如果您从主页导航到文章,您将获得该版本。

跳跃后更多! 继续往下看↓

现在让我们看一下文章的AMP版本:

BBC 新闻 AMP 页面的屏幕截图,其中包含与以前相同的内容,但内容已被剪辑并具有“显示更多”按钮
这看起来与普通文章的内容相同,但引入了专门为 AMP 设计的不同 iframe

虽然规范版本和 AMP 版本看起来相同,但它们实际上是具有不同行为的两个不同端点

  • 当您提交表格时,规范版本会将您滚动到您选择的国家/地区。
  • AMP 版本不会滚动您,因为您无法从 AMP iframe 中滚动父页面。
  • AMP 版本显示带有“显示更多”按钮的裁剪 iframe,具体取决于视口大小和滚动位置。 这是 AMP 的一个功能。

除了本文的规范版本和 AMP 版本外,该项目还发布到 News App,这是另一个具有自身复杂性和局限性的平台。 那么我们如何支持所有这些平台呢?

工具是关键

我们不会从头开始构建我们的内容。 我们有一个基于 Yeoman 的脚手架,它使用 Node 通过单个命令生成样板项目。

新项目带有开箱即用的 Webpack、SASS、部署和组件化结构。 国际化也融入了我们的项目,使用 Handlebars 模板系统。 Tom Maslen 在他的帖子中详细描述了这一点,13 条使响应式网页设计成为多语言的技巧。

开箱即用,这对于为一个平台进行编译非常有效,但我们需要支持多个平台。 让我们深入研究一些代码。

嵌入与独立

在视觉新闻中,我们有时会在 iframe 中输出我们的内容,以便它可以独立“嵌入”文章中,不受全局脚本和样式的影响。 这方面的一个例子是嵌入在本文前面的规范示例中的 Donald Trump 交互。

另一方面,有时我们将内容输出为原始 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 交互都做同样的事情。 一旦你完成了颤抖,给自己泡一杯放松的茶,然后继续阅读。

抽象是关键

我们没有强迫我们的开发人员在他们的代码中处理这些条件,而是在他们的内容和环境之间建立了一个抽象层。 我们将此层称为“包装器”。

我们现在可以通过wrapper模块代理我们的请求,而不是直接查询 DOM 或本机浏览器事件。

 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包含您的应用程序代码不需要自己定义的特定于包装器的样式。 例如,嵌入包装器在 iframe 中复制了许多 BBC 新闻样式。

最后, js/wrapper.js包含包装器 API 的 iframed 实现,详情如下。 它是单独交付到项目中的,而不是与应用程序代码一起编译的——我们在 Webpack 构建过程中将wrapper标记为全局。 这意味着尽管我们将应用程序交付到多个平台,但我们只编译一次代码。

包装器 API

包装 API 抽象了许多关键的浏览器交互。 以下是最重要的:

scrollTo(int)

滚动到活动窗口中的给定位置。 包装器将在触发滚动之前对提供的整数进行规范化,以便将主机页面滚动到正确的位置。

getScrollPosition: int

返回用户的当前(标准化)滚动位置。 在 iframe 的情况下,这意味着传递给您的应用程序的滚动位置实际上是负数,直到 iframe 位于视口的顶部。 这非常有用,可以让我们做一些事情,比如只在组件出现时才对其进行动画处理。

onScroll(callback)

提供滚动事件的挂钩。 在独立包装器中,这本质上是挂钩到本机滚动事件。 在嵌入包装器中,接收滚动事件会稍有延迟,因为它是通过 postMessage 传递的。

viewport: {height: int, width: int}

一种检索视口高度和宽度的方法(因为从 iframe 中查询时实现方式非常不同)。

toggleFullScreen

在独立模式下,我们隐藏 BBC 菜单和页脚并设置position: fixed在我们的内容上。 在 News App 中,我们什么都不做——内容已经全屏显示。 复杂的是 iframe,它依赖于在 iframe 内部和外部应用样式,通过 postMessage 进行协调。

markPageAsLoaded

告诉包装器您的内容已加载。 这对于我们的内容在 News App 中的工作至关重要,在我们明确告诉应用我们的内容已准备好之前,它不会尝试向用户显示我们的内容。 它还删除了我们内容的网络版本上的加载微调器。

包装器列表

未来,我们设想为 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 渲染器做了一点魔法来渲染一些引用我们内容的 AMP HTML,将/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

...等等

该解决方案可以扩展以在出现更多平台类型时合并它们。

抽象是困难的

构建“一次编写,随处部署”的架构听起来很理想化,而且确实如此。 为了使包装器架构正常工作,我们必须非常严格地在抽象中工作。 这意味着我们必须抵制“做这个骇人听闻的事情以使其在 [insert platform name here] 中工作”的诱惑。 我们希望我们的内容完全不了解它的交付环境——但这说起来容易做起来难。

平台的功能难以抽象配置

在我们采用抽象方法之前,我们可以完全控制输出的各个方面,例如,包括 iframe 的标记。 如果我们需要在每个项目的基础上调整任何内容,例如出于可访问性原因向 iframe 添加title属性,我们可以只编辑标记。

现在包装器标记与项目隔离存在,配置它的唯一方法是在脚手架本身中公开一个钩子。 对于跨平台功能,我们可以相对轻松地做到这一点,但是为特定平台公开挂钩会破坏抽象。 我们真的不想公开仅由一个包装器使用的“iframe 标题”配置选项。

我们可以更通用地命名该属性,例如title ,然后将此值用作 iframe title属性。 但是,要跟踪在哪里使用了什么变得越来越困难,并且我们冒着将配置抽象到不再理解它的风险。 总的来说,我们尽量保持我们的配置精简,只设置具有全局使用的属性。

组件行为可能很复杂

在网络上,我们的 sharetools 模块吐出社交网络共享按钮,这些按钮可以单独点击,并在新窗口中打开预先填充的共享消息。

BBC sharetools 部分的屏幕截图包含 Twitter 和 Facebook 社交媒体图标。
BBC 视觉新闻分享工具提供社交分享选项列表

在新闻应用中,我们不想通过移动网络分享。 如果用户安装了相关的应用程序(例如 Twitter),我们希望在应用程序本身中共享。 理想情况下,我们希望向用户展示原生 iOS/Android 共享菜单,然后让他们选择他们的共享选项,然后再通过预先填充的共享消息为他们打开应用程序。 我们可以通过调用专有的bbcvisualjournalism://协议从应用程序触发本机共享菜单。

Android 上共享菜单的屏幕截图,其中包含通过消息、蓝牙、复制到剪贴板等进行共享的选项。
Android 上的本机共享菜单

但是,无论您在“分享您的结果”部分中点击“Twitter”还是“Facebook”,都会触发此屏幕,因此用户最终不得不做出两次选择; 第一次在我们的内容中,第二次在本机弹出窗口中。

这是一个奇怪的用户旅程,所以我们想从新闻应用程序中删除单个共享图标,并显示一个通用的共享按钮。 我们可以通过在渲染组件之前显式检查正在使用的包装器来做到这一点。

新闻应用分享按钮的屏幕截图。这是一个带有以下文本的按钮:“分享你的做法”。
新闻应用程序中使用的通用分享按钮

构建包装抽象层对于整个项目来说效果很好,但是当您选择的包装影响组件级别的更改时,很难保持干净的抽象。 在这种情况下,我们失去了一点抽象,我们的代码中有一些混乱的分叉逻辑。 值得庆幸的是,这些案例很少见。

我们如何处理缺失的功能?

保持抽象一切都很好。 我们的代码告诉包装器它希望平台做什么,例如“全屏显示”。 但是,如果我们要运送到的平台实际上不能全屏显示怎么办?

包装器将尽最大努力不完全破坏,但最终您需要一个设计,无论该方法是否成功,它都能优雅地回退到一个工作解决方案。 我们必须进行防守设计。

假设我们有一个包含一些条形图的结果部分。 我们经常喜欢将条形图的值保持为零,直到图表滚动到视图中,此时我们触发条形动画到正确的宽度。

将用户区域与全国平均水平进行比较的一组条形图的屏幕截图。每个条的值都显示为条右侧的文本。
显示与我所在区域相关的值的条形图

但是,如果我们没有机制来挂钩滚动位置(就像我们的 AMP 包装器中的情况一样),那么这些条将永远保持为零,这是一种完全误导的体验。

条形图的屏幕截图与以前相同,但条形图有 0&#37;宽度和每个条的值固定为 0&#37;。这是不正确的。
如果不转发滚动事件,条形图的外观

我们越来越多地尝试在我们的设计中采用更多的渐进增强方法。 例如,我们可以提供一个按钮,默认情况下对所有平台都可见,但如果包装器支持滚动,它会隐藏。 这样,如果滚动未能触发动画,用户仍然可以手动触发动画。

与错误 0&#37; 相同的条形图屏幕截图条形图,但这次带有微妙的灰色叠加层和一个居中的按钮,邀请用户“查看结果”。
我们可以改为显示一个后备按钮,它会在点击时触发动画。

对未来的计划

我们希望为 Apple News 和 Facebook Instant Articles 等平台开发新的包装器,并为所有新平台提供开箱即用的我们内容的“核心”版本。

我们也希望在渐进增强方面做得更好; 在这个领域取得成功意味着在防守上有所发展。 你永远不能假设现在和未来的所有平台都会支持给定的交互,但是一个设计良好的项目应该能够在不遇到第一个技术障碍的情况下传达其核心信息

在包装器的范围内工作是一种范式转变,就长期解决方案而言,感觉有点像中途旅行。 但在行业成熟到跨平台标准之前,出版商将被迫推出自己的解决方案,或使用 Distro 等工具进行平台到平台的转换,或者完全忽略他们的整个受众群体。

这对我们来说还处于早期阶段,但到目前为止,我们已经在使用包装器模式构建我们的内容并将其交付到我们的受众现在使用的无数平台方面取得了巨大成功。