如何在Angular中动态加载外部脚本?

Cal*_*aNN 116 javascript typescript ecmascript-6 angular

我有这个模块,它将外部库与其他逻辑组件化,而无需将<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 {...}
Run Code Online (Sandbox Code Playgroud)

我注意到importES6规范是静态的,并且在TypeScript转换过程中而不是在运行时解析.

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

Rah*_*mar 120

您可以使用以下技术在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'}
];
Run Code Online (Sandbox Code Playgroud)

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

}
Run Code Online (Sandbox Code Playgroud)

ScriptService在您需要的地方注入它并加载像这样的js库

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

  • 这非常出色.初始加载大小超过1MB - 我有很多库! (5认同)
  • 看来我说得太早了。此解决方案在 iOS Safari 中不起作用。 (3认同)
  • 如何调用js文件中的函数?它们是否附加到全局窗口? (2认同)

dre*_*ore 52

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

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

如果您正在使用webpack,您可以充分利用其强大的代码拆分支持require.ensure:

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

  • 据我所知,这些选项不适用于互联网上的脚本,仅适用于本地文件.这似乎没有回答问题的原始问题. (7认同)
  • "Sublime Text的TypeScript插件"对TypeScript代码中的"System"不满意:`找不到名称'System',但在转换和运行过程中没有错误.'Angular2`和`System`脚本文件都已添加到`index.html`中.无论如何``导入``系统`并使插件开心? (6认同)
  • @drewmoore我不记得我为什么这么说,`require()`/`import`应该可以正常作为你的答案,+ 1 (2认同)

ng-*_*ren 40

这可能会奏效.此代码在单击按钮时动态地将<script>标记附加到headhtml文件的标记.

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 谢谢男人为我工作。您现在也可以使用 ngonit 方法在页面加载时加载它。所以不需要点击按钮。 (2认同)

Joe*_*oel 21

我修改了@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
}
Run Code Online (Sandbox Code Playgroud)

  • `private scripts: {ScriptModel}[] = [];` 行中存在错误。它应该是`私有脚本:ScriptModel[] = [];` (2认同)

Vad*_*hev 13

另一种选择是利用scriptjs包装来解决这个问题

允许您从任何URL按需加载脚本资源

安装包:

npm i scriptjs
Run Code Online (Sandbox Code Playgroud)

类型定义scriptjs:

npm install --save @types/scriptjs
Run Code Online (Sandbox Code Playgroud)

然后导入$script.get()方法:

import { get } from 'scriptjs';
Run Code Online (Sandbox Code Playgroud)

最后加载脚本资源,在我们的案例中谷歌地图库:

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

演示

  • 很好,但不是 npm install --save @types/scriptjs,应该是 npm install --save-dev @types/scriptjs (或者只是 npm i -D @types/scriptjs) (3认同)

Asw*_*kan 12

您可以在文件中动态加载多个脚本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);
    }
  }
Run Code Online (Sandbox Code Playgroud)

并在构造函数中调用此方法,

constructor() {
    this.loadScripts();
}
Run Code Online (Sandbox Code Playgroud)

注意:要动态加载更多脚本,请将它们添加到dynamicScripts数组中.

  • 这种类型的脚本注入的问题在于,由于 Angular 是一个 SPA,即使您从 DOM 中删除了脚本标记,这些脚本基本上仍保留在浏览器缓存中。 (4认同)
  • 我不得不说,这看起来很简单,但效果很好,很容易实现:-) (3认同)

Het*_*dev 11

嗨,您只需几行代码即可使用 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);
}
Run Code Online (Sandbox Code Playgroud)

onload函数可用于在脚本加载后调用脚本函数,如果您必须在 ngOnInit() 中进行调用,这将非常有用


Edu*_*gas 7

我已经用新的渲染器 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;
  }
Run Code Online (Sandbox Code Playgroud)

然后这样称呼

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

闪电战


小智 6

我有一个动态加载脚本的好方法!现在我在我的项目中使用 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);
  }
}
Run Code Online (Sandbox Code Playgroud)

然后在 submodule-routing.module.ts 中导入这个 PreloadScriptResolver:

const routes: Routes = [
  {
    path: "",
    component: DashboardComponent,
    canActivate: [AuthGuardService],
    canActivateChild: [AuthGuardService],
    resolve: {
      preloadScripts: PreloadScriptResolver
    },
    data: {
      preloadScripts: ["echarts"]  // important!
    },
    children: [.....]
}
Run Code Online (Sandbox Code Playgroud)

这段代码运行良好,它的承诺是:加载 js 文件后,然后模块开始加载!这个解析器可以在许多路由器中使用


rob*_*ing 5

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);
    }
  }
}
Run Code Online (Sandbox Code Playgroud)


F.H*_*.H. 5

对于那些也想动态加载样式的人。 (基于@Rahul Kumar 的精彩回答)

脚本.store.ts

interface Scripts {
    name: string;
    src: string;
}

export const StyleStore: Scripts[] = [
    { name: 'fancybox-css', src: 'https://cdn.jsdelivr.net/gh/fancyapps/fancybox@3.5.7/dist/jquery.fancybox.min.css' }
];

export const ScriptStore: Scripts[] = [
    { name: 'jquery', src: 'https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js' },
    { name: 'other', src: '[other script source]'}
];
Run Code Online (Sandbox Code Playgroud)

脚本加载器.service.ts

import { Injectable } from '@angular/core';
import { ScriptStore, StyleStore } from '../../stores/script.store';

@Injectable({
  providedIn: 'root'
})
export class ScriptLoaderService {

  private scripts: any = {};
  private styles: any = {};

  constructor() {
    ScriptStore.forEach((script: any) => {
      this.scripts[script.name] = {
        loaded: false,
        src: script.src
      };
    });

    StyleStore.forEach((script: any) => {
      this.styles[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);
  }

  loadStyles(...scripts: string[]) {
    var promises: any[] = [];
    scripts.forEach((script) => promises.push(this.loadStyle(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') as any;
        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);
      }
    });
  }

  loadStyle(name: string) {
    return new Promise((resolve, reject) => {
      //resolve if already loaded
      if (this.styles[name].loaded) {
        resolve({ script: name, loaded: true, status: 'Already Loaded' });
      }
      else {
        //load style
        let style = document.createElement('link') as any;
        style.type = "text/css";
        style.rel = "stylesheet";
        style.href = this.styles[name].src;
        if (style.readyState) {  //IE
          style.onreadystatechange = () => {
            if (style.readyState === "loaded" || style.readyState === "complete") {
              style.onreadystatechange = null;
              this.styles[name].loaded = true;
              resolve({ style: name, loaded: true, status: 'Loaded' });
            }
          };
        } else {  //Others
          style.onload = () => {
            this.styles[name].loaded = true;
            resolve({ style: name, loaded: true, status: 'Loaded' });
          };
        }
        style.onerror = (error: any) => resolve({ style: name, loaded: false, status: 'Loaded' });
        document.getElementsByTagName('head')[0].appendChild(style);
      }
    });
  }

}
Run Code Online (Sandbox Code Playgroud)

应用程序组件.ts

constructor(private scriptLoaderService: ScriptLoaderService) {
  this.scriptLoaderService.loadStyles('fancybox-css').then(x => {
    this.scriptLoaderService.load('jquery', 'fancybox').then(data => {
    }).catch(error => console.log(error));
  });
}
Run Code Online (Sandbox Code Playgroud)