Skip to content

External script doesn't always load when added to Nuxt page via "head()" #5052

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
markplewis opened this issue Feb 17, 2019 · 26 comments
Closed

Comments

@markplewis
Copy link

markplewis commented Feb 17, 2019

Version

v2.4.3

Reproduction link

https://github.com/markplewis/nuxt-external-resource-loading-issue

Steps to reproduce

Clone my demo repo and then do the following:

Case 1

  1. Launch the app via npm run dev and open it up in your browser.
  2. Click the "Test 1" link.
  3. The following error will appear in your console:
TypeError: window.jQuery is not a function
  at VueComponent.mounted (test-1.js:34)
  1. Refresh the page.
  2. Everything will load correctly.
  3. Click the "Home" link.
  4. Click the "Test 1" link.
  5. Everything will load correctly.
  6. Click the "Home" link.
  7. Refresh the page.
  8. Click the "Test 1" link.
  9. The following error will appear in your console:
TypeError: window.jQuery is not a function
  at VueComponent.mounted (test-1.js:34)
  1. Refresh the page.
  2. Everything will load correctly.

Case 2

  1. Launch the app via npm run dev and open it up in your browser.
  2. Click the "Test 2" link.
  3. The "(CDN script has loaded)" message will not be appended to the <h1> element.
  4. Refresh the page.
  5. The "(CDN script has loaded)" message will appear inside the <h1>.
  6. Click the "Home" link.
  7. Click the "Test 2" link.
  8. The "(CDN script has loaded)" message will appear inside the <h1>.
  9. Click the "Home" link.
  10. Refresh the page.
  11. Click the "Test 2" link.
  12. The "(CDN script has loaded)" message will not be appended to the <h1> element.
  13. Refresh the page.
  14. The "(CDN script has loaded)" message will appear inside the <h1>.

What is expected ?

I'm trying to load an external script into a Nuxt page via the page's head:

head() {
  return {
    script: [{
      src: "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.slim.min.js"
    }]
  };
}

I expect the script to load and be available every time the page loads, regardless of whether it was served directly from the server or rendered on the client-side.

What is actually happening?

Unfortunately, the external script doesn't seem to load when the page is server-rendered - it only seems to work when the page is client-rendered. I discovered the following issues, but neither of them have helped me solve this problem:

In that first issue, manniL said the following:

"Loading external scripts through the head function works fine for me. I'd suggest to do this on a layout basis anyway. For pages there is the caveat that you can't ensure the readiness easily."

What does "you can't ensure the readiness easily" mean? I'd prefer not to move this script into my layout because it will only be needed for one particular page, and I don't want to force users to download unnecessary resources.

I can avoid errors (see test case 1, above) by wrapping my calls to window.jQuery, like this:

mounted() {
  if (!process.server && window.jQuery) {
    ...  
  }
}

But this leaves me with the problem that I described in test case 2 (see above).

This bug report is available on Nuxt community (#c8677)
@ghost ghost added the cmty:bug-report label Feb 17, 2019
@stale
Copy link

stale bot commented Mar 10, 2019

Thanks for your contribution to Nuxt.js!
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.
If you would like this issue to remain open:

  1. Verify that you can still reproduce the issue in the latest version of nuxt-edge
  2. Comment the steps to reproduce it

Issues that are labeled as 🕐Pending will not be automatically marked as stale.

@stale stale bot added the stale label Mar 10, 2019
@stale stale bot closed this as completed Mar 17, 2019
@chanmathew
Copy link

I also faced this issue trying to load the Stripe API (v3) in a page, often it'll say Stripe library isn't loaded.

@markplewis
Copy link
Author

Apologies for not replying in time to prevent the bot from closing this issue. I just updated my test repo to Nuxt 2.6.1 and I'm still experiencing the problems described above.

@TheAlexLichter TheAlexLichter reopened this Apr 6, 2019
@stale stale bot removed the stale label Apr 6, 2019
@axdyng
Copy link

axdyng commented Apr 24, 2019

I'm having similar issue when trying to load Youtube API on one particular page.

I've also tried using npm vue-script2 to have more control over external scripts loading.

on mounted() of the page

VueScript2.load('https://www.youtube.com/iframe_api').then(() => {
    console.log(new YT.Player('ytplayer')); (YT is the global object inserted from the API)
});

getting the below error
Uncaught (in promise) TypeError: YT.Player is not a constructor

It could technically be solved if I load the script across all pages. However, I don't want user to download unnecessarily.

@avks
Copy link

avks commented Apr 24, 2019

Does anyone have a solution to this problem?

@stale
Copy link

stale bot commented May 15, 2019

Thanks for your contribution to Nuxt.js!
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.
If you would like this issue to remain open:

  1. Verify that you can still reproduce the issue in the latest version of nuxt-edge
  2. Comment the steps to reproduce it

Issues that are labeled as pending will not be automatically marked as stale.

@stale stale bot added the stale label May 15, 2019
@stale stale bot closed this as completed May 22, 2019
@DidelotK
Copy link

DidelotK commented Jun 2, 2019

I have the same problem with Stripe and Google maps :p, anyone have a solution to this problem ?
Is it due to nuxt specific version ?

Nuxt version: 2.6.2

Example to reproduce: pages/toto.vue

     script: [
        { src: 'https://js.stripe.com/v3/', defer: true },
        {
          src: `https://maps.googleapis.com/maps/api/js?key=${googleApiKey}&libraries=places`
        }
      ],

@avks
Copy link

avks commented Jun 3, 2019

@DidelotK All versions are affected, AFAIK.

@rchl
Copy link

rchl commented Jun 3, 2019

@markplewis and others: It all works as expected and has to do with how html/JS works, not Nuxt.

In Case 1, you are navigating in SPA fashion to another page and that adds a script and you expect it to be loaded in mounted hook already. That's false assumption as scripts appended dynamically don't load in sync fashion. You have to listen to load event to know when they have loaded and only then you can start using code that it defines.

In Case 2 is a slight variation but pretty much same reason for the behavior. It works on reloading the page as then you'll get the script appended already on the server so it will load synchronously and you'll have jQuery loaded by the time mounted is fired.

Basically you have to understand the difference between script added programatically (through DOM) and script that is in HTML of the page already. Former will load asynchronously while latter will load synchronously.

So to make this work consistently, rather than using head function, you need to use something like VueScript2 to load script dynamically and wait for it to load. For jQuery this is enough. For other APIs like YouTube you might also need to wait for the API code to tell you that it has loaded because it might have some async initialisation routine that has to run even after script has loaded (refer to APIs documentation how to handle that).

@markplewis
Copy link
Author

Thanks @rchl. This is the simplest example that I could come up with and it seems to be working:

export default {
  mounted() {
    if (!process.server && !window.jQuery) {
      const script = document.createElement("script");
      script.onload = this.onScriptLoaded;
      script.type = "text/javascript";
      script.src = "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.slim.min.js";
      document.head.appendChild(script);
    } else {
      this.onScriptLoaded();
    }
  },
  methods: {
    onScriptLoaded(event = null) {
      if (event) {
        console.log("Was added");
      } else {
        console.log("Already existed");
      }
      console.log(window.jQuery);
      window.jQuery("h1").append(` <span>(CDN script has loaded)</span>`);
    }
  }
}

@rchl
Copy link

rchl commented Jun 4, 2019

Looks good to me. Something like VueScript2 would abstract that and make it look a bit nicer IMO but this is nice, no-lib solution.

BTW. mounted is only executed on the client so check for !process.server is not needed.

@hung5s
Copy link

hung5s commented Dec 23, 2019

I created this utility function to load external JS file as Nuxt page's head property doesn't work. In my case, It only adds <script> to if the page is requested directly, not as a route-view component.

loadJsFile(url, id, onLoadedCallback, defer, async) {
    if (process.browser) {
      let test = document.getElementById(id);
      if (test) return onLoadedCallback();
      else {
        const script = document.createElement("script")
        script.src = url
        script.id = id
        script.onload = onLoadedCallback
        script.type = "text/javascript"
        script.defer = defer == true || defer == undefined ? true : false
        script.async = async == true || defer == undefined ? true : false
        document.head.appendChild(script)
      }
    }
  }

@TheAlexLichter
Copy link
Member

Thanks to vue-meta, this should be way easier now. I've wrote a bit about my preferred way in this article.

@oceangravity
Copy link

This solve my issue: https://vue-meta.nuxtjs.org/api/#callback

You only need to ensure to add vmid and set the callback

{
  metaInfo() {
    return {
      script: [
        {
          vmid: 'extscript',
          src: '/my-external-script.js',
          callback: () => (this.externalLoaded = true)
        },
        {
          skip: !this.externalLoaded,
          innerHTML: `
            /* this is only added once external script has been loaded */
            /* and e.g. window.$externalVar exists */
          `
        }
      ]
    }
  },
  data() {
    return {
      externalLoaded: false
    }
  }
}

@weleo
Copy link

weleo commented Jan 11, 2021

This solve my issue: https://vue-meta.nuxtjs.org/api/#callback

You only need to ensure to add vmid and set the callback

{
  metaInfo() {
    return {
      script: [
        {
          vmid: 'extscript',
          src: '/my-external-script.js',
          callback: () => (this.externalLoaded = true)
        },
        {
          skip: !this.externalLoaded,
          innerHTML: `
            /* this is only added once external script has been loaded */
            /* and e.g. window.$externalVar exists */
          `
        }
      ]
    }
  },
  data() {
    return {
      externalLoaded: false
    }
  }
}

This works for me, but not when loading the page for the first time. It works only when navigating to the page.

@petkoneo
Copy link

petkoneo commented Mar 9, 2021

I know this thread is closed, but I may have found a new solution. I just wrote a script tag into template of the specific page body (anyway for external links like hubspot, it needs to come after the div tag of element). So if you have a specific page where the external embeded code should be seen only just add it into the template and nuxt will make sure to load it after the HTML element is rendered (with use of v-if for example). Works only in NUXT for me.

@bchen1029
Copy link

This solve my issue: https://vue-meta.nuxtjs.org/api/#callback
You only need to ensure to add vmid and set the callback

{
  metaInfo() {
    return {
      script: [
        {
          vmid: 'extscript',
          src: '/my-external-script.js',
          callback: () => (this.externalLoaded = true)
        },
        {
          skip: !this.externalLoaded,
          innerHTML: `
            /* this is only added once external script has been loaded */
            /* and e.g. window.$externalVar exists */
          `
        }
      ]
    }
  },
  data() {
    return {
      externalLoaded: false
    }
  }
}

This works for me, but not when loading the page for the first time. It works only when navigating to the page.

replace vmid with hid might solve the problem

@rchl
Copy link

rchl commented Apr 14, 2021

Nuxt uses vmid, not hid. If you are gonna rename it to hid it will likely just act like neither vmid or hid are set, which has its own implications.

EDIT: It's the opposite.

@bchen1029
Copy link

bchen1029 commented Apr 14, 2021

Nuxt uses vmid, not hid. If you are gonna rename it to hid it will likely just act like neither vmid or hid are set, which has its own implications.

but using vmid the script fire callback only when navigating to the page, refresh the page won't fire callback. Is this a bug need to be fix? Did I do something wrong ?

@rchl
Copy link

rchl commented Apr 14, 2021

If you reload the page then the script loads synchronously so it's already loaded.

Now I'm not sure if that's a bug in VueMeta or expected behavior but I guess you can condition the logic to set externalLoaded to true if process.server.

Removing vmid means that your script will load twice which, depending on the script, can cause issues.

@bchen1029
Copy link

bchen1029 commented Apr 14, 2021

@rchl reload the page then the script loads synchronously so it's already loaded. How does this happen?
when I reload the page, nuxt will rerun on server and client side, why I will get script appended already on the server ?

And set externalLoaded to true if process.server meaning to set the value directly in asyncData ?
If so how do I confirm the external script is loaded, rather than using callback. Is it VueMeta expected behavior ?

really appreciate your answer

@bchen1029
Copy link

https://github.com/nuxt/nuxt.js/blob/dec8f99fc3ffbdcdf731b671b40ef3fd199e7b6b/packages/vue-renderer/src/renderers/spa.js
in line 22.
I found that, nuxt use hid as VueMeta's tagIDKeyName rather than vmid

https://vue-meta.nuxtjs.org/api/#callback
The doc shows that when using callback vmid required on SSR, vmid represent VueMeta's tagIDKeyName.
So, we should use hid rather than vmid as a attribute of script object.

In case I misunderstood something, please tell me where I got wrong. really appreciate that.

@rchl
Copy link

rchl commented Apr 14, 2021

Ops. Sorry, I got totally confused. Yes, Nuxt used hid, not vmid.

wilsonw13 added a commit to staten-island-tech/fullstack-frontend-4-plus-1-frontend that referenced this issue Jan 27, 2022
@broox
Copy link

broox commented Feb 4, 2022

i'm attempting to use VueMeta to inject an external library into a component and am experiencing an issue where, in some cases, while navigating between pages, the script.callback() function is not called. is this expected if the JS is cached? or should this callback be fired no matter what?

export default {
  head () {
    return {
      script: [
        {
          hid: 'mapbox',
          src: 'https://api.mapbox.com/mapbox-gl-js/v2.6.1/mapbox-gl.js',
          defer: true,
          callback: () => { console.log('Map script loaded'); this.render() }
        }
      ]
    }
  },
  ...
}

it seems to only happen when navigating from another "SPA mode" page that has this component on it... but not in all cases.

@jonathan-ingram
Copy link

i'm attempting to use VueMeta to inject an external library into a component and am experiencing an issue where, in some cases, while navigating between pages, the script.callback() function is not called. is this expected if the JS is cached? or should this callback be fired no matter what?

export default {
  head () {
    return {
      script: [
        {
          hid: 'mapbox',
          src: 'https://api.mapbox.com/mapbox-gl-js/v2.6.1/mapbox-gl.js',
          defer: true,
          callback: () => { console.log('Map script loaded'); this.render() }
        }
      ]
    }
  },
  ...
}

it seems to only happen when navigating from another "SPA mode" page that has this component on it... but not in all cases.

Hello @broox Did you ever locate a solution to this? I only ask because I'm experiencing this random behaviour too. At first I could only recreate this bug on Safari, but have now concluded it's browser-agnostic.

For my environment it seems to only occur once in roughly 50 page loads. And when it does occur it's always when transitioning between two pages who both refer to the same 3rd-party JavaScript file. And it's the same outcome that you're experiencing (in that the callback function never fires).

Currently using Nuxt v2.15.8

@simoroma
Copy link

simoroma commented Apr 21, 2023

I still have problems with this with Nuxt 3.4.3

My external scripts are written in nuxt.config.ts though I only need them on a particular route, say /page. If I load /page directly the scripts do not load. If I load / and then browse to /page they are loaded correctly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests