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

編寫消息時(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 API在WKWebView中播放視頻。
- 函數(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)容,并通過SystemVideoContentNode或NativeVideoContentNode將其作為本機(jī)視頻提供。
Telegram已經(jīng)在自己的后端維護(hù)了大量的用戶交互數(shù)據(jù)和媒體內(nèi)容。
Instant View

讓我們使用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)。