版權(quán)聲明
本文內(nèi)容均為搬運贰您,目的只為更方便的學(xué)習(xí)Telegram編碼思維拢操。
Telegram構(gòu)建了一組功能展父,使用戶長時間停留在應(yīng)用程序內(nèi)。本篇文章將闡述Telegram為什么需要這些功能以及如何有效地實現(xiàn)它們篮绿。
即時通訊中的內(nèi)容系統(tǒng)
在深入探討技術(shù)細(xì)節(jié)之前亲配,我們可以從即時通訊的角度考慮內(nèi)容系統(tǒng)的作用惶凝。盡管Telegram中沒有集中的新聞源苍鲜,為什么它如此重要?
如果我們只能為IM達成一個目標(biāo)洒疚,那么絕對是消息的可達性油湖。高可達性可以給用戶更多的信心领跛,他們發(fā)送的消息將被對方可靠地查看吠昭,這應(yīng)該是使他們喜歡即時通訊的最終原因怎诫。事實證明,內(nèi)容系統(tǒng)是提高用戶可訪問性的阻礙蹦误,因為即使沒有消息可查看肉津,用戶也會更頻繁地使用內(nèi)容平臺應(yīng)用程序妹沙,這最終有助于他們更快地查看新消息距糖。牵寺。
為了提供從第三方網(wǎng)站閱讀內(nèi)容的愉快體驗俩块,即時通訊產(chǎn)品需要一種獲取結(jié)構(gòu)化數(shù)據(jù)的機制。否則捎拯,就必須在瀏覽器插件中打開鏈接狸眼,由于頁面加載時間較長和非本機頁面呈現(xiàn),這會給用戶帶來支離破碎的體驗屡限。主流的即時通訊應(yīng)用程序采用不同的方法來改進它罩旋。外部分享是方法之一,它要求發(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ù)據(jù)宴偿,例如標(biāo)題,圖片和說明活翩。同樣的穆趴,這是微信(WeChat)使用的策略空入,它節(jié)省了構(gòu)建一個通用的web爬蟲程序的工程工作量单料,這類爬蟲可以從網(wǎng)頁中提取結(jié)構(gòu)化數(shù)據(jù)白对。
只有當(dāng)你的產(chǎn)品在中國市場占據(jù)一定地位時沉颂,這種方法才有效钉蒲。在全球市場上這樣做是不現(xiàn)實的线梗。Telegram已應(yīng)用智能設(shè)計來構(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ù)中心上運行,并且不會將任何客戶端信息泄漏到第三方網(wǎng)站。
- 同年贤重,添加了應(yīng)用內(nèi)媒體播放功能祭犯,可播放Youtube键畴,Vimeo和SoundCloud中的媒體,而無需在瀏覽器小部件中查看。隨后添加了更多受支持的媒體服務(wù)锋叨,例如Instagram薄湿,Twitch等。
- Instant View于2016年推出,這是一種以零頁面加載時間從新聞服務(wù)中打開文章的優(yōu)雅方法。從工程的角度來看,它類似于2015年首次亮相的Facebook Instant Articles。
- Telegraph也與Instant View一同推出动知。它是一種用于在Telegram數(shù)據(jù)中心上托管格式豐富的文章發(fā)布工具奠滑。
- Instant View平臺&競賽于2017年啟動弃甥。提供了在線模板編輯器和一些慷慨的獎項,以激勵用戶為更多網(wǎng)站貢獻模板。
- Instant View 2.0于2018年年底交付蝉娜,支持RTL,表格纸镊,相關(guān)文章的塊等峰搪。
總而言之尽纽,鏈接預(yù)覽通過格式豐富的氣泡快速鏈接到用戶矫膨。應(yīng)用內(nèi)媒體播放使用戶可以在鏈接中享受核心媒體內(nèi)容,而無需離開聊天界面。Instant View原生地以零頁面加載時間呈現(xiàn)文章。Instant View平臺使用戶可以貢獻模板,以擴展對更多網(wǎng)站的支持。
鏈接預(yù)覽(Link Preview)
如上一篇關(guān)于Bubble的文章所述,ChatMessageItem可以包含許多類型的Media。其中一種實現(xiàn)是TelegramMediaWebpage,它可以對Web鏈接的數(shù)據(jù)進行建模。
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的過程。
編寫消息時鸥鹉,客戶端檢測到輸入文本中存在鏈接灸异,會啟動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
)
}
點擊發(fā)送按鈕后,將觸發(fā)RPCmessages.sendMessage 窜骄,并且客戶端正在等待來自后端的響應(yīng)邻遏。等待時虐骑,已發(fā)送的消息提示會添加到聊天提示列表中廷没。如果客戶端已經(jīng)收到的響應(yīng)messages.getWebPagePreview颠黎,則Bubble會渲染成為漂亮的預(yù)覽Bubble狭归。否則过椎,它只會先顯示一條純文本消息讼撒,然后等待發(fā)送結(jié)果中Updates.updates的預(yù)覽數(shù)據(jù)诬乞,然后再渲染逮栅。
點擊播放按鈕后,功能openChatMessageImpl將啟動氢架,并最終創(chuàng)建一個WebEmbedPlayerNode實例來播放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
實現(xiàn)協(xié)議方法以通過JavaScript調(diào)用播放,暫停和尋找播放器李请。
類似的方法被應(yīng)用到提供的長視頻或直播流內(nèi)容的其他媒體服務(wù)恩急,如Vimeo,Twitch以及generic可以嵌入作為一個iframe的網(wǎng)站拉讯。
對于主要托管短視頻和照片的Instagram和TikTok之類的服務(wù)隧熙,Telegram Crawler積極地在Telegram數(shù)據(jù)中心緩存媒體內(nèi)容,并通過SystemVideoContentNode或NativeVideoContentNode將其作為本機視頻提供损谦。
Telegram已經(jīng)在自己的后端維護了大量的用戶交互數(shù)據(jù)和媒體內(nèi)容。
Instant View
讓我們使用Telegram在Covid-19上的官方博客解釋Instant View的內(nèi)部結(jié)構(gòu)。輸入鏈接時,要求使用相同的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和布局绪颖。它枚舉頁面塊并為每個塊創(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é)果甜奄。同時柠横,它還通過方法actualizedWebpage發(fā)送一個RPC messages.getWebPage來更新。因此课兄,布局函數(shù)updateLayout
通常至少被調(diào)用兩次或更多次牍氛。
考慮到布局功能始終在主線程中運行,因此如果即時頁面具有大量內(nèi)容塊烟阐,它可能會阻塞UI搬俊。例如,從電子書站點中提取的包含1MB文本的段落會大大降低整個應(yīng)用程序的速度曲饱,而相同數(shù)量的文本可以通過WKWebView
輕松處理悠抹。當(dāng)前版本的Instant View假定頁面通常很短。
題外話扩淀,微信過去經(jīng)常以移動網(wǎng)站的形式發(fā)布來自官方帳戶的文章楔敌。在2018年,客戶端開始獲取結(jié)構(gòu)化數(shù)據(jù)并在本地構(gòu)建HTML內(nèi)容驻谆,這還將提前緩存CSS和JavaScript文件卵凑。它以某種方式呈現(xiàn)了類似的Instant View體驗。
Instant View Platform
在搜索工程師和移動瀏覽器領(lǐng)域胜臊,將鏈接從原始HTML轉(zhuǎn)換為干凈的結(jié)構(gòu)化塊是一個棘手的工業(yè)問題勺卢。Telegram發(fā)明了自己的規(guī)則語言來對內(nèi)容提取過程進行建模。該語言非常復(fù)雜象对,支持變量黑忱,函數(shù),擴展的XPath等勒魔。您可以查看為Medium甫煞,Telegraph和Telegram Blog構(gòu)建的示例模板,以快速理解它冠绢。
為了鼓勵用戶為更多的網(wǎng)站做出貢獻并定義規(guī)則抚吠,Telegram建立了一個在線IDE,并舉辦了兩次競賽弟胀,總獎金為50萬美元楷力。它還使您可以自由地對所有用戶公開制作模板喊式,也可以將其私下保存在自己的網(wǎng)站上。
結(jié)論
Telegram分享了如何構(gòu)建功能強大的內(nèi)容系統(tǒng)萧朝,以支持許多外部發(fā)行商岔留,在即時通訊內(nèi)提供流暢的閱讀體驗。它涉及復(fù)雜的產(chǎn)品思維和精心的工程工作剪勿,為競爭對手樹立了高標(biāo)準(zhǔn)贸诚。