javascript - ¿Cómo cargar scripts externos dinámicamente en Angular?
Tengo este módulo que componentiza la biblioteca externa junto con lógica adicional sin agregar la etiqueta <script> 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 {...}
Noto que el import según la especificación ES6 es estático y se resuelve durante la transpilación de TypeScript en lugar de en tiempo de ejecución.
¿De todos modos, hay alguna manera de hacerlo configurable para que el archivo.js se cargue desde el CDN o desde la carpeta local? ¿Cómo decirle a Angular 2 que cargue un script dinámicamente?
Respuesta #1
Puedes usar la siguiente técnica para cargar dinámicamente scripts y librerías de JS bajo demanda en tu proyecto Angular.
script.store.ts contendrá la ruta del script ya sea localmente o en un servidor remoto y un nombre que se utilizará para cargar el script de manera dinámica
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 manejará la carga de scripts, copia script.service.ts tal 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);
}
});
}
}
Inyecta este ScriptService donde lo necesites y carga las librerías js así
this.script.load('filepicker', 'rangeSlider').then(data => {
console.log('script loaded ', data);
}).catch(error => console.log(error));
Respuesta #2
Si estás usando system.js, puedes usar System.import() en tiempo de ejecución:
export class MyAppComponent {
constructor(){
System.import('path/to/your/module').then(refToLoadedModule => {
refToLoadedModule.someFunction();
}
);
}
Si estás usando webpack, puedes 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();
});
}
}
Respuesta #3
Esto podría funcionar. Este código agrega dinámicamente la etiqueta <script> al head del archivo 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);
}
}
Respuesta #4
Otra opción sería utilizar el paquete scriptjs para ese asunto, lo cual
te permite cargar recursos de scripts bajo demanda desde cualquier URL
Ejemplo
Instala el paquete:
npm i scriptjs
y definiciones de tipo para scriptjs:
npm install --save @types/scriptjs
Luego importe el método $script.get():
import { get } from 'scriptjs';
y finalmente cargar el recurso del 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...
});
}
}
Demostración
Respuesta #5
He modificado la respuesta de @rahul kumar, 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
}
Respuesta #6
Puedes cargar múltiples scripts dinámicamente así en tu archivo component.ts:
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 de manera dinámica, agréguelos al array dynamicScripts.
Respuesta #7
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 función onload se puede usar para llamar a las funciones del script después de que el script se haya cargado, esto es muy útil si tienes que hacer las llamadas en el ngOnInit()
Respuesta #8
He hecho este fragmento de código con la nueva API de renderizado
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
