Comment charger dynamiquement des scripts externes dans Angular?


151

J'ai ce module qui compose la bibliothèque externe avec une logique supplémentaire sans ajouter la <script>balise directement dans l'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 {...}

Je remarque que la importspécification by ES6 est statique et résolue lors du transpiling TypeScript plutôt qu'au moment de l'exécution.

Quoi qu'il en soit, pour le rendre configurable afin que le fichier.js soit chargé à partir du CDN ou du dossier local? Comment dire à Angular 2 de charger un script dynamiquement?


Réponses:


61

Si vous utilisez system.js, vous pouvez utiliser System.import()au moment de l'exécution:

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

Si vous utilisez webpack, vous pouvez profiter pleinement de son support robuste de fractionnement de code avec require.ensure:

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

1
Il semble que j'ai besoin d'utiliser le chargeur de module spécifiquement dans le composant. Eh bien, c'est bien puisque Systemc'est l'avenir du chargeur, je pense.
CallMeLaNN

6
TypeScript Plugin for Sublime Textne satisfait pas Systemdans le code TypeScript: Cannot find name 'System'mais aucune erreur lors du transpiling et de l'exécution. Les deux Angular2et le Systemfichier de script ont déjà été ajoutés dans index.html. Quoi qu'il en soit à importla Systemet faire le plaisir de plug - in?
CallMeLaNN

1
mais que faire si vous n'utilisez pas system.js!
Murhaf Sousli

2
@drewmoore Je ne me souviens pas pourquoi je l'ai dit, require()/ importdevrait bien fonctionner comme votre réponse maintenant, +1
Murhaf Sousli

9
D'après ce que je peux dire, ces options ne fonctionnent pas pour les scripts sur Internet, juste pour les fichiers locaux. Cela ne semble pas répondre à la question initiale posée.
WillyC

140

Vous pouvez utiliser la technique suivante pour charger dynamiquement des scripts et des bibliothèques JS à la demande dans votre projet Angular.

script.store.ts contiendra le chemin du script localement ou sur un serveur distant et un nom qui sera utilisé pour charger le script dynamiquement

 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 est un service injectable qui gérera le chargement du script, copié script.service.tstel quel

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);
        }
    });
}

}

Injectez-le ScriptServicepartout où vous en avez besoin et chargez des bibliothèques js comme ceci

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

7
Cela fonctionne à merveille. Rase plus de 1 Mo de la taille de chargement initiale - j'ai beaucoup de bibliothèques!
Our_Benefactors

2
On dirait que j'ai parlé trop tôt. Cette solution ne fonctionne PAS dans iOS Safari.
Our_Benefactors

après le chargement du script, vérifiez s'il s'ajoute à la tête de votre dom
Rahul Kumar

Peut-être que je ne m'injecte pas correctement, mais je reçoisthis.script.load is not a function
Towelie

Merci. Cela fonctionne, mais je reçois mon script (une animation d'arrière-plan) chargé dans tous les autres composants. Je veux qu'il soit chargé uniquement sur mon premier composant (c'est une page de connexion). Que puis-je faire pour y parvenir? Merci
Joel Azevedo

47

Cela pourrait fonctionner. Ce code ajoute dynamiquement la <script>balise au headfichier html sur le bouton cliqué.

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
Thanx man a travaillé pour moi. Vous pouvez maintenant le charger lors du chargement de la page en utilisant également la méthode ngonit. Il n'est donc pas nécessaire de cliquer sur le bouton.
Niraj

J'utilise la même manière .. mais pas travaillé pour moi. pouvez-vous s'il vous plaît m'aider pour la même chose ?? this.loadJs ("chemin javascriptfile"); public loadjs (fichier) {let node = document.createElement ('script'); node.src = fichier; node.type = 'texte / javascript'; node.async = vrai; node.charset = 'utf-8'; document.getElementsByTagName ('head') [0] .appendChild (node); }
Mitesh Shah

Essayez-vous de le faire au clic ou au chargement de la page? Pouvez-vous nous en dire plus sur le résultat ou l'erreur?
ng-darren

bonjour, j'ai essayé de l'implémenter et fonctionne bien mais new Promisen'est pas résolu. Pouvez-vous me dire car Promisedois-je importer quelque chose?
user3145373 ツ

Salut, je n'ai rien importé de spécial lorsque j'utilise Promise.
ng-darren

23

J'ai modifié la réponse de @rahul kumars, afin qu'elle utilise Observables à la place:

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
Vous oubliez de définir le asyncsur true.
Will Huang

J'ai une question, lorsque nous ajoutons le script et naviguons entre les itinéraires, disons que j'ai chargé le script à la maison et que je navigue jusqu'à environ et que je suis retourné à la maison, y a-t-il un moyen de réinitier le script actuel? Cela signifie-t-il que je souhaite en supprimer un précédemment chargé et le charger à nouveau? Merci beaucoup
d123546

@ d123546 Oui, si vous voulez que ce soit possible. Mais vous devrez écrire une logique qui supprime la balise de script. Regardez ici: stackoverflow.com/questions/14708189/…
Joel

Merci Joel, mais y a-t-il un moyen de supprimer d'une manière ou d'une autre les fonctions initialisées par le script? Actuellement, je suis confronté à un problème dans mon projet angular4, car je charge un script qui initialise le curseur jquery, donc lorsque la première route se charge, il s'affiche correctement, mais lorsque je navigue vers une autre route et retourne à mon itinéraire, le script est chargé deux fois et le curseur ne se charge plus :( Pourriez-vous me conseiller à ce sujet s'il vous plaît
d123546

1
Il y a une erreur dans la ligne private scripts: {ScriptModel}[] = [];. Ça devrait êtreprivate scripts: ScriptModel[] = [];
Chris Haines

20

Encore une autre option serait d'utiliser le scriptjspackage pour cette question qui

vous permet de charger des ressources de script à la demande à partir de n'importe quelle URL

Exemple

Installez le package:

npm i scriptjs

et définitions de type pourscriptjs :

npm install --save @types/scriptjs

Puis $script.get()méthode d' importation :

import { get } from 'scriptjs';

et enfin charger la ressource de script, dans notre cas la bibliothèque Google Maps:

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

Démo


Merci pour le partage, mais comment ajouter une propriété supplémentaire avec une URL comme async defer ?? Pouvez-vous me guider comment charger ce script en utilisant la bibliothèque ci-dessus? <script type = 'text / javascript' src = ' bing.com/api/maps/mapcontrol?branch=release ' async defer > </script>
Pushkar Rathod

Puisque la ligne d'objet est la même, je suis en train de répondre ici uniquement
Pushkar Rathod

Génial, mais au lieu de npm install --save @ types / scriptjs, ce devrait être npm install --save-dev @ types / scriptjs (ou juste npm i -D @ types / scriptjs)
Youness Houdass

18

Vous pouvez charger plusieurs scripts dynamiquement comme ceci dans votre component.tsfichier:

 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);
    }
  }

et appelez cette méthode dans le constructeur,

constructor() {
    this.loadScripts();
}

Remarque: pour que plus de scripts soient chargés dynamiquement, ajoutez-les au dynamicScriptstableau.


3
Je dois dire que cela a l'air simple mais fonctionne bien, très facile à mettre en œuvre :-)
Pierre

3
Le problème avec ce type d'injection de script est que, comme Angular est un SPA, ce script reste essentiellement dans le cache du navigateur même après avoir supprimé la balise scripts du DOM.
j4rey

1
Réponse géniale! J'ai sauvé ma journée, cela fonctionne avec Angular 6 & 7. Testé!
Nadeeshan Herath

Pour envoyer des données à des js externes et obtenir des données à partir de js externes, appelez cette fonction sur app.component.ts et utilisez declare var d3Sankey: any à n'importe quel composant du module de niveau de fonctionnalité. Ça marche.
gnganpath

5

J'ai fait cet extrait de code avec la nouvelle API de rendu

 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;
  }

Et puis appelle ça comme ça

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

StackBlitz


4

J'ai un bon moyen de charger dynamiquement des scripts! J'utilise maintenant ng6, echarts4 (> 700Kb), ngx-echarts3 dans mon projet. quand je les utilise par les docs de ngx-echarts, j'ai besoin d'importer des echarts dans angular.json: "scripts": ["./ node_modules / echarts / dist / echarts.min.js"] donc dans le module de connexion, page lors du chargement des scripts .js, c'est un gros fichier! Je n'en veux pas.

Donc, je pense que angular charge chaque module sous forme de fichier, je peux insérer un résolveur de routeur pour précharger js, puis commencer le chargement du module!

// 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);
  }
}

Puis dans le submodule-routing.module.ts , importez ce PreloadScriptResolver:

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

Ce code fonctionne bien, et il promet que: Une fois le fichier js chargé, le module commence le chargement! ce résolveur peut utiliser dans de nombreux routeurs


Pourriez-vous s'il vous plaît partager le code de l'interface IPreloadScriptResult?
Mehul Bhalala

3

Une solution universelle angulaire; J'avais besoin d'attendre qu'un élément particulier soit sur la page avant de charger un script pour lire une vidéo.

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
Cela semble en quelque sorte douteux
Mustafa

2

La solution de @ rahul-kumar fonctionne bien pour moi, mais je voulais appeler ma fonction javascript dans mon manuscrit

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

Je l'ai corrigé en le déclarant dans mon manuscrit:

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

Et maintenant, je peux appeler foo n'importe où dans mon fichier typecript


si j'importe plusieurs scripts foo, foo1, foo2 et que je ne sais qu'au moment de l'exécution comment faire la déclaration dynamiquement?
natrayan

2

Dans mon cas, j'ai chargé les fichiers jset css visjsen utilisant la technique ci-dessus - qui fonctionne très bien. J'appelle la nouvelle fonction dengOnInit()

Remarque: je n'ai pas pu le charger en ajoutant simplement unebalise<script>et<link>au fichier de modèle 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);    
}


Je souhaite charger le fichier CSS à partir d'une source Web externe. Actuellement, je suis confronté à l'erreur en disant "Pas autorisé à charger la ressource locale" .. Veuillez suggérer.
Karan

J'ai essayé de cette façon. Bien que cela fonctionne bien pour les fichiers Scripts, cela ne fonctionne pas pour les
feuilles de style

2

Bonjour, vous pouvez utiliser Renderer2 et elementRef avec seulement quelques lignes de code:

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 onloadfonction peut être utilisée pour appeler les fonctions de script après le chargement du script, c'est très utile si vous devez faire les appels dans le ngOnInit ()


1

@ d123546 J'ai rencontré le même problème et je l'ai fait fonctionner maintenant en utilisant ngAfterContentInit (Lifecycle Hook) dans le composant comme ceci:

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 échantillon peut être

fichier 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});
  }
 });
 }
 }

et utilisation

première injection

  constructor(
  private _script: ScriptLoaderService) {
  }

puis

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

}

ou

    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();
});
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.