1.2.3 • Published 5 years ago

script-loading-toolkit v1.2.3

Weekly downloads
20
License
MIT
Repository
github
Last release
5 years ago

Script Loading Toolkit Build Status Coverage Status

A browser library for handling asynchronously loading and interacting with third party script dependencies without race conditions or render blocking. Written in TypeScript.

Installation

For EcmaScript and CommonJS module distributions install with the package manager of your choice. (Recommended)

# npm
npm install script-loading-toolkit --save
# yarn
yarn add script-loading-toolkit

or include the UMD browser distribution directly:

<script src="https://unpkg.com/script-loading-toolkit"></script>

IMPORTANT: Script Loading Toolkit requires Promises. If you wish to support older browsers that do not implement promises then you will need to pollyfill this functionality yourself. You can do so with this NPM library or with Babel.

Usage

The script loading toolkit provides three major tools (Script, BasicScript and FunctionQueue) for managing script loading. These can be used directly by newing them up and setting a script src; however their intended use is for extending or mixing into your own classes to create facades or adapters to wrap third party libraries in order decouple your code from a third party interface you do not control and may or may not yet exist in the global scope.

We recommend you use Async/Await when dealing with promises to simplify your code and reduce callback chains, however the below examples will also demonstrate Promise/then callback syntax.

Script

The Script class can be used to load any script (by setting the src attribute to a url) and has an asynchronous queueing API so you can start queueing up functions to be run once it has finished loading:

import { Script } from 'script-loading-toolkit';
// or use scriptToolkit.Script if using the unpkg browser distro.

class AcmeScript extends Script {
    constructor() {
        super();
        this.src = "http://acme.com/acmeScript.js";
    }
};

Script class can be used directly aswell by passing a valid url string to the constructor, or an object with an src attribute.

const myScript = new Script("http://acme.com/acmeScript.js");
const myScript2 = new Script({ src: "http://acme.com/acmeScript.js" });

console.log(myScript.src); // > "http://acme.com/acmeScript.js"
console.log(myScript2.src); // > "http://acme.com/acmeScript.js"

Loading

Script.load() => Promise<this>

The .load() method will return a promise that will reject if the script fails to load or resolve with the instance itself once loading is complete.

class AcmeScript extends Script {
    constructor() {
        super();
        this.src = "http://acme.com/acmeScript.js";
    }
};

const myScript = new AcmeScript();

/** Promise/then **/
myScript.load().then(() => {
    // The scipt has loaded. Do something here!
}).catch(err => {
    // Oh no it failed to load!
});

/** Async/Await **/
try {
    await acmeScript.load();
} catch(err) {
    // Oh no it failed to load!
}

Calling .load() multiple times will not cause the script to load more than once. Subsequent calls to .load() will all return the same promise or resolve immediatly if the script has already loaded.

// These will all return the same promise.
myScript.load();
await myScript.load();

// This will resolve immediatly
myScript.load();
Enabling / Disabling loading
Script.disable() => this
Script.enable() => this

You can disable a script from loading, or re enable it with the .disable() and .enable() methods.

myScript.disable();
await myScript.load(); // Console Warning > Could not load disabled script.
console.log(myScript.isLoaded) // > false

Queueing

Script.enqueue(fnc: () => T) => Promise<T>

You can queue callbacks to run once your script has loaded. When using .enqueue() a promise will be returned that will resolve with the return value of the passed function.

class AcmeScript extends Script {
    constructor() {
        super();
        this.src = "http://acme.com/acmeScript.js";
    }
};
const myScript = new AcmeScript();

/** Promise/then **/
// The enqueued function will not execute until the script has loaded.
myScript.enqueue(() => "Loaded!").then((result) => {
    // The returned value of your function
    console.log(result) // > Loaded!
});

myScript.load();

/** Async/Await **/
const result = await myScript.enqueue(() => "Loaded!");
console.log(result) // > Loaded!

If the callback function returns a promise that promise will be resolved with .enqueue.

myScript.load();
const result = await myScript.enqueue(async () => {
    await somethingAsynchronous();
    return "Loaded!"
});
console.log(result) // > Loaded!

Once the script has loaded, enqueued callbacks will be executed immediatly.

await myScript.load();
await myScript.enqueue(() => "This will execute straight away.");

The .enqueue() method is most powerful for use when extending Script to create a facade that hides the need for the rest of your code to know whether the script has loaded to begin calling it's methods.

class AcmeScript extends Script {
    constructor() {
        super();
        this.src = "http://acme.com/acmeScript.js";
    }

    foo() {
        return this.enqueue(() => window.acmeScript.foo());
    }

    bar() {
        return this.enqueue(() => window.acmeScript.bar());
    }
};

const myScript = new AcmeScript();

// Methods of AcmeScript can be called before it is loaded and they will execute once it has loaded.
myScript.foo();
myScript.bar();

Initialization

After the script has loaded, executing queued callbacks is triggered by the initialize() method. This method is not intended to be used directly, as it is called automatically after script loading finishes. If the script you have loaded requires initialization/configuration before it can be used; you can override the initialize() method and add your initialization logic there. Make sure to call super.initialize() after your initialization, in order to continue the Script lifecyle's completion.

class AcmeScript extends Script {
    constructor() {
        super();
        this.src = "http://acme.com/acmeScript.js";
    }
    
    async initialize() {
        window.acmeScript.configure({accountId: 123456});
        super.initialize(); // Make sure to call the super method after your initialization is complete!
    }
};

Dependencies

Script.addDependency(dependency: (Script | BasicScript), hasSideEffects?: boolean = false) => this

If other scripts are required for a script to function and you don't want to handle loading them separately you can add them as dependencies. By default dependencies will be loaded simultaneously with the dependant script.

class DependencyScript extends Script {
    constructor() {
        super();
        this.src = "http://acme.com/someDependency.js";
    }
};
class AcmeScript extends Script {
    constructor() {
        super();
        this.src = "http://acme.com/acmeScript.js";
    }
};

const myDependency = new DependencyScript();
const myScript = new AcmeScript();

// 'myScript' and 'myDependency' will start loading simultaneously. 'myScript' will not finsih loading until 'myDependency' has also finished.
myScript.addDependency(myDependency);
await myScript.load();
console.log(myDependency.isLoaded); // > True!

If a dependency MUST be loaded before it's dependant script (i.e loading it has side effects that must be in place for the dependant to not error), add it with the hasSideEffects argument set to true.

// 'myScript' will not begin loading until 'myDependency' has finished loading.
myScript.addDependency(myDependency, true);
await myScript.load();

Properties

The following properties are available on Script instances:

PropertyTypeDefaultDescription
.srcstring""URL of the script to load, including protocol (//, http://, https://, etc).
.htmlElementHTMLScriptElementnew HTMLScriptElement()HTML element for the script that will be added to the DOM on loading.
.isEnabledbooleantrueTrue if loading this script is enabled.
.isLoadingbooleanfalseTrue if the script is currently loading.
.isLoadedbooleanfalseTrue if the script has finished loading without error.
.isErroredbooleanfalseTrue if the script has failed to load for some reason.
.isExecutedbooleanfalseTrue if the script's callback queue has been executed.
.isInitializedbooleanfalseTrue if the script has been initialized.
.hasDependenciesbooleanfalseTrue if dependencies have been added to load with this script.

Lifecycle Methods

MethodDescription
onEnabledCalled every time after the .enable() method is called.
onDisabledCalled every time after the .disable() method is called.
onLoadingCalled the first time .load() method is called, if the script is enabled.
onLoadedCalled the first time after script loading completes.
onErroredCalled if the script fails to load (only if it was enabled).
onExecutedCalled the first time after all queued callbacks execute; triggered automatically after loading completes, as part of initialization.
onInitializedCalled the first time the .initialize() method is called, this happens automatically after loading completes.

Lifecycle methods are intended for use by overriding them when extending Script.

class AcmeScript extends Script {
    constructor() {
        super();
        this.src = "http://acme.com/acmeScript.js";
    }
    // Use lifecycle methods like this:
    onLoaded() {
        console.log('This will excecute when the script finishes loading!');
    }
};

// It is *not* recommended to override lifecycle methods directly:
const myScript = new Script();
myScript.onLoaded = () => console.log("Anti pattern!"); // Don't do this.

Direct Usage

You can use script directly without extension by creating an instance and overriding the src property.

import { Script } from 'script-loading-toolkit';
// or use scriptToolkit.Script if using the unpkg browser distro.

const acmeScript = new Script();
acmeScript.src = "http://acme.com/acmeScript.js";

acmeScript.load();

BasicScript

BasicScript is a leaner implementation of Script without the asynchronous queueing API. You can use this when you don't need queueing functionality. This is mainly inteded to give you flexibility when composing your own objects with the provided Mixin or with extension.

import { BasicScript } from 'script-loading-toolkit';

const acmeScript = new BasicScript("http://acme.com/acmeScript.js");

acmeScript.load().then(() => {
    // The scipt has loaded. Do something here!
}).catch(err => {
    // Oh no it failed to load!
});

Properties

PropertyTypeDefaultDescription
.srcstring""URL of the script to load, including protocol (//, http://, https://, etc).
.htmlElementHTMLScriptElementnew HTMLScriptElement()HTML element for the script that will be added to the DOM on loading.
.isEnabledbooleantrueTrue if loading this script is enabled.
.isLoadingbooleanfalseTrue if the script is currently loading.
.isLoadedbooleanfalseTrue if the script has finished loading without error.
.isErroredbooleanfalseTrue if the script has failed to load for some reason.
.hasDependenciesbooleanfalseTrue if dependencies have been added to load with this script.

Lifecycle Methods

MethodDescription
onEnabledCalled every time after the .enable() method is called.
onDisabledCalled every time after the .disable() method is called.
onLoadingCalled the first time .load() method is called, if the script is enabled.
onLoadedCalled the first time after script loading completes.
onErroredCalled if the script fails to load (only if it was enabled).

FunctionQueue

FunctionQueue is only the queueing functionality from Script without the script loading functionality. This can be useful for objects that might rely on a third party library being loaded, but you do not want to couple them with the logic to determine when that script should load.

import { FunctionQueue, BasicScript } from 'script-loading-toolkit';

const myQueue = new FunctionQueue;

const myVideo = {
    id: 123,
    play: () => {
        return myQueue.enqueue(() => window.acmeVideo.play(this.id));
    },
    pause: () => {
        return myQueue.enqueue(() => window.acmeVideo.pause(this.id));
    }
}

// It is safe to call this method even if window.acmeVideo doesn't exist yet.
// It wont run until we execute the queue.
myVideo.play();

const acmeScript = new BasicScript();
acmeVideo.src = "http://acme.com/acme-video-library.js";

acmeScript.load().then(() => {
    // Execute the queue because now window.acmeVideo will exist.
    myQueue.execute();
});

Properties

PropertyTypeDefaultDescription
.isExecutedbooleanfalseTrue if the script's callback queue has been executed.

Lifecycle Methods

MethodDescription
onEnabledCalled every time after the .enable() method is called.
onExecutedCalled the first time after all queued callbacks execute; triggered automatically after loading completes, as part of initialization.

Mixins

The Script Loading Toolkit includes 'Mixin' implementations of each of the above classes to allow you greater flexibility over classic inheritence. The mixin function will add all the functionality of one of the toolkit classes to the given constructor:

import { ScriptMixin } from 'script-loading-toolkit';

class AcmeSuperClass {
    someMethod() {
        console.log('hi!');
    }
}

class AcmeScript extends ScriptMixin(AcmeSuperClass) {
    constructor() {
        this.src = "http://acme.com/acme-video-library.js";
    }
}
const acmeScriptInstance = new AcmeScript();
acmeScriptInstance.someMethod(); // > hi!
acmeScriptInstance.load();
FunctionDescription
ScriptMixinAdds Script functionality.
FunctionQueueMixinAdds FunctionQueue functionality.
BasicScriptMixinAdds BasicScript functionality.
ScriptInitializerMixinThis mixin is to be used specifically on constructors/classes that implement both the FunctionQueue and BasicScript interfaces and adds the initializing functionality from Script.
1.2.3

5 years ago

1.2.2

5 years ago

1.2.1

5 years ago

1.2.0

5 years ago

1.1.0

5 years ago

1.0.1

5 years ago

1.0.0

5 years ago