Last active
January 7, 2022 21:39
-
-
Save digggggggggg/a16c244b0b86d644ee0b60640441bfc0 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function AppLoader(){ | |
let loaded = {}; | |
let handlers = {}; | |
const ext = (u,e) => !!u.match(new RegExp("\.("+e+")([\?\#].*?)?$","gi")); | |
const join = (a,b) => `${a.replace(/\/+$/,'')}/${b.replace(/^\/+/,'')}`; | |
const install = function(AppName,AppRender){ | |
handlers[AppName] = AppRender; | |
document.addEventListener(`${AppName}:render`, function (event) { | |
const options = event && event.detail; | |
const { element = null } = options || {}; | |
// must be CustomEvent, must use detail prop | |
// See https://developer.mozilla.org/en-US/docs/Web/Events/Creating_and_triggering_events#creating_custom_events | |
// See https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent | |
if (!element) { | |
console.error('Must define element in detail prop of CustomEvent', {name: AppName, event}); | |
return; | |
} | |
AppRender(element, options); | |
}); | |
}; | |
const render = function(AppName, element = null, data = {}){ | |
const event = `${AppName}:render`; | |
const detail = Object.assign({ element }, data ); | |
document.dispatchEvent(new CustomEvent(event, { detail })); | |
// must be CustomEvent, must use detail prop | |
// See https://developer.mozilla.org/en-US/docs/Web/Events/Creating_and_triggering_events#creating_custom_events | |
// See https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent | |
}; | |
const getScript = function(url,success){ | |
var done=false, | |
head=document.getElementsByTagName('head')[0], | |
script=document.createElement('script') | |
; | |
script.src = url; | |
script.onload=script.onreadystatechange = function(){ | |
if ( !done && (!this.readyState || this.readyState == 'loaded' || this.readyState == 'complete') ) { | |
done=true; | |
if(typeof(success)=='function') success(script); //{url,script}); | |
script.onload = script.onreadystatechange = null; | |
} | |
}; | |
head.appendChild(script); | |
}; | |
const getStyle = function(url,success){ | |
var done=false, | |
head=document.getElementsByTagName('head')[0], | |
style=document.createElement('link') | |
; | |
style.href = url; style.rel = 'stylesheet'; | |
style.onload = style.onreadystatechange = function(){ | |
if ( !done && (!this.readyState || this.readyState == 'loaded' || this.readyState == 'complete') ) { | |
done=true; | |
if(typeof(success)=='function') success(style); //{url,style}); | |
style.onload = style.onreadystatechange = null; | |
} | |
}; | |
head.appendChild(style); | |
}; | |
const getAsset = function(url){ | |
return new Promise((resolve,reject)=>{ | |
const done = (obj) => { | |
resolve(obj); | |
} | |
if(!!loaded[url]){//loaded.filte(x=>x==url).length){ | |
done(loaded[url]) | |
} | |
else{ | |
try{ | |
const got = (obj) => { | |
loaded[url] = obj; | |
done(obj); | |
} | |
if(ext(url,"js")){ | |
getScript(url,got); | |
} | |
else if(ext(url,"css")){ | |
getStyle(url,got); | |
} | |
else{ | |
console.warn("AppLoader: error loading resource", {url}); | |
//throw new Error("can't load this type of file", {url}) | |
got(url); | |
} | |
} | |
catch(e){ | |
console.error("AppLoader: error loading resource", {url}) | |
reject(e); | |
} | |
} | |
}) | |
}; | |
const getManifest = async function(manifest){ | |
if(!!loaded[manifest]){ | |
return loaded[manifest]; | |
} | |
const assets = await fetch(manifest,{ | |
credentials:"omit", | |
//mode:"no-cors" | |
}) | |
.then(x=>x.json()) | |
.then(x=>{ | |
// detect other types of manifests | |
return x.entrypoints.filter( | |
x=> | |
!!x.match(/\b(static)\b/) | |
); | |
}); | |
loaded[manifest] = assets; | |
return assets; | |
} | |
const load = async function(props = {}){ | |
let { host, manifest, assets, name } = props; | |
if(!name){ | |
return; | |
} | |
if(!assets){ | |
assets = props.source || props.js || props.scripts; | |
} | |
if(!assets){ | |
if(!manifest && !!host){ | |
manifest = "asset-manifest.json"; | |
} | |
if(!!manifest && !!host && !manifest.match(/^(\/\/|http)/gi)){ | |
manifest = join(host, manifest); | |
} | |
if(!!manifest){ | |
assets = await getManifest(manifest); | |
if(!!host){ | |
assets = assets.map(x=>{ | |
if(!x.match(/^(\/\/|http)/)){ | |
x = join(host, x); //`${host.replace(/\/+$/,'')}/${x.replace(/^\/+/,'')}`; | |
} | |
return x; | |
}) | |
} | |
} | |
} | |
else{ | |
if(typeof(assets)=="string"){ | |
// split string into array of assets to load | |
assets = assets.split(/[\s\t\r\n\;\,]+/gi) | |
} | |
} | |
let scripts = [], styles = [], images = []; | |
assets.forEach((url)=>{ | |
if(ext(url,"js")){ | |
scripts = scripts.concat(url); | |
} | |
else if(ext(url,"css")){ | |
styles = styles.concat(url); | |
} | |
else{ | |
images = images.concat(url); | |
} | |
}); | |
// get all styles at once, don't care about how they load | |
styles.map(url=>getAsset(url)); | |
// start with current being an "empty" already-fulfilled promise | |
var current = Promise.resolve(); | |
scripts = await Promise.all(scripts.map((url)=>{ | |
return current.then( ()=> getAsset(url) ); | |
})).then((results)=>{ | |
return results; | |
}); | |
const result = { name, assets, styles, scripts, images }; | |
return result; | |
}; | |
const watch = function(){ | |
const observerElement = document.body; | |
const observerOptions = | |
{ | |
subtree: true, | |
childList: true, | |
} | |
; | |
let observer = new MutationObserver(mutations => { | |
for(let mutation of mutations) { | |
// examine new nodes, is there anything to highlight? | |
for(let node of mutation.addedNodes) { | |
// we track only elements, skip other nodes (e.g. text nodes) | |
if (!(node instanceof HTMLElement)) continue; | |
// check the inserted element for being a code snippet | |
if (node.matches('[app-loader]')) { | |
mount(node); | |
} | |
// or maybe there's a code snippet somewhere in its subtree? | |
for(let elem of node.querySelectorAll('[app-loader]')) { | |
mount(elem); | |
} | |
} | |
} | |
}); | |
observer.observe( | |
observerElement, | |
observerOptions | |
); | |
return observer; | |
}; | |
const mount = function(element){ | |
return new Promise((resolve,reject)=>{ | |
let props = {}; | |
Array.prototype.slice.call(element.attributes) | |
.filter(p=>p.nodeName && p.nodeName!="app-loader" && p.nodeName.match(/^app-/)) | |
.forEach(p=>{ | |
props[p.nodeName.replace(/^app-/gi,'')] = p.nodeValue | |
}) | |
; | |
const { name } = props; | |
load(props) | |
.then(()=>{ | |
render(name, element, props); | |
resolve(element, props); | |
}) | |
.catch((err)=>{ | |
console.error("AppLoader: mount error", {err,element,name}); | |
reject(err, {element,name}); | |
}) | |
}) | |
} | |
const start = function(){ | |
document.querySelectorAll('[app-loader]').forEach(element=>{ | |
mount(element); | |
}); | |
watch(); | |
}; | |
return { | |
loaded, | |
handlers, | |
install, | |
render, | |
getScript, | |
getStyle, | |
getAsset, | |
watch, | |
mount, | |
start, | |
} | |
}; | |
(function(){ | |
const appL = ()=> { | |
window.fwx = window.fwx || {}; | |
window.fwx.app = AppLoader(); | |
window.fwx.app.start(); | |
} | |
var raf = requestAnimationFrame || mozRequestAnimationFrame || webkitRequestAnimationFrame || msRequestAnimationFrame; | |
if(raf) raf(appL); | |
else if(window.addEventListener) window.addEventListener('load', appL); | |
else if(window.attachEvent) window.attachEvent('onload', appL); | |
else window.onload = appL; | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment