《JavaScript 模式》读书笔记(8)— DOM和浏览器模式2

四、长期运行脚本

  可能会注意到有时候浏览器会提示某个脚本已经运行了很长时间,是否应该停止该脚本。实际上无论要处理多么复杂的任务,都不希望应用程序发生上述事情。而且,如果该脚本的工作十分繁重,那么浏览器的UI将会无法响应用户的任何操作。这将给用户带来十分不好的体验,应该尽量避免。

  在JavaScript中没有线程,但是可以在浏览器中使用setTimeout()来模拟线程,在最新版本的浏览器中可以使用Web Workers。

 

setTimeout()

  这样做的一个思想是将一个大任务分解为多个小任务,并为每一个小任务设置超时时间为1毫秒。通过为每一个小任务设置超时为1毫秒,会导致完成整个任务需要耗费更长的时间,但是通过这样做,可以使得用户接口保持响应,用户体验更好。

  注意:超时时间设置为1毫秒(或者设置为0毫秒)实际上是与浏览器和操作系统相关的。将超时事件设置为0并不意味着没有超时,而是指尽可能快的处理。例如在IE中,最快的时钟周期是15毫秒。

 

Web Workers

  最近的浏览器为长期运行的脚本提供了另外一个解决方案:Web Workers。Web Workers为浏览器提供了背景线程支持。可以将任务比较繁重的计算放在单独一个文件中,例如my_web_workers.js。从主程序(网页)中调用该文件,如下所示:

var ww = new Worker('my_web_worker.js');
    ww.onmessage = function (event) {
        document.body.innerHTML += 
        "<p>message from the background thread" + event.data + "</p>";
    };

  下面示例中Web Workers做了1e8(100000000)次简单算术操作:

var end = 1e8,tmp = 1;

    postMessage('hello there');

    while (end) {
        end -= 1;
        tmp += end;
        if(end = 5e7) {
            postMessage('halfway there, tmp is now' + tmp);
        }
    }
    postMessage('all done');

  Web Workers使用postMessage()来与调用者通信,并且调用者订阅onmessage事件来接收更新。onmessage回调函数接收事件对象作为参数,并且该对象包含data属性。类似地,调用者可以使用ww.postMessage()将数据传递给Web Workers,Web Worker会使用onmessage回调函数来订阅这些消息。

 

五、远程脚本

  当今Web应用程序经常食用远程脚本本来在无需重新载入当前页面时与服务器通信。该方法可以获取更多响应,并使得类似桌面的网页应用程序成为可能。现在我们讨论一些使用JavaScript与远程服务器通信的方法。

  

XMLHttpRequest

  当今HMLHttpRequest是一个在大多数浏览器中都支持的特殊对象,该对象可以让您采用JavaScript建立HTTP请求。建立一个HTTP请求分为如下三个步骤:

  1. 建立一个XMLHttpRequest对象(简写为XHR)。
  2. 提供一个回调函数来告知请求对象改变状态。
  3. 发送请求。

  第一步十分简单:

var xhr =  new XMLHttpRequest();

  但是在IE浏览器在7.0之前的版本中,XHR功能性是以ActiveX对象的方式实现的,因此对于那些版本需要做一些特殊处理。

  第二步是为readystatechange事件提供一个回调函数。

xhr.onreadystatechange = handleResponse;

  最后一步是使用open()和send()两个方法来启动该请求。先使用open()方法指定HTTP请求方法(例如是GET和POST)和URL。然后使用send()方法传递POST的数据或者仅仅一个空白字符串(在GET模式下)。open()方法的最后一个参数指定该请求是否是异步的。异步模式意味着浏览器将不会停下来以等待回应。这当然会给用户更佳的用户体验,因此除非在有特点的理由以外,其他情况都应该将异步参数设置为true:

xhr.open("GET", "page.html", true); 
xhr.send();

  下面是一个完整的范例,展示了获取网页内容,并采用新的内容更新当前网页的过程。(演示文件在这里http://www.jspatterns.com/book/8/xhr.html)。

var i, xhr, activeXids = [
    'MSXML2.XMLHTTP.3.0',
    'MSXML2.XMLHTTP',
    'Microsoft.XMLHTTP'
];

if (typeof XMLHttpRequest === "function") { // native XHR
    xhr =  new XMLHttpRequest();        
} else { // IE before 7
    for (i = 0; i < activeXids.length; i += 1) {
        try {
            xhr = new ActiveXObject(activeXids[i]);
            break;
        } catch (e) {}
    }
}

xhr.onreadystatechange = function () {
    if (xhr.readyState !== 4) {
        return false;
    }
    if (xhr.status !== 200) {
        alert("Error, status code: " + xhr.status);
        return false;
    }
    document.body.innerHTML += "<pre>" + xhr.responseText + "<\/pre>";
};

xhr.open("GET", "page.html", true); 
xhr.send("");

  下面是对该范例的一些注释:

  • 对于IE来说,在IE6.0及之前的版本中新建XHR对象的过程有一些复杂。范例中依次通过一个ActiveX标识符列表(从最新版本到更早期版本)来尝试创建新对象来确定IE的版本,并将这部分操作封装在try-catch块中。
  • 回调函数检查xhr对象的readyState属性。该属性取值范围从0~4,共5个可能的属性值,其中属性值为4意味着“完成”。如果xhr对象的状态不是完整状态,那么继续等待下一个readystatechange事件。
  • 回调函数也会检查xhr对象的status属性。该属性对应于HTTP的状态码,例如200就对应于OK,而404对应于Not found。这里只关心状态码为200的情况,而将其他状态码都按照错误处理(这是为了简便起见,否则就需要检查其他的有效状态)。
  • 以上列出来的代码将会在每次创建请求的时候,就检查浏览器支持的方法来创建XHR对象。由于已经在之前的章节学习了一些模式(例如初始化分支模式),可以重写该段代码,以使得只需要检查一次浏览器可以支持的方法。

 

JSONP

  JSONP(有填充的JSON)是另外一种创建远程请求的方法。和XHR有所不同,它不受同源策略的限制,出于从第三方网站载入数据的安全性考虑,需要小心使用。

  对应于XHR请求,JSONP的请求可以是任意类型的文档:

  • XML文档(过去常用的)。
  • HTML块(现在常见的)。
  • JSON数据(轻量级, 并且方便)。
  • 简单文本文件或者其他文档。

  对于JSONP,最常见的使用函数调用封装的JSON,函数名由请求来提供。

  JSONP请求的URL通常格式如下所示:

http://example.org/getdata.php?callback=myHandler

  getdata.php可以是任意类型的网页,callback参数指定采用哪个JavaScript函数来处理该请求。

  然后像下面这样将URL载入到动态的<script>元素:

var script = document.createElement("script");
script.src = url;
document.body.appendChild(script);

  服务器响应一些JSONP数据,这些数据作为回调函数的参数。最终的结果是在网页中包含了一个新的脚本,该脚本碰巧是一个函数调用,例如:

myHandler("hello": "world");

 

JSONP范例“字棋游戏(Tic-tac-toe)

  下面展示一个使用JSONP的范例,一个字棋游戏,这里玩家即是客户端(浏览器),也是服务器。客户端和服务器都会生成一个1~9的随机数,并使用JSONP来获取服务器的值。可以在http://www.jspatterns.com/book/8/ttt.html这个网址查看源码。

  这里有两个按钮:一个新建游戏;另外一个按钮切换为服务器方(在一定超时后,会自动切换为客户方):

<button id="new">New game</button>
<button id="server">Server play</button>

  面板中包含9个小格子,分别对应于不同id属性:

<table>
    <tr>
        <td id="cell-1">&nbsp;</td>
        <td id="cell-2">&nbsp;</td>
        <td id="cell-3">&nbsp;</td>
    </tr>
    <tr>
        <td id="cell-4">&nbsp;</td>
        <td id="cell-5">&nbsp;</td>
        <td id="cell-6">&nbsp;</td>            
    </tr>
    <tr>
        <td id="cell-7">&nbsp;</td>
        <td id="cell-8">&nbsp;</td>
        <td id="cell-9">&nbsp;</td>
    </tr>
</table>

  完整游戏代码在ttt全局对象中实现:

var ttt = {
    // cells played so far
    played: [], 
    
    // shorthand
    get: function (id) { 
        return document.getElementById(id);
    },
    
    // handle clicks
    setup: function () {
        this.get('new').onclick = this.newGame;
        this.get('server').onclick = this.remoteRequest;
    },
    
    // clean the board
    newGame: function () {
        var tds = document.getElementsByTagName("td"),
            max = tds.length,
            i;
        for (i = 0; i < max; i += 1) {
            tds[i].innerHTML = "&nbsp;";
        }
        ttt.played = [];        
    },
    
    // make a request
    remoteRequest: function () {
        var script = document.createElement("script");
        script.src = "server.php?callback=ttt.serverPlay&played=" + ttt.played.join(',');
        document.body.appendChild(script);
    },
    
    // callback, server's turn to play
    serverPlay: function (data) {
        if (data.error) {
            alert(data.error);
            return;
        }
        data = parseInt(data, 10);
        this.played.push(data);

        this.get('cell-' + data).innerHTML = '<span class="server">X<\/span>';

        setTimeout(function () {
            ttt.clientPlay();
        }, 300); // as if thinking hard

    },
    
    // client's turn to play
    clientPlay: function () {
        var data = 5;

        if (this.played.length === 9) {
            alert("Game over");
            return;
        }
        
        // keep coming up with random numbers 1-9 
        // until one not taken cell is found
        while (this.get('cell-' + data).innerHTML !== "&nbsp;") {
            data = Math.ceil(Math.random() * 9);
        }
        this.get('cell-' + data).innerHTML = 'O';
        this.played.push(data);
        
    }
    
};

  ttt对象维持一个目前为止已经选择的空格列表,并将其发给服务器,因此服务器可以排除已经选择的数字来返回一个新数字。如果有错误发生,服务器将会返回类似如下信息:

ttt.serverPlay({"error":"Error description here"});

  正如您所看到的那样,JSONP中的回调函数必须是一个共有的和全局有效的函数。该回调函数可以不必是一个全局函数,但是必须是全局对象的一个方法。如果这里没有错误的话,服务器将会返回如下函数调用:

ttt.serverPlay(3);

  这里3意味着服务器给出的随机选择第三个空格。在这种情形下,由于数据十分简单,甚至不需要使用JSON格式,只需要使用一个数值表示就行。

 

框架和图像灯塔

  使用框架也是一种处理远程脚本的备选方案。可以使用JavaScript创建一个iframe元素,并修改其src属性的URL。新的URL可以包含更新调用者(在iframe之外的父页面)的数据和函数调用。

  使用远程脚本最简单的场景是只需要向服务器发送数据,而无需服务器回应的时候。在这种情形下,可以创建一个新图像,并将其src属性设置为服务器伤的脚本文件,如下所示:

new Image().src = "http://example.org/some/page.php";

  这种模式称之为图像灯塔(image beacon),这在希望向服务器发送日志数据时是非常有用的。举例来说,该模式可以用于收集访问者统计信息。因为用户并不需要使用服务器对这些日志数据的响应,通常的做法是服务器用一个1x1像素的gif图片作为响应(这是一种不好的模式)。使用“204 Not Content”这样的HTTP响应是更好的选择。该HTTP响应的意思是指仅向客户端发送HTTP报头文件,而不发送HTTP内容体。

 

六、配置JavaScript

  在采用JavaScript时,还有一些性能上需要考虑的因素。这里将在比较高的层面上讨论一下这方面最重要的问题,如需要了解更多的详细内容,可以查阅资料或其他相关书籍。

 

合并脚本文件

  构建快速载入页面的第一条规则就是尽可能少的使用外部组件,因为HTTP请求是十分耗费资源的。对于JavaScript来说,可以通过合并外部脚本文件来明显提高页面载入速度。

  假定网页使用了jQuery库,这是一个js文件。然后需要使用一些jQuery插件,每个插件都是一个独立的文件。这样在编写代码前,就拥有了4~5个文件。将这些文件合并为一个文件是十分有意义的,特别是考虑到这些文件通常都十分小(2~3kb),因而导致HTTP开销比实际下载文件的开销大得多。将这些脚本文件合并的方法很简单,只需要创建一个新文件,并将这些脚本文件的内容复制进去就行。

  当然,应该在编写代码之前合并这些文件,而不能在开始开发的过程中合并文件,因为那样会导致很多调试的开销。

  合并文件的做法也有一些缺点,比如:

  • 尽管在正式开始编写代码前需要增加一个合并脚本文件的步骤,但该操作可以采用命令行自动完成,例如在Linux/Unix可以使用cat命令来合并:$ cat jquery.js jquery.quickselect.js jquery.limit.js > all.js
  • 丢失一些缓存效益。当对其中某一个脚本文件进行修改后,该修改并不会体现到整个合并后的文件中。这就是为什么对于大型项目需要有发布规划,或者是采用两个脚本文件包:一个包含那些可能会改变的文件;另外一个包含那些不会发生修改的文件。
  • 对于文件包最好是使用版本号或者其他内容来命名。例如使用时间戳:all_201100326.js,也可以使用文件内容的哈希值来命名。

  以上这些缺点可以归纳为主要在于不方便,但是使用合并脚本文件的方法带来的收益远大于带来的不便性。

 

精简和压缩脚本文件

  在第二章中已经涉及了代码的精简。将代码精简作为构建JavaScript脚本的一部分是十分重要的。

  当从用户视角考虑时,用户没必要下载所有的注释语句,删除这些注释语句对应用程序正常运行没有影响。

  精简脚本文件大力来的收益依赖于使用的注释语句和空格的数量,也和具体精简工具有关。但通常来说,可以精简大约50%的文件大小。

  应该经常维护对脚本文件的压缩,这只需要在服务器配置中启用gzip压缩支持就可以实现,这样的配置会立即提高速度。如果使用了共享主机的服务,无法获取足够的自由来对服务器进行配置,大部分服务提供商至少会允许您使用Apache的.htaccess配置文件。因此应该在Web根目录中,将下列代码添加到.htaccess文件中:

AddOutputFilterByType DEFLATE text/html text/css text/palin text/xml application/javascript application/json

  通常这样的压缩配置会减少70%的文件大小。将精简和压缩两种操作相结合,最后只需要下载的文件大小仅有未精简、压缩之前的文件的15%

 

Expires报头

  与通常人们的想法相反,文件并不会在浏览器缓存中保存太久事件。可以通过使用expires报头来增加重复访问时,请求的文件依然在缓存中的概率。

  该操作也仅需要在.htaccess文件中增加如下代码:

ExpiresActive On 
ExpiresByType application/x-javascript "access plus 10 years"

  这样做的缺点在于如果希望修改文件,就需要重命名该文件。但可能已经为合并后的文件确定了一个命名约定。

 

使用CDN

  CDN是内容分发网络(Content Delivery Network)的缩写。CDN提供付费的主机服务,它允许您将文件副本放置于全球各个数据中心,以便用户可以选择速度最快的服务器进行连接,而您文件代码中的URL地址不需要修改。

  如果不希望使用付费CDN,也还有一些免费的选择。

 

七、载入策略

  乍看之下,如何将脚本文件包含到网页文件中是一个十分简单直白的问题。只需要使用<script>元素,要么直接使用内联的JavaScript代码,要么在src属性中使用到单独文件的链接,如下所示:

<script>
    console.log('hello world');
</script>
<script src="external.js"></script>

  但是当希望构建高性能网页应用程序时,需要意识到还有更多的模式可以考虑。

 

<script>元素的位置

  脚本元素会阻止下载网页内容。浏览器可以同时下载多个组件,但一旦遇到一个外部脚本文件后,浏览器会停止进一步下载,直到这个脚本文件狭隘、解析并执行完毕。这会严重影响网页载入的总时间,特别是在网页载入时会发生多次这类事件。

  为了最小化阻止的影响,可以将脚本元素放置于网页的最后部分,刚好在</body>标签之前。在这个位置脚本文件不会阻止其他任何文件块。网页组件的其他部分将会被下载并执行。

  在文档抬头使用单独文件是最坏的模式:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="123"></script>
    <script src="456"></script>
    <script src="789"></script>
</head>
<body>
    
</body>
</html>

  将所有文件合并式更好的做法:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="all.js"></script>
</head>
<body>
    
</body>
</html>

  最好的做法是将合并后的脚本放于网页的最后部分:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
<script src="all.js"></script>
</body>
</html>

 

HTTP块

  HTTP支持所谓的块编码,该技术允许分片发送网页。因此如果有一个很复杂的网页,不需等待服务器完成所有运算工作,就可以提前将一些静态页面报头先发送给用户。

  最简单的策略是将<head>部分内容作为HTTP的第一个块,而将网页中其他部分内容作为第二个块。换句话说,网页的分块类似下面的范例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<!-- end of chunk #1 -->
<body>
<script src="all.js"></script>
</body>
</html>
<!-- end of chunk #2 -->

  一个简单的改进是将第二块中的JavaScript代码移到第一块的<head>中。这样做使得浏览器可以在服务器没有准备好第二块的时候,就开始下载脚本文件:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="all.js"></script>
</head>
<!-- end of chunk #1 -->
<body>
</body>
</html>
<!-- end of chunk #2 -->

  还有一个更好的做法就是在网页文件的底部建立一个仅包含脚本文件的第三个块。如果在每个页面的顶部都有一些静态报头,可以将这部分内容放置在第一个块中:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div id="header"></div>
    <!-- end of chunk #1 -->
    <!-- full body of the page -->
    <!-- end of chunk #2 -->
    <script src="all.js"></script>
</body>

</html>
<!-- end of chunk #3 -->

  这种方法非常适合渐进增强的思想,并且不会影响到JavaScript代码的执行。一旦下载完HTML文件的第二部分后,就已经拥有一个完全载入、显示和可用的网页了,给用户看到的效果就好像JavaScript已经在浏览器中禁用了一样。等到JavaScript代码下载完毕后,它会增强网页的功能,并增加所有附加功能。

 

使用动态<script>元素来无阻塞地下载

  如上所述,JavaScript会阻止所有后续文件的下载,但是有一些模式可以防范这个问题:

  • 使用XHR请求载入脚本,并使用eval()将其转换为字符串。该方法受到同源策略的限制,并且使用了eval()这种不好的模式。不要使用!
  • 使用的defer和async属性,但是这种方法并不能在所有的浏览器上有效。
  • 使用动态的<script>元素。

  最后一种方法是一种比较好的,可实现的模式。类似于JSONP中所示,需要创建一个新的脚本元素,设置该元素的src属性,最后将该元素添加到网页文件中。

  下面是一个异步载入JavaScript文件的范例,该过程不会阻塞网页文件中其他部分的下载:

var script = document.createElement('script');
script.src = 'all_20100426.js';
document.documentElement.firstChild.appendChild(script);

  该模式的缺点在于如果JavaScript脚本依赖载入主js文件,那么采用该模式后不能有其他脚本元素。主js文件是异步载入的,因此无法保证该文件什么时候能载入完毕,所以紧跟着主js文件的脚本可能要假设所需的对象都还是未定义的。

  为了解决该缺点,可以让所有内敛的家考本都不要立即执行,而是将这些脚本都收集起来放在一个数组里面。然后当主脚本文件载入完毕后,就可以执行缓存数组中收集的函数了。为了实现该目的,需要三步:

  首先,创建一个数组来储存所有的内联代码,这部分代码应该放在页面尽可能前面的位置:

var mynamespce = {
    inline_scripts:[]
};

  然后,需要将所有单独的内联脚本封装到一个函数中,并将每个函数增加到inline_scripts数组中,如下所示:

// 过去是
// <script>console.log('I am inline')<\/script>

// 修改为
<script>
mynamespce.inline_scripts.push(function () {
    console.log('I am inline');
}) ;

</script>

  最后,循环执行缓存中的所有内联脚本:

var i ,scripts = mynamespce.inline_scripts,max = scripts.length;

for(i = 0; i < max; max += 1) {
    scripts[i]();
}

 

增加<script>元素

  通常来说,脚本是防止于文档的<head>区域,但是也可以将脚本文件放置于任何元素之内,包含body区域(和JSONP范例中类似)。在之前的范例中,我们使用documentElement来添加<head>,这是因为documentElement是指<html>,而他的第一个自元素就是<head>:

document.documentElement.firstChild.appendChild(script);

  通常也可以这样写:

document.getElementsByTagName('head')[0].appendChild(script);

 在能够掌控标记的时候,这样写是没问题的。但是如果是创建一个小部件或者是一个广告,无法确定网页的类型该如何办呢?从技术上来说,可以在网页中不使用<head>和<body>,尽管document.body通常能够在没有<body>标签后正常运作:

document.body.appendChild(script);

  但是,实际上有一个标签一直会在脚本运行的网页中存在——<script>标签。如果没有<script>标签(用于内联或者外联文件),那么里面的JavaScript代码就不会运行。基于上述事实,可以在网页中使用insertBefore()来在第一个有效的元素之前插入元素:

var first_script = document.getElementsByTagName('script')[0];
first_script.parentNode.insertBefore(script,first_script);

  在这里,first_script是脚本元素,开发人员保证该脚本元素位于网页中,并且script是创建的最新脚本。

 

延迟加载

  关于在页面载入王成后,载入外部文件的这种技术称为延迟加载。通常将一大段代码切分成两部分是十分有益的:

  •  一部分代码适用于初始化页面并将事件处理器附加到UI元素上的。
  • 第二部分代码只是在用户交互或者其他条件下才用得上。

  这样做的目的是希望渐进式的载入页面,尽可能快的提供目前需要使用的信息,而其余的内容可以在用户浏览该页面时在后台载入。

  载入第二部分JavaScript代码的方法非常简单,只需要再一次为head或者body添加动态脚本元素:

window.onload = function () {
    var script = document.createElement('script');
    script.src = "all_lazy_20102012.js";
    document.documentElement.firstChild.appendChild(script);
}

  对于许多应用程序来说,延迟加载的代码部分远远大于立即加载的核心部分,因为很多有趣的“操作”(例如拖放操作、XHR和动画等)只在用户发出后发生。

 

按需加载

  之前的模式在页面载入后,无条件的载入附加的JavaScript脚本,假定这些代码极有可能用得上。但是有没有办法可以设法只载入那部分确实需要的代码呢?

  想象一下,在网页上有一个具有多个不同标签的侧边栏。单击一次标签会发出一个XHR请求来获取内容、更新标签内容,并且更新过程中标签颜色还有动画变化。假设这是意义的一个需要XHR和动画库的地方呢?又假设用户从未点击该标签呢?

  这时,请使用按需加载模式。可以创建一个require()方法,该方法包含需要按需加载的脚本的名称和当附加脚本加载后需要执行的回调函数。

  require()函数的用法如下:

require('extra.js',function () {
    functionDefinedInExtraJs();
})

  让我们来看看该如何实现该函数。很明显,需要请求附加脚本,只需要按照动态<script>模式元素模式即可。根据不同的浏览器,计算出脚本家在的事件需要一些小技巧:

function require(file,callback) {
    var script = document.getElementsByTagName('script')[0],
        newjs = document.createElement('script');

        // IE浏览器
        newjs.onreadystatechange = function () {
            if(newjs.readyState === 'loaded' || newjs.readyState === 'complete') {
                newjs.onreadystatechange = null;
                callback();
            }
        };

        // 其他
        newjs.onload = function () {
            callback();
        };

        newjs.src = file;
        script.parentNode.insertBefore(newjs,script);
}

  下面是对于上述实现的一些解释:

  • 在IE中订阅readystatechange事件,并寻找readyState状态为“loaded”或“complete”的状态。其他浏览器将会忽略这部分代码。
  • 在Firefox、Safari和Opera中,需要通过onload属性订阅load事件。
  • 这种方法不适用于Safari 2。如果确实需要支持该版本浏览器,请创建一个时间间隔来定期检查是否指定变量(在附加文件中定义的变量)已经定义。当该变量被定义后,就意味着新脚本已经加载并执行了。

  可以创建一个人为的延迟脚本(用于模拟网络延迟)来测试上述实现,为其命名为ondemand.js.php。该文件内容如下:

<?php
header('Content-type: application/javascript');
sleep(1);
?>

function extraFunction(logthis){
    console.log('loaded and executed');
    console.log(logthis)
}

  现在测试一下require()函数:

document.getElementById('gogo').onclick = function () {
    require('ondemand.js.php', function () {
        extraFunction('loaded from the parent page');
        document.body.appendChild(document.createTextNode('done!'));
    });
};

  这段代码将会在控制台打印两条直线,并更新网页,显示“done!”。完整的代码在http://www.jspatterns.com/book/8/ondemand.html

 

预加载JavaScript

  在延迟加载模式和按需加载模式中,我们延迟加载当前页面需要的脚本。此外,还可以延迟加载当前页面不需要,但是在后续页面中可能需要的脚本。如此,当用户打开接下来的网页后,所需要的脚本已经预先加载了,今儿用户感觉速度会快了许多。

  预加载可以使用动态脚本模式来实现。但是这意味着该脚本将被解析和执行。解析仅仅会增加预加载的事件,而执行脚本可能会导致JavaScript错误,因为这些脚本本应该在第二个页面执行的。例如寻找某个特定的DOM节点。

  预加载JavaScript模式是可以加载脚本而并不解析和执行这些脚本的。该方法对css和图像也同样有效。

  在IE中可以使用熟悉的图像灯塔模式来发出请求:

new Image().src = "preloadme.js";

  在所有其他浏览器中可以使用一个<object>来代替脚本元素,并将其data属性指向脚本的URL:

var obj = document.createElement('object');
obj.data = "preloadme.js";
document.body.appendChild(obj);

  为了避免显示出该对象,可以将该对象的width和height属性都设置为0。

  可以创建一个通用的preload()方法,并使用初始化分支模式(参考第四章)来处理浏览器差异:

var preload;
if(/*@cc_on!@*/false) { //使用条件注释的IE 嗅探
    preload = function (file){
        new Image().src = file;
    };
} else {
    preload = function (file) {
        var obj = document.createElement('object'),
            body = document.body;
        
        obj.width = 0;
        obj.height = 0;
        obj.data = file;
        body.appendChild(obj);
    }
}

  这样就可以使用新函数了:

preload('my_web_worker.js');

  这种模式的缺点在于使用了用户代理嗅探,但是这是无法避免的。因为在这种情形下, 使用特性检测技术无法告知关于浏览器行为的足够信息。举例来说,在这种模式下如果typeof Image是一个函数,那么理论上可以使用该函数来代替嗅探进行测试。然而在这里该方法没有作用,因为所有的浏览器都支持new Image();区别仅仅在于有的浏览器的图像有独立的缓存,这也就意味着作为图像预加载的组件不会被用作缓存中的脚本,因此下一个页面会再次下载该图像。

  预加载模式可以用于各种类型组件,而不限于脚本。举例来说,这在登录页面就十分有用。当用户开始输入用户名时,可以使用输入的事件来启动预加载,因为用户下一步极有可能进入登录后的页面。

  注意:浏览器嗅探使用的分支注释是十分有趣的。该方法比在navigator.userAgent中寻找字符串要安全一些,因为那些字符串很容易被用户修改。例如:

var isIE = /*@cc_on!@*/false;

  上述语句会在除IE以外的所有浏览器中将isIE设置为false(因为这些浏览器会忽视注释语句)。但是在IE中isIE值为true,因为在注释语句中有一个“!”。因此,在IE中该语句为:

var isIE = !false; //true

 

小结

  本章主要讨论了在特定客户端浏览器环境下的模式:

  • 关注点分离的思想、不引入注目的JavaScript、以及与浏览器嗅探相对的特性检测。
  • DOM脚本,加速DOM访问和处理的方式。主要包括批处理DOM操作。
  • 事件,跨浏览器事件处理和使用事件授权来减少事件监听器的数量,以增强性能。
  • 两种处理长期高运算量脚本的模式。
  • 多种用于远程脚本的模式,这些远程脚本实现服务器和客户端直接的通信,主要包括XHR、JSONP、框架和图像灯塔。
  • 在产品环境配置JavaScript。确保将脚本合并为较少的文件、精简并压缩、将内容放置在CDN中和设置Expires报头来改善缓存。
  • 如何将脚本合理的放置在网页中,以改进性能的模式。以及,在加载大脚本文件时为了提高命中率,介绍了各种模式,包括延迟加载、预加载和按需加载JavaScript等。
posted @ 2020-09-02 14:55  Zaking  阅读(222)  评论(0编辑  收藏  举报