Related
I am recently started working with THREE.js and WebGL to load car models. I want to call the custom function using on click event listener so that function gets an address from the data- attribute and sends it to another custom function of external JavaScript file type=module which will use that address to load my model files.
in all the above-mentioned steps, I am getting an error in the console that the function(load model) is not defined. I am using ES6 Javascript to complete my work after my research on this problem. But I am not familiar with this ES6 javascript.
here is my HTML code.
<body>
<canvas class="webgl"></canvas>
<input class="but" type="button" value="click" onclick="myfun()">
<script type="module">
var first = "ASSET/model/unt.glb";
var valuee;
function myfun(valuee){
var second = valuee;
localStorage.setItem("model_value_local", second);
loadmodel();
};
myfun(first);
</script>
<script type="module" src="./script.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
</body>
here is my script.js (type=module) code
export function loadmodel()
{
var model_value = localStorage.getItem("model_value_local");
alert(model_value);
let model;
const loader = new GLTFLoader()
loader.load(model_value,
(gltf) => {
model = gltf.scene;
model.scale.set(3, 3, 3);
scene.add(model);
}
)
};
window.loadmodel = loadmodel;
You have not import the module and you have not set type="module" to the script tag. Also you don't need to import with <script src="..."> the script.js file.
Like :
<canvas class="webgl"></canvas>
<script type="module">
import { loadmodel } from './script.js';
var first = "ASSET/model/unt.glb";
// ...
</script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
Also, in your script.js you don't need to finish with window.loadmodel = loadmodel; since you already exported the function with the export keyword.
If you need more information on the subject here is a link to the MDN documentation which is very complete on the subject.And here is a link to the related (to mdn) github repository with some example.
This is not the issue, but as you say you are not familiar with ES6, and I see that you import JQuery... JQuery is a framework that I consider obsolete since 95-99% of its features has its own equivalent in Native JavaScript. And the remaining 1-5% are only needed in very special cases where it's more easier to use another newer library (or just export the necessary code fragment from the JQuery sources).All this to say that, if you use it and it is not a dependency (e.g. for a framework) you may do not need it.
This is somewhat similar to this question:
Adding script tag to React/JSX
But in my case I am loading a script like this:
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','ID');</script>
<!-- End Google Tag Manager -->
Now I know there is a npm package for the google tag manager but I am curious if I would like to do this in a custom way how would I go about?
In the above question I see a lot of:
const script = document.createElement("script");
script.src = "https://use.typekit.net/foobar.js";
script.async = true;
document.body.appendChild(script);
Which is fine but if I have a function inside of the loaded script how would I go about executing this correctly?
To add a random script like this, you could:
Add the script to your index.html
Paste the code to a file and use an import statement.
Dynamically load the script once the user does something, using code splitting.
1. Adding the script to your HTML
Just stick the script tags in your index.html file, preferably at the end of the body tags. If using create-react-app, the index.html file is located in the public directory:
<body>
<div id="root"></div>
<script>/* your script here */</script>
</body>
2. Import from file
Alternatively, you could paste the script into a .js file, and import it from anywhere in your code. A good place to import general scripts would be in your index.js entry point file. This approach has the benefit of including the script with the rest of your js bundle, enabling minification and tree shaking.
// index.js
import "../path/to/your/script-file";
3. Code splitting
Lastly, if you would like to dynamically load a piece of js code in a certain point in time, while making sure it isn't part of your starting bundle, you could do code splitting, using dynamic imports. https://create-react-app.dev/docs/code-splitting
function App() {
function handleLoadScript() {
import('./your-script')
.then(({ functionFromModule }) => {
// Use functionFromModule
})
.catch(err => {
// Handle failure
});
};
return <button onClick={handleLoadScript}>Load</button>;
}
Usually, one can update an HTML element in react using the dangerouslySetInnerHTML prop.
But for the case of a script that is to be executed, this won't work, as discussed in this other SO question.
An option you have to achieve this, is appending the element inside a new document context, using the document Range API, createContextualFragment
Working example below.
Note that I've tweaked your script a bit to show some ways to customize it.
const { useState, useRef, useEffect, memo } = React;
const MyCustomScriptComponent = () => {
const [includeScript, setIncludeScript] = useState(false)
// just some examples of customizing the literal script definition
const labelName = 'dataLayer'
const gtmId = 'ID' // your GTM id
// declare the custom <script> literal string
const scriptToInject = `
<script>
(function(w,d,s,l,i){
const gtmStart = new Date().getTime();
w[l]=w[l]||[];w[l].push({'gtm.start':
gtmStart,event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
console.log("loaded at gtmStart:", gtmStart);
})(window,document,'script','${labelName}','${gtmId}');
console.log("fetching GTM using id '${gtmId}'");
</script>`
const InjectScript = memo(({ script }) => {
const divRef = useRef(null);
useEffect(() => {
if (divRef.current === null) {
return;
}
// create a contextual fragment that will execute the script
// beware of security concerns!!
const doc = document
.createRange()
.createContextualFragment(script)
// clear the div HTML, and append the doc fragment with the script
divRef.current.innerHTML = ''
divRef.current.appendChild(doc)
})
return <div ref={divRef} />
})
const toggleIncludeScript = () => setIncludeScript((include) => !include)
return (
<div>
{includeScript && <InjectScript script={scriptToInject} />}
<p>Custom script {includeScript ? 'loaded!' : 'not loaded.'}</p>
<button onClick={toggleIncludeScript}>Click to load</button>
</div>
)
}
ReactDOM.render(<MyCustomScriptComponent />, document.getElementById('app'))
Try it live on codepen.
For additional reference, you can find more alternatives to inject a script in this medium post.
I've been experimenting with new native ECMAScript module support that has recently been added to browsers. It's pleasant to finally be able import scripts directly and cleanly from JavaScript.
/example.html 🔍
<script type="module">
import {example} from '/example.js';
example();
</script>
/example.js
export function example() {
document.body.appendChild(document.createTextNode("hello"));
};
However, this only allows me to import modules that are defined by separate external JavaScript files. I usually prefer to inline some scripts used for the initial rendering, so their requests don't block the rest of the page. With a traditional informally-structured library, that might look like this:
/inline-traditional.html 🔍
<body>
<script>
var example = {};
example.example = function() {
document.body.appendChild(document.createTextNode("hello"));
};
</script>
<script>
example.example();
</script>
However, naively inlining modules files obviously won't work, since it would remove the filename used to identify the module to other modules. HTTP/2 server push may be the canonical way to handle this situation, but it's still not an option in all environments.
Is it possible to perform an equivalent transformation with ECMAScript modules?
Is there any way for a <script type="module"> to import a module exported by another in the same document?
I imagine this could work by allowing the script to specify a file path, and behave as though it had already been downloaded or pushed from the path.
/inline-name.html 🔍
<script type="module" name="/example.js">
export function example() {
document.body.appendChild(document.createTextNode("hello"));
};
</script>
<script type="module">
import {example} from '/example.js';
example();
</script>
Or maybe by an entirely different reference scheme, such as is used for local SVG references:
/inline-id.html 🔍
<script type="module" id="example">
export function example() {
document.body.appendChild(document.createTextNode("hello"));
};
</script>
<script type="module">
import {example} from '#example';
example();
</script>
But neither of these hypotheticals actually work, and I haven't seen an alternative which does.
Hacking Together Our Own import from '#id'
Exports/imports between inline scripts aren't natively supported, but it was a fun exercise to hack together an implementation for my documents. Code-golfed down to a small block, I use it like this:
<script type="module" data-info="https://stackoverflow.com/a/43834063">let l,e,t
='script',p=/(from\s+|import\s+)['"](#[\w\-]+)['"]/g,x='textContent',d=document,
s,o;for(o of d.querySelectorAll(t+'[type=inline-module]'))l=d.createElement(t),o
.id?l.id=o.id:0,l.type='module',l[x]=o[x].replace(p,(u,a,z)=>(e=d.querySelector(
t+z+'[type=module][src]'))?a+`/* ${z} */'${e.src}'`:u),l.src=URL.createObjectURL
(new Blob([l[x]],{type:'application/java'+t})),o.replaceWith(l)//inline</script>
<script type="inline-module" id="utils">
let n = 1;
export const log = message => {
const output = document.createElement('pre');
output.textContent = `[${n++}] ${message}`;
document.body.appendChild(output);
};
</script>
<script type="inline-module" id="dogs">
import {log} from '#utils';
log("Exporting dog names.");
export const names = ["Kayla", "Bentley", "Gilligan"];
</script>
<script type="inline-module">
import {log} from '#utils';
import {names as dogNames} from '#dogs';
log(`Imported dog names: ${dogNames.join(", ")}.`);
</script>
Instead of <script type="module">, we need to define our script elements using a custom type like <script type="inline-module">. This prevents the browser from trying to execute their contents itself, leaving them for us to handle. The script (full version below) finds all inline-module script elements in the document, and transforms them into regular script module elements with the behaviour we want.
Inline scripts can't be directly imported from each other, so we need to give the scripts importable URLs. We generate a blob: URL for each of them, containing their code, and set the src attribute to run from that URL instead of running inline. The blob: URLs acts like normal URLs from the server, so they can be imported from other modules. Each time we see a subsequent inline-module trying to import from '#example', where example is the ID of a inline-module we've transformed, we modify that import to import from the blob: URL instead. This maintains the one-time execution and reference deduplication that modules are supposed to have.
<script type="module" id="dogs" src="blob:https://example.com/9dc17f20-04ab-44cd-906e">
import {log} from /* #utils */ 'blob:https://example.com/88fd6f1e-fdf4-4920-9a3b';
log("Exporting dog names.");
export const names = ["Kayla", "Bentley", "Gilligan"];
</script>
The execution of module script elements is always deferred until after the document is parsed, so we don't need to worry about trying to support the way that traditional script elements can modify the document while it's still being parsed.
export {};
for (const original of document.querySelectorAll('script[type=inline-module]')) {
const replacement = document.createElement('script');
// Preserve the ID so the element can be selected for import.
if (original.id) {
replacement.id = original.id;
}
replacement.type = 'module';
const transformedSource = original.textContent.replace(
// Find anything that looks like an import from '#some-id'.
/(from\s+|import\s+)['"](#[\w\-]+)['"]/g,
(unmodified, action, selector) => {
// If we can find a suitable script with that id...
const refEl = document.querySelector('script[type=module][src]' + selector);
return refEl ?
// ..then update the import to use that script's src URL instead.
`${action}/* ${selector} */ '${refEl.src}'` :
unmodified;
});
// Include the updated code in the src attribute as a blob URL that can be re-imported.
replacement.src = URL.createObjectURL(
new Blob([transformedSource], {type: 'application/javascript'}));
// Insert the updated code inline, for debugging (it will be ignored).
replacement.textContent = transformedSource;
original.replaceWith(replacement);
}
Warnings: this simple implementation doesn't handle script elements added after the initial document has been parsed, or allow script elements to import from other script elements that occur after them in the document. If you have both module and inline-module script elements in a document, their relative execution order may not be correct. The source code transformation is performed using a crude regex that won't handle some edge cases such as periods in IDs.
This is possible with service workers.
Since a service worker should be installed before it will be able to process a page, this requires to have a separate page to initialize a worker to avoid chicken/egg problem - or a page can reloaded when a worker is ready.
Example
Here's a demo that is supposed to be workable in modern browsers that support native ES modules and async..await (namely Chrome):
index.html
<html>
<head>
<script>
(async () => {
try {
const swInstalled = await navigator.serviceWorker.getRegistration('./');
await navigator.serviceWorker.register('sw.js', { scope: './' })
if (!swInstalled) {
location.reload();
}
} catch (err) {
console.error('Worker not registered', err);
}
})();
</script>
</head>
<body>
World,
<script type="module" data-name="./example.js">
export function example() {
document.body.appendChild(document.createTextNode("hello"));
};
</script>
<script type="module">
import {example} from './example.js';
example();
</script>
</body>
</html>
sw.js
self.addEventListener('fetch', e => {
// parsed pages
if (/^https:\/\/run.plnkr.co\/\w+\/$/.test(e.request.url)) {
e.respondWith(parseResponse(e.request));
// module files
} else if (cachedModules.has(e.request.url)) {
const moduleBody = cachedModules.get(e.request.url);
const response = new Response(moduleBody,
{ headers: new Headers({ 'Content-Type' : 'text/javascript' }) }
);
e.respondWith(response);
} else {
e.respondWith(fetch(e.request));
}
});
const cachedModules = new Map();
async function parseResponse(request) {
const response = await fetch(request);
if (!response.body)
return response;
const html = await response.text(); // HTML response can be modified further
const moduleRegex = /<script type="module" data-name="([\w./]+)">([\s\S]*?)<\/script>/;
const moduleScripts = html.match(new RegExp(moduleRegex.source, 'g'))
.map(moduleScript => moduleScript.match(moduleRegex));
for (const [, moduleName, moduleBody] of moduleScripts) {
const moduleUrl = new URL(moduleName, request.url).href;
cachedModules.set(moduleUrl, moduleBody);
}
const parsedResponse = new Response(html, response);
return parsedResponse;
}
Script bodies are being cached (native Cache can be used as well) and returned for respective module requests.
Concerns
The approach is inferior to the application built and chunked with bundling tool like Webpack or Rollup in terms of performance, flexibility, solidity and browser support - especially if blocking concurrent requests are the primary concern.
Inline scripts increase bandwidth usage. This is naturally avoided when scripts are loaded once and cached by the browser.
Inline scripts aren't modular and contradict the concept of ECMAScript modules (unless they are generated from real modules by server-side template).
Service worker initialization should be performed on a separate page to avoid unnecessary requests.
The solution is limited to a single page and doesn't take <base> into account.
A regular expression is used for demonstration purposes only. When used like in the example above it enables the execution of arbitrary JavaScript code that is available on the page. A proven library like parse5 should be used instead (it will result in performance overhead, and still, there may be security concerns). Never use regular expressions to parse the DOM.
I don't believe that's possible.
For inline scripts you're stuck with one of the more traditional ways of modularizing code, like the namespacing you demonstrated using object literals.
With webpack you can do code splitting which you could use to grab a very minimal chunk of code on page load and then incrementally grab the rest as needed. Webpack also has the advantage of allowing you to use the module syntax (plus a ton of other ES201X improvements) in way more environments than just Chrome Canary.
I tweaked Jeremy's answer with the use of this article to prevent scripts from executing
<script data-info="https://stackoverflow.com/a/43834063">
// awsome guy on [data-info] wrote 90% of this but I added the mutation/module-type part
let l,e,t='script',p=/(from\s+|import\s+)['"](#[\w\-]+)['"]/g,x='textContent',d=document,s,o;
let evls = event => (
event.target.type === 'javascript/blocked',
event.preventDefault(),
event.target.removeEventListener( 'beforescriptexecute', evls ) )
;(new MutationObserver( mutations =>
mutations.forEach( ({ addedNodes }) =>
addedNodes.forEach( node =>
( node.nodeType === 1 && node.matches( t+'[module-type=inline]' )
&& (
node.type = 'javascript/blocked',
node.addEventListener( 'beforescriptexecute', evls ),
o = node,
l=d.createElement(t),
o.id?l.id=o.id:0,
l.type='module',
l[x]=o[x].replace(p,(u,a,z)=>
(e=d.querySelector(t+z+'[type=module][src]'))
?a+`/* ${z} */'${e.src}'`
:u),
l.src=URL.createObjectURL(
new Blob([l[x]],
{type:'application/java'+t})),
o.replaceWith(l)
)//inline
) ) )))
.observe( document.documentElement, {
childList: true,
subtree: true
} )
// for(o of d.querySelectorAll(t+'[module-type=inline]'))
// l=d.createElement(t),
// o.id?l.id=o.id:0,
// l.type='module',
// l[x]=o[x].replace(p,(u,a,z)=>
// (e=d.querySelector(t+z+'[type=module][src]'))
// ?a+`/* ${z} */'${e.src}'`
// :u),
// l.src=URL.createObjectURL(
// new Blob([l[x]],
// {type:'application/java'+t})),
// o.replaceWith(l)//inline</script>
I'm hoping that this solves the dynamic-script-appending issue (using MutationObserver), vs-code not syntax-highlighting (preserving type=module) and I imagine that using the same MutationObserver one could execute scripts once the imported ids are added to the DOM.
Please tell me if this has issues!
We can use blob and importmap to import inline scripts.
https://github.com/xitu/inline-module
<div id="app"></div>
<script type="inline-module" id="foo">
const foo = 'bar';
export {foo};
</script>
<script src="https://unpkg.com/inline-module/index.js" setup></script>
<script type="module">
import {foo} from '#foo';
app.textContent = foo;
</script>
I've been experimenting with new native ECMAScript module support that has recently been added to browsers. It's pleasant to finally be able import scripts directly and cleanly from JavaScript.
/example.html 🔍
<script type="module">
import {example} from '/example.js';
example();
</script>
/example.js
export function example() {
document.body.appendChild(document.createTextNode("hello"));
};
However, this only allows me to import modules that are defined by separate external JavaScript files. I usually prefer to inline some scripts used for the initial rendering, so their requests don't block the rest of the page. With a traditional informally-structured library, that might look like this:
/inline-traditional.html 🔍
<body>
<script>
var example = {};
example.example = function() {
document.body.appendChild(document.createTextNode("hello"));
};
</script>
<script>
example.example();
</script>
However, naively inlining modules files obviously won't work, since it would remove the filename used to identify the module to other modules. HTTP/2 server push may be the canonical way to handle this situation, but it's still not an option in all environments.
Is it possible to perform an equivalent transformation with ECMAScript modules?
Is there any way for a <script type="module"> to import a module exported by another in the same document?
I imagine this could work by allowing the script to specify a file path, and behave as though it had already been downloaded or pushed from the path.
/inline-name.html 🔍
<script type="module" name="/example.js">
export function example() {
document.body.appendChild(document.createTextNode("hello"));
};
</script>
<script type="module">
import {example} from '/example.js';
example();
</script>
Or maybe by an entirely different reference scheme, such as is used for local SVG references:
/inline-id.html 🔍
<script type="module" id="example">
export function example() {
document.body.appendChild(document.createTextNode("hello"));
};
</script>
<script type="module">
import {example} from '#example';
example();
</script>
But neither of these hypotheticals actually work, and I haven't seen an alternative which does.
Hacking Together Our Own import from '#id'
Exports/imports between inline scripts aren't natively supported, but it was a fun exercise to hack together an implementation for my documents. Code-golfed down to a small block, I use it like this:
<script type="module" data-info="https://stackoverflow.com/a/43834063">let l,e,t
='script',p=/(from\s+|import\s+)['"](#[\w\-]+)['"]/g,x='textContent',d=document,
s,o;for(o of d.querySelectorAll(t+'[type=inline-module]'))l=d.createElement(t),o
.id?l.id=o.id:0,l.type='module',l[x]=o[x].replace(p,(u,a,z)=>(e=d.querySelector(
t+z+'[type=module][src]'))?a+`/* ${z} */'${e.src}'`:u),l.src=URL.createObjectURL
(new Blob([l[x]],{type:'application/java'+t})),o.replaceWith(l)//inline</script>
<script type="inline-module" id="utils">
let n = 1;
export const log = message => {
const output = document.createElement('pre');
output.textContent = `[${n++}] ${message}`;
document.body.appendChild(output);
};
</script>
<script type="inline-module" id="dogs">
import {log} from '#utils';
log("Exporting dog names.");
export const names = ["Kayla", "Bentley", "Gilligan"];
</script>
<script type="inline-module">
import {log} from '#utils';
import {names as dogNames} from '#dogs';
log(`Imported dog names: ${dogNames.join(", ")}.`);
</script>
Instead of <script type="module">, we need to define our script elements using a custom type like <script type="inline-module">. This prevents the browser from trying to execute their contents itself, leaving them for us to handle. The script (full version below) finds all inline-module script elements in the document, and transforms them into regular script module elements with the behaviour we want.
Inline scripts can't be directly imported from each other, so we need to give the scripts importable URLs. We generate a blob: URL for each of them, containing their code, and set the src attribute to run from that URL instead of running inline. The blob: URLs acts like normal URLs from the server, so they can be imported from other modules. Each time we see a subsequent inline-module trying to import from '#example', where example is the ID of a inline-module we've transformed, we modify that import to import from the blob: URL instead. This maintains the one-time execution and reference deduplication that modules are supposed to have.
<script type="module" id="dogs" src="blob:https://example.com/9dc17f20-04ab-44cd-906e">
import {log} from /* #utils */ 'blob:https://example.com/88fd6f1e-fdf4-4920-9a3b';
log("Exporting dog names.");
export const names = ["Kayla", "Bentley", "Gilligan"];
</script>
The execution of module script elements is always deferred until after the document is parsed, so we don't need to worry about trying to support the way that traditional script elements can modify the document while it's still being parsed.
export {};
for (const original of document.querySelectorAll('script[type=inline-module]')) {
const replacement = document.createElement('script');
// Preserve the ID so the element can be selected for import.
if (original.id) {
replacement.id = original.id;
}
replacement.type = 'module';
const transformedSource = original.textContent.replace(
// Find anything that looks like an import from '#some-id'.
/(from\s+|import\s+)['"](#[\w\-]+)['"]/g,
(unmodified, action, selector) => {
// If we can find a suitable script with that id...
const refEl = document.querySelector('script[type=module][src]' + selector);
return refEl ?
// ..then update the import to use that script's src URL instead.
`${action}/* ${selector} */ '${refEl.src}'` :
unmodified;
});
// Include the updated code in the src attribute as a blob URL that can be re-imported.
replacement.src = URL.createObjectURL(
new Blob([transformedSource], {type: 'application/javascript'}));
// Insert the updated code inline, for debugging (it will be ignored).
replacement.textContent = transformedSource;
original.replaceWith(replacement);
}
Warnings: this simple implementation doesn't handle script elements added after the initial document has been parsed, or allow script elements to import from other script elements that occur after them in the document. If you have both module and inline-module script elements in a document, their relative execution order may not be correct. The source code transformation is performed using a crude regex that won't handle some edge cases such as periods in IDs.
This is possible with service workers.
Since a service worker should be installed before it will be able to process a page, this requires to have a separate page to initialize a worker to avoid chicken/egg problem - or a page can reloaded when a worker is ready.
Example
Here's a demo that is supposed to be workable in modern browsers that support native ES modules and async..await (namely Chrome):
index.html
<html>
<head>
<script>
(async () => {
try {
const swInstalled = await navigator.serviceWorker.getRegistration('./');
await navigator.serviceWorker.register('sw.js', { scope: './' })
if (!swInstalled) {
location.reload();
}
} catch (err) {
console.error('Worker not registered', err);
}
})();
</script>
</head>
<body>
World,
<script type="module" data-name="./example.js">
export function example() {
document.body.appendChild(document.createTextNode("hello"));
};
</script>
<script type="module">
import {example} from './example.js';
example();
</script>
</body>
</html>
sw.js
self.addEventListener('fetch', e => {
// parsed pages
if (/^https:\/\/run.plnkr.co\/\w+\/$/.test(e.request.url)) {
e.respondWith(parseResponse(e.request));
// module files
} else if (cachedModules.has(e.request.url)) {
const moduleBody = cachedModules.get(e.request.url);
const response = new Response(moduleBody,
{ headers: new Headers({ 'Content-Type' : 'text/javascript' }) }
);
e.respondWith(response);
} else {
e.respondWith(fetch(e.request));
}
});
const cachedModules = new Map();
async function parseResponse(request) {
const response = await fetch(request);
if (!response.body)
return response;
const html = await response.text(); // HTML response can be modified further
const moduleRegex = /<script type="module" data-name="([\w./]+)">([\s\S]*?)<\/script>/;
const moduleScripts = html.match(new RegExp(moduleRegex.source, 'g'))
.map(moduleScript => moduleScript.match(moduleRegex));
for (const [, moduleName, moduleBody] of moduleScripts) {
const moduleUrl = new URL(moduleName, request.url).href;
cachedModules.set(moduleUrl, moduleBody);
}
const parsedResponse = new Response(html, response);
return parsedResponse;
}
Script bodies are being cached (native Cache can be used as well) and returned for respective module requests.
Concerns
The approach is inferior to the application built and chunked with bundling tool like Webpack or Rollup in terms of performance, flexibility, solidity and browser support - especially if blocking concurrent requests are the primary concern.
Inline scripts increase bandwidth usage. This is naturally avoided when scripts are loaded once and cached by the browser.
Inline scripts aren't modular and contradict the concept of ECMAScript modules (unless they are generated from real modules by server-side template).
Service worker initialization should be performed on a separate page to avoid unnecessary requests.
The solution is limited to a single page and doesn't take <base> into account.
A regular expression is used for demonstration purposes only. When used like in the example above it enables the execution of arbitrary JavaScript code that is available on the page. A proven library like parse5 should be used instead (it will result in performance overhead, and still, there may be security concerns). Never use regular expressions to parse the DOM.
I don't believe that's possible.
For inline scripts you're stuck with one of the more traditional ways of modularizing code, like the namespacing you demonstrated using object literals.
With webpack you can do code splitting which you could use to grab a very minimal chunk of code on page load and then incrementally grab the rest as needed. Webpack also has the advantage of allowing you to use the module syntax (plus a ton of other ES201X improvements) in way more environments than just Chrome Canary.
I tweaked Jeremy's answer with the use of this article to prevent scripts from executing
<script data-info="https://stackoverflow.com/a/43834063">
// awsome guy on [data-info] wrote 90% of this but I added the mutation/module-type part
let l,e,t='script',p=/(from\s+|import\s+)['"](#[\w\-]+)['"]/g,x='textContent',d=document,s,o;
let evls = event => (
event.target.type === 'javascript/blocked',
event.preventDefault(),
event.target.removeEventListener( 'beforescriptexecute', evls ) )
;(new MutationObserver( mutations =>
mutations.forEach( ({ addedNodes }) =>
addedNodes.forEach( node =>
( node.nodeType === 1 && node.matches( t+'[module-type=inline]' )
&& (
node.type = 'javascript/blocked',
node.addEventListener( 'beforescriptexecute', evls ),
o = node,
l=d.createElement(t),
o.id?l.id=o.id:0,
l.type='module',
l[x]=o[x].replace(p,(u,a,z)=>
(e=d.querySelector(t+z+'[type=module][src]'))
?a+`/* ${z} */'${e.src}'`
:u),
l.src=URL.createObjectURL(
new Blob([l[x]],
{type:'application/java'+t})),
o.replaceWith(l)
)//inline
) ) )))
.observe( document.documentElement, {
childList: true,
subtree: true
} )
// for(o of d.querySelectorAll(t+'[module-type=inline]'))
// l=d.createElement(t),
// o.id?l.id=o.id:0,
// l.type='module',
// l[x]=o[x].replace(p,(u,a,z)=>
// (e=d.querySelector(t+z+'[type=module][src]'))
// ?a+`/* ${z} */'${e.src}'`
// :u),
// l.src=URL.createObjectURL(
// new Blob([l[x]],
// {type:'application/java'+t})),
// o.replaceWith(l)//inline</script>
I'm hoping that this solves the dynamic-script-appending issue (using MutationObserver), vs-code not syntax-highlighting (preserving type=module) and I imagine that using the same MutationObserver one could execute scripts once the imported ids are added to the DOM.
Please tell me if this has issues!
We can use blob and importmap to import inline scripts.
https://github.com/xitu/inline-module
<div id="app"></div>
<script type="inline-module" id="foo">
const foo = 'bar';
export {foo};
</script>
<script src="https://unpkg.com/inline-module/index.js" setup></script>
<script type="module">
import {foo} from '#foo';
app.textContent = foo;
</script>
I am writing a simple counter, and I would like to make installation of this counter very simple for users. One of the simplest counter code (for users who install it) I ever see was Google Analytics Code
So I would like to store main code in a file and user who will install my counter will need just to set websiteID like this:
<html><head><title></title></head><body>
<script type="text/javascript" src="http://counterhost.lan/tm.js">
var websiteId = 'XXXXX';
</script>
</body></html>
Here is my code:
<script type="text/javascript" src="http://counterhost.lan/tm.js">
var page = _gat.init('new');
</script>
and this is my JS file:
(function() {
var z = '_gat';
var aa = function init(data) { alert(data); alert(z);};
function na() {
return new z.aa();
}
na();
})();
I tried to understand Google Analytics javascript code but I failed to do this. Can anyone suggest how can I specify variable between tags and then read it in anonymous function which is located in a javascript file ?
Thanks.
In your example, websiteId is a global variable. So it is accessible everywhere including anonymous functions unless there is a local variable with the same name
<script> var websiteId = "something"; </script>
Later in the page or included js file...
(function() {
alert(websiteId); //this should work
})();
Can anyone suggest how can I specify variable between tags and then read it [...]
Not if your tag has both a SRC attribute and JS content.
<script type="text/javascript" src="http:/x.com/x.js"></script>
.. is different from,
<script type="text/javascript">
var x = 1;
</script>
One framework that optionally adds JS variables to SCRIPT tags is Dojo. So if you're using Dojo you can add variables to the global djConfig hash by writing,
<script type="text/javascript" src="mxclientsystem/dojo/dojo.js"
djConfig="
usePlainJson: true,
parseOnLoad: true
">
</script>
Dojo does this by running through the SCRIPT tags and evaluating the custom djConfig attribute.
This does not, however solve your problem.
You do really want two SCRIPT tags. One saying,
<script type="text/javascript">
var websiteId = '123456';
</script>
which will set a global variable websiteId and a second one,
<script type="text/javascript" src="http:/x.com/myreporter.js"></script>
which can load from anywhere and read out the websiteId variable and, I assume, report it back.
You can pass variables to an anonymous function like so:
(function(arg1, arg2, arg3) {
alert(arg1);
alert(arg2);
alert(arg3);
})("let's", "go", "redsox");
// will alert "let's", then "go", then "redsox" :)
I'm not entirely clear about what you're asking, but...
You can tag any HTML element with an id attribute, then use
document.getEntityById() to retrieve that specific element.
You can also give any HTML element user-defined attributes having names of your own choosing, then get and set them for that element within Javascript.
I think you've got a bit confused with how JS objects are called.
z is a String, '_gat'. You can't call aa() on it because a String has no member called aa. aa is a standalone function stored in a local variable. Even if you did call aa(), it doesn't return anything, so using the new operator on its results is meaningless. new can only be called on constructor-functions.
I guess you mean something like:
var _gat= function() {
// Private variable
//
var data= null;
// Object to put in window._gat
//
return {
// Set the private variable
//
init: function(d) {
data= d;
}
};
}();
Then calling _gat.init('foo') as in your second example would set the variable to website ID 'foo'. This works because the _gat object is the return {init: function() {...}} object defined inside the anonymous function, keeping a reference (a ‘closure’) on the hidden data variable.
If you specify a src attribute as part of a script element, any code within the script element tags themselves will not be executed. However, you can add this functionality with the following code. I got this technique from Crockford (I believe it was him), where he uses it in of his talks on the unrelated topic of rendering performance and asynchronously loading scripts into a page to that end.
JavaScript:
(function() {
// Using inner class example from bobince's answer
var _gat = (function() {
var data= null;
return {
init: function(d) {
console.info("Configuration data: ", d);
data = d;
}
}
})();
// Method 1: Extract configuration by ID (SEE FOOT NOTE)
var config = document.getElementById("my-counter-apps-unique-and-long-to-avoid-collision-id").innerHTML;
// Method 2: search all script tags for the script with the expected name
var scripts = document.getElementsByTagName("script");
for ( var i=0, l=scripts.length; i<l; ++i ) {
if ( scripts[i].src = "some-script.js" ) {
config = scripts[i].innerHTML;
break;
}
}
_gat.init( eval("(" +config+ ")") );
})();
HTML:
<script type="text/javascript" src="some-script.js" id="my-counter-apps-unique-and-long-to-avoid-collision-id">
{some: "foo", config: "bar", settings: 123}
</script>
Both methods have their draw backs:
Using a unique and non-colliding ID will make determining the proper script element more precise and faster; however, this is not valid HTML4/XHTML markup. In HTML5, you can define arbitrary attributes, so it wont be an issue at that time
This method is valid HTML markup; however, the simple comparison that I have shown can be easily broken if your url is subject to change (e.g.: http vs https) and a more robust comparison method may be in order
A note on eval
Both methods make use of eval. The typical mantra concerning this feature is that "eval is evil." However, that goes with say that using eval without knowing the dangers of eval is evil.
In this case, AFAIK, the data contained within the script tags is not subject to inject attack since the eval'ing script (the code shown) is executed as soon as that element is reached when parsing the HTML into the DOM. Scripts that may have been defined previously are unable to access the data contained within the counter's script tags as that node does not exist in the DOM tree at the point when they are executed.
It may be the case that a well timed setTimeout executed from a previously included script may be able to run at the time between the counter's script's inclusion and the time of the eval; however, this may or may not be the case, and if possible, may not be so consistently depending on CPU load, etc.
Moral of the story, if you're worried about it, include a non-eval'ing JSON parser and use that instead.