Telegram-iOS 源碼分析:第七部分(Link Preview and Instant View)

版權(quán)聲明
本文內(nèi)容均為搬運(yùn),目的只為更方便的學(xué)習(xí)Telegram編碼思維。

如需查閱原作者文章,附贈(zèng)原文章機(jī)票

Telegram構(gòu)建了一組功能,使用戶長時(shí)間停留在應(yīng)用程序內(nèi)。本篇文章將闡述Telegram為什么需要這些功能以及如何有效地實(shí)現(xiàn)它們。

即時(shí)通訊中的內(nèi)容系統(tǒng)

在深入探討技術(shù)細(xì)節(jié)之前,我們可以從即時(shí)通訊的角度考慮內(nèi)容系統(tǒng)的作用。盡管Telegram中沒有集中的新聞源,為什么它如此重要?

如果我們只能為IM達(dá)成一個(gè)目標(biāo),那么絕對是消息的可達(dá)性。高可達(dá)性可以給用戶更多的信心,他們發(fā)送的消息將被對方可靠地查看,這應(yīng)該是使他們喜歡即時(shí)通訊的最終原因。事實(shí)證明,內(nèi)容系統(tǒng)是提高用戶可訪問性的阻礙,因?yàn)榧词箾]有消息可查看,用戶也會(huì)更頻繁地使用內(nèi)容平臺應(yīng)用程序,這最終有助于他們更快地查看新消息。。

為了提供從第三方網(wǎng)站閱讀內(nèi)容的愉快體驗(yàn),即時(shí)通訊產(chǎn)品需要一種獲取結(jié)構(gòu)化數(shù)據(jù)的機(jī)制。否則,就必須在瀏覽器插件中打開鏈接,由于頁面加載時(shí)間較長和非本機(jī)頁面呈現(xiàn),這會(huì)給用戶帶來支離破碎的體驗(yàn)。主流的即時(shí)通訊應(yīng)用程序采用不同的方法來改進(jìn)它。外部分享是方法之一,它要求發(fā)布者自愿提供結(jié)構(gòu)化數(shù)據(jù):

  • 托管發(fā)布服務(wù)。通過遷移發(fā)行方使用托管的出版編輯界面,微信(WeChat)等大型信使應(yīng)用程序直接通過官方賬戶平臺從作者那里獲取結(jié)構(gòu)化數(shù)據(jù)。它是中國最大的內(nèi)容分發(fā)服務(wù)之一,每天在應(yīng)用程序內(nèi)產(chǎn)生數(shù)十億的頁面瀏覽量。
  • 共享SDK供其他應(yīng)用程序使用,將鏈接發(fā)送到即時(shí)通訊中并手動(dòng)填充所需的數(shù)據(jù),例如標(biāo)題,圖片和說明。同樣的,這是微信(WeChat)使用的策略,它節(jié)省了構(gòu)建一個(gè)通用的web爬蟲程序的工程工作量,這類爬蟲可以從網(wǎng)頁中提取結(jié)構(gòu)化數(shù)據(jù)。

只有當(dāng)你的產(chǎn)品在中國市場占據(jù)一定地位時(shí),這種方法才有效。在全球市場上這樣做是不現(xiàn)實(shí)的。Telegram已應(yīng)用智能設(shè)計(jì)來構(gòu)建其當(dāng)前的內(nèi)容系統(tǒng):

  • 2015年4月發(fā)布的鏈接預(yù)覽可顯示大多數(shù)網(wǎng)站的豐富預(yù)覽氣泡。Telegram為了從鏈接中提取內(nèi)容構(gòu)建了搜尋器。它類似于Facebook Crawler,它讀取HTML內(nèi)容中的開放圖標(biāo)記。搜尋器在Telegram數(shù)據(jù)中心上運(yùn)行,并且不會(huì)將任何客戶端信息泄漏到第三方網(wǎng)站。
  • 同年,添加了應(yīng)用內(nèi)媒體播放功能,可播放Youtube,Vimeo和SoundCloud中的媒體,而無需在瀏覽器小部件中查看。隨后添加了更多受支持的媒體服務(wù),例如Instagram,Twitch等。
  • Instant View于2016年推出,這是一種以零頁面加載時(shí)間從新聞服務(wù)中打開文章的優(yōu)雅方法。從工程的角度來看,它類似于2015年首次亮相的Facebook Instant Articles。
  • Telegraph也與Instant View一同推出。它是一種用于在Telegram數(shù)據(jù)中心上托管格式豐富的文章發(fā)布工具。
  • Instant View平臺&競賽于2017年啟動(dòng)。提供了在線模板編輯器和一些慷慨的獎(jiǎng)項(xiàng),以激勵(lì)用戶為更多網(wǎng)站貢獻(xiàn)模板。
  • Instant View 2.0于2018年年底交付,支持RTL,表格,相關(guān)文章的塊等。

總而言之,鏈接預(yù)覽通過格式豐富的氣泡快速鏈接到用戶。應(yīng)用內(nèi)媒體播放使用戶可以在鏈接中享受核心媒體內(nèi)容,而無需離開聊天界面。Instant View原生地以零頁面加載時(shí)間呈現(xiàn)文章。Instant View平臺使用戶可以貢獻(xiàn)模板,以擴(kuò)展對更多網(wǎng)站的支持。

鏈接預(yù)覽(Link Preview)

如上一篇關(guān)于Bubble的文章所述,ChatMessageItem可以包含許多類型的Media。其中一種實(shí)現(xiàn)是TelegramMediaWebpage,它可以對Web鏈接的數(shù)據(jù)進(jìn)行建模。

final public class TelegramMediaWebpage : Postbox.Media, Equatable {
    public var id: Postbox.MediaId? { get }
    public let peerIds: [Postbox.PeerId]
    public let webpageId: Postbox.MediaId
    public let content: SyncCore.TelegramMediaWebpageContent
    ...
}

public enum TelegramMediaWebpageContent {
    case Pending(Int32, String?)
    case Loaded(TelegramMediaWebpageLoadedContent)
}

public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable {
    public let url: String
    public let displayUrl: String
    public let hash: Int32
    public let type: String?
    public let websiteName: String?
    public let title: String?
    public let text: String?
    public let embedUrl: String?
    public let embedType: String?
    public let embedSize: PixelDimensions?
    public let duration: Int?
    public let author: String?
    public let image: TelegramMediaImage?
    public let file: TelegramMediaFile?
    public let attributes: [TelegramMediaWebpageAttribute]
    public let instantPage: InstantPage?
}

ChatMessageWebpageBubbleContentNode 在消息Bubble中呈現(xiàn)鏈接預(yù)覽:

final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
    private var webPage: TelegramMediaWebpage?
    private let contentNode: ChatMessageAttachedContentNode
}

final class ChatMessageAttachedContentNode: ASDisplayNode {
    private let lineNode: ASImageNode
    private let textNode: TextNode
    private let inlineImageNode: TransformImageNode
    private var contentImageNode: ChatMessageInteractiveMediaNode?
    private var contentInstantVideoNode: ChatMessageInteractiveInstantVideoNode?
    private var contentFileNode: ChatMessageInteractiveFileNode?
    private var buttonNode: ChatMessageAttachedContentButtonNode?
    
    private let statusNode: ChatMessageDateAndStatusNode
    private var additionalImageBadgeNode: ChatMessageInteractiveMediaBadge?
    private var linkHighlightingNode: LinkHighlightingNode?
    
    private var message: Message?
    private var media: Media?
}

讓我們使用YouTube鏈接來說明發(fā)送它并呈現(xiàn)其預(yù)覽消息Bubble的過程。

part7-webpage.png

編寫消息時(shí),客戶端檢測到輸入文本中存在鏈接,會(huì)啟動(dòng)RPCmessages.getWebPagePreview預(yù)覽數(shù)據(jù)。后端響應(yīng)MessageMedia.messageMediaWebPage,其中包含鏈接預(yù)覽數(shù)據(jù):

public enum MessageMedia: TypeConstructorDescription {
    case messageMediaWebPage(webpage: Api.WebPage)
}

public enum WebPage: TypeConstructorDescription {
    case webPage(
      flags: Int32,          // 127
      id: Int64,             // 1503448449063263326
      url: String,           // https://www.youtube.com/watch?v=GEZhD3J89ZE
      displayUrl: String,    // youtube.com/watch?v=GEZhD3J89ZE
      hash: Int32,           // 0
      type: String?,         // video
      siteName: String?,     // YouTube
      title: String?,        // WWDC 2020 Special Event Keynote —  Apple
      description: String?,  // Apple WWDC 2020 kicked off with big announcement...
      photo: Api.Photo?,     // TelegramApi.Api.Photo.photo(flags: 0, id: 6020589086160562979, ...
      embedUrl: String?,     // https://www.youtube.com/embed/GEZhD3J89ZE
      embedType: String?,    // iframe
      embedWidth: Int32?,    // 1280
      embedHeight: Int32?,   // 720
      duration: Int32?,      // nil
      author: String?,       // nil
      document: Api.Document?,  // nil
      cachedPage: Api.Page?,    // nil
      attributes: [Api.WebPageAttribute]? // nil
    )
}

點(diǎn)擊發(fā)送按鈕后,將觸發(fā)RPCmessages.sendMessage ,并且客戶端正在等待來自后端的響應(yīng)。等待時(shí),已發(fā)送的消息提示會(huì)添加到聊天提示列表中。如果客戶端已經(jīng)收到的響應(yīng)messages.getWebPagePreview,則Bubble會(huì)渲染成為漂亮的預(yù)覽Bubble。否則,它只會(huì)先顯示一條純文本消息,然后等待發(fā)送結(jié)果中Updates.updates的預(yù)覽數(shù)據(jù),然后再渲染。

點(diǎn)擊播放按鈕后,功能openChatMessageImpl將啟動(dòng),并最終創(chuàng)建一個(gè)WebEmbedPlayerNode實(shí)例來播放YouTube視頻。

應(yīng)用內(nèi)媒體播放(In-App Media Playback)

WebEmbedPlayerNode利用YouTube IFrame Player APIWKWebView中播放視頻。

  • 函數(shù)webEmbedType通過嘗試extractYoutubeVideoIdAndTimestamp從URL字符串中提取YouTube視頻ID來檢測嵌入內(nèi)容的類型。
  • WebEmbedPlayerNode通過YoutubeEmbedImplementation初始化。
  • YoutubeEmbedImplementation從Bundle資源加載HTML模板Youtube.html,通過視頻ID生成頁面內(nèi)容,然后使用https://youtube.com/基礎(chǔ)網(wǎng)址通過WKWebView加載它。
  • 注入了Bundle目錄下的JavaScript文件 YoutubeUserScrip.js,以從嵌入式Y(jié)ouTube播放器中隱藏水印控件。
  • YoutubeEmbedImplementation 實(shí)現(xiàn)協(xié)議方法以通過JavaScript調(diào)用播放,暫停和尋找播放器。

類似的方法被應(yīng)用到提供的長視頻或直播流內(nèi)容的其他媒體服務(wù),如Vimeo,Twitch以及generic可以嵌入作為一個(gè)iframe的網(wǎng)站。

對于主要托管短視頻和照片的Instagram和TikTok之類的服務(wù),Telegram Crawler積極地在Telegram數(shù)據(jù)中心緩存媒體內(nèi)容,并通過SystemVideoContentNodeNativeVideoContentNode將其作為本機(jī)視頻提供。
Telegram已經(jīng)在自己的后端維護(hù)了大量的用戶交互數(shù)據(jù)和媒體內(nèi)容。

Instant View

part7-instantview.png

讓我們使用Telegram在Covid-19上的官方博客解釋Instant View的內(nèi)部結(jié)構(gòu)。輸入鏈接時(shí),要求使用相同的RPCmessages.getWebPagePreview ,這一次,響應(yīng)已為其字段設(shè)置了值cachedPage

public enum WebPage: TypeConstructorDescription {
    case webPage(
      flags: Int32,          // 1311
      id: Int64,             // 4108701751117811561
      url: String,           // https://telegram.org/blog/coronavirus
      displayUrl: String,    // telegram.org/blog/coronavirus
      hash: Int32,           // 702078769
      type: String?,         // photo
      siteName: String?,     // Telegram
      title: String?,        // Coronavirus News and Verified Channels
      description: String?,  // Channels are a tool for broadcasting your public messages...
      photo: Api.Photo?,     // TelegramApi.Api.Photo.photo(flags: 0, id: 5777291004297194213, ...
      embedUrl: String?,     // nil
      embedType: String?,    // nil
      embedWidth: Int32?,    // nil
      embedHeight: Int32?,   // nil
      duration: Int32?,      // nil
      author: String?,       // Telegram
      document: Api.Document?,  // nil
      cachedPage: Api.Page?,    // TelegramApi.Api.Page.page(...)
      attributes: [Api.WebPageAttribute]? // nil
    )
}

public enum Page: TypeConstructorDescription {
    case page(
      flags: Int32,             // 0
      url: String,              // https://telegram.org/blog/coronavirus
      blocks: [Api.PageBlock],  // [TelegramApi.Api.PageBlock] 37 values
      photos: [Api.Photo],      // [TelegramApi.Api.Photo] 5 values
      documents: [Api.Document],// [TelegramApi.Api.Document] 2 values
      views: Int32?             // nil
    )
}

// inside blocks
[
  PageBlock.pageBlockCover,
  PageBlock.pageBlockChannel,
  PageBlock.pageBlockTitle,
  PageBlock.pageBlockAuthorDate,
  PageBlock.pageBlockParagraph,
  ...
  PageBlock.pageBlockRelateArticles
]

Api.Page將鏈接的結(jié)構(gòu)化數(shù)據(jù)建模為PageBlock的列表。PageBlock定義了28種類型的blocks,它們要么是顯示unit,要么是blocks的容器。擁有容器類型可以呈現(xiàn)具有嵌套結(jié)構(gòu)的復(fù)雜頁面。

indirect public enum PageBlock: TypeConstructorDescription {
    case pageBlockUnsupported
    case pageBlockTitle(text: Api.RichText)
    case pageBlockSubtitle(text: Api.RichText)
    case pageBlockAuthorDate(author: Api.RichText, publishedDate: Int32)
    case pageBlockHeader(text: Api.RichText)
    case pageBlockSubheader(text: Api.RichText)
    case pageBlockParagraph(text: Api.RichText)
    case pageBlockPreformatted(text: Api.RichText, language: String)
    case pageBlockFooter(text: Api.RichText)
    case pageBlockDivider
    case pageBlockAnchor(name: String)
    case pageBlockBlockquote(text: Api.RichText, caption: Api.RichText)
    case pageBlockPullquote(text: Api.RichText, caption: Api.RichText)
    case pageBlockCover(cover: Api.PageBlock) // container
    case pageBlockChannel(channel: Api.Chat)
    case pageBlockKicker(text: Api.RichText)
    case pageBlockTable(flags: Int32, title: Api.RichText, rows: [Api.PageTableRow])
    case pageBlockPhoto(flags: Int32, photoId: Int64, caption: Api.PageCaption, url: String?, webpageId: Int64?)
    case pageBlockVideo(flags: Int32, videoId: Int64, caption: Api.PageCaption)
    case pageBlockAudio(audioId: Int64, caption: Api.PageCaption)
    case pageBlockEmbed(flags: Int32, url: String?, html: String?, posterPhotoId: Int64?, w: Int32?, h: Int32?, caption: Api.PageCaption) // container to embed a web view
    case pageBlockEmbedPost(url: String, webpageId: Int64, authorPhotoId: Int64, author: String, date: Int32, blocks: [Api.PageBlock], caption: Api.PageCaption) // container
    case pageBlockCollage(items: [Api.PageBlock], caption: Api.PageCaption) // container
    case pageBlockSlideshow(items: [Api.PageBlock], caption: Api.PageCaption) // container
    case pageBlockList(items: [Api.PageListItem]) // container
    case pageBlockOrderedList(items: [Api.PageListOrderedItem]) // container
    case pageBlockDetails(flags: Int32, blocks: [Api.PageBlock], title: Api.RichText) // container
    case pageBlockRelatedArticles(title: Api.RichText, articles: [Api.PageRelatedArticle])
    case pageBlockMap(geo: Api.GeoPoint, zoom: Int32, w: Int32, h: Int32, caption: Api.PageCaption)
}

InstantPageUI模塊包含Instant View的所有UI代碼文件。InstantPageController是核心控制器,它的content node InstantPageControllerNode通過函數(shù)updateLayout管理子node和布局。它枚舉頁面塊并為每個(gè)塊創(chuàng)建相應(yīng)的InstantPageItem類型。

private func updateLayout() {
    ...
    let currentLayout = instantPageLayoutForWebPage(webPage, ...)
}

func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, ...) -> InstantPageLayout {
    var items: [InstantPageItem] = []
    ...
    for block in pageBlocks {
        let blockLayout = layoutInstantPageBlock(webpage: webPage, rtl: rtl, block: block, ...)
        let blockItems = blockLayout.flattenedItemsWithOrigin(CGPoint(x: 0.0, y: contentSize.height + spacing))
        items.append(contentsOf: blockItems)
    }
    ...
}

func layoutInstantPageBlock(webpage: TelegramMediaWebpage, rtl: Bool, block: InstantPageBlock, ...) {
    ...
    switch block {
        case let .title(text):
            return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
        case let .authorDate(author: author, date: date):
            ...
    ...
}

final class InstantPageLayout {
    let origin: CGPoint
    let contentSize: CGSize
    let items: [InstantPageItem]
}

InstantPageController使用緩存的頁面數(shù)據(jù)立即顯示渲染結(jié)果。同時(shí),它還通過方法actualizedWebpage發(fā)送一個(gè)RPC messages.getWebPage來更新。因此,布局函數(shù)updateLayout通常至少被調(diào)用兩次或更多次。

考慮到布局功能始終在主線程中運(yùn)行,因此如果即時(shí)頁面具有大量內(nèi)容塊,它可能會(huì)阻塞UI。例如,從電子書站點(diǎn)中提取的包含1MB文本的段落會(huì)大大降低整個(gè)應(yīng)用程序的速度,而相同數(shù)量的文本可以通過WKWebView輕松處理。當(dāng)前版本的Instant View假定頁面通常很短。

題外話,微信過去經(jīng)常以移動(dòng)網(wǎng)站的形式發(fā)布來自官方帳戶的文章。在2018年,客戶端開始獲取結(jié)構(gòu)化數(shù)據(jù)并在本地構(gòu)建HTML內(nèi)容,這還將提前緩存CSS和JavaScript文件。它以某種方式呈現(xiàn)了類似的Instant View體驗(yàn)。

Instant View Platform

在搜索工程師和移動(dòng)瀏覽器領(lǐng)域,將鏈接從原始HTML轉(zhuǎn)換為干凈的結(jié)構(gòu)化塊是一個(gè)棘手的工業(yè)問題。Telegram發(fā)明了自己的規(guī)則語言來對內(nèi)容提取過程進(jìn)行建模。該語言非常復(fù)雜,支持變量,函數(shù),擴(kuò)展的XPath等。您可以查看為Medium,Telegraph和Telegram Blog構(gòu)建的示例模板,以快速理解它。

為了鼓勵(lì)用戶為更多的網(wǎng)站做出貢獻(xiàn)并定義規(guī)則,Telegram建立了一個(gè)在線IDE,并舉辦了兩次競賽,總獎(jiǎng)金為50萬美元。它還使您可以自由地對所有用戶公開制作模板,也可以將其私下保存在自己的網(wǎng)站上。

結(jié)論

Telegram分享了如何構(gòu)建功能強(qiáng)大的內(nèi)容系統(tǒng),以支持許多外部發(fā)行商,在即時(shí)通訊內(nèi)提供流暢的閱讀體驗(yàn)。它涉及復(fù)雜的產(chǎn)品思維和精心的工程工作,為競爭對手樹立了高標(biāo)準(zhǔn)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容