Come caricare gli script esterni in modo dinamico in angolare?


151

Ho questo modulo che componentizza la libreria esterna insieme a una logica aggiuntiva senza aggiungere il <script>tag direttamente in index.html:

import 'http://external.com/path/file.js'
//import '../js/file.js'

@Component({
    selector: 'my-app',
    template: `
        <script src="http://iknow.com/this/does/not/work/either/file.js"></script>
        <div>Template</div>`
})
export class MyAppComponent {...}

Ho notato che le importspecifiche di ES6 sono statiche e risolte durante la traspilazione di TypeScript piuttosto che in fase di esecuzione.

Ad ogni modo per renderlo configurabile in modo che il file.js verrà caricato dalla CDN o dalla cartella locale? Come dire ad Angular 2 di caricare uno script in modo dinamico?


Risposte:


61

Se stai usando system.js, puoi usare System.import()in fase di esecuzione:

export class MyAppComponent {
  constructor(){
    System.import('path/to/your/module').then(refToLoadedModule => {
      refToLoadedModule.someFunction();
    }
  );
}

Se stai utilizzando il webpack, puoi sfruttare appieno il suo solido supporto di suddivisione del codice con require.ensure:

export class MyAppComponent {
  constructor() {
     require.ensure(['path/to/your/module'], require => {
        let yourModule = require('path/to/your/module');
        yourModule.someFunction();
     }); 
  }
}

1
Sembra che debba usare il caricatore di moduli specificamente nel componente. Bene, va bene dato che Systemè il futuro del caricatore, penso.
CallMeLaNN,

6
TypeScript Plugin for Sublime Textnon è soddisfatto Systemdel codice TypeScript: Cannot find name 'System'ma nessun errore durante la traspilazione e l'esecuzione. Entrambi Angular2e il Systemfile di script sono già stati aggiunti index.html. In ogni caso per importl' Systeme rendere il plug-felice?
CallMeLaNN,

1
ma cosa succede se non si utilizza system.js!
Murhaf Sousli,

2
@drewmoore Non ricordo perché l'ho detto, require()/ importdovrei funzionare bene come risposta ora, +1
Murhaf Sousli

9
Da quello che posso dire queste opzioni non funzionano per gli script su Internet, solo per i file locali. Questo non sembra rispondere alla domanda originale come chiesto.
WillyC,

140

È possibile utilizzare la seguente tecnica per caricare dinamicamente script e librerie JS su richiesta nel progetto Angular.

script.store.ts conterrà il percorso dello script localmente o su un server remoto e un nome che verrà utilizzato per caricare lo script in modo dinamico

 interface Scripts {
    name: string;
    src: string;
}  
export const ScriptStore: Scripts[] = [
    {name: 'filepicker', src: 'https://api.filestackapi.com/filestack.js'},
    {name: 'rangeSlider', src: '../../../assets/js/ion.rangeSlider.min.js'}
];

script.service.ts è un servizio iniettabile che gestirà il caricamento dello script, copiandolo script.service.tscosì com'è

import {Injectable} from "@angular/core";
import {ScriptStore} from "./script.store";

declare var document: any;

@Injectable()
export class ScriptService {

private scripts: any = {};

constructor() {
    ScriptStore.forEach((script: any) => {
        this.scripts[script.name] = {
            loaded: false,
            src: script.src
        };
    });
}

load(...scripts: string[]) {
    var promises: any[] = [];
    scripts.forEach((script) => promises.push(this.loadScript(script)));
    return Promise.all(promises);
}

loadScript(name: string) {
    return new Promise((resolve, reject) => {
        //resolve if already loaded
        if (this.scripts[name].loaded) {
            resolve({script: name, loaded: true, status: 'Already Loaded'});
        }
        else {
            //load script
            let script = document.createElement('script');
            script.type = 'text/javascript';
            script.src = this.scripts[name].src;
            if (script.readyState) {  //IE
                script.onreadystatechange = () => {
                    if (script.readyState === "loaded" || script.readyState === "complete") {
                        script.onreadystatechange = null;
                        this.scripts[name].loaded = true;
                        resolve({script: name, loaded: true, status: 'Loaded'});
                    }
                };
            } else {  //Others
                script.onload = () => {
                    this.scripts[name].loaded = true;
                    resolve({script: name, loaded: true, status: 'Loaded'});
                };
            }
            script.onerror = (error: any) => resolve({script: name, loaded: false, status: 'Loaded'});
            document.getElementsByTagName('head')[0].appendChild(script);
        }
    });
}

}

Iniettalo ScriptServiceovunque ne hai bisogno e carica js libs in questo modo

this.script.load('filepicker', 'rangeSlider').then(data => {
    console.log('script loaded ', data);
}).catch(error => console.log(error));

7
Funziona alla grande. Elimina oltre 1 MB dalla dimensione del caricamento iniziale: ho molte librerie!
Our_Benefactors

2
Sembra che abbia parlato troppo presto. Questa soluzione NON funziona in iOS Safari.
Our_Benefactors

dopo aver caricato lo script controlla se è in aggiunta al capo della tua dom
Rahul Kumar,

Forse non sto iniettando correttamente, ma sto ricevendothis.script.load is not a function
Towelie,

Grazie. Funziona, ma sto ricevendo il mio script (un'animazione di sfondo) caricato in ogni altro componente. Voglio che sia caricato solo sul mio primo componente (è una pagina di accesso). Cosa posso fare per raggiungerlo? Grazie
Joel Azevedo il

47

Questo potrebbe funzionare. Questo codice aggiunge dinamicamente il <script>tag al headfile html sul pulsante selezionato.

const url = 'http://iknow.com/this/does/not/work/either/file.js';

export class MyAppComponent {
    loadAPI: Promise<any>;

    public buttonClicked() {
        this.loadAPI = new Promise((resolve) => {
            console.log('resolving promise...');
            this.loadScript();
        });
    }

    public loadScript() {
        console.log('preparing to load...')
        let node = document.createElement('script');
        node.src = url;
        node.type = 'text/javascript';
        node.async = true;
        node.charset = 'utf-8';
        document.getElementsByTagName('head')[0].appendChild(node);
    }
}

1
Grazie uomo ha lavorato per me. Ora puoi caricarlo sul caricamento della pagina anche usando il metodo ngonit. Quindi non è richiesto il clic sul pulsante.
Niraj,

Sto usando lo stesso modo .. ma non ha funzionato per me. puoi per favore aiutarmi per lo stesso ?? this.loadJs ("percorso javascriptfile"); public loadjs (file) {let node = document.createElement ('script'); node.src = file; node.type = 'text / javascript'; node.async = true; node.charset = 'utf-8'; document.getElementsByTagName ( 'testa') [0] .appendChild (nodo); }
Mitesh Shah,

Stai cercando di farlo al clic o al caricamento della pagina? Potresti dirci di più sul risultato o sull'errore?
ng-darren,

ciao, ho provato a implementarlo e funzionando bene ma new Promisenon sono stato risolto. Puoi dirmi per Promisedovrei importare qualcosa?
user3145373 ツ

Ciao, non ho importato nulla di speciale quando uso Promise.
ng-darren,

23

Ho modificato la risposta di @rahul kumars, in modo che utilizzi invece Observables:

import { Injectable } from "@angular/core";
import { Observable } from "rxjs/Observable";
import { Observer } from "rxjs/Observer";

@Injectable()
export class ScriptLoaderService {
    private scripts: ScriptModel[] = [];

    public load(script: ScriptModel): Observable<ScriptModel> {
        return new Observable<ScriptModel>((observer: Observer<ScriptModel>) => {
            var existingScript = this.scripts.find(s => s.name == script.name);

            // Complete if already loaded
            if (existingScript && existingScript.loaded) {
                observer.next(existingScript);
                observer.complete();
            }
            else {
                // Add the script
                this.scripts = [...this.scripts, script];

                // Load the script
                let scriptElement = document.createElement("script");
                scriptElement.type = "text/javascript";
                scriptElement.src = script.src;

                scriptElement.onload = () => {
                    script.loaded = true;
                    observer.next(script);
                    observer.complete();
                };

                scriptElement.onerror = (error: any) => {
                    observer.error("Couldn't load script " + script.src);
                };

                document.getElementsByTagName('body')[0].appendChild(scriptElement);
            }
        });
    }
}

export interface ScriptModel {
    name: string,
    src: string,
    loaded: boolean
}

1
Si dimentica di impostare la asynca true.
Will Huang,

Ho una domanda, quando aggiungiamo lo script e navighiamo tra le rotte, diciamo che ho caricato lo script su home e navigo su e sono tornato a casa, c'è un modo per ripristinare lo script corrente? Vuoi rimuovere uno precedentemente caricato e caricarlo di nuovo? Grazie mille
d123546

@ d123546 Sì, se lo desideri è possibile. Ma dovresti scrivere della logica che rimuove il tag dello script. Dai un'occhiata qui: stackoverflow.com/questions/14708189/…
Joel

Grazie Joel, ma c'è un modo per rimuovere in qualche modo le funzioni inizializzate dallo script? Attualmente sto affrontando un problema nel mio progetto angular4, perché sto caricando lo script che inizializza il dispositivo di scorrimento jquery, quindi quando il primo percorso lo carica visualizzato correttamente, ma poi quando navigo su un altro percorso e torno al mio percorso, lo script viene caricato due volte e cursore non si sta più caricando :( Potresti consigliarmi su questo per favore
d123546

1
C'è un errore nella linea private scripts: {ScriptModel}[] = [];. Dovrebbe essereprivate scripts: ScriptModel[] = [];
Chris Haines il

20

Un'altra opzione sarebbe quella di utilizzare il scriptjspacchetto per quella materia che

consente di caricare risorse di script su richiesta da qualsiasi URL

Esempio

Installa il pacchetto:

npm i scriptjs

e digitare le definizioni perscriptjs :

npm install --save @types/scriptjs

Quindi $script.get()metodo di importazione :

import { get } from 'scriptjs';

e infine carica la risorsa script, nel nostro caso la libreria di Google Maps:

export class AppComponent implements OnInit {
  ngOnInit() {
    get("https://maps.googleapis.com/maps/api/js?key=", () => {
        //Google Maps library has been loaded...
    });
  }
}

dimostrazione


Grazie per la condivisione, ma come aggiungere proprietà aggiuntive con url come il differimento asincrono ?? Potete guidarmi come caricare questo script usando la libreria sopra? <script type = 'text / javascript' src = ' bing.com/api/maps/mapcontrol?branch=release ' async defer > </script>
Pushkar Rathod

Dato che la riga dell'oggetto è la stessa, sto solo saltando la risposta qui
Pushkar Rathod

Fantastico, ma invece di npm install --save @ types / scriptjs, dovrebbe essere npm install --save-dev @ types / scriptjs (o solo npm i -D @ types / scriptjs)
Youness Houdass

18

Puoi caricare più script in modo dinamico come questo nel tuo component.tsfile:

 loadScripts() {
    const dynamicScripts = [
     'https://platform.twitter.com/widgets.js',
     '../../../assets/js/dummyjs.min.js'
    ];
    for (let i = 0; i < dynamicScripts.length; i++) {
      const node = document.createElement('script');
      node.src = dynamicScripts[i];
      node.type = 'text/javascript';
      node.async = false;
      node.charset = 'utf-8';
      document.getElementsByTagName('head')[0].appendChild(node);
    }
  }

e chiama questo metodo all'interno del costruttore,

constructor() {
    this.loadScripts();
}

Nota: per caricare più script in modo dinamico, aggiungerli a dynamicScripts all'array.


3
Devo dire che sembra semplice ma funziona bene, molto facile da implementare :-)
Pierre

3
Il problema con questo tipo di iniezione di script è che poiché Angular è una SPA, questo script rimane sostanzialmente nella cache del browser anche dopo aver rimosso il tag script dal DOM.
j4rey,

1
Risposta fantastica! Mi ha salvato la giornata, funziona con Angular 6 e 7. Testato!
Nadeeshan Herath,

Per inviare dati a js esterni e ottenere dati da js esterni, chiama questa funzione su app.component.ts e usa declare var d3Sankey: qualsiasi in qualsiasi componente del modulo a livello di funzionalità. Sta funzionando.
gnganpath

5

Ho fatto questo frammento di codice con il nuovo renderer api

 constructor(private renderer: Renderer2){}

 addJsToElement(src: string): HTMLScriptElement {
    const script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = src;
    this.renderer.appendChild(document.body, script);
    return script;
  }

E poi chiamalo così

this.addJsToElement('https://widgets.skyscanner.net/widget-server/js/loader.js').onload = () => {
        console.log('SkyScanner Tag loaded');
} 

StackBlitz


4

Ho un buon modo per caricare gli script in modo dinamico! Ora uso ng6, echarts4 (> 700Kb), ngx-echarts3 nel mio progetto. quando li utilizzo dai documenti di ngx-echarts, ho bisogno di importare echarts in angular.json: "scripts": ["./ node_modules / echarts / dist / echarts.min.js"] quindi nel modulo di login, pagina durante il caricamento degli script .js, questo è un file di grandi dimensioni! Non lo voglio

Quindi, penso che angolare carichi ogni modulo come un file, posso inserire un resolver del router per precaricare js, quindi iniziare il caricamento del modulo!

// PreloadScriptResolver.service.js

/**动态加载js的服务 */
@Injectable({
  providedIn: 'root'
})
export class PreloadScriptResolver implements Resolve<IPreloadScriptResult[]> {
  // Here import all dynamically js file
  private scripts: any = {
    echarts: { loaded: false, src: "assets/lib/echarts.min.js" }
  };
  constructor() { }
  load(...scripts: string[]) {
    const promises = scripts.map(script => this.loadScript(script));
    return Promise.all(promises);
  }
  loadScript(name: string): Promise<IPreloadScriptResult> {
    return new Promise((resolve, reject) => {
      if (this.scripts[name].loaded) {
        resolve({ script: name, loaded: true, status: 'Already Loaded' });
      } else {
        const script = document.createElement('script');
        script.type = 'text/javascript';
        script.src = this.scripts[name].src;
        script.onload = () => {
          this.scripts[name].loaded = true;
          resolve({ script: name, loaded: true, status: 'Loaded' });
        };
        script.onerror = (error: any) => reject({ script: name, loaded: false, status: 'Loaded Error:' + error.toString() });
        document.head.appendChild(script);
      }
    });
  }

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<IPreloadScriptResult[]> {
   return this.load(...route.routeConfig.data.preloadScripts);
  }
}

Quindi in submodule-routing.module.ts , importa questo PreloadScriptResolver:

const routes: Routes = [
  {
    path: "",
    component: DashboardComponent,
    canActivate: [AuthGuardService],
    canActivateChild: [AuthGuardService],
    resolve: {
      preloadScripts: PreloadScriptResolver
    },
    data: {
      preloadScripts: ["echarts"]  // important!
    },
    children: [.....]
}

Questo codice funziona bene e promette che: Dopo aver caricato il file js, il modulo inizia a caricarsi! questo resolver può essere utilizzato su molti router


Potresti condividere il codice dell'interfaccia IPreloadScriptResult?
Mehul Bhalala,

3

Una soluzione universale angolare; Prima di caricare uno script per riprodurre un video, dovevo attendere che un particolare elemento fosse sulla pagina.

import {Inject, Injectable, PLATFORM_ID} from '@angular/core';
import {isPlatformBrowser} from "@angular/common";

@Injectable({
  providedIn: 'root'
})
export class ScriptLoaderService {

  constructor(
    @Inject(PLATFORM_ID) private platformId: Object,
  ) {
  }

  load(scriptUrl: string) {
    if (isPlatformBrowser(this.platformId)) {
      let node: any = document.createElement('script');
      node.src = scriptUrl;
      node.type = 'text/javascript';
      node.async = true;
      node.charset = 'utf-8';
      document.getElementsByTagName('head')[0].appendChild(node);
    }
  }
}

1
Questo in qualche modo sembra complicato
Mustafa l'

2

La soluzione di @ rahul-kumar funziona bene per me, ma volevo chiamare la mia funzione javascript nel mio dattiloscritto

foo.myFunctions() // works in browser console, but foo can't be used in typescript file

L'ho risolto dichiarandolo nel mio dattiloscritto:

import { Component } from '@angular/core';
import { ScriptService } from './script.service';
declare var foo;

E ora, posso chiamare foo ovunque nel mio file dattiloscritto


se importare più script foo, foo1, foo2 e so solo al momento dell'esecuzione come fare la dichiarazione in modo dinamico?
natrayan,

2

Nel mio caso, ho caricato entrambi i file jse css visjsusando la tecnica sopra - che funziona alla grande. Chiamo la nuova funzione dangOnInit()

Nota: ho potuto non farlo a carico semplicemente aggiungendo un<script>e<link>tag per il file di modello HTML.

loadVisJsScript() {
  console.log('Loading visjs js/css files...');
  let script = document.createElement('script');
  script.src = "../../assets/vis/vis.min.js";
  script.type = 'text/javascript';
  script.async = true;
  script.charset = 'utf-8';
  document.getElementsByTagName('head')[0].appendChild(script);    
	
  let link = document.createElement("link");
  link.type = "stylesheet";
  link.href = "../../assets/vis/vis.min.css";
  document.getElementsByTagName('head')[0].appendChild(link);    
}


Voglio caricare il file CSS da una fonte Web esterna. Al momento, sto riscontrando l'errore "Impossibile caricare risorse locali". Si prega di suggerire.
Karan,

Ho provato in questo modo. Nonostante funzioni bene per i file degli script, non funziona per i fogli di stile
118218,

2

Ciao puoi usare Renderer2 ed elementRef con solo poche righe di codice:

constructor(private readonly elementRef: ElementRef,
          private renderer: Renderer2) {
}
ngOnInit() {
 const script = this.renderer.createElement('script');
 script.src = 'http://iknow.com/this/does/not/work/either/file.js';
 script.onload = () => {
   console.log('script loaded');
   initFile();
 };
 this.renderer.appendChild(this.elementRef.nativeElement, script);
}

la onloadfunzione può essere utilizzata per chiamare le funzioni di script dopo che lo script è stato caricato, questo è molto utile se devi fare le chiamate in ngOnInit ()


1

@ d123546 Ho riscontrato lo stesso problema e ora ho funzionato usando ngAfterContentInit (Lifecycle Hook) nel componente in questo modo:

import { Component, OnInit, AfterContentInit } from '@angular/core';
import { Router } from '@angular/router';
import { ScriptService } from '../../script.service';

@Component({
    selector: 'app-players-list',
    templateUrl: './players-list.component.html',
    styleUrls: ['./players-list.component.css'],
    providers: [ ScriptService ]
})
export class PlayersListComponent implements OnInit, AfterContentInit {

constructor(private router: Router, private script: ScriptService) {
}

ngOnInit() {
}

ngAfterContentInit() {
    this.script.load('filepicker', 'rangeSlider').then(data => {
    console.log('script loaded ', data);
    }).catch(error => console.log(error));
}

1
import { Injectable } from '@angular/core';
import * as $ from 'jquery';

interface Script {
    src: string;
    loaded: boolean;
}

@Injectable()
export class ScriptLoaderService {
    public _scripts: Script[] = [];

    /**
     * @deprecated
     * @param tag
     * @param {string} scripts
     * @returns {Promise<any[]>}
     */
    load(tag, ...scripts: string[]) {
        scripts.forEach((src: string) => {
            if (!this._scripts[src]) {
                this._scripts[src] = { src: src, loaded: false };
            }
        });

        const promises: any[] = [];
        scripts.forEach(src => promises.push(this.loadScript(tag, src)));

        return Promise.all(promises);
    }

    /**
     * Lazy load list of scripts
     * @param tag
     * @param scripts
     * @param loadOnce
     * @returns {Promise<any[]>}
     */
    loadScripts(tag, scripts, loadOnce?: boolean) {
        debugger;
        loadOnce = loadOnce || false;

        scripts.forEach((script: string) => {
            if (!this._scripts[script]) {
                this._scripts[script] = { src: script, loaded: false };
            }
        });

        const promises: any[] = [];
        scripts.forEach(script => promises.push(this.loadScript(tag, script, loadOnce)));

        return Promise.all(promises);
    }

    /**
     * Lazy load a single script
     * @param tag
     * @param {string} src
     * @param loadOnce
     * @returns {Promise<any>}
     */
    loadScript(tag, src: string, loadOnce?: boolean) {
        debugger;
        loadOnce = loadOnce || false;

        if (!this._scripts[src]) {
            this._scripts[src] = { src: src, loaded: false };
        }

        return new Promise((resolve, _reject) => {
            // resolve if already loaded
            if (this._scripts[src].loaded && loadOnce) {
                resolve({ src: src, loaded: true });
            } else {
                // load script tag
                const scriptTag = $('<script/>')
                    .attr('type', 'text/javascript')
                    .attr('src', this._scripts[src].src);

                $(tag).append(scriptTag);

                this._scripts[src] = { src: src, loaded: true };
                resolve({ src: src, loaded: true });
            }
        });
    }

    reloadOnSessionChange() {
        window.addEventListener('storage', function(data) {
            if (data['key'] === 'token' && data['oldValue'] == null && data['newValue']) {
                document.location.reload();
            }
        });
    }
}

0

un campione può essere

file script-loader.service.ts

import {Injectable} from '@angular/core';
import * as $ from 'jquery';

declare let document: any;

interface Script {
  src: string;
  loaded: boolean;
}

@Injectable()
export class ScriptLoaderService {
public _scripts: Script[] = [];

/**
* @deprecated
* @param tag
* @param {string} scripts
* @returns {Promise<any[]>}
*/
load(tag, ...scripts: string[]) {
scripts.forEach((src: string) => {
  if (!this._scripts[src]) {
    this._scripts[src] = {src: src, loaded: false};
  }
});

let promises: any[] = [];
scripts.forEach((src) => promises.push(this.loadScript(tag, src)));

return Promise.all(promises);
}

 /**
 * Lazy load list of scripts
 * @param tag
 * @param scripts
 * @param loadOnce
 * @returns {Promise<any[]>}
 */
loadScripts(tag, scripts, loadOnce?: boolean) {
loadOnce = loadOnce || false;

scripts.forEach((script: string) => {
  if (!this._scripts[script]) {
    this._scripts[script] = {src: script, loaded: false};
  }
});

let promises: any[] = [];
scripts.forEach(
    (script) => promises.push(this.loadScript(tag, script, loadOnce)));

return Promise.all(promises);
}

/**
 * Lazy load a single script
 * @param tag
 * @param {string} src
 * @param loadOnce
 * @returns {Promise<any>}
 */
loadScript(tag, src: string, loadOnce?: boolean) {
loadOnce = loadOnce || false;

if (!this._scripts[src]) {
  this._scripts[src] = {src: src, loaded: false};
}

return new Promise((resolve, reject) => {
  // resolve if already loaded
  if (this._scripts[src].loaded && loadOnce) {
    resolve({src: src, loaded: true});
  }
  else {
    // load script tag
    let scriptTag = $('<script/>').
        attr('type', 'text/javascript').
        attr('src', this._scripts[src].src);

    $(tag).append(scriptTag);

    this._scripts[src] = {src: src, loaded: true};
    resolve({src: src, loaded: true});
  }
 });
 }
 }

e utilizzo

prima iniezione

  constructor(
  private _script: ScriptLoaderService) {
  }

poi

ngAfterViewInit()  {
this._script.loadScripts('app-wizard-wizard-3',
['assets/demo/default/custom/crud/wizard/wizard.js']);

}

o

    this._script.loadScripts('body', [
  'assets/vendors/base/vendors.bundle.js',
  'assets/demo/default/base/scripts.bundle.js'], true).then(() => {
  Helpers.setLoading(false);
  this.handleFormSwitch();
  this.handleSignInFormSubmit();
  this.handleSignUpFormSubmit();
  this.handleForgetPasswordFormSubmit();
});
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.