Giter Club home page Giter Club logo

blog's People

blog's Issues

浏览器端自动加载脚本方式简单总结

内容介绍

前端开发中,在未做特殊优化的情况下,业务脚本过多,依赖关系较为复杂。将全部的脚本以script方式手动引入到页面中,会导致引入项太多,脚本之间依赖关系容易搞错,不易维护。因此,在较为复杂的页面,需要考虑动态引入脚本的技术。

在浏览器端动态引入JavaScript脚本并不是新鲜的技术,但由于以前开发工作涉及较少,团队成员对此了解不多,因此这里对相关技术做一个简要介绍。需要兼容的浏览器较多,浏览器间的兼容性问题较多,代码写起来也不是那么容易的。因此本文将对常用的几个类库中动态加载的部分做一个比较和总结,实际开发中酌情参考。

基本原理

加载脚本一般有两种方式:script元素加载和ajax加载,因此动态脚本的加载也可以用这两种方式。

  1. script元素加载的方式,一般的实现方式是动态生成script标签,设置其src属性和加载成功/失败回调函数,之后将其插入到headdocument中。这样浏览器加载完毕后会触发回调函数。
//示例代码来自:
//  http://segmentfault.com/blog/civerzhu/1190000000471722

var script = document.createElement('script');
script.setAttribute('src', 'example.js');
script.onload = function() {
    console.log("script loaded!");
};
document.body.appendChild(script);
  1. ajax方式,一般通过ajax请求得到脚本的文本内容,再利用eval执行脚本。(也可以插入到script节点中执行)
//示例代码来自jQuery
// 其中data是ajax得到的代码文本
……
globalEval: function( data ) {
    if ( data && jQuery.trim( data ) ) {
        // We use execScript on Internet Explorer
        // We use an anonymous function so that context is window
        // rather than jQuery in Firefox
        ( window.execScript || function( data ) {
            window[ "eval" ].call( window, data );
        } )( data );
    }
},
……

上面两种方法都能实现加载js脚本的功能,但:

  • 第1种方式
  • 第2种方式最大的问题是无法跨域,只能请求本域的脚本文件。

简单实现

我们很容易根据上面的原理写出动态加载脚本的代码:
注意: 由于IE6/7/8的script不支持onload事件, 我们需要根据readyState状态的变化来知道脚本是否已加载成功。

function loadScript(url, callback, failCallback){
    var script = document.createElement("script")
    script.type = "text/javascript";

    if ("onload" in script) {
        script.onload = function(){
            callback();
            script.onerror = script.onload = null;
        };
        if (failCallback) {
            script.onerror = function () {
                failCallback();
                script.onerror = script.onload = null;
            }    
        };
    }else{
        script.onreadystatechange = function(){
            if (script.readyState == "loaded" ||
                    script.readyState == "complete"){
                script.onreadystatechange = null;
                callback();
            } 
        };
    }

    script.src = url;

    var hd;
    var hs = document.getElementsByTagName("head");
    if (hs.length) {
        hd = hs[0];
    }else{
        hd = document;
    }
    hd.appendChild(script);
}

//调用
loadScript("http://xxx/xxx.js", function(){
    //加载完成后
}, function(){
    //加载失败后
});

其他实现

上述代码能在主流最新版本浏览器中执行,但由于script标签属性兼容性和其他浏览器差异,在部分浏览器中可能仍然会不正常。

部分广泛应用的第三方库也包含类似加载代码,他们是怎么实现的呢?

seajs

关键代码:
seajs加载脚本的关键代码如下:

//取自seajs(https://github.com/seajs/seajs)
function request(url, callback, charset, crossorigin) {
  var isCSS = IS_CSS_RE.test(url)
  var node = doc.createElement(isCSS ? "link" : "script")

  if (charset) {
    node.charset = charset
  }

  // crossorigin default value is `false`.
  if (!isUndefined(crossorigin)) {
    node.setAttribute("crossorigin", crossorigin)
  }
  addOnload(node, callback, isCSS, url)

  if (isCSS) {
    node.rel = "stylesheet"
    node.href = url
  }
  else {
    node.async = true
    node.src = url
  }

  // For some cache cases in IE 6-8, the script executes IMMEDIATELY after
  // the end of the insert execution, so use `currentlyAddingScript` to
  // hold current node, for deriving url in `define` call
  currentlyAddingScript = node

  // ref: #185 & http://dev.jquery.com/ticket/2709
  baseElement ?
      head.insertBefore(node, baseElement) :
      head.appendChild(node)

  currentlyAddingScript = null
}

function addOnload(node, callback, isCSS, url) {
  var supportOnload = "onload" in node

  // for Old WebKit and Old Firefox
  if (isCSS && (isOldWebKit || !supportOnload)) {
    setTimeout(function() {
      pollCss(node, callback)
    }, 1) // Begin after node insertion
    return
  }

  if (supportOnload) {
    node.onload = onload
    node.onerror = function() {
      emit("error", { uri: url, node: node })
      onload()
    }
  }
  else {
    node.onreadystatechange = function() {
      if (/loaded|complete/.test(node.readyState)) {
        onload()
      }
    }
  }

  function onload() {
    // Ensure only run once and handle memory leak in IE
    node.onload = node.onerror = node.onreadystatechange = null

    // Remove the script to reduce memory leak
    if (!isCSS && !data.debug) {
      head.removeChild(node)
    }

    // Dereference the node
    node = null

    callback()
  }
}

由于seajs支持js和css的动态加载,我们从代码可以看到,seajs考虑了对脚本字符编码、跨域、IE6-9、旧版本WebKit浏览器的兼容支持,脚本指明用异步加载的方式。为减少内存泄露而在脚本加载完毕后删除了对应script节点。

requirejs

关键代码:

//取自requirejs(https://github.com/jrburke/requirejs)
//省略了部分注释和代码
……
req.createNode = function (config, moduleName, url) {
        var node = config.xhtml ?
                document.createElementNS('http://www.w3.org/1999/xhtml', 'html:script') :
                document.createElement('script');
        node.type = config.scriptType || 'text/javascript';
        node.charset = 'utf-8';
        node.async = true;
        return node;
    };
……
req.load = function (context, moduleName, url) {
        var config = (context && context.config) || {},
            node;
        if (isBrowser) {
            //In the browser so use a script tag
            node = req.createNode(config, moduleName, url);

            node.setAttribute('data-requirecontext', context.contextName);
            node.setAttribute('data-requiremodule', moduleName);

            if (node.attachEvent &&
                    !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) &&
                    !isOpera) {
                useInteractive = true;
                node.attachEvent('onreadystatechange', context.onScriptLoad);
            } else {
                node.addEventListener('load', context.onScriptLoad, false);
                node.addEventListener('error', context.onScriptError, false);
            }
            node.src = url;

            currentlyAddingScript = node;
            if (baseElement) {
                head.insertBefore(node, baseElement);
            } else {
                head.appendChild(node);
            }
            currentlyAddingScript = null;
……
}

从代码中可以看出requirejs也是采用script节点的方式加载脚本。其考虑了xhtml的情况,并支持指定scriptType。其也支持非普通浏览器。
与seajs一样的是考虑了字符编码、部分浏览器兼容性(IE、Opera)。最终加载完毕后也删除了script节点。

LABjs

关键代码:

//来自LABjs()
……
script = document.createElement("script");
            if (script_obj.type) script.type = script_obj.type;
            if (script_obj.charset) script.charset = script_obj.charset;

            // should preloading be used for this script?
            if (preload_this_script) {
                // real script preloading?
                if (real_preloading) {
                    /*!START_DEBUG*/if (chain_opts[_Debug]) log_msg("start script preload: "+src);/*!END_DEBUG*/
                    registry_item.elem = script;
                    if (explicit_preloading) { // explicit preloading (aka, Zakas' proposal)
                        script.preload = true;
                        script.onpreload = onload;
                    }
                    else {
                        script.onreadystatechange = function(){
                            if (script.readyState == "loaded") onload();
                        };
                    }
                    script.src = src;
                    // NOTE: no append to DOM yet, appending will happen when ready to execute
                }
                // same-domain and XHR allowed? use XHR preloading
                else if (preload_this_script && src.indexOf(root_domain) == 0 && chain_opts[_UseLocalXHR]) {
                    xhr = new XMLHttpRequest(); // note: IE never uses XHR (it supports true preloading), so no more need for ActiveXObject fallback for IE <= 7
                    /*!START_DEBUG*/if (chain_opts[_Debug]) log_msg("start script preload (xhr): "+src);/*!END_DEBUG*/
                    xhr.onreadystatechange = function() {
                        if (xhr.readyState == 4) {
                            xhr.onreadystatechange = function(){}; // fix a memory leak in IE
                            registry_item.text = xhr.responseText + "\n//@ sourceURL=" + src; // http://blog.getfirebug.com/2009/08/11/give-your-eval-a-name-with-sourceurl/
                            onload();
                        }
                    };
                    xhr.open("GET",src);
                    xhr.send();
                }
                // as a last resort, use cache-preloading
                else {
                    /*!START_DEBUG*/if (chain_opts[_Debug]) log_msg("start script preload (cache): "+src);/*!END_DEBUG*/
                    script.type = "text/cache-script";
                    create_script_load_listener(script,registry_item,"ready",function() {
                        append_to.removeChild(script);
                        onload();
                    });
                    script.src = src;
                    append_to.insertBefore(script,append_to.firstChild);
                }
            }
……

从代码中可以看出LABjs与前面两个的不同之处:在同域(非IE)下,采用ajax方式得到脚本文本,之后插入到script节点中执行。其他情况下,加载方式基本和前两个一致。

总结

从上述库的实现代码来看,总体思路是一样的。在脚本中动态添加script节点加载脚本文件,并监听节点的加载完成事件,就可以实现动态加载脚本的功能了。功能实现并不复杂,只是对浏览器兼容上需要考虑全面些。

由于缺少相应标准,笫三方开源库的实现各不相同。实际使用中需要仔细甄别,选择最适合自己的一个。当然也可根据自己的需求定制。


参考资料:

  1. http://www.cnblogs.com/chyingp/archive/2012/10/17/2726898.html
  2. https://pie.gd/test/script-link-events/
  3. http://www.stevesouders.com/blog/2009/04/27/loading-scripts-without-blocking/

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.