Como carregar scripts externos dinamicamente no Angular?

150

Eu tenho este módulo que componente a biblioteca externa junto com lógica adicional sem adicionar a <script>tag diretamente no 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 {...}

Eu percebo o import especificação do ES6 é estática e resolvida durante a transpilação do TypeScript em vez de em tempo de execução.

De qualquer forma, para configurá-lo para que o file.js seja carregado na CDN ou na pasta local? Como dizer ao Angular 2 para carregar um script dinamicamente?

CallMeLaNN
fonte
Possível duplicata do nome da importação da variável ES6 no node.js?
Felix Kling

Respostas:

61

Se você estiver usando system.js, poderá usar System.import()em tempo de execução:

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

Se você estiver usando o webpack, poderá aproveitar ao máximo seu robusto suporte à divisão de código com require.ensure:

export class MyAppComponent {
  constructor() {
     require.ensure(['path/to/your/module'], require => {
        let yourModule = require('path/to/your/module');
        yourModule.someFunction();
     }); 
  }
}
chamou moore
fonte
1
Parece que eu preciso usar o carregador de módulo especificamente no componente. Bem, tudo bem, já que Systemé o futuro do carregador, eu acho.
CallMeLaNN
6
TypeScript Plugin for Sublime Textnão está satisfeito com Systemo código TypeScript: Cannot find name 'System'mas nenhum erro durante a transpilação e a execução. O arquivo de script Angular2e Systemjá foi adicionado index.html. De qualquer maneira para importo Systeme tornar o feliz plugin?
CallMeLaNN
1
mas e se você não estiver usando o system.js!
Murhaf Sousli
2
@drewmoore Eu não me lembro porque eu disse assim, require()/ importdeve funcionar bem como a sua resposta agora, +1
Murhaf Sousli
9
Pelo que sei, essas opções não funcionam para scripts na internet, apenas para arquivos locais. Isso não parece responder à pergunta original, conforme solicitado.
WillyC 2/17
139

Você pode usar a técnica a seguir para carregar dinamicamente scripts JS e bibliotecas sob demanda no seu projeto Angular.

script.store.ts conterá o caminho do script localmente ou em um servidor remoto e um nome que será usado para carregar o script dinamicamente

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

O script.service.ts é um serviço injetável que manipulará o carregamento do script, copie script.service.tscomo 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);
        }
    });
}

}

Injete isso ScriptServicesempre que precisar e carregue js libs como este

this.script.load('filepicker', 'rangeSlider').then(data => {
    console.log('script loaded ', data);
}).catch(error => console.log(error));
Rahul Kumar
fonte
7
Isso funciona de maneira brilhante. Raspa mais de 1 MB do tamanho de carregamento inicial - tenho muitas bibliotecas!
Our_Benefactors
2
Parece que falei cedo demais. Esta solução NÃO funciona no iOS Safari.
Our_Benefactors
depois de carregar a verificação de script se ele está anexando a cabeça de seu dom
Rahul Kumar
Talvez eu não estou injetar corretamente, mas estou ficandothis.script.load is not a function
Towelie
Obrigado. Isso funciona, mas estou recebendo meu script (uma animação em segundo plano) carregado em todos os outros componentes. Quero que ele seja carregado apenas no meu primeiro componente (é uma página de login). O que posso fazer para conseguir isso? Obrigado
Joel Azevedo
47

Isso pode funcionar. Este código anexa dinamicamente a <script>tag ao headarquivo html no botão clicado.

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);
    }
}
ng-darren
fonte
1
Homem Thanx Trabalhou para mim. Agora você pode carregá-lo no carregamento da página também usando o método ngonit. Portanto, o clique no botão não é necessário.
Niraj
Estou usando da mesma maneira .. mas não funcionou para mim. u pode por favor me ajudar para o mesmo? this.loadJs ("caminho do arquivo javascript"); public loadjs (arquivo) {let node = document.createElement ('script'); node.src = arquivo; node.type = 'text / javascript'; node.async = true; node.charset = 'utf-8'; document.getElementsByTagName ('head') [0] .appendChild (nó); }
Mitesh Shah 22/10
Você está tentando fazer isso no clique ou no carregamento da página? Você poderia nos dizer mais sobre o resultado ou erro?
N / darren
Olá, tentei implementar isso e funcionando bem, mas new Promisenão foi resolvido. Você pode me dizer por Promiseque devo importar alguma coisa?
user3145373ツ
Olá, não importei nada de especial quando uso o Promise.
N / darren
23

Modifiquei a resposta @rahul kumars, para que ele use 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
}
Joel
fonte
1
Você esquece de definir asynccomo true.
Will Huang
Eu tenho uma pergunta: quando anexamos o script e navegamos entre as rotas, digamos que eu carreguei o script em casa e navegue por aproximadamente e voltei para casa, existe uma maneira de reiniciar o script atual? Significa que eu gostaria de remover um carregado anteriormente e carregá-lo novamente? Muito obrigado
d123546
@ d123546 Sim, se você deseja que isso seja possível. Mas você teria que escrever alguma lógica que remova a tag de script. Dê uma olhada aqui: stackoverflow.com/questions/14708189/…
Joel
Obrigado Joel, mas existe uma maneira de remover de alguma forma as funções inicializadas pelo script? Atualmente, estou enfrentando um problema no meu projeto angular4, porque estou carregando um script que inicializa o controle deslizante jquery; portanto, quando a primeira rota é carregada, ele é exibido corretamente, mas quando eu navego para outra rota e retorno à minha rota, o script é carregado duas vezes e o controle deslizante não está carregando mais :( você poderia me aconselhar sobre isso, por favor
d123546
1
Há um erro na linha private scripts: {ScriptModel}[] = [];. Deveria serprivate scripts: ScriptModel[] = [];
Chris Haines
20

Ainda outra opção seria utilizar scriptjspacotes para esse assunto que

permite carregar recursos de script sob demanda a partir de qualquer URL

Exemplo

Instale o pacote:

npm i scriptjs

e definições de tipo parascriptjs :

npm install --save @types/scriptjs

Em seguida, importe o $script.get()método:

import { get } from 'scriptjs';

e finalmente carregar o recurso de script, no nosso caso, a biblioteca do Google Maps:

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

Demo

Vadim Gremyachev
fonte
Obrigado por compartilhar, mas como adicionar propriedades adicionais com URL como adiamento assíncrono? Você pode me orientar como carregar esse script usando a biblioteca acima? <script type = 'text / javascript' src = ' bing.com/api/maps/mapcontrol?branch=release ' async defer > </script>
Pushkar Rathod
Desde que a linha de assunto é o mesmo que eu estou pulando resposta aqui apenas
Pushkar Rathod
Ótimo, mas em vez de npm install --save @ types / scriptjs, ele deve ser npm install --save-dev @ types / scriptjs (ou apenas npm i -D @ types / scriptjs)
Youness Houdass
18

Você pode carregar vários scripts dinamicamente como este em seu component.tsarquivo:

 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 chame esse método dentro do construtor,

constructor() {
    this.loadScripts();
}

Nota: Para que mais scripts sejam carregados dinamicamente, adicione-os à dynamicScriptsmatriz.

Aswin Sanakan
fonte
3
Eu tenho que dizer, isso parece simples, mas funciona bem, muito fácil de implementar :-)
Pierre
3
O problema com esse tipo de injeção de script é que, como Angular é um SPA, esses scripts basicamente permanecem no cache do navegador, mesmo após a remoção da tag de scripts do DOM.
J4rey
1
Resposta incrível! Salvei o meu dia, ele funciona com Angular 6 e 7. Testado!
Nadeeshan Herath 13/02/19
Para enviar dados para js externos e obter dados de js externos, chame esta função em app.component.ts e use declare var d3Sankey: qualquer componente de qualquer módulo do nível de recurso. Está funcionando.
Gnganpath 12/11/19
5

Eu fiz esse trecho de código com a nova API do 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;
  }

E então chame assim

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

StackBlitz

Eduardo Vargas
fonte
4

Eu tenho uma boa maneira de carregar scripts dinamicamente! Agora eu uso ng6, echarts4 (> 700Kb), ngx-echarts3 no meu projeto. quando os uso pelos documentos da ngx-echarts, preciso importar echarts no angular.json: "scripts": ["./ node_modules / echarts / dist / echarts.min.js"], portanto, no módulo de login, na página ao carregar scripts .js, esse é um arquivo grande! Eu não quero isso

Então, acho que o angular carrega cada módulo como um arquivo, posso inserir um resolvedor de roteador para pré-carregar o js e começar o carregamento do 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);
  }
}

Em 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: [.....]
}

Esse código funciona bem e promete que: Após o arquivo js ser carregado, o módulo começa a ser carregado! este resolvedor pode usar em muitos roteadores

申君健
fonte
Você poderia compartilhar o código da interface IPreloadScriptResult?
Mehul Bhalala
3

Uma solução universal angular; Eu precisava esperar que um elemento específico estivesse na página antes de carregar um script para reproduzir um vídeo.

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);
    }
  }
}
Robert King
fonte
1
Isso de alguma forma se sente desonesto
Mustafa
2

A solução do @ rahul-kumar funciona bem para mim, mas eu queria chamar minha função javascript no meu texto datilografado

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

Corrigi-o declarando-o no meu texto datilografado:

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

E agora, eu posso chamar foo em qualquer lugar do meu arquivo de digitação

Z3nk
fonte
se eu importar vários scripts foo, foo1, foo2 e eu souber apenas durante o tempo de execução como fazer a declaração dinamicamente?
Natrayan 17/08/19
2

No meu caso, carreguei os arquivos jse css visjsusando a técnica acima - o que funciona muito bem. Eu chamo a nova função dengOnInit()

Nota: eu poderia não fazê-lo de carga, simplesmente adicionando um<script>e<link>tag ao arquivo de modelo 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);    
}

bob.mazzo
fonte
Quero carregar o arquivo CSS de uma fonte da web externa. Atualmente, estou enfrentando o erro dizendo "Não é permitido carregar o recurso local" .. Por favor, sugira.
19418 Karan
Eu tentei assim. Apesar de que está funcionando bem para arquivos de scripts, ele não funciona para Stylesheets
118218
2

Oi, você pode usar o Renderer2 e o elementRef com apenas algumas linhas 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);
}

a onloadfunção pode ser usada para chamar as funções de script após o carregamento do script, isso é muito útil se você precisar fazer as chamadas no ngOnInit ()

Hetdev
fonte
1

@ d123546 Enfrentei o mesmo problema e o fiz funcionar agora usando o ngAfterContentInit (Lifecycle Hook) no componente como este:

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));
}
Waseem
fonte
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();
            }
        });
    }
}
Aniket
fonte
0

uma amostra pode ser

arquivo 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 uso

primeiro injetar

  constructor(
  private _script: ScriptLoaderService) {
  }

então

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();
});
unos baghaii
fonte