summer-2014 / blog Goto Github PK
View Code? Open in Web Editor NEWblog
blog
前端开发中,在未做特殊优化的情况下,业务脚本过多,依赖关系较为复杂。将全部的脚本以script方式手动引入到页面中,会导致引入项太多,脚本之间依赖关系容易搞错,不易维护。因此,在较为复杂的页面,需要考虑动态引入脚本的技术。
在浏览器端动态引入JavaScript脚本并不是新鲜的技术,但由于以前开发工作涉及较少,团队成员对此了解不多,因此这里对相关技术做一个简要介绍。需要兼容的浏览器较多,浏览器间的兼容性问题较多,代码写起来也不是那么容易的。因此本文将对常用的几个类库中动态加载的部分做一个比较和总结,实际开发中酌情参考。
加载脚本一般有两种方式:script元素加载和ajax加载,因此动态脚本的加载也可以用这两种方式。
script
元素加载的方式,一般的实现方式是动态生成script
标签,设置其src
属性和加载成功/失败回调函数,之后将其插入到head
或document
中。这样浏览器加载完毕后会触发回调函数。//示例代码来自:
// 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);
//示例代码来自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脚本的功能,但:
我们很容易根据上面的原理写出动态加载脚本的代码:
注意: 由于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(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(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()
……
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节点加载脚本文件,并监听节点的加载完成事件,就可以实现动态加载脚本的功能了。功能实现并不复杂,只是对浏览器兼容上需要考虑全面些。
由于缺少相应标准,笫三方开源库的实现各不相同。实际使用中需要仔细甄别,选择最适合自己的一个。当然也可根据自己的需求定制。
参考资料:
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.