admin管理员组

文章数量:1417104

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:

  • appending the new script tag and then removing the old one instead of using replaceWith/replaceChild
  • creating the new script without copying attributes from the old script but instead defining them manually
  • giving each script tag a unique ID so that every time a script tag is added it has a new ID
  • appending the new script to head or body of index.html instead of the container div and then removing the (inactive) old script inside the container

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.

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:

  • appending the new script tag and then removing the old one instead of using replaceWith/replaceChild
  • creating the new script without copying attributes from the old script but instead defining them manually
  • giving each script tag a unique ID so that every time a script tag is added it has a new ID
  • appending the new script to head or body of index.html instead of the container div and then removing the (inactive) old script inside the container

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.

Share Improve this question edited Feb 3 at 8:50 DarkBee 15.5k8 gold badges72 silver badges118 bronze badges asked Feb 3 at 0:29 Unfrosted9025Unfrosted9025 11 silver badge4 bronze badges 5
  • 4 Are you sure about the window.addEventListener('popstate', spaRouting())? Shouldn't that be window.addEventListener('popstate', spaRouting)? – Kostas Minaidis Commented Feb 3 at 0:44
  • I had both and both work. – Unfrosted9025 Commented Feb 3 at 6:14
  • 4 @Unfrosted9025 no, both can't work. See addEventListener calls the function without me even asking it to - by appending () you are immediately calling the function, not attaching it as an event listener. Compare with () vs without () – VLAZ Commented Feb 3 at 7:44
  • I see, thank you for pointing that out. Originally I had it without the parentheses and it worked as expected, so maybe after adding them I just didn't notice that anything changed. Afterall, the popstate behavior is not what I am focused on right now but I appreciate your clarification and will revert it back to no parentheses. – Unfrosted9025 Commented Feb 3 at 9:10
  • Why do you want to load and execute the whole script again? Export a function, call that as many times as you want. – Bergi Commented Feb 4 at 0:27
Add a comment  | 

1 Answer 1

Reset to default -1

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. :)

本文标签: javascriptInserted scripts execute only onceStack Overflow