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?


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

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

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

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
}

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


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.


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


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


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

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


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


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 ()


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

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

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();
});
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.