Last active
June 29, 2019 08:38
-
-
Save jokeyrhyme/2c57fb01e734e999eded24f1e52e1a6e to your computer and use it in GitHub Desktop.
experimentation with a script loader that de-duplicates requests for scripts
This file contains hidden or 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
/** | |
load this prior to to all other scripts on the page, | |
then it will be able to monitor script loading properly | |
requires: Promise, MutationObserver, querySelectorAll, Map, Set, const, let | |
*/ | |
;(() => { | |
'use strict' | |
function noop () {} | |
// note: never expose Promise of this type, not safe | |
function newOpenPromise () { | |
let temp | |
const promise = new Promise((resolve, reject) => { | |
temp = { resolve, reject } | |
}) | |
Object.assign(promise, temp) | |
return promise | |
} | |
function once (fn) { | |
let isCalled = false | |
return function () { | |
if (!isCalled) { | |
try { | |
fn() | |
} catch (err) {} | |
} | |
isCalled = true | |
} | |
} | |
function isExternalScriptNode (node) { | |
return !!(node && node.tagName === 'SCRIPT' && node.src.trim()) | |
} | |
function isExternalScriptNodeList (nodeList) { | |
return !!(nodeList && Array.prototype.some.call(nodeList, isExternalScriptNode)) | |
} | |
function isExternalScriptMutation (mutation) { | |
return !!(mutation && mutation.addedNodes && isExternalScriptNodeList(mutation.addedNodes)) | |
} | |
function getAddedNodes (mutation) { | |
return mutation.addedNodes | |
} | |
/* | |
interface ScriptRequest { | |
loaded: Array // args from onload | |
errored: Array // args from onerror, | |
promise: Promise | |
} | |
*/ | |
const scriptRequests = new Map() // { String: ScriptRequest, ... } | |
function newRequest () { | |
return { | |
promise: newOpenPromise() | |
} | |
} | |
function getMatchingRequest (url) { | |
const request = scriptRequests.get(url) || newRequest() | |
scriptRequests.set(url, request) | |
return request | |
} | |
const SCRIPT_STATUS_ABSENT = 'SCRIPT_STATUS_ABSENT' | |
const SCRIPT_STATUS_REQUESTED = 'SCRIPT_STATUS_REQUESTED' | |
const SCRIPT_STATUS_LOADED = 'SCRIPT_STATUS_LOADED' | |
const SCRIPT_STATUS_EXECUTED = 'SCRIPT_STATUS_EXECUTED' | |
const SCRIPT_STATUS_DELETED = 'SCRIPT_STATUS_DELETED' | |
const SCRIPT_STATUS_ERROR = 'SCRIPT_STATUS_ERROR' | |
function getMatchingScriptElements (doc, url) { | |
return Array.prototype.filter.call( | |
doc.querySelectorAll('script[src]'), | |
(script) => script.src.trim() === url.trim() | |
) | |
} | |
function getScriptStatus (doc, url) { | |
const scripts = getMatchingScriptElements(doc, url) | |
if (!scripts.length) { | |
return SCRIPT_STATUS_ABSENT // or SCRIPT_STATUS_DELETED (??) | |
} | |
const request = scriptRequests.get(url.trim()) | |
if (request && !(request.loaded || request.errored)) { | |
return SCRIPT_STATUS_REQUESTED | |
} | |
if (request.loaded) { | |
return SCRIPT_STATUS_LOADED | |
} | |
if (request.errored) { | |
return SCRIPT_STATUS_ERROR | |
} | |
} | |
const scriptObserver = new MutationObserver((mutations) => { | |
mutations | |
.filter(isExternalScriptMutation) | |
.map(getAddedNodes) | |
.forEach((addedNodes) => { | |
Array.prototype.filter.call(addedNodes, isExternalScriptNode) | |
.forEach((script) => { | |
const request = getMatchingRequest(script.src.trim()) | |
script.addEventListener('load', (...args) => { | |
request.loaded = args | |
request.promise.resolve(args) | |
}, false) | |
script.addEventListener('error', (...args) => { | |
request.errored = args | |
request.promise.reject(args) | |
}, false) | |
}) | |
}) | |
}) | |
scriptObserver.observe(document.documentElement, { | |
childList: true, | |
subtree: true | |
}) | |
function injectScript (parent, url, cb = noop) { | |
const script = parent.ownerDocument.createElement('script') | |
script.async = true | |
script.defer = true | |
script.src = url.trim() | |
if (cb !== noop) { | |
const request = getMatchingRequest(url) | |
request.promise | |
.then((args) => { | |
cb(...args) | |
}) | |
.catch((args) => { | |
cb(...args) | |
}) | |
} | |
parent.appendChild(script) | |
} | |
window.injectScript = injectScript | |
function loadScriptOnce (parent, url, cb = noop) { | |
const doc = parent.ownerDocument | |
const status = getScriptStatus(doc, url) | |
if (status === SCRIPT_STATUS_ABSENT) { | |
injectScript(parent, url, cb) | |
return | |
} | |
if (cb !== noop) { | |
const request = getMatchingRequest(url) | |
request.promise | |
.then((args) => { | |
cb(...args) | |
}) | |
.catch((args) => { | |
cb(...args) | |
}) | |
} | |
} | |
window.loadScriptOnce = loadScriptOnce | |
})() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment