如何在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 {...}

我注意到importES6规范是静态的,并且在TypeScript编译期间而不是在运行时进行了解析。

无论如何使其可配置,以便从CDN或本地文件夹加载file.js?如何告诉Angular 2动态加载脚本?


Answers:


61

如果使用的是system.js,则可以System.import()在运行时使用:

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

如果您使用的是webpack,则可以通过以下方式充分利用其强大的代码拆分支持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 TextSystem对TypeScript代码不满意:Cannot find name 'System'但是在编译和运行过程中没有错误。这两个Angular2System脚本文件已经加入index.html。反正到importSystem,并把这个插件幸福吗?
CallMeLaNN 2015年

1
但是,如果您不使用system.js,该怎么办!
Murhaf Sousli,2016年

2
@drewmoore我不记得我为什么这么说,require()/ import现在应该可以正常工作了,+ 1
Murhaf Sousli

9
据我所知,这些选项不适用于Internet上的脚本,仅适用于本地文件。这似乎并不能回答最初提出的问题。
WillyC

139

您可以使用以下技术在Angular项目中按需动态加载JS脚本和库。

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库

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

7
这非常出色。可以将初始负载大小减少1MB以上-我有很多库!
Our_Benefactors

2
看来我讲得太早了。此解决方案在iOS Safari中不起作用。
Our_Benefactors

加载脚本后,检查脚本是否附加到您的dom头上
Rahul Kumar

也许我没有正确注射,但是我得到了this.script.load is not a function
Towelie

谢谢。这行得通,但是我正在其他所有组件中加载我的脚本(背景动画)。我希望仅将其加载到我的第一个组件上(这是一个登录页面)。我该怎么做呢?谢谢
Joel Azevedo '18

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方法将其加载到页面加载中。因此,不需要单击按钮。
尼拉吉

我正在用同样的方式..但对我没有用。你能帮我吗?this.loadJs(“ javascriptfile path”); 公共loadjs(file){让节点= document.createElement('script'); node.src =文件;node.type ='文本/ javascript'; node.async = true; node.charset ='utf-8'; document.getElementsByTagName('head')[0] .appendChild(node); }
Mitesh Shah

您是要在点击还是加载页面时执行此操作?您能告诉我们更多有关结果或错误的信息吗?
ng-darren

您好,我已尝试实现此功能并正常工作,new Promise但未解决。你能告诉我Promise我要进口什么东西吗?
user3145373 1616年

嗨,我使用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
您忘记将设置asynctrue

我有一个问题,当我们追加脚本并在路线之间导航时,可以说我将脚本加载到home上并浏览至并返回home,是否可以重新初始化当前脚本?意味着我想删除以前加载的一个并重新加载?非常感谢您
d123546 '17

@ d123546是的,如果您希望这样做是可能的。但是您必须编写一些删除脚本标签的逻辑。在这里看看:stackoverflow.com/questions/14708189/…–
Joel

感谢Joel,但是有什么方法可以删除脚本初始化的函数吗?目前我在我的angular4项目中遇到了一个问题,因为我正在加载脚本,该脚本初始化了jquery滑块,因此当第一条路线加载时,它显示正确,但是当我导航到其他路线并返回到我的路线时,脚本被加载了两次并且滑块不再加载了:(您能给我建议吗
d123546

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 Maps库:

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

演示版


感谢您的分享,但是如何添加带有url的其他属性,例如异步延迟?您可以指导我如何使用上述库加载此脚本吗?<script type ='text / javascript'src =' bing.com / api / maps / mapcontrol?branch = release '异步延迟> </ script>
Pushkar Rathod

由于主题相同,因此我只想在此处回答
Pushkar Rathod

很好,但是应该不是npm install --save @ types / scriptjs,而是npm install --save-dev @ types / scriptjs(或者只是npm i -D @ types / scriptjs)
Youness Houdass

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
我不得不说,这看起来很简单,但是效果很好,很容易实现:-)
Pierre

3
这种脚本注入的问题在于,由于Angular是SPA,因此即使从DOM中删除了scripts标记,该脚本也基本上保留在浏览器缓存中。
j4rey

1
很棒的答案!保存了我的一天,它可与Angular 6和7一起使用。
Nadeeshan Herath

要将数据发送到外部js并从外部js获取数据,请在app.component.ts上调用此函数,并在任何功能级别模块的组件处使用clarify var d3Sankey:any。工作正常
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的文档使用它们时,我需要在angular.json中导入echarts:“ scripts”:[“ ./ node_modules / echarts / dist / echarts.min.js”]因此在登录模块的页面中,加载脚本时.js,这是大文件!我不要

因此,我认为angular将每个模块作为文件加载,我可以插入一个路由器解析器来预加载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);
  }
}

然后在submodule-routing.module.ts中,导入此PreloadScriptResolver:

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

该代码运行良好,并且承诺:加载js文件后,然后开始加载模块!该解析器可以在许多路由器中使用


您能否分享IPreloadScriptResult接口的代码?
Mehul Bhalala

3

Angular通用解决方案;在加载脚本播放视频之前,我需要等待页面上出现特定元素。

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
这在某种程度上让人感到狡猾
Mustafa

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;

现在,我可以在我的Typecript文件中的任何地方调用foo


如果我导入多个脚本foo,foo1,foo2,而我仅在执行期间知道如何动态进行声明?
natrayan

2

就我而言,我已经使用上述技术加载了jscss 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文件。当前,我遇到错误消息“不允许加载本地资源”。.建议。
卡兰(Karan)

我尝试过这种方式。尽管它对于脚本文件运行良好,但不适用于样式表
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();
});
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.