Vue Mastery: The smart way to add external scripts to your Vue app

Make full use of Vue's component to load external scripts asynchronously

ยท

6 min read

Prerequisites

To follow this tutorial, you will need:

  1. An IDE
  2. Some JavaScript and Vue.js knowledge
  3. Five minutes to spare

Introduction

So, there I was, hacking NASA with a simple Vue webapp (y'know, as one does), when I realized that I needed to programmatically and asynchronously load some external JavaScript code into my Vue page.

Rather than install a separate vue plugin, I decided to create a quick solution that could be re-implemented across multiple projects with ease.

Here's how I did it.

In this tutorial, we will be creating a vue component which can load any number of external scripts into our Vue app without blocking the smooth execution and rendering of our code, and emit events to notify us when each script has been downloaded.

Note:

  • Asynchronously means to execute operations concurrently, without interfering with the execution of other operations.
  • Programmatically simply means doing something with code (in this case, JavaScript) instead of a template, markup, configuration, or xml script.

Lets get started.

Creating our component

First, let's define our Vue component:

<template>
  <div></div>
</template>

<style>
    /* css goes here */
</style>

<script>
</script>

Next, lets add some code between those empty <script> tags to instantiate our component:

<script>
export default {
  name: "AddScripts",
  props: {
    scripts: {
      type: Array,
      required: true
    }
  },

  data(){
    return{
        addedScripts: []
    }
  },
  watch:{
    scripts : {
        handler(srcList){
            this.loadScripts(srcList.slice());
        },
        immediate: true,
        deep: true
    }
  },
  methods: {
    scriptIsAdded(src){
        return this.addedScripts.includes(src);
    },
    loadScripts(scripts){

    }
  }
}
</script>

There are a couple things to unpack there.

First, we gave our component a prop, which we called scripts. This will be used to receive data from our parent component. This prop is required and must be an array.

Next, we create the instance property addedScripts, which we will use to keep track of which scripts have been added to our app.

Next, we create a watcher for our scripts prop. Note the watcher has a property 'immediate', which we set to true. This ensures that the scripts handler is called as soon as our component is instantiated, in addition to every time the scripts prop is updated. Because our scripts prop is an array, 'deep' is set to true. Note that this watcher calls our loadScripts() function whenever it is run, and passes a cloned copy of its latest value as an argument.

Finally, we created two methods on our component. The scriptIsAdded() method is a simple, single-line function that takes an src string as an argument and returns a Boolean telling us if our addedScripts property includes that string.

THe loadScripts() method is where the magic happens. Let's get to it! ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ’ช

Adding external scripts to our Vue webapp

Here's our loadScripts() function:

loadScripts(scripts){
        scripts = scripts.filter(src=>!this.scriptIsAdded(src));
        Promise.allSettled(scripts.map(src => {
            return new Promise((resolve) => {
                let newScript = document.createElement('script');
                let uniqueID = "SCRIPT_"+new Date().getTime();
                newScript.setAttribute('src', src);
                newScript.setAttribute("type", "text/javascript"); 
                newScript.setAttribute("id", uniqueID);
                document.head.appendChild(newScript);
                this.addedScripts.push(src);
                resolve(newScript);
            });
        })).then((scripts)=>{
            scripts.forEach(v=> {
                let element = v.value;
                if(!element) return;
                element.onload = () => {
                    this.$emit("success", element.src);
                }
                element.onerror = () => {
                    this.$emit("error", element.src);
                }
            });
        }).catch((err) => {
            console.log("Error loading scripts:", err);
        });
    }

Let's go over that, line by line. First, we have:

scripts = scripts.filter(src=>!this.scriptIsAdded(src));

This removes any scripts duplicate scripts. Next, we have a promise:

Promise.allSettled(scripts.map(src => {
    return new Promise((resolve) => 
    // ...
    })
}))

This calls an asynchronous function for each item in our scripts array. We use Promise.allSettled to wrap all these functions and return a value when they've all been resolved.

For each element in our scripts array, we have the following code:

let newScript = document.createElement('script'); // create a new <script> element
let uniqueID = "SCRIPT_"+new Date().getTime(); // create a unique ID
newScript.setAttribute('src', src); // set the script's src
newScript.setAttribute("type", "text/javascript"); // set the script's MIME type
newScript.setAttribute("id", uniqueID); // gives the script a unique ID
document.head.appendChild(newScript); // add the new script to our page
this.addedScripts.push(src); // add the script's src to our 'addedScripts' component property
resolve(newScript); // exit our promise and return the newly created <script> element

Checking if each script has loaded

Remember, Promise.allSettled() returns an array containing a result for every item in our scripts. Each element in the array is an object containing either a 'value' or a 'reason' property depending on whether the corresponding promise was resolved or not. Click here if you do not know how promises work.

.then((scripts)=>{
    scripts.forEach(v=> {
        let element = v.value;
        if(!element) return;
        element.onload = () => {
            this.$emit("success", element.src);
        }
        element.onerror = () => {
            this.$emit("error", element.src);
        }
    });
}).catch((err) => {
    console.log("Error loading scripts:", err);
});

Next we check the result for each newly created script, and use the HTML onload and onerror event listeners to detect when the script has loaded or failed to load. We then emit a 'success' or 'error' event from our component once the listener is triggered.

Wrapping up

We're almost there. Here's the code for our entire component, all in one place:

<template>
  <div></div>
</template>

<script>
export default {
  name: "AddScripts",
  props: {
    scripts: {
      type: Array,
      required: true
    }
  },
  data(){
    return{
        addedScripts: []
    }
  },
  methods: {
    scriptIsAdded(src){
        return this.addedScripts.includes(src);
    },
    loadScripts(scripts){
        scripts = scripts.filter(src=>!this.scriptIsAdded(src));
        // ^ removes scripts that have already been added
        console.log("New scripts: %o", scripts);
        Promise.allSettled(scripts.map(src => {
            return new Promise((resolve) => {
                let newScript = document.createElement('script'); // create a new <script> element
                let uniqueID = "SCRIPT_"+new Date().getTime(); // create a unique ID
                newScript.setAttribute('src', src); // set the script's src
                newScript.setAttribute("type", "text/javascript"); 
                newScript.setAttribute("id", uniqueID); // add unique ID
                document.head.appendChild(newScript); // add the new script to the page
                this.addedScripts.push(src);
                resolve(newScript);
            });
        })).then((scripts)=>{
            scripts.forEach(v=> {
                let element = v.value;
                if(!element) return;
                element.onload = () => {
                    this.$emit("success", element.src);
                }
                element.onerror = () => {
                    this.$emit("error", element.src);
                }
            });
        }).catch((err) => {
            console.log("Error loading scripts:", err);
        });
    }
  },
  watch:{
    scripts : {
        handler(srcList){
            this.loadScripts(srcList.slice());
        },
        immediate: true,
        deep: true
    }
  }
}
</script>

<style>
</style>

You can go ahead and save that as AddScripts.vue. ๐Ÿ˜ Finally, lets create a basic Vue app to illustrate how we can import and use our component in a page.

<template>
  <div>
    <h1>Hello World!</h1>
    <add-scripts
      :scripts="scripts" 
      v-on:success="scriptSuccess" 
      v-on:error="scriptError"
    ></add-scripts>
  </div>
</template>

<script>
import AddScripts from '@/components/AddScripts.vue';

export default {
  name: "Home",
  components: {
    'add-scripts': AddScripts
  },
  data(){
    return{
      scripts : [
        "https://code.jquery.com/jquery-3.6.3.min.js", // jquery
        "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js", // Lodash
        "https://www.my-cool-site.com/fancy-script.js", // your awesome site
      ],
    }
  },
  methods:{
    scriptSuccess(src){
      console.log(`Loaded Script: ${src}`);
      // do some stuff
    },
    scriptError(src){
      console.log(`Script Error! ${src}`);
      // do some stuff
    }
  }
};
</script>

<style>
    /* css goes here */
</style>

Aaaaand we're done! ๐Ÿคฉ

Here are some things you can do to improve this component:

  • Can you modify the component so that it can load CSS files as well as JavaScript files?

  • How about adding a 'loading' event to the component so that we can show a loading indicator while the scripts are loading?

  • Can you add a 'timeout' property to the component so that it can automatically cancel loading a script if it takes too long?

  • Some scripts may depend on other scripts. Can you modify the component so that it can load scripts in the correct order? Tip: you can use promise chaining to do this.

  • Some scripts expose a global variable that can be used to detect when the script has loaded. Can you modify the component so that it can detect when a script has loaded using this method?

ย