简体   繁体   中英

Angular: How to lazy load google analytics script?

I'm currently using Angular 8. I wonder how to lazily load my google analytic script. I found some docs about global script lazily loaded using the script array from angular.json :

"scripts": [
  "src/global-script.js",
  { "input": "src/lazy-script.js", "lazy": true },
  { "input": "src/pre-rename-script.js", "bundleName": "renamed-script" },
],

Via https://github.com/angular/angular-cli/wiki/stories-global-scripts

But it says it is now depreciated and they refer their docs but i can't find anything about it.

GTM google tag manager slows down the paint time. So i'm looking to active GTM only once my angular app has initialy been loaded doing that.

Do you guys have some example to show me about this ?

Thank you.

You can use combination of promises and services to achieve this. Basically what you need to do is create a service which dynamically injects script tag in body and sets path to script, once path is set resolve the promise so the script's functionality can be used. Ben Nadel has complete example on his blog Below is a service which does that:

export class DelayedScriptLoader {

private delayInMilliseconds: number;
private scriptPromise: Promise<void> | null;
private urls: string[];

// I initialize the delayed script loader service.
constructor( urls: string[], delayInMilliseconds: number );
constructor( urls: string, delayInMilliseconds: number );
constructor( urls: any, delayInMilliseconds: number ) {

    this.delayInMilliseconds = delayInMilliseconds;
    this.scriptPromise = null;
    this.urls = Array.isArray( urls )
        ? urls
        : [ urls ]
    ;

}

// ---
// PUBLIC METHODS.
// ---

// I load the the underlying Script tags. Returns a Promise.
public load() : Promise<void> {

    // If we've already configured the script request, just return it. This will
    // naturally queue-up the requests until the script is resolved.
    if ( this.scriptPromise ) {

        return( this.scriptPromise );

    }

    // By using a Promise-based workflow to manage the deferred script loading,
    // requests will naturally QUEUE-UP IN-MEMORY (not a concern) until the delay has
    // passed and the remote-scripts have been loaded. In this case, we're not even
    // going to load the remote-scripts until they are requested FOR THE FIRST TIME.
    // Then, we will use they given delay, after which the in-memory queue will get
    // flushed automatically - Promises rock!!
    this.scriptPromise = this.delay( this.delayInMilliseconds )
        .then(
            () => {

                var scriptPromises = this.urls.map(
                    ( url ) => {

                        return( this.loadScript( url ) );

                    }
                );

                return( Promise.all( scriptPromises ) );

            }
        )
        .then(
            () => {

                // No-op to generate a Promise<void> from the Promise<Any[]>.

            }
        )
    ;

    return( this.scriptPromise );

}

// ---
// PRIVATE METHODS.
// ---

// I return a Promise that resolves after the given delay.
private delay( delayInMilliseconds: number ) : Promise<any> {

    var promise = new Promise(
        ( resolve ) => {

            setTimeout( resolve, delayInMilliseconds );

        }
    );

    return( promise );

}


// I inject a Script tag with the given URL into the head. Returns a Promise.
private loadScript( url: string ) : Promise<any> {

    var promise = new Promise(
        ( resolve, reject ) => {

            var commentNode = document.createComment( " Script injected via DelayedScriptLoader. " );

            var scriptNode = document.createElement( "script" );
            scriptNode.type = "text/javascript";
            scriptNode.onload = resolve;
            scriptNode.onerror = reject;
            scriptNode.src = url;

            document.head.appendChild( commentNode );
            document.head.appendChild( scriptNode );

        }
    );

    return( promise );

}

}

// Import the core angular services.
import { Injectable } from "@angular/core";

// Import the application components and services.
import { DelayedScriptLoader } from "./delayed-script-loader";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

// Since I don't have a Type Definition for this demo library, I'm just going to declare
// the interface here and then explicitly cast the global value when I reference it.
interface AnalyticsScript {
    identify( userID: UserIdentifier, traits: UserTraits ) : void;
    track( eventID: EventIdentifier, eventProperties: EventProperties ) : void;
}

export type UserIdentifier = string | number;

export interface UserTraits {
    [ key: string ]: any;
}

export type EventIdentifier = string;

export interface EventProperties {
    [ key: string ]: any;
}

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

@Injectable({
    providedIn: "root"
})
export class AnalyticsService {

    private scriptLoader: DelayedScriptLoader;

    // I initialize the analytics service.
    constructor() {

        this.scriptLoader = new DelayedScriptLoader( "./analytics-service.js", ( 10 * 1000 ) );

    }

    // ---
    // PUBLIC METHODS.
    // ---

    // I identify the user to be associated with subsequent tracking events.
    public identify( userID: UserIdentifier, traits: UserTraits ) : void {

        this.run(
            ( analytics ) => {

                analytics.identify( userID, traits );

            }
        );

    }


    // I track the given event for the previously-identified user.
    public track( eventID: EventIdentifier, eventProperties: EventProperties ) : void {

        this.run(
            ( analytics ) => {

                analytics.track( eventID, eventProperties );

            }
        );

    }

    // ---
    // PRIVATE METHODS.
    // ---

    // I return a Promise that resolves with the 3rd-party Analytics Script.
    private async getScript() : Promise<AnalyticsScript> {

        // CAUTION: For the sake of simplicity, I am not going to worry about the case in
        // which the analytics scripts fails to load. Ideally, I might create some sort
        // of "Null Object" version of the analytics API such that the rest of the code
        // can run as expected with various no-op method implementations.
        await this.scriptLoader.load();
        // NOTE: Since I don't have an installed Type for this service, I'm just casting
        // Window to ANY and then re-casting the global service that we know was just
        // injected into the document HEAD.
        return( ( window as any ).analytics as AnalyticsScript );

    }


    // I run the given callback after the remote analytics library has been loaded.
    private run( callback: ( analytics: AnalyticsScript ) => void ) : void {

        this.getScript()
            .then( callback )
            .catch(
                ( error ) => {
                    // Swallow underlying analytics error - they are not important.
                }
            )
        ;

    }

}

and below is the component which uses this service:

// Import the core angular services.
import { Component } from "@angular/core";

// Import the application components and services.
import { AnalyticsService } from "./analytics.service";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

@Component({
    selector: "my-app",
    styleUrls: [ "./app.component.less" ],
    template:
    `
        <p>
            <a (click)="doThis()">Do This</a>
            &mdash;
            <a (click)="doThat()">Do That</a>
        </p>
    `
})
export class AppComponent {

    private analyticsService: AnalyticsService;

    // I initialize the app component.
    constructor( analyticsService: AnalyticsService ) {

        this.analyticsService = analyticsService;

    }

    // ---
    // PUBLIC METHODS.
    // ---

    // I execute an action (that we're going to track).
    public doThat() : void {

        this.analyticsService.track(
            "do.that",
            {
                now: Date.now()
            }
        );

    }


    // I execute an action (that we're going to track).
    public doThis() : void {

        this.analyticsService.track(
            "do.this",
            {
                now: Date.now()
            }
        );

    }


    // I get called once after the inputs have been bound for the first time.
    public ngOnInit() : void {

        this.analyticsService.identify(
            "bennadel",
            {
                group: "admin"
            }
        );

    }

}

Better save those scripts inside the folder src\\assets and add them directly to the index.html file

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8"/>
  <title>Angular App</title>
  <base href="/"/>
  <meta name="viewport" content="width=device-width, initial-scale=1"/>
  ...
  <script src="assets/global-script.js"></script>
  <script src="assets/lazy-script.js"></script>
  <script src="assets/pre-rename-script.js"></script>
</head>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM