哈Ha! 这是我们博客上的第一篇文章。 很多人都知道我们是该站点的聊天对象,正是我们与他聊天开始了,现在我们在商业使者领域占据领先地位。 我们逐渐演变成一个全面的业务解决方案,为客户提供许多机会:回叫,通过即时通讯程序,社交网络,移动应用程序,虚拟PBX,CRM功能与客户进行通信。
几年来,我们成功解决了许多技术问题,积累了很多有趣的东西,在某些地方,当然还有独特的经验,写下了拐杖和自行车。 在这篇文章中,我们开始撰写一系列文章,在这些文章中,我们将分享我们在一个完全远程的团队中开发,构建流程的经验,介绍我们的体系结构和技术解决方案,使我们能够有效地服务于全球成千上万的客户。
今天的Jivosite是:- 全球有25万客户;
- 每天有1.5亿个小部件印象;
- 每天350万条消息;
- 每月1000万次聊天;
- 1M同时连接;
- 生产中的服务器超过250台。
由于大多数人都将我们称为网站聊天,因此我们可能会从它开始。 在本文中,我们将演示如何将您的代码连接到第三方站点,以及您应注意的事项,以此作为我们多年聊天经验的示例。 本文对计划或已经在开发插件服务的人员,以及对本主题感兴趣的每个人都非常有用。
入口点
剧院以衣架开始,连接的服务以插入代码开始。 它是站点上任何服务或模块的入口点。 通常,可以在安装说明中找到它,然后有必要将其添加到站点的HTML代码中,然后出现“魔术”,该魔术以某种方式加载和初始化脚本。
<script src="https://site.com/file.js"></script>
将脚本连接到站点似乎更容易些?
按照标准,您只需要在页面的HTML代码中添加脚本标签即可。 但是实际上,这是一个重要的阶段,隐藏了很多陷阱。 例如,用户标识,备份脚本加载通道的实现,外观或逻辑的自定义,页面加载速度等。 但是,让我们依次讨论所有内容。
身份证明
仅仅因为对任何人来说连接脚本都不是很有趣,所以请确保脚本执行某种逻辑,并且此逻辑与用户相关。 例如,在本例中,来自社交网络的计数器ID APP_ID是创建的通信渠道的ID。 也就是说,脚本应在对服务器的请求中标识用户。 为了通过插入代码识别客户端,有三个实现选项。
选项#1 <script async src="https://site.com/file.js?id=123"></script>
将ID直接在文件的链接中传递,并在服务器端以某种方式将其放入脚本中。 在这种情况下,服务器将必须将ID即时写入文件中,或者形成一个带有ID的JS字符串,以加载file.js。 此逻辑类似于JSONP请求的实现。

长期以来,我们一直致力于这一原理,但是这种方法的缺点是增加了服务器上的“空闲”负载和实现服务器缓存的需求。
选项#2 <script src="https://site.com/file.js" [async]></script> <script type=”text/javascript”> window.serviceNameId = “123”; </script>
异步属性-告诉浏览器无需等待脚本加载即可构建DOM,脚本必须在加载后立即执行。 这减少了页面加载时间,但也有一个缺点:可以在DOM准备工作之前执行脚本。包括大型服务在内的最受欢迎的实现之一就是做到这一点,只是语法不同,但是所有的本质都是相同的。

这种方法有两个主要缺点,第一个-嵌入代码很复杂,第二个-此代码的执行顺序非常重要,否则将无济于事。 另外,您需要在速度(异步)和稳定性(没有异步)之间做出选择,大多数情况下选择第二个选项。
选项#3 <script async src="https://site.com/file.js?id=123"></script>
与第一个选项类似,将链接中的ID传输到文件,但在浏览器中而不是在服务器上检索它。 它并不像看起来那么简单,但是可能。 浏览器API具有document.currentScript属性,它返回指向已加载且当前正在浏览器中运行的脚本的链接。 知道了这一点,您可以计算ID,为此,您需要获取document.currentScript.src属性并定期从中提取ID。

但是有一件事:不是所有浏览器都支持document.currentScript。 对于不支持此属性的浏览器,我们提出了一个有趣的技巧。 在file.js代码中,您可以引发一个特殊的“假”异常,该异常包装在try / catch中,此后将抛出错误堆栈中发生错误的脚本的URL。 该网址将包含我们获得的具有相同规律性的ID。
获得了这种魔力,但是它起作用了。 执行顺序没有麻烦,插入代码看起来很简单,服务器上没有任何开销。 在过去的两年中,我们一直使用这种方法,尽管插入代码本身是不同的,但是原理是相同的。
设定值
在大多数情况下,插件脚本具有负责外观或工作逻辑的任何设置。 必须将这些设置“抛出”到插件脚本中,因为这有两种根本不同的方法。
方法1 <script async src="https://site.com/file.js"></script> <script type=”text/javascript”> window.serviceName = {color: “red”, title: “”, ...}; </script>
此方法还包括将GET参数中的设置传递到脚本URL,类似于“标识”部分中的选项#1。 该方法是,如果客户想要更改设置,则他需要编辑嵌入代码并在站点上对其进行更新。

这样做很好,因为所有设置都存储在客户端上,而无需将它们存储在服务器上,因此可以开发和维护与此相关的所有业务逻辑。 这种方法的主要缺点是给客户带来不便,他必须手动进行所有操作,并且如果设置很多,则嵌入代码将变成难以维护的工作表,很容易出错。 为了使更新生效,您需要更新网站,这是开发人员和管理员的额外手势。
方法#2 <script async src="https://site.com/file.js?id=123"></script>
第二种方法是,如果必须更改设置,则客户端不需要修改插入代码,所有设置都存储在服务器上。 为了更改设置,请转到图形面板,更改必要的参数,然后单击“保存”按钮。 之后,设置将自动应用于他的网站!

无需了解代码并为此进行部署,这可以由远离JavaScript的人员(例如经理)完成。 当然,此选项对用户来说更加方便和简单,这就是我们使用它的原因。 但是,您必须付出便利的代价,这种方法需要开发和支持服务器上的逻辑,并为此增加了负担。 在下面的文章中,我们一定会告诉您每天如何处理150M这样的请求。
向后兼容
尽快获得成熟版本的嵌入代码非常重要。 因为更新已经安装的插入代码将非常困难。 我们的实践中的一个例子:在第一个版本中,我们使用数字ID,但出于安全原因,我们将其替换为字母数字。 事实证明,要更改已经安装的嵌入代码非常困难。 许多人甚至都不知道HTML是什么以及网站的设计方式。 例如,一个网站是由自由职业者制作的,一个工作室或一个网站是通过CMS /构造函数创建的,等等。在大多数情况下,我们的客户只能使用小部件设置面板。 从那时起,我们在nginx中仍然有一个将旧ID重写为新ID的映射,该映射具有约4万条记录。
.... /script/widget/config/15**90 /script/widget/config/bqZB**rjW5; /script/widget/config/15**94 /script/widget/config/qtfx**xnTi; /script/widget/config/15**95 /script/widget/config/fqmpa**4YX; /script/widget/config/15**97 /script/widget/config/Vr21g**nuT; /script/widget/config/15**98 /script/widget/config/8NXL5**F8E; /script/widget/config/15**00 /script/widget/config/Th2HN**6RJ; ....
由于此功能,我们不得不为所有重构保持嵌入代码的向后兼容性,其中我们的内存中只有大约5个。
代码隔离
由于脚本已连接到第三方站点,该站点已经具有用于站点和其他服务的JavaScript和CSS代码,因此主要目标是不损害站点,以使我们的代码不会更改逻辑,更不用说破坏逻辑了。 这可能是JavaScript错误导致停止执行流程,或者是覆盖网站样式的样式。 但是站点代码也会影响所连接的脚本,例如,使用了一个库来修改浏览器API,此后该代码将停止工作或无法正常运行。
<script type="text/javascript"> </script>
<style type="text/css"> // body * { padding: 20px; } form input { display: block; border: 2px solid red; } </style>
隔离代码有不同的选择。 例如,您可以在JS变量,闭包中使用前缀,以免阻塞全局上下文,请使用BEM之类的样式。 但是最简单的方法是在iframe中执行代码,它解决了大多数隔离问题,但施加了某些限制。 我们使用混合版本,在以下文章中我们将告诉您更多有关代码隔离的信息。
块加载站点

Onload事件-网页完全加载后发生,包括图像,样式和外部脚本。 一个重要的功能是,在大多数网站上,JS逻辑,第三方脚本和广告都会在发生此事件时开始起作用。 对于所有连接的脚本,非常重要的一点是防止对该事件产生负面影响。
在从中加载脚本的服务器长时间响应或根本不响应的情况下会发生这种情况:然后onload事件被延迟,进一步阻止了进一步的页面加载。 在服务器不可用的情况下,仅在请求超时(超过60秒)后才会发生onload事件。 因此,脚本上载服务器上的问题实质上“破坏”了站点,这是不可接受的。
个人经历过去,我曾在一家拥有同时在线100K在线约会网站的公司工作。 当时,“共享社交网络”按钮很流行。 为了使它们出现在网站上,您必须从所需的社交网络连接脚本(sdk)。 有一天,同事跑来找我们,说我们的网站没有工作! 我们查看了所有正常情况下的监视,起初我们不了解问题出在哪里。 当他们开始更深入地研究时,他们意识到Twitter的cdn服务器正在躺下,并且他们的SDK无法加载,这使我们无法加载网站约1.5分钟。 也就是说,在打开网站后,加载了一些HTML(SPA的其余部分),并且仅在1.5分钟后加载了所有内容,该请求超时才起作用。 我们必须紧急组织一个修复程序,并从站点中删除其脚本。 重复这种情况后,我们决定完全删除Share块。
在插入代码的第一个版本中,我们没有考虑到这一点,如果出现技术问题,请放心,我们给客户带来了不便,但是随着时间的推移,我们对其进行了修复。
解决方案 <script type='text/javascript'> (function(){ var initCode = function () { </script>
解决方案很简单,您需要订阅网站完全加载的事件,然后才加载脚本,为此,您需要使用嵌入代码,而不是script标签。
Google Pagespeed
habr.com移动版分析他们中的大多数人都关注网站的加载速度,根据许多研究,这直接影响到利润,此外,排名时的搜索算法开始考虑页面加载时间。 在这方面,网站所有者通常使用类似的工具来评估网站性能。 因此,以最佳方式将代码连接到站点非常重要,因为它直接影响其下载时间。
这意味着您必须使用现代技术来优化页面加载。 例如,使用Gzip,缓存静态文件和请求,使用异步脚本加载,使用WebP / Brotli /等现代算法压缩静态文件以及使用其他优化。 我们会定期进行审核,并对警告和建议做出回应,以满足当前的要求。
光盘
在第一个版本中,我们从应用程序服务器下载了静态信息。 但是这种方法有一些缺点:流量昂贵,站点访问者无法访问以及服务器通道上的负载过大。 由于静态流量非常“繁重”,您可以轻松利用站点的habr效应阻塞应用程序服务器的通道。
为了节省预算,稳定性和减少网络延迟,最好从专门为此目的设计的服务器中加载静态数据。 您可以使用现成的CDN提供程序,但从规模上讲它并不便宜,并且您必须受到该提供程序提供的功能的限制。

我们实施了它简单的方法,在俄罗斯,欧洲和美国订购了廉价的服务器,流量无限,渠道广泛。 它价格便宜,对我们没有任何限制,我们可以为自己定制所有内容,并且通过浏览器中有效的机制来确保容错能力。 当前,每天从我们的CDN服务器中加载1TB的静态数据。
容错能力
不幸的是,世界并不完美,发生火灾,上行链路掉落,DC完全被淹没,ILV阻塞了子网,人们犯了错误。 但是,必须能够处理这种情况并继续工作。
监控方式首先,您需要了解出问题了。 当然,您可以等到用户来投诉为止,但是最好设置监视和警报,并在发布之后检查是否一切正常。 我们监视服务器和客户端的许多不同参数,如果出现问题,我们会立即看到它。 例如,小部件下载的数量或CDN服务器上流量的异常激增已经减少。
每个版本的小部件下载总数错误收集JavaScript是一种非常特殊的语言,很容易出错。 另外,现代网络上的浏览器动物园非常大; 在最新版的Chrome浏览器中无法正常工作的事实并非在Safari或Firefox中都能正常工作。 因此,从浏览器配置错误收集并及时响应峰值非常重要。 如果您的代码在iframe中工作,则可以通过跟踪全局window.onerror处理程序来完成此操作,并在发生错误的情况下将数据发送到服务器。 如果代码在iframe之外运行,则很难实现错误收集。
来自所有站点和浏览器的错误总数
特定错误信息CDN故障转移我已经在上面写过,一切都具有下降的特性,因此自动应对这些情况并更好地进行处理非常重要。 从手册开始,我们经历了CDN服务器回退的多个阶段,最终找到了一种针对浏览器自动,最佳地进行此操作的方法。
在手动模式下,这很简单:SMS收到管理员说CDN已关闭的消息,他们执行了某些操作,此后小部件开始从应用程序服务器加载。 这可能需要5分钟到2个小时。
要实现自动回退,您需要以某种方式检测脚本已开始加载,但这并不像看起来那样容易。 浏览器不提供监视脚本标记加载的中间状态的功能,例如XMLHttpRequest中的onprogress事件,但是仅报告加载和执行脚本时的事件。 同样不可能在可接受的时间内发现服务器当前不可用,只有请求超时到期(超过1分钟)后才会触发唯一的onerror事件。 一分钟后,访问者可能已经离开页面,但是脚本不会加载。
我们尝试了简单和复杂的不同选项,但最后我们提出了对CDN服务器的ping请求。 它的工作方式是这样的:我们首先对CDN服务器执行ping操作,如果它回答了,则从其中加载小部件。 为了针对浏览器和我们的服务器最佳地实现此方案,我们使用了轻量级的HEAD请求(没有主体),并且在后续下载过程中,直到小部件版本更新后我们才执行该请求,因为小部件已位于浏览器缓存中。

因此,我们收到了非常快速,自动的静态服务器可用性检测信息,一旦发生故障,我们几乎没有延迟地切换到备份服务器。
装载机
要将脚本上载到第三方站点,您需要考虑很多方面,但是很难在嵌入代码中实现此逻辑,因为它会变成“肉”。 但是您仍然需要执行此操作,为此,我们创建了一个小模块来“在后台”管理所有这些逻辑并加载小部件的主要代码。 它首先加载并实现CDN故障转移,缓存,与旧嵌入代码的向后兼容性,A / B测试,新版小部件的逐步布局以及许多其他功能。

因此,分阶段介绍了一种方案,该方案涵盖了加载和初始化小部件的主要情况。 多年来,她已在许多不同的站点上使用证明了其有效性。 同时,插入代码保持简单和通用,因为其中没有逻辑,我们可以随时更改它,而不会强迫用户更改插入代码。
第三方服务
最后,值得一提的是连接到网站或以某种方式与网站交互的第三方服务:搜索机器人,分析工具,各种解析器等。 这些服务在工作中留下了烙印,您也不应该忘记这一点。 我将通过实践告诉您一些案例。
Googlebot我们运营商的应用程序具有“访问者”功能,您可以在其中查看当前正在查看站点的访问者以及有关他们的各种信息:站点时间,页面,查看的页面数等。 在某个时候,客户开始抱怨他们“吊死”了其他网站的访客,也就是说,在销售iPhone的网站上,一位客户据说有一个名为“购买面霜”的页面。 当他们开始弄清楚它的时候,原来是GoogleBot,当从一个站点切换到另一个站点时,它首先缓存LocalStorage,然后将错误的数据传输到服务器。
解决方案很简单,服务器开始忽略来自GoogleBot的数据。
Yandex.Metrica指标中有一个很棒的功能-网络浏览器,它使您能够以截屏的形式查看用户看到的内容和所做的事情。 为此,该度量标准记录了所有用户操作,并在一个特殊的度量标准bot遍历站点之后执行相同的操作并将其记录下来。 问题在于,要模拟用户的移动浏览器,根据我们的数据,Firefox是在移动仿真模式下打开的,但是该漫游器中的userAgent是桌面的。
这导致以下事实:在Web浏览器中查看移动用户会话时,窗口小部件的桌面版本会在录音中打开,尽管实际上用户已打开了移动版本。 客户认为是这样,并用投诉轰炸了我们。
结果,我们必须检测到该窗口小部件已加载到Web浏览器中,以了解其中已打开了移动版本,在这种情况下,请移开那里的移动窗口小部件。还有更多示例,但是我认为这足以理解这一点。结论
, . , . , , NodeJS , 270 - , .
, !