¿Cómo cargar scripts externos dinámicamente en Angular?


151

Tengo este módulo que compone la biblioteca externa junto con lógica adicional sin agregar la <script>etiqueta directamente en el 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 {...}

Me he dado cuenta de la importespecificación por ES6 es estática y se resolvieron durante el mecanografiado transpiling en lugar de en tiempo de ejecución.

¿De todos modos para que sea configurable para que el archivo.js se cargue desde CDN o carpeta local? ¿Cómo decirle a Angular 2 que cargue un script dinámicamente?


Respuestas:


61

Si usa system.js, puede usar System.import()en tiempo de ejecución:

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

Si usa webpack, puede aprovechar al máximo su sólido soporte de división de código con require.ensure:

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

1
Parece que necesito usar el cargador de módulos específicamente en el componente. Bueno, eso está bien ya que Systemes el futuro del cargador, creo.
CallMeLaNN

66
TypeScript Plugin for Sublime Textno está satisfecho con Systemel código TypeScript: Cannot find name 'System'pero no hay error durante la transpiración y ejecución. Tanto Angular2y Systemarchivo de script ya agrega en index.html. De todas formas para importel Systemy hacer que el plug-in feliz?
CallMeLaNN

1
pero qué pasa si no estás usando system.js!
Murhaf Sousli

2
@drewmoore No recuerdo por qué lo dije, require()/ importdebería funcionar bien como su respuesta ahora, 1
Murhaf Sousli

9
Por lo que puedo decir, estas opciones no funcionan para scripts en Internet, solo para archivos locales. Esto no parece responder a la pregunta original como se le preguntó.
WillyC

140

Puede usar la siguiente técnica para cargar dinámicamente scripts y bibliotecas JS a pedido en su proyecto Angular.

script.store.ts contendrá la ruta del script localmente o en un servidor remoto y un nombre que se usará para cargar el script dinámicamente

 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 es un servicio inyectable que se encargará de cargar el script, copiarlo script.service.tstal como está

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

}

Inyecte esto ScriptServicedonde lo necesite y cargue bibliotecas js como esta

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

77
Esto funciona de manera brillante. Reduce más de 1 MB del tamaño de carga inicial. ¡Tengo muchas bibliotecas!
Our_Benefactors

2
Parece que hablé demasiado pronto. Esta solución NO funciona en iOS Safari.
Our_Benefactors

después de cargar el script, verifique si es apropiado para el jefe de su dom
Rahul Kumar

Tal vez no estoy inyectando correctamente, pero estoy recibiendothis.script.load is not a function
Towelie

Gracias. Esto funciona, pero estoy cargando mi script (una animación de fondo) en todos los demás componentes. Quiero que se cargue solo en mi primer componente (es una página de inicio de sesión). ¿Qué puedo hacer para lograr eso? Gracias
Joel Azevedo

47

Esto podría funcionar. Este código agrega dinámicamente la <script>etiqueta al headarchivo html al hacer clic en el botón.

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
Gracias hombre trabajó para mí. Ahora puede cargarlo en la carga de la página también utilizando el método ngonit. Por lo tanto, no es necesario hacer clic en el botón.
Niraj

Estoy usando la misma manera ... pero no funcionó para mí. ¿Puedes ayudarme por lo mismo? this.loadJs ("ruta del archivo javascript"); public loadjs (archivo) {let node = document.createElement ('script'); nodo.src = archivo; node.type = 'texto / javascript'; node.async = verdadero; node.charset = 'utf-8'; document.getElementsByTagName ('cabeza') [0] .appendChild (nodo); }
Mitesh Shah

¿Estás intentando hacerlo al hacer clic o al cargar la página? ¿Podría contarnos más sobre el resultado o error?
ng-darren

hola, he intentado implementar esto y funciona bien pero new Promiseno está resuelto. ¿Me puede decir Promisesi debo importar algo?
user3145373 ツ

Hola, no importé nada especial cuando uso Promise.
ng-darren

23

He modificado la respuesta de @rahul kumars, para que use Observables en su lugar:

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
Te olvidas de configurar el asynca true.
Will Huang

Tengo una pregunta, cuando agregamos el guión y navegamos entre rutas, digamos que cargué el guión en mi página de inicio y busco y regresé a mi hogar, ¿hay alguna manera de reiniciar el guión actual? ¿Significa que me gustaría eliminar uno previamente cargado y volver a cargarlo? Muchas gracias
d123546

@ d123546 Sí, si lo desea, es posible. Pero tendría que escribir algo de lógica que elimine la etiqueta del script. Echa un vistazo aquí: stackoverflow.com/questions/14708189/…
Joel

Gracias Joel, pero ¿hay alguna forma de eliminar de alguna manera las funciones inicializadas por el script? Actualmente estoy enfrentando un problema en mi proyecto angular4, porque estoy cargando un script que inicializa el control deslizante jquery, por lo que cuando se carga la primera ruta se muestra correctamente, pero luego cuando navego a otra ruta y regreso a mi ruta, el script se carga dos veces y el control deslizante ya no se carga :( ¿Podría aconsejarme sobre esto por favor
d123546

1
Hay un error en la línea private scripts: {ScriptModel}[] = [];. Debería serprivate scripts: ScriptModel[] = [];
Chris Haines

20

Otra opción más sería utilizar el scriptjspaquete para el caso que

le permite cargar recursos de script a pedido desde cualquier URL

Ejemplo

Instala el paquete:

npm i scriptjs

y definiciones de tipo parascriptjs :

npm install --save @types/scriptjs

Luego $script.get()método de importación :

import { get } from 'scriptjs';

y finalmente cargar el recurso de script, en nuestro caso la biblioteca de Google Maps:

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

Manifestación


Gracias por compartir, pero ¿cómo puedo agregar propiedades adicionales con URL como aplazamiento asíncrono? ¿Me puede guiar cómo cargar este script usando la biblioteca anterior? <script type = 'text / javascript' src = ' bing.com/api/maps/mapcontrol?branch=release ' async defer> </script>
Pushkar Rathod

Dado que la línea de asunto es la misma, solo estoy respondiendo aquí
Pushkar Rathod

Genial, pero en lugar de npm install --save @ types / scriptjs, debería ser npm install --save-dev @ types / scriptjs (o simplemente npm i -D @ types / scriptjs)
Youness Houdass

18

Puede cargar múltiples scripts dinámicamente como este en su component.tsarchivo:

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

y llama a este método dentro del constructor,

constructor() {
    this.loadScripts();
}

Nota: Para que se carguen más scripts dinámicamente, agréguelos a la dynamicScriptsmatriz.


3
Tengo que decir que esto parece simple pero funciona bien, muy fácil de implementar :-)
Pierre

3
El problema con este tipo de inyección de script es que, dado que Angular es un SPA, estos scripts permanecen básicamente en la memoria caché del navegador incluso después de eliminar la etiqueta de scripts del DOM.
j4rey

1
Respuesta impresionante! Salvó mi día, funciona con Angular 6 y 7. Probado!
Nadeeshan Herath

Para enviar datos a js externos y obtener datos de js externos, llame a esta función en app.component.ts y use declare var d3Sankey: cualquiera en cualquier componente del módulo de nivel de característica. Esta funcionando.
gnganpath

5

He hecho este fragmento de código con la nueva API de renderizador

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

Y luego llámalo así

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

StackBlitz


4

¡Tengo una buena forma de cargar scripts dinámicamente! Ahora uso ng6, echarts4 (> 700Kb), ngx-echarts3 en mi proyecto. cuando los uso en los documentos de ngx-echarts, necesito importar echarts en angular.json: "scripts": ["./ node_modules / echarts / dist / echarts.min.js"] por lo tanto en el módulo de inicio de sesión, página mientras se cargan los scripts .js, este es un archivo grande! No lo quiero

Entonces, creo que angular carga cada módulo como un archivo, puedo insertar un enrutador para precargar js, ¡y luego comenzar la carga del módulo!

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

Luego, en submodule-routing.module.ts, importe este PreloadScriptResolver:

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

Este código funciona bien y promete que: ¡Después de cargar el archivo js, ​​el módulo comenzará a cargar! este Resolver puede usar en muchos enrutadores


¿Podría compartir el código de la interfaz IPreloadScriptResult?
Mehul Bhalala

3

Una solución universal angular; Necesitaba esperar a que un elemento en particular estuviera en la página antes de cargar un script para reproducir un video.

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
Esto de alguna manera se siente dudoso
Mustafa

2

La solución de @ rahul-kumar funciona bien para mí, pero quería llamar a mi función javascript en mi mecanografiado

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

Lo arreglé declarándolo en mi mecanografiado:

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

Y ahora, puedo llamar a foo en cualquier parte de mi archivo de mecanografía


si importo múltiples scripts foo, foo1, foo2 y solo sé durante el tiempo de ejecución cómo hacer la declaración dinámicamente?
natrayan

2

En mi caso, he cargado los archivos jsy css visjsutilizando la técnica anterior, que funciona muy bien. Llamo a la nueva función desdengOnInit()

Nota: Podría no conseguir que se carga mediante la simple adición de una<script>y<link>etiqueta para el archivo de plantilla 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);    
}


Quiero cargar el archivo CSS desde una fuente web externa. Actualmente, me enfrento al error que dice "No se permite cargar recursos locales". Por favor sugiérale.
Karan

Lo intenté de esta manera. A pesar de que funciona bien para los archivos de Scripts, no funciona para las
hojas

2

Hola, puedes usar Renderer2 y elementRef con solo unas pocas líneas de código:

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 onloadfunción se puede usar para llamar a las funciones del script después de cargar el script, esto es muy útil si tiene que hacer las llamadas en ngOnInit ()


1

@ d123546 Me enfrenté al mismo problema y lo puse a funcionar ahora usando ngAfterContentInit (Lifecycle Hook) en el componente de esta manera:

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

una muestra puede ser

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

y uso

primera inyección

  constructor(
  private _script: ScriptLoaderService) {
  }

luego

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();
});
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.