Як динамічно завантажувати зовнішні скрипти в Angular?


150

У мене є цей модуль, який компонентує зовнішню бібліотеку разом з додатковою логікою без додавання <script>тегу безпосередньо в 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 {...}

Я помічаю, що importспецифікація ES6 є статичною і вирішується під час перенесення TypeScript, а не під час виконання.

У будь-якому випадку, щоб зробити його налаштованим, щоб file.js завантажувався або з CDN або з локальної папки? Як сказати Angular 2, щоб динамічно завантажував сценарій?


Відповіді:


61

Якщо ви використовуєте system.js, його можна використовувати System.import()під час виконання:

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

Якщо ви використовуєте веб-пакет, ви можете повністю скористатися його надійною підтримкою розбиття коду за допомогою require.ensure:

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

1
Здається, мені потрібно використовувати модуль-завантажувач спеціально в компоненті. Що ж, це добре, адже Systemя думаю , що це майбутнє вантажника.
CallMeLaNN

6
TypeScript Plugin for Sublime Textне задоволений Systemкодом TypeScript: Cannot find name 'System'але немає помилки під час трансляції та запуску. І файл, Angular2і Systemфайл сценарію вже додані в index.html. У всякому разі для і зробити плагін щасливим? importSystem
CallMeLaNN

1
але що робити, якщо ви не використовуєте system.js!
Мурхаф Суслі

2
@drewmoore Я не пам'ятаю, чому я це сказав, require()/ importповинен працювати добре, як відповідь зараз, +1
Murhaf Sousli

9
Як я можу сказати, ці параметри не працюють для сценаріїв в Інтернеті, лише для локальних файлів. Здається, це не відповідає на початкове запитання, як було задано.
WillyC

139

Ви можете використовувати наступну техніку для динамічного завантаження JS-скриптів та бібліотек на вимогу у вашому проекті Angular.

script.store.ts міститиме шлях сценарію локально або на віддаленому сервері та ім'я, яке буде використовуватися для динамічного завантаження сценарію

 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 - це послуга, яка ін’єкційно працює, і вона буде працювати з завантаженням сценарію, скопіювати script.service.tsяк є

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

}

Введіть це ScriptServiceкуди завгодно і завантажте js libs, як це

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

7
Це працює блискуче. Гоління перевищує 1 Мб від початкового розміру завантаження - у мене багато бібліотек!
Our_Benefactors

2
Схоже, я говорив занадто рано. Це рішення НЕ працює в iOS Safari.
Our_Benefactors

після завантаження сценарію перевірте, чи додається він голові вашого будинку
Рахул Кумар

Можливо, я не ввожу правильно, але я отримуюthis.script.load is not a function
Towelie

Дякую. Це працює, але я завантажую свій сценарій (фонову анімацію) у всі інші компоненти. Я хочу, щоб він завантажувався лише на мій перший компонент (це сторінка входу). Що я можу зробити, щоб досягти цього? Спасибі
Джоель Азеведо

47

Це може спрацювати. Цей Код динамічно додає <script>тег до headфайлу HTML на натиснуту кнопку.

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
Спасибі людина працював на мене. Тепер ви можете завантажити його на завантаження сторінки, також використовуючи метод ngonit. Тому натискання кнопки не потрібно.
Niraj

Я використовую той самий спосіб .. але не працював для мене. Ви можете, будь ласка, допомогти мені для того ж ?? this.loadJs ("шлях до javascriptfile"); public loadjs (файл) {let node = document.createElement ('script'); node.src = файл; node.type = 'текст / javascript'; node.async = вірно; node.charset = 'utf-8'; document.getElementsByTagName ('head') [0] .appendChild (вузол); }
Мітеш Шах

Ви намагаєтеся це зробити при натисканні чи завантаженні сторінки? Чи можете ви розповісти більше про результат чи помилку?
ng-darren

привіт, я намагався це реалізувати і добре працюю, але new Promiseце не вирішено. Ви можете мені сказати, чи Promiseповинен я щось імпортувати?
user3145373 ツ

Привіт, я не імпортував нічого особливого, коли використовую Promise.
ng-darren

23

Я змінив @rahul kumars відповідь, щоб він використовував 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
Ви забуваєте встановити asyncв true.
Буде Хуан

У мене виникає запитання, коли ми додаємо скрипт і переходимо між маршрутами, скажімо, що я завантажив сценарій вдома і переглянув про, і повернувся додому, чи є спосіб повторно використати поточний сценарій? Значить, я хотів би видалити раніше завантажений і завантажити його знову? Дуже дякую
d123546

@ d123546 Так, якщо ви хочете, щоб це можливо. Але вам доведеться написати певну логіку, яка видаляє тег сценарію. Подивіться тут: stackoverflow.com/questions/14708189/…
Joel

Дякую Джоелю, але чи є спосіб якось зняти функції, ініціалізовані сценарієм? В даний час я стикаюся з проблемою в моєму проекті angular4, тому що я завантажую скрипт, який ініціалізує повзунок jquery, тож коли перший маршрут завантажує його відображається належним чином, але потім, коли я переходжу на інший маршрут і повертаюся назад до свого маршруту, сценарій завантажується двічі та повзунок більше не завантажується :( Не могли б ви порадити мене з цього
приводу

1
У рядку є помилка private scripts: {ScriptModel}[] = [];. Це має бутиprivate scripts: ScriptModel[] = [];
Кріс Хайнс

20

Ще одним варіантом буде використання scriptjsпакету для цього питання

дозволяє завантажувати ресурси сценарію на вимогу з будь-якої URL-адреси

Приклад

Встановіть пакет:

npm i scriptjs

та визначення типів дляscriptjs :

npm install --save @types/scriptjs

Тоді $script.get()спосіб імпорту :

import { get } from 'scriptjs';

і, нарешті, завантажте ресурс сценарію, у нашому випадку бібліотеку Карт Google:

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

Демо


Дякуємо за спільний доступ, але як можна додати додаткову властивість за допомогою url, наприклад, async defer ?? Чи можете ви порадити мені, як завантажити цей скрипт за допомогою вищевказаної бібліотеки? <script type = 'text / javascript' src = ' bing.com/api/maps/mapcontrol?branch=release ' async defer > </script>
Pushkar Rathod

Оскільки тема є такою ж, я
Pushkar Rathod

Чудово, але замість npm install --save @ types / scriptjs, це повинно бути npm install --save-dev @ types / scriptjs (або просто npm i -D @ types / scriptjs)
Юнес Хоудас

18

Ви можете завантажувати декілька сценаріїв динамічно таким чином у свій 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);
    }
  }

і викликати цей метод всередині конструктора,

constructor() {
    this.loadScripts();
}

Примітка. Щоб більше сценаріїв динамічно завантажувалося, додайте їх у dynamicScriptsмасив.


3
Треба сказати, це виглядає просто, але працює добре, дуже легко здійснити :-)
П'єр,

3
Проблема цього типу введення скриптів полягає в тому, що оскільки Angular є SPA, цей сценарій в основному залишається в кеш-пам’яті браузера навіть після вилучення тегу сценаріїв з DOM.
j4rey

1
Дивовижна відповідь! Врятував мій день, він працює з кутовими 6 та 7. Випробуваний!
Надешан Герат

Щоб надсилати дані до зовнішніх js та отримувати дані від зовнішніх js, зателефонуйте на цю функцію на app.component.ts та використовуйте оголосити var d3Sankey: будь-який компонент будь-якого модуля рівня функцій. Це працює.
gnganpath

5

Я зробив цей фрагмент коду з новим api рендером

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

А потім називайте це так

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

StackBlitz


4

У мене є гарний спосіб динамічно завантажувати сценарії! Зараз я використовую ng6, echarts4 (> 700Kb), ngx-echarts3 у своєму проекті. коли я використовую їх у документах ngx-echarts, мені потрібні імпортні echarts у angular.json: "scriptpts": ["./ node_modules / echarts / dist / echarts.min.js"], таким чином, в модуль входу, на сторінці під час завантаження скриптів .js, це великий файл! Я цього не хочу.

Отже, я думаю, що кутовий завантажує кожен модуль як файл, я можу вставити розв'язчик маршрутизатора для попереднього завантаження js, а потім розпочати завантаження модуля!

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

Потім у підмодуль-routing.module.ts , імпортуємо цей PreloadScriptResolver:

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

Цей код працює добре, і він обіцяє, що: Після завантаження файлу js, модуль розпочнеться завантаження! цей Resolver можна використовувати в багатьох маршрутизаторах


Чи можете ви поділитися кодом інтерфейсу IPreloadScriptResult?
Мехул Бхалала

3

Кутове універсальне рішення; Мені потрібно було зачекати, коли певний елемент з’явиться на сторінці, перш ніж завантажити сценарій для відтворення відео.

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
Це якось відчуває
Мустафа

2

Рішення @ rahul-kumar для мене працює добре, але я хотів викликати функцію javascript у своєму машинописі

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

Я це виправив, оголосивши його у своєму машинописі:

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

І тепер я можу зателефонувати в foo будь-де в моєму файлі машинопису


якщо я імпортую кілька скриптів foo, foo1, foo2, і я знаю лише під час виконання, як робити декларацію динамічно?
натраян

2

У моєму випадку я завантажив jsі css visjsфайли, і файли, використовуючи вищезгадану техніку - яка чудово працює. Я називаю нову функцію відngOnInit()

Примітка. Я не мігзмусити його завантажити, просто додавши<script>та<link>тег у файл шаблону 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);    
}


Я хочу завантажити CSS-файл із зовнішнього веб-джерела. В даний час я зіткнувся з помилкою: "Не дозволяється завантажувати локальний ресурс". Будь ласка, підкажіть.
Каран

Я намагався таким чином. Незважаючи на це добре працює для Scripts файлів, це не працює для Stylesheets
118218

2

Привіт, ви можете використовувати Renderer2 та elementRef лише з кількома рядками коду:

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

onloadфункція може бути використана для виклику функції скрипта після завантаження сценарію, це дуже корисно , якщо ви повинні робити виклики в ngOnInit ()


1

@ d123546 Я зіткнувся з тією ж проблемою і змусив її працювати зараз, використовуючи ngAfterContentInit (гачок життєвого циклу) у такому компоненті:

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

зразок може бути

файл 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});
  }
 });
 }
 }

та використання

спочатку вводити

  constructor(
  private _script: ScriptLoaderService) {
  }

тоді

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

}

або

    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();
});
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.