410 lines
15 KiB
JavaScript
410 lines
15 KiB
JavaScript
/** !
|
|
* Created by EYHN on 2017/4/17. https://github.com/EYHN
|
|
* 修改自 sexdevil/LSLoader https://github.com/sexdevil/LSLoader
|
|
*/
|
|
|
|
(function () {
|
|
|
|
window.lsloader = {
|
|
jsRunSequence: [], //js 运行队列 {name:代码name,code:代码,status:状态 failed/loading/comboJS,path:线上路径}
|
|
jsnamemap: {}, //js name map 防fallback 重复请求资源
|
|
cssnamemap: {} //css name map 防fallback 重复请求资源
|
|
};
|
|
|
|
/*
|
|
* 封装localStorage get set remove 方法
|
|
* try catch保证ls写满或者不支持本地缓存环境能继续运行js
|
|
* */
|
|
lsloader.removeLS = function (key) {
|
|
try {
|
|
localStorage.removeItem(key)
|
|
} catch (e) { }
|
|
};
|
|
lsloader.setLS = function (key, val) {
|
|
try {
|
|
localStorage.setItem(key, val);
|
|
} catch (e) {
|
|
}
|
|
}
|
|
lsloader.getLS = function (key) {
|
|
var val = ''
|
|
try {
|
|
val = localStorage.getItem(key);
|
|
} catch (e) {
|
|
val = '';
|
|
}
|
|
return val
|
|
}
|
|
|
|
versionString = "/*" + (window.materialVersion || 'unknownVersion') + "*/";
|
|
|
|
lsloader.clean = function () {
|
|
try {
|
|
var keys = [];
|
|
for (var i = 0; i < localStorage.length; i++) {
|
|
keys.push(localStorage.key(i))
|
|
}
|
|
keys.forEach(function (key) {
|
|
var data = lsloader.getLS(key);
|
|
if (window.oldVersion) {
|
|
var remove = window.oldVersion.reduce(function(p,c) {
|
|
return p || data.indexOf('/*' + c + '*/') !== -1
|
|
}, false)
|
|
if (remove) {
|
|
lsloader.removeLS(key);
|
|
}
|
|
}
|
|
})
|
|
} catch (e) {
|
|
}
|
|
}
|
|
|
|
lsloader.clean();
|
|
|
|
/*
|
|
* load资源
|
|
* name 作为key path/分割符/代码值 作为value ,存储资源
|
|
* 如果value值中取出的版本号和线上模版的一致,命中缓存用本地,
|
|
* 否则 调用requestResource 请求资源
|
|
* jsname 文件name值,取相对路径,对应存在localStroage里的key
|
|
* jspath 文件线上路径,带md5版本号,用于加载资源,区分资源版本
|
|
* cssonload css加载成功时候调用,用于配合页面展现
|
|
* */
|
|
lsloader.load = function (jsname, jspath, cssonload, isJs) {
|
|
if (typeof cssonload === 'boolean') {
|
|
isJs = cssonload;
|
|
cssonload = undefined;
|
|
}
|
|
isJs = isJs || false;
|
|
cssonload = cssonload || function () { };
|
|
var code;
|
|
code = this.getLS(jsname);
|
|
if (code && code.indexOf(versionString) === -1) { //ls 版本 codestartv* 每次换这个版本 所有ls作废
|
|
this.removeLS(jsname);
|
|
this.requestResource(jsname, jspath, cssonload, isJs);
|
|
return
|
|
}
|
|
//取出对应文件名下的code
|
|
if (code) {
|
|
var versionNumber = code.split(versionString)[0]; //取出路径版本号 如果要加载的和ls里的不同,清理,重写
|
|
if (versionNumber != jspath) {
|
|
console.log("reload:" + jspath)
|
|
this.removeLS(jsname);
|
|
this.requestResource(jsname, jspath, cssonload, isJs);
|
|
return
|
|
}
|
|
code = code.split(versionString)[1];
|
|
if (isJs) {
|
|
this.jsRunSequence.push({ name: jsname, code: code })
|
|
this.runjs(jspath, jsname, code);
|
|
} else {
|
|
document.getElementById(jsname).appendChild(document.createTextNode(code));
|
|
cssonload();
|
|
}
|
|
} else {
|
|
//null xhr获取资源
|
|
this.requestResource(jsname, jspath, cssonload, isJs);
|
|
}
|
|
};
|
|
|
|
|
|
/*
|
|
* load请求资源
|
|
* 根据文件名尾部不同加载,js走runjs方法,加入运行队列中
|
|
* css 直接加载并且写入对应的<style>标签,根据style的顺序
|
|
* 保证css能正确覆盖规则 css 加载成功后调用cssonload 帮助控制
|
|
* 异步加载样式造车的dom树渲染错乱问题
|
|
* */
|
|
lsloader.requestResource = function (name, path, cssonload, isJs) {
|
|
var that = this
|
|
if (isJs) {
|
|
this.iojs(path, name, function (path, name, code) {
|
|
that.setLS(name, path + versionString + code)
|
|
that.runjs(path, name, code);
|
|
})
|
|
} else {
|
|
this.iocss(path, name, function (code) {
|
|
document.getElementById(name).appendChild(document.createTextNode(code));
|
|
that.setLS(name, path + versionString + code)
|
|
}, cssonload)
|
|
}
|
|
|
|
};
|
|
|
|
/*
|
|
* iojs
|
|
* 请求js资源,失败后调用jsfallback
|
|
* */
|
|
lsloader.iojs = function (path, jsname, callback) {
|
|
var that = this;
|
|
that.jsRunSequence.push({ name: jsname, code: '' })
|
|
try {
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.open("get", path, true);
|
|
xhr.onreadystatechange = function () {
|
|
if (xhr.readyState == 4) {
|
|
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
|
|
if (xhr.response != '') {
|
|
callback(path, jsname, xhr.response);
|
|
return;
|
|
}
|
|
}
|
|
that.jsfallback(path, jsname);
|
|
}
|
|
};
|
|
xhr.send(null);
|
|
} catch (e) {
|
|
that.jsfallback(path, jsname);
|
|
}
|
|
|
|
};
|
|
|
|
/*
|
|
* iocss
|
|
* 请求css资源,失败后调用cssfallback
|
|
* */
|
|
|
|
lsloader.iocss = function (path, jsname, callback, cssonload) {
|
|
var that = this;
|
|
try {
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.open("get", path, true);
|
|
xhr.onreadystatechange = function () {
|
|
if (xhr.readyState == 4) {
|
|
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
|
|
if (xhr.response != '') {
|
|
callback(xhr.response);
|
|
cssonload();
|
|
return;
|
|
}
|
|
}
|
|
that.cssfallback(path, jsname, cssonload);
|
|
}
|
|
};
|
|
xhr.send(null);
|
|
|
|
} catch (e) {
|
|
that.cssfallback(path, jsname, cssonload);
|
|
}
|
|
};
|
|
|
|
lsloader.iofonts = function (path, jsname, callback, cssonload) {
|
|
var that = this;
|
|
try {
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.open("get", path, true);
|
|
xhr.onreadystatechange = function () {
|
|
if (xhr.readyState == 4) {
|
|
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
|
|
if (xhr.response != '') {
|
|
callback(xhr.response);
|
|
cssonload();
|
|
return;
|
|
}
|
|
}
|
|
that.cssfallback(path, jsname, cssonload);
|
|
}
|
|
};
|
|
xhr.send(null);
|
|
|
|
} catch (e) {
|
|
that.cssfallback(path, jsname, cssonload);
|
|
}
|
|
};
|
|
|
|
/*
|
|
* runjs
|
|
* 运行js主方法
|
|
* path js线上路径
|
|
* name js相对路径
|
|
* code js代码
|
|
* */
|
|
|
|
lsloader.runjs = function (path, name, code) {
|
|
//如果有 name code ,xhr来的结果,写入ls 否则是script.onload调用
|
|
if (!!name && !!code) {
|
|
for (var k in this.jsRunSequence) {
|
|
if (this.jsRunSequence[k].name == name) {
|
|
this.jsRunSequence[k].code = code;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!!this.jsRunSequence[0] && !!this.jsRunSequence[0].code && this.jsRunSequence[0].status != 'failed') {
|
|
//每次进入runjs检查jsRunSequence,如果第一项有代码并且状态没被置为failed,执行并剔除队列,回调
|
|
var script = document.createElement('script');
|
|
script.appendChild(document.createTextNode(this.jsRunSequence[0].code));
|
|
script.type = 'text/javascript';
|
|
document.getElementsByTagName('head')[0].appendChild(script);
|
|
this.jsRunSequence.shift();
|
|
//如果jsSequence还有排队的 继续运行
|
|
if (this.jsRunSequence.length > 0) {
|
|
this.runjs();
|
|
}
|
|
|
|
} else if (!!this.jsRunSequence[0] && this.jsRunSequence[0].status == 'failed') {
|
|
/*每次进入runjs检查jsRunSequence,如果第一项存在并且状态为failed,用script标签异步加载,
|
|
* 并且该项status置为loading 其他资源加载调用runjs时候就不会通过这个js项,等候完成
|
|
*/
|
|
var that = this;
|
|
var script = document.createElement('script');
|
|
script.src = this.jsRunSequence[0].path;
|
|
script.type = 'text/javascript';
|
|
this.jsRunSequence[0].status = 'loading'
|
|
script.onload = function () {
|
|
that.jsRunSequence.shift();
|
|
//如果jsSequence还有排队的 继续运行
|
|
if (that.jsRunSequence.length > 0) {
|
|
that.runjs();
|
|
}
|
|
};
|
|
document.body.appendChild(script);
|
|
}
|
|
}
|
|
/*
|
|
* tagLoad 用script标签加载不支持xhr请求的js资源
|
|
* 方法时jsRunSequence队列中添加一项name path为该资源,但是status=failed的项
|
|
* runjs调用检查时就会把这个项当作失败取用script标签请求
|
|
* */
|
|
lsloader.tagLoad = function (path, name) {
|
|
this.jsRunSequence.push({ name: name, code: '', path: path, status: 'failed' });
|
|
this.runjs();
|
|
}
|
|
|
|
//js回退加载 this.jsnamemap[name] 存在 证明已经在队列中 放弃
|
|
lsloader.jsfallback = function (path, name) {
|
|
if (!!this.jsnamemap[name]) {
|
|
return;
|
|
} else {
|
|
this.jsnamemap[name] = name;
|
|
}
|
|
//jsRunSequence队列中 找到fail的文件,标记他,等到runjs循环用script请求
|
|
for (var k in this.jsRunSequence) {
|
|
if (this.jsRunSequence[k].name == name) {
|
|
this.jsRunSequence[k].code = '';
|
|
this.jsRunSequence[k].status = 'failed';
|
|
this.jsRunSequence[k].path = path;
|
|
}
|
|
}
|
|
this.runjs();
|
|
};
|
|
/*cssfallback 回退加载
|
|
* path 同上
|
|
* name 同上
|
|
* cssonload 同上
|
|
* xhr加载css失败的话 使用link标签异步加载样式,成功后调用cssonload
|
|
*/
|
|
lsloader.cssfallback = function (path, name, cssonload) {
|
|
if (!!this.cssnamemap[name]) {
|
|
return;
|
|
} else {
|
|
this.cssnamemap[name] = 1;
|
|
}
|
|
var link = document.createElement('link');
|
|
link.type = 'text/css';
|
|
link.href = path;
|
|
link.rel = 'stylesheet';
|
|
link.onload = link.onerror = cssonload;
|
|
var root = document.getElementsByTagName('script')[0];
|
|
root.parentNode.insertBefore(link, root)
|
|
}
|
|
|
|
/*runInlineScript 运行行内脚本
|
|
* 如果有依赖之前加载的js的内联脚本,用该方法执行,
|
|
* scriptId js队列中的name值,可选
|
|
* codeId 包含内连脚本的textarea容器的id
|
|
* js队列中添加name code值进入,运行到该项时runjs函数直接把代码append到顶部运行
|
|
*/
|
|
lsloader.runInlineScript = function (scriptId, codeId) {
|
|
var code = document.getElementById(codeId).innerText;
|
|
this.jsRunSequence.push({ name: scriptId, code: code })
|
|
this.runjs()
|
|
}
|
|
/*loadCombo combo加载,顺序执行一系列js
|
|
*
|
|
* jslist :[
|
|
* {
|
|
* name:名称,
|
|
* path:线上路径
|
|
* }
|
|
* ]
|
|
* 遍历jslist数组,按照顺序加入jsRunSequence
|
|
* 其中,如果本地缓存成功,直接写入code准备执行
|
|
* 否则status值为comboloading code写入null 不会执行
|
|
* 所有comboloading的模块拼接成一个url请求线上combo服务
|
|
* 成功后执行runcombo方法运行脚本
|
|
* 失败的话所有requestingModules请求的js文件都置为failed
|
|
* runjs会启用script标签加载
|
|
*/
|
|
lsloader.loadCombo = function (jslist) {
|
|
var updateList = '';// 待更新combo模块列表
|
|
var requestingModules = {};//存储本次更新map
|
|
for (var k in jslist) {
|
|
var LS = this.getLS(jslist[k].name);
|
|
if (!!LS) {
|
|
var version = LS.split(versionString)[0]
|
|
var code = LS.split(versionString)[1]
|
|
} else {
|
|
var version = '';
|
|
}
|
|
if (version == jslist[k].path) {
|
|
this.jsRunSequence.push({ name: jslist[k].name, code: code, path: jslist[k].path }) // 缓存有效 代码加入runSequence
|
|
} else {
|
|
this.jsRunSequence.push({ name: jslist[k].name, code: null, path: jslist[k].path, status: 'comboloading' }) // 缓存无效 代码加入运行队列 状态loading
|
|
requestingModules[jslist[k].name] = true;
|
|
updateList += (updateList == '' ? '' : ';') + jslist[k].path;
|
|
}
|
|
}
|
|
var that = this;
|
|
if (!!updateList) {
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.open("get", combo + updateList, true);
|
|
xhr.onreadystatechange = function () {
|
|
if (xhr.readyState == 4) {
|
|
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
|
|
if (xhr.response != '') {
|
|
that.runCombo(xhr.response, requestingModules);
|
|
return;
|
|
}
|
|
} else {
|
|
for (var i in that.jsRunSequence) {
|
|
if (requestingModules[that.jsRunSequence[i].name]) {
|
|
that.jsRunSequence[i].status = 'failed'
|
|
}
|
|
}
|
|
that.runjs();
|
|
}
|
|
}
|
|
};
|
|
xhr.send(null);
|
|
}
|
|
|
|
this.runjs();
|
|
}
|
|
/*runcombo
|
|
* comboCode 服务端返回的用/combojs/注释分隔开的js代码
|
|
* requestingModules 所有被combo请求的modules map
|
|
* requestingModules:{
|
|
* js文件name : true
|
|
* }
|
|
* combo服务返回代码后,用分隔符把所有js模块分隔成数组,
|
|
* 用requestingModules查找jsRunSequence中该模块对应的项,
|
|
* 更改该项,code为当前代码,status改为comboJS
|
|
* 所有combo返回的模块遍历成功后,runjs()
|
|
* runjs会把所有有代码的项当作成功项执行
|
|
*/
|
|
lsloader.runCombo = function (comboCode, requestingModules) {
|
|
comboCode = comboCode.split('/*combojs*/');
|
|
comboCode.shift();//去除首个空code
|
|
for (var k in this.jsRunSequence) {
|
|
if (!!requestingModules[this.jsRunSequence[k].name] && !!comboCode[0]) {
|
|
this.jsRunSequence[k].status = 'comboJS';
|
|
this.jsRunSequence[k].code = comboCode[0];
|
|
this.setLS(this.jsRunSequence[k].name, this.jsRunSequence[k].path + versionString + comboCode[0]);
|
|
comboCode.shift();
|
|
}
|
|
}
|
|
this.runjs();
|
|
}
|
|
|
|
})() |