65.9K
CodeProject 正在变化。 阅读更多。
Home

ensure - 按需加载 JavaScripts/HTML/CSS

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (21投票s)

2008年6月9日

CPOL

8分钟阅读

viewsIcon

158047

downloadIcon

628

一个轻量级的 JavaScript 库,提供一个方便的 "ensure" 函数,允许您按需加载 JavaScript、HTML、CSS,然后执行您的代码。ensure 确保相关的 JavaScript 和 HTML 片段在执行使用它们的代码之前已经存在于浏览器 DOM 中。

Ensure.png

引言

ensure 是一个轻量级的 JavaScript 库,提供一个方便的 ensure 函数,允许您按需加载 JavaScript、HTML、CSS,然后执行您的代码。ensure 确保相关的 JavaScript 和 HTML 片段在执行使用它们的代码之前已经存在于浏览器 DOM 中。

例如

ensure( { js: "Some.js" }, function()
{
    SomeJS(); // The function SomeJS is available in Some.js only
});

这里 下载最新的代码。

您可以在 此网站 上看到 ensure 的实际应用。

ensure 支持 jQueryMicrosoft ASP.NET AJAXPrototype 框架。这意味着您可以在任何使用以上任一框架的 HTML、ASP.NET、PHP、JSP 页面上使用它。

背景

具有丰富客户端效果(动画、验证、菜单、弹出窗口)和 AJAX 网站的网站需要将大量的 JavaScript、HTML 和 CSS 在同一个网页上交付给浏览器。因此,富网页的初始加载时间会显著增加,因为下载必需的组件需要相当长的时间。此外,一次性交付所有可能的组件会使页面变得臃肿,浏览器在响应操作时会变得迟钝。您有时会看到下拉菜单卡顿、弹出窗口出现缓慢、窗口滚动迟钝等情况。

解决方案不是在初始加载时交付所有可能的 HTML、JavaScript 和 CSS,而是根据需要交付它们。例如,当用户将鼠标悬停在菜单栏上时,下载下拉菜单效果所需的 JavaScript 和 CSS,以及出现在下拉菜单中的菜单 HTML。同样,如果您有客户端验证,则在用户单击“提交”按钮时交付客户端验证库、相关的警告 HTML 片段和 CSS。如果您有一个按需显示页面的 AJAX 站点,则只在用户执行导致 AJAX 调用的操作时才加载 AJAX 库本身。因此,通过将一个包含大量 HTML、CSS 和 JavaScript 的复杂页面分解成更小的部分,您可以显著降低初始交付的大小,从而非常快速地加载初始页面,并为用户提供快速流畅的浏览体验。

ensure 的优点

ensure 避免了在初始阶段交付不必要的 JavaScript、HTML 和 CSS,而是在需要时按需加载它们。由 ensure 加载的 JavaScript、HTML 和 CSS 会保留在浏览器中,下次 ensure 使用相同的 JavaScript、CSS 或 HTML 调用时,它不会重新加载它们,从而节省了重复下载。

例如,您可以使用 ensure 按需下载 JavaScript

ensure( { js: "Some.js" }, function()
{
    SomeJS(); // The function SomeJS is available in Some.js only
}); 

上面的代码确保在执行代码之前 Some.js 可用。如果 SomeJS.js 已经加载,它会立即执行函数。否则,它会下载 Some.js,等待其完全加载,然后才执行函数。因此,当您只需要在某些用户操作时才需要 Some.js 时,可以避免在初始时交付它。

同样,您可以等待某些 HTML 片段可用,例如一个弹出对话框。您无需在默认网页上交付您将要显示给用户的所有可能弹出框的 HTML。您可以在需要时获取 HTML。

ensure( {html: "Popup.html"}, function()
{
    // The element "Popup" is available only in Popup.html
    document.getElementById("Popup").style.display = "";    
}); 

上面的代码从 "Popup.html" 下载 HTML,并将其添加到文档的 body 中,然后触发函数。因此,您的代码可以安全地使用该 HTML 中的 UI 元素。

您可以在一个 ensure 调用中混合搭配 JavaScript、HTML 和 CSS。例如

ensure( { js: "popup.js", html: "popup.html", css: "popup.css" }, function()
{
    PopupManager.show();
}); 

您还可以指定多个 JavaScript、HTML 或 CSS 文件,以确保在执行代码之前所有这些文件都可用。

ensure( { js: ["blockUI.js","popup.js"], html: ["popup.html", "blockUI.html"], 
css: ["blockUI.css", "popup.css"] }, function()
{
    BlockUI.show();
    PopupManager.show();
}); 

您可能会认为您将在 JavaScript 代码中编写大量 ensure 代码,这将导致 JavaScript 文件比以前更大。为了节省 JavaScript 的大小,您可以为常用的文件定义简写。

var JQUERY = { js: "jquery.js" };
var POPUP = { js: ["blockUI.js","popup.js"], html: ["popup.html", "blockUI.html"], 
    css: ["blockUI.css", "popup.css"] };
...
...
ensure( JQUERY, POPUP, function() {
    
$("DeleteConfirmPopupDIV").show();
});
...
...
ensure( POPUP, function()
{
    $("SaveConfirmationDIV").show();
); 

在加载 HTML 时,您可以指定一个容器元素,ensure 可以在其中注入加载的 HTML。例如,您可以说加载 HtmlSnippet.html,然后将内容注入名为 "exampleDiv" 的 DIV 中。

ensure( { html: ["popup.html", "blockUI.html"], parent: "exampleDiv"}, function(){}); 

您还可以指定与 HTML 一起加载的 JavaScript 和 CSS。

ensure 具有测试功能,您可以检查特定的 JavaScript 类或某些 UI 元素是否已可用。如果可用,它不会下载指定的组件,并立即执行您的代码。如果不可用,它会下载它们,然后执行您的代码。当您尝试使用某些实用函数或 UI 元素并希望确保它们已存在时,此功能非常方便。

ensure( {test:"Sys", js:"MicrosoftAjax.js"}, function(){ Sys.Application.init(); });

上面的示例检查 Microsoft AJAX 库的 Sys 类是否已存在。如果 Microsoft AJAX 库已加载,它就会存在。如果不存在,它会加载库,然后调用代码。

同样,您可以确保某个 UI 元素已存在

ensure( {test:"PopupDIV", js:"Popup.js", html:"popup.html"}, 
function()
{ 
    document.getElementById("PopupDIV").style.display = "block";
});

这确保了 ID 为 PopupDIV 的 HTML 元素已存在。如果不存在,它会下载相关的 JavaScript/HTML,然后执行您的代码。

工作原理

该库只有一个 JavaScript 文件 - ensure.js。但是,它需要以下任何 JavaScript 框架。

  • jQuery
  • Microsoft ASP.NET AJAX
  • 原型

首先是 ensure 函数的定义。

window.ensure = function( data, callback, scope )
{    
    if( typeof jQuery == "undefined" && typeof Sys == "undefined" 
    && typeof Prototype == "undefined" )
        return alert("jQuery, Microsoft ASP.NET AJAX or 
        Prototype library not found. One must be present for ensure to work");
        
    // There's a test criteria which when false, 
    //the associated components must be loaded. But if true, 
    // no need to load the components
    if( typeof data.test != "undefined" )
    {
        var test = function() { return data.test };
        
        if( typeof data.test == "string" )
        {
            test = function() 
            { 
                // If there's no such Javascript variable and there's 
        // no such DOM element with ID then
                // the test fails. If any exists, then test succeeds
                return !(eval( "typeof " + data.test ) == "undefined" 
                    && document.getElementById(data.test) == null); 
            }
        }    
        else if( typeof data.test == "function" )      
        {
            test = data.test;
        }
        
        // Now we have test prepared, time to execute the test 
    // and see if it returns null, undefined or false in any 
        // scenario. If it does, then load the specified javascript/html/css    
        if( test() === false || typeof test() == "undefined" || test() == null ) 
            new ensureExecutor(data, callback, scope);
        // Test succeeded! Just fire the callback
        else
            callback();
    }
    else
    {
        // No test specified. So, load necessary javascript/html/css 
        // and execute the callback
        new ensureExecutor(data, callback, scope);
    }
}

然而,真正的工作是由 ensureExecutor 完成的。基本上,ensure 创建一个 ensureExecute 实例,并将相关数据、回调和作用域传递给它。加载内容和调用回调的实际工作是在 ensureExecutor 中完成的。

首先,ensureExecutor 对参数进行一些准备,并确保存在有效的参数。然后,它调用 init 函数来初始化当前可用的框架(jQuery/Microsoft AJAX/Prototype),以进行一些常见的 AJAX 操作。然后,它调用 load 函数来加载必需的组件并触发回调。

window.ensureExecutor.prototype = {
    init : function()
    {
        // Fetch Javascript using Framework specific library
        if( typeof jQuery != "undefined" )
        {
            this.getJS = HttpLibrary.loadJavascript_jQuery;
            this.httpGet = HttpLibrary.httpGet_jQuery;
        }
        else if( typeof Prototype != "undefined" )
        {   
            this.getJS = HttpLibrary.loadJavascript_Prototype;
            this.httpGet = HttpLibrary.httpGet_Prototype; 
        }
        else if( typeof Sys != "undefined" )
        {
            this.getJS = HttpLibrary.loadJavascript_MSAJAX;
            this.httpGet = HttpLibrary.httpGet_MSAJAX;
        }
        else
        {
            throw "jQuery, Prototype or MS AJAX framework not found";
        }
    },

在这里,init 函数检查当前加载的框架,并据此初始化两个函数 getJShttpGet,它们分别加载外部脚本和外部 HTML。

load : function()
{
    this.loadJavascripts( this.delegate( function() { 
        this.loadCSS( this.delegate( function() { 
            this.loadHtml( this.delegate( function() { 
                this.callback() 
            } ) ) 
        } ) ) 
    } ) );
},

load 函数按顺序调用 loadJavascriptsloadCSSloadHtml。这确保在 JavaScript 成功加载后才加载 HTML,而 CSS 加载要么已完成,要么已经开始。

loadJavascripts 是一个棘手的函数。它通过创建 <script> 标签或使用 XMLHTTP 下载外部脚本来加载外部脚本。Safari 需要 XMLHTTP,因为没有办法知道 <script> 标签何时已成功下载。

loadJavascripts : function(complete)
{
    var scriptsToLoad = this.data.js.length;
    if( 0 === scriptsToLoad ) return complete();
    
    this.forEach(this.data.js, function(href)
    {
        if( HttpLibrary.isUrlLoaded(href) || 
        this.isTagLoaded('script', 'src', href) )
        {
            scriptsToLoad --;
        }
        else
        {
            this.getJS({
                url:        href, 
                success:    this.delegate(function(content)
                            {
                                scriptsToLoad --; 
                                HttpLibrary.registerUrl(href);
                            }), 
                error:      this.delegate(function(msg)
                            {
                                scriptsToLoad --; 
                                if(typeof this.data.error == "function") 
                this.data.error(href, msg);
                            })
            });
        }
    });
    
    // wait until all the external scripts are downloaded
    this.until({ 
        test:       function() { return scriptsToLoad === 0; }, 
        delay:      50,
        callback:   this.delegate(function()
        {
            complete();
        })
    });
},

思路是发出脚本下载请求,并等待所有脚本下载完成。完成后,它会触发完成回调,然后调用 loadCSSloadHTML 函数。

loadCSS 函数相对简单。唯一的注意事项是,在 Internet Explorer 6 中,您必须仅在代码在 window 对象的上下文执行时才添加 <link> 标签。我在 CodeProject 上关于 UFrame 的另一篇文章详细解释了这个问题。

loadCSS : function(complete)
{
    if( 0 === this.data.css.length ) return complete();
    
    var head = HttpLibrary.getHead();
    this.forEach(this.data.css, function(href)
    {
        if( HttpLibrary.isUrlLoaded(href) || this.isTagLoaded('link', 'href', href) )
        {
            // Do nothing
        }
        else
        {            
            var self = this;
            try
            {   
                (function(href, head)
                {
                    var link = document.createElement('link');
                    link.setAttribute("href", href);
                    link.setAttribute("rel", "Stylesheet");
                    link.setAttribute("type", "text/css");
                    head.appendChild(link);

                    HttpLibrary.registerUrl(href);
                }).apply(window, [href, head]);
            }
            catch(e)
            {
                if(typeof self.data.error == "function") 
            self.data.error(href, e.message);
            }
        }
    });
    
    complete();
}

最后,loadHTML 函数下载 HTML 并将其注入 document.body 或您指定的任何父容器元素中。

loadHtml : function(complete)
{
  var htmlToDownload = this.data.html.length;
  if( 0 === htmlToDownload ) return complete();
  
  this.forEach(this.data.html, function(href)
  {
    if( HttpLibrary.isUrlLoaded(href) )
    {
      htmlToDownload --;
    }
    else
    {
      this.httpGet({
        url:        href, 
        success:    this.delegate(function(content)
              {
                htmlToDownload --; 
                HttpLibrary.registerUrl(href);
                
                var parent = (this.data.parent || 
                  document.body.appendChild(
                    document.createElement("div")));
                if( typeof parent == "string" ) 
                  parent = document.getElementById(parent);
                parent.innerHTML = content;
              }), 
        error:      this.delegate(function(msg)
              {
                htmlToDownload --; 
                if(typeof this.data.error == "function") 
                  this.data.error(href, msg);
              })
      });
    }            
  });
  
  // wait until all the external scripts are downloaded
  this.until({ 
    test:       function() { return htmlToDownload === 0; }, 
    delay:      50,
    callback:   this.delegate(function()
    {                
      complete();
    })
  });

就是这样。

等等,还有这个 HttpLibrary 类,它完成了最复杂的工作——加载和执行 JavaScript,以及进行 AJAX 调用。

这是如何使用 <SCRIPT> 标签加载外部脚本并知道脚本何时加载,以跨浏览器兼容的方式。

createScriptTag : function(url, success, error)
{
    var scriptTag = document.createElement("script");
    scriptTag.setAttribute("type", "text/javascript");
    scriptTag.setAttribute("src", url);
    scriptTag.onload = scriptTag.onreadystatechange = function()
    {
        if ( (!this.readyState || this.readyState == "loaded" 
    || this.readyState == "complete") ) {
        success();
    }
};
    scriptTag.onerror = function()
    {
        error(data.url + " failed to load");
    };
    var head = HttpLibrary.getHead();
    head.appendChild(scriptTag);
}, 

看起来很简单,但为了使其在所有流行浏览器中完美运行,付出了很多心血。但是,Safari 2 不支持 onloadonreadystatechange 事件。因此,对于 Safari,技巧是发出 XMLHTTP 请求来下载脚本,然后执行它。但是,这意味着您无法在 Safari 上确保来自外部域的脚本,因为 XMLHTTP 调用仅适用于当前域。

在这里,您可以看到下载脚本并执行它的三种方法。

loadJavascript_jQuery : function(data)
{
    if( HttpLibrary.browser.safari )
    {
       return jQuery.ajax({
            type:       "GET",
            url:        data.url,
            data:       null,
            success:    function(content)
                        {
                            HttpLibrary.globalEval(content);
                            data.success();
                        },
            error:      function(xml, status, e) 
                        { 
                            if( xml && xml.responseText )
                                data.error(xml.responseText);
                            else
                                data.error(url +'\n' + e.message);
                        },
            dataType: "html"
        });
    }
    else
    {
        HttpLibrary.createScriptTag(data.url, data.success, data.error);
    }
},    
loadJavascript_MSAJAX : function(data)
{
    if( HttpLibrary.browser.safari )
    {
        var params = 
        { 
            url: data.url, 
            success: function(content)
            {
                HttpLibrary.globalEval(content);
                data.success(content);
            },
            error : data.error 
        };
        HttpLibrary.httpGet_MSAJAX(params);
    }
    else
    {
        HttpLibrary.createScriptTag(data.url, data.success, data.error);
    }
},
loadJavascript_Prototype : function(data)
{
    if( HttpLibrary.browser.safari )
    {
        var params = 
        { 
            url: data.url, 
            success: function(content)
            {
                HttpLibrary.globalEval(content);
                data.success(content);
            },
            error : data.error 
        };
        HttpLibrary.httpGet_Prototype(params);
    }
    else
    {
        HttpLibrary.createScriptTag(data.url, data.success, data.error);
    }
},

一个很酷的技巧是在全局上下文中执行下载的脚本。您可能会认为使用 eval 函数很容易做到。但它不起作用。eval 仅在当前作用域内执行调用。所以,您可能会认为,您可以调用 eval 在窗口对象的范围。不行,不起作用。唯一快速、跨浏览器的解决方案是这种方法。

globalEval : function(data)
{
    var script = document.createElement("script");
    script.type = "text/javascript";
    if ( HttpLibrary.browser.msie )
        script.text = data;
    else
        script.appendChild( document.createTextNode( data ) );

    var head = HttpLibrary.getHead();
    head.appendChild( script );
    //head.removeChild( script );
}

它在 <head> 节点内创建一个 <script> 标签,然后将脚本文本传递给它。

实际应用示例

测试应用程序 向您展示了 ensure 的一些常见用法。例如,当单击一个按钮时,您需要调用一个 JavaScript 函数。该 JavaScript 函数(或可以轻松地)位于外部 JavaScript 文件中。以下是做法。

<input id="example1button" type="button" value="Click me" 
    onclick="
    this.value='Loading...'; 
    ensure({js:'Components/SomeJS.js'}, function(){ 
        SomeJS(); 
        this.value='Click me'; 
    }, this)" /> 

因此,您可以看到 SomeJSSomeJS.js 中可用。

下一个示例演示了如何按需在一个 DIV 中加载一些 HTML 片段。

<input id="example2button" type="button" value="Load Html, CSS on-demand" 
onclick="
    this.value='Loading...'; 
    ensure({
        html:'Components/HtmlSnippet.htm',
        css:'Components/HtmlSnippet.css',
        parent:'resultDiv'}, 
        function(){ 
            document.getElementById('clickMe').onclick = function() 
            { 
                alert('Clicked');  
            }; 
        this.value='Load Html, CSS on-demand' 
    }, this)" />

单击按钮时,HtmlSnippet.htmlHtmlSnippet.css 会被加载。HtmlSnippet.html 中的内容被注入到名为 resultDIVDIV 中。当内容可用并成功注入后,会触发回调函数,在该函数中代码尝试挂接来自 HtmlSnippet.html 的按钮。

您可能错过了另一个很酷的方面,整个回调函数是在 <input> 按钮的上下文中触发的。ensure 的第三个参数确保了这一点。您看到,我将 this 作为作用域传递,即按钮本身。因此,当回调触发时,您仍然可以使用 this 来访问按钮。

测试应用程序中的第三个示例展示了如何加载多个 HTML、JavaScript 和 CSS 来提供两个 UI 效果 - 背景渐入和弹出对话框。

function showPopup()
{
    ensure({
        js:     'Components/BlockUI.js', 
        html:   ['Components/BlockUI.html','Components/Popup.aspx'], 
        css:    'Components/Popup.css'
        }, 
        function()
        {
            BlockUI.show();
            var popup = document.getElementById('Popup');
            if( null == popup ) alert('Popup is not loaded!');
            else popup.style.display = 'block';
            
            document.getElementById('example3button').value = "Show me the UI";
        });
} 

此代码演示了如何一次性下载多个 JavaScript、HTML 和 CSS。

下载代码

CodePlex 下载最新的源代码。

结论

现在您可以确保在使用所需的 JavaScript、HTML、CSS 之前它们已经可用。确保在整个 Web 应用程序中使用 ensure 来确保快速下载时间,同时确保 UI 功能不受影响,从而确保更丰富的用户体验,确保快速的页面加载。

© . All rights reserved.