I'm trying to build a vanilla js single page application (using node.js and express) but I've ran into an issue with executing .js files that I just can't find any solution to, after hours of scouring the web and stackoverflow...
Below is the fairly simple structure with this problem. I have a static header for navigation in my index.html with <a> elements triggering the SPA routing and a container div for the SPA views.
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<script src='../scripts/index.js' type='module' defer></script>
</head>
<body>
<nav>
<a href='/testA' class='route'>Test A</a>
<a href='/testB' class='route'>Test B</a>
</nav>
<div id='views'></div>
</body>
</html>
Exemplary TestA.html
<!DOCTYPE html>
<html lang="en">
<head>
<script src='../scripts/TestA.js' type='module' defer></script>
</head>
<body>
<p> View: Test A </p>
</body>
</html>
TestA.js
const obj = {name: 'A', val: '000'}
console.log(obj)
index.js
const nav = document.querySelectorAll('.route')
for (const element of nav){
element.addEventListener('click', (event) => {
event.preventDefault()
history.pushState(null, '', element.href)
spaRouting()
})
}
const container = document.getElementById('views')
function spaRouting(){
const path = window.location.pathname
const html = '...' //fetch request grabbing HTML file from node server
container.innerHTML = html
const oldScripts = container.getElementsByTagName('script')
for (const oldScript of oldScripts){
const newScript = document.createElement('script')
for(const attribute of oldScript.attributes){newScript.setAttribute(attribute.name, attribute.value)}
newScript.addEventListener('load', () => console.log('script was loaded'))
oldScript.replaceWith(newScript)
}
}
window.addEventListener('popstate', spaRouting())
The routing itself works perfectly fine, as does the HTML injection. Jumping back and forth between Test A and Test B I can see the HTML content change. Replacing the scripts with newly created ones also works great and Test??.js is executing and logging to console.
However, it does so only exactly one time for each navigation element / view. If I click a view a second time the HTML content still updates successfully but no scripts are executed. I do get the "script was loaded" logged to console from the eventListener of the newly created script element but the content of the .js files does not get executed (no object logged to console).
For the life of me I can't figure out why this works but does so only once for every view and not any subsequent time.
I tried:
What I can not do is preload all script files inside index.html because in the actual project many of them reference DOM elements which don't exist until the corresponding view is loaded (because the project is coming from a multi page application structure).
I really hope someone can explain this behavior to me and knows a way how to fix it because I'm at a loss.
Okay so I actually found the reason (kind of) and more importantly, the solution!
I had the suspicion that it had something to do with the script being cached and that's why it isn't executed again after the first time. However, when I tried the "disable cache" setting in my browser's developer's tools it didn't help.
However, it IS a caching issue, just on a different level. Basically, what appears to happen is that after, let's say, TestA.js is executed, the browser remembers that file name and the next time it is invoked by a new script element it recognizes it as having been already executed earlier so it is ignored (if anyone has a deeper understanding of the mechanics underneath all this feel free to comment and elaborate on this).
The solution is to add a parameter with a unique value to the file source on each iteration, like so (using Date.now() as unique value):
function spaRouting(){
const path = window.location.pathname
const fileID = Date.now() // <==== HERE
const html = '...' //fetch request grabbing HTML file from node server
container.innerHTML = html
const oldScripts = container.getElementsByTagName('script')
for (const oldScript of oldScripts){
const newScript = document.createElement('script')
for(const attribute of oldScript.attributes){newScript.setAttribute(attribute.name, attribute.value)}
newScript.src = `${newScript.src}?fileID=${fileID}` // <==== AND HERE
newScript.addEventListener('load', () => console.log('script was loaded'))
oldScript.replaceWith(newScript)
}
}
With this it now works like a charm. :)