从 Angular 组件动态加载外部 javascript 文件

Posted

技术标签:

【中文标题】从 Angular 组件动态加载外部 javascript 文件【英文标题】:Dynamically load external javascript file from Angular component 【发布时间】:2017-10-27 12:22:15 【问题描述】:

我正在使用 Angular 4 和 CLI 创建一个 Angular 应用程序。我正在尝试将 SkyScanner 搜索小部件添加到我的一个组件中。

Skyscanner Widget Example

部分实现需要添加新的外部脚本:

<script src="https://widgets.skyscanner.net/widget-server/js/loader.js" async></script>

我不确定引用此文件的正确方法。如果我将脚本添加到我的 index.html 文件中,则除非执行整页刷新,否则不会加载小部件。我假设脚本尝试在加载时操作 DOM,并且脚本运行时元素不存在。

只有在加载包含 Skyscanner 小部件的组件时才加载脚本的正确方法是什么?

【问题讨论】:

***.com/questions/34489916/… 【参考方案1】:

尝试在组件加载时加载外部 javascript,如下所示:

loadAPI: Promise<any>;

constructor()         
    this.loadAPI = new Promise((resolve) => 
        this.loadScript();
        resolve(true);
    );


public loadScript()         
    var isFound = false;
    var scripts = document.getElementsByTagName("script")
    for (var i = 0; i < scripts.length; ++i) 
        if (scripts[i].getAttribute('src') != null && scripts[i].getAttribute('src').includes("loader")) 
            isFound = true;
        
    

    if (!isFound) 
        var dynamicScripts = ["https://widgets.skyscanner.net/widget-server/js/loader.js"];

        for (var i = 0; i < dynamicScripts.length; i++) 
            let 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);
        

    

【讨论】:

这看起来是迄今为止最好的答案。谢谢你。对 loadScript 的调用应该在构造函数中还是在 ngOnInit 中更好? 但是这个文件加载会延迟,所以我的函数在没有这些知识的情况下呈现。后来它加载没有用。如何解决这个问题。 @karthik.. 如果 dom 中已经存在,该检查是为了避免重复文件加载 我试过这个......它只适用于重新加载......例如。如果您要从一个组件导航到另一个组件。它会在第一次冻结,重新加载后页面将与动态脚本一起正确显示 @GoutamBSeervi 你有没有找到任何延迟加载的解决方案。我也面临同样的问题..【参考方案2】:

我遇到了同样的问题,但在我的例子中,我在 html 文件的末尾导入了 10 个库,这些库有很多方法、侦听器、事件等等,而在我的例子中,我没有不需要专门调用方法。

我所拥有的例子:

<!-- app.component.html -->

<div> 
 ...
</div>

<script src="http://www.some-library.com/library.js">
<script src="../assets/js/my-library.js"> <!-- a route in my angular project -->

如前所述,它不起作用。然后,我找到了一些对我有帮助的东西:Milad response

    删除 app.component.html 中的脚本调用。您必须在 app.component.ts 文件中链接这些脚本。

    在ngOnInit()中,使用方法来追加库,例如:

``

<!-- app.component.ts -->

export class AppComponent implements OnInit 
   title = 'app';
   ngOnInit() 
     this.loadScript('http://www.some-library.com/library.js');
     this.loadScript('../assets/js/my-library.js');
   
  

  public loadScript(url: string) 
    const body = <HTMLDivElement> document.body;
    const script = document.createElement('script');
    script.innerHTML = '';
    script.src = url;
    script.async = false;
    script.defer = true;
    body.appendChild(script);
  

它对我有用。我使用 Angular 6,希望对您有所帮助。

【讨论】:

太棒了! 太棒了,花了几个小时试图完成这项工作,这是最简单的解决方案 我真的真的真的很感激,你拯救了我的一天,这工作非常好 如何在另一个组件中使用加载的库? 找了好久终于找到方法了,太有帮助了,非常感谢!!!【参考方案3】:

我已经完成了这段代码sn-p

 addJsToElement(src: string): HTMLScriptElement 
    const script = document.createElement('script');
    script.type = 'text/javascript';
    script.src = src;
    this.elementRef.nativeElement.appendChild(script);
    return script;
  

然后这样称呼它

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

编辑:使用新的渲染器 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;
  

StackBlitz

【讨论】:

这里的element.ref 是什么? 嗨 pardeep 这是对 body 标签的引用,使用新的渲染器 api 可以重写它。检查我的编辑。 如果您想知道,新的渲染器是:angular.io/api/core/Renderer2 感谢@emp 的澄清【参考方案4】:

loader.js 添加到您的资产文件夹,然后在您的angular-cli.json

"scripts": ["./src/assets/loader.js",]

然后将其添加到您的typings.d.ts

 declare var skyscanner:any;

你可以使用它

  skyscanner.load("snippets","2");

【讨论】:

我添加的最后一行 (skyscanner.load...) 是否添加到组件中?假设是这样,我看到一个错误,说找不到名称 skyscanner。 如何将它用于不同的构建环境? IE。根据是集成、测试还是生产构建加载不同的脚本?【参考方案5】:

您可以创建自己的指令来加载脚本,如下所示

import  Directive, OnInit, Input  from '@angular/core';

@Directive(
    selector: '[appLoadScript]'
)
export class LoadScriptDirective implements OnInit

    @Input('script') param:  any;

    ngOnInit() 
        let node = document.createElement('script');
        node.src = this.param;
        node.type = 'text/javascript';
        node.async = false;
        node.charset = 'utf-8';
        document.getElementsByTagName('head')[0].appendChild(node);
    


您可以在组件模板中的任何位置使用它,如下所示

<i appLoadScript  [script]="'script_file_path'"></i>

例如,要在组件中动态加载 JQuery,请在组件的模板中插入以下代码

<i appLoadScript  [script]="'/assets/baker/js/jquery.min.js'"></i>

【讨论】:

如果脚本需要参数? :) @Ismaestro 它是一个指令,您可以随脚本传递任意数量的参数。甚至脚本也是该指令的参数。【参考方案6】:

接受的答案是正确的,但不起作用,因为浏览器在下载脚本后需要更多时间来解析脚本。因此,如果从加载的脚本中使用了任何变量,则需要在新创建的 html 脚本元素的 onload 事件上使用它。如下所述,我已经改进了接受的答案-

loadAPI: Promise<any>;

constructor() 
    this.loadAPI = new Promise((resolve) => 
        let node = this.loadScript();
        if (node) 
            node.onload = () => 
                resolve(true);
            ;
         else 
            resolve(true);
        
    );


ngOnInit() 
    this.loadAPI
        .then((flag) => 
        //Do something when script is loaded and parsed by browser
    );


loadScript() 
    let node = undefined;
    let isFound = false;
    const scripts = document.getElementsByTagName('script')
    for (let i = 0; i < scripts.length; ++i) 
        // Check if script is already there in html
        if (scripts[i].getAttribute('src') != null && scripts[i].getAttribute('src').includes("loader")) 
          isFound = true;
        
    

    if (!isFound) 
        const dynamicScript = 'https://widgets.skyscanner.net/widget-server/js/loader.js';
        node = document.createElement('script');
        node.src = dynamicScript;
        node.type = 'text/javascript';
        node.async = false;
        node.charset = 'utf-8';
        document.getElementsByTagName('head')[0].appendChild(node);
        return node;
    
    return node;

【讨论】:

感谢脚本的更改,它对我很有用。但我有一个问题。我要包含的来自贝宝的脚本加载另一个 js 文件,然后在 127.0.0.1:4200 处搜索该文件(但未找到)。有没有办法让 require.js 的二次加载正常工作?【参考方案7】:

你可以做一件事

如果你有 angular-cli.json

然后你可以声明到脚本

喜欢

"scripts": ["../src/assets/js/loader.js"]

然后在你的组件中声明skyscanner

喜欢

declare var skyscanner:any;

就是这样!

希望对你有帮助

【讨论】:

我已在本地添加脚本并将其包含在 angular-cli.json 文件中。不幸的是,这不起作用。请您解释一下将声明添加到组件中需要做什么? 对于 Angular 8,我认为 7,angular-cli.json 现在是 angular.json 并且在数组中放置一个外部脚本会使 angular 启动崩溃。 这是每个文档的格式,它不会使应用程序崩溃: "input": "src/external-module/main.js", "inject": false, "bundleName" :“外部模块”angular.io/guide/…【参考方案8】:

注意:这是专门针对外部js链接的! 步骤 1. 建议将 Angular 脚本添加到正文底部的 index.html 文件中!我尝试了所有其他方法,但都失败了。

<!-- File Name: index.html and its inside src dir-->

<body class="">
  <app-root></app-root>

    <!-- Icons -->
        <script src="https://unpkg.com/feather-icons/dist/feather.min.js"></script>

</body>

接下来有两种方法可以做到这一点... 在 Anguar5 中使用此代码顶部类型的组件文件夹中

declare var feather:any;

然后在你的类中调用你需要的方法。例如

//FileName: dashboard.component.ts
import  Component, OnInit  from '@angular/core';
declare var feather:any;
export class DashboardComponent implements OnInit
    ngOnInit()
        feather.replace();
    

这应该运行你的代码! 另一种可能适用于旧版本的方式。我没查过!

//FileName: dashboard.component.ts
import  Component, OnInit  from '@angular/core';

export class DashboardComponent implements OnInit

     ngOnInit()
    
    
        let node = document.createElement('script');
        node.innerText='feather.replace()';
        node.type = 'text/javascript';
        node.async = false;
        node.charset = 'utf-8';
        
        document.getElementsByTagName('body')[0].appendChild(node);
    
    

如果你没有得到我的代码,那么也试试这个link。

希望这会有所帮助!

【讨论】:

【参考方案9】:

有点晚了,但我更喜欢这样做(服务方式)....

import  Injectable  from '@angular/core';
import  Observable  from "rxjs";

interface Scripts 
  name: string;
  src: string;


export const ScriptStore: Scripts[] = [
   name: 'script-a', src: 'assets/js/a.js' ,
   name: 'script-b', src: 'assets/js/b.js' ,
   name: 'script-c', src: 'assets/js/c.js' 
];

declare var document: any;

@Injectable()
export class FileInjectorService 

  private scripts: any = ;

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

  loadJS(...scripts: string[]) 
    const promises: any[] = [];
    scripts.forEach((script) => promises.push(this.loadJSFile(script)));
    return Promise.all(promises);
  

  loadJSFile(name: string) 
    return new Promise((resolve, reject) => 
      if (!this.scripts[name].loaded) 
        let script = document.createElement('script');
        script.type = 'text/javascript';
        script.src = this.scripts[name].src;
        if (script.readyState) 
            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 
            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);
       else 
        resolve( script: name, loaded: true, status: 'Already Loaded' );
      
    );
  


然后在我的组件中,我可以执行以下操作:

ngOnInit() 
  this.fileInjectorService.loadJS('script-a', 'script-c').then(data => 
    // Loaded A and C....
  ).catch(error => console.log(error));

在 Angular 6/7 中测试

【讨论】:

应该参考原文-medium.com/@zainzafar/…【参考方案10】:

经过这么多代码试验,这对我有用

ngOnInit() 
    this.loadFormAssets().then(() => console.log("Script Loaded");).catch(() => console.log("Script Problem"););
  

 public loadFormAssets() 
    return new Promise(resolve => 

      const scriptElement = document.createElement('script');
      scriptElement.src =this.urls.todojs;
      scriptElement.onload = resolve;
      document.body.appendChild(scriptElement);

      const scriptElement1 = document.createElement('script');
      scriptElement1.src =this.urls.vendorjs;
      scriptElement1.onload = resolve;
      document.body.appendChild(scriptElement1);

    );
  

【讨论】:

【参考方案11】:

在我的情况下,我必须加载一些相互依赖的不同文件(某种使用引导程序的东西,然后使用 jquery 插件,然后使用 jquery)并且它们都在加载时立即初始化,假设它们已加载在网页上同步。所有其他答案都假设您正在加载完全不相关的文件(或在所有内容加载后等待您初始化) - 而我的设置会引发各种缺少变量的问题。

我的解决方案是创建一个Promise 链(而不是像@carlitoxenlaweb 那样的Promise 列表 - 这将并行解决所有问题),以便下一个文件仅在前一个文件完成初始化后加载:

    private myScripts = [
        '/assets/js/jquery-2.2.4.min.js',
        '/assets/js/bootstrap.min.js',
        '/assets/js/jquery.bootstrap.js',
        '/assets/js/jquery.validate.min.js',
        '/assets/js/somescript.js',
    ];
    private loadScripts() 
        let container:HTMLElement = this._el.nativeElement;
        let promise = Promise.resolve();
        for (let url of this.myScripts) 
            promise = promise.then(_ => new Promise((resolve, reject) => 
                let script = document.createElement('script');
                script.innerHTML = '';
                script.src = url;
                script.async = true;
                script.defer = false;
                script.onload = () =>  resolve(); 
                script.onerror = (e) =>  reject(e); 
                container.appendChild(script);
            ));
        
    

【讨论】:

即使文件位于 node_modules 文件夹中,此方法是否有效? 我猜这不会起作用 - 这种加载发生在 node_modules 文件夹不存在的运行时。您可以从要使用的模块中复制相关文件,但可能最好使用另一种方法 - 例如将模块导入打字稿文件(然后将被编译)。 是的,我尝试从 node_modules 加载,但没有成功。我别无选择,只能将所需的东西复制到 assets js 文件夹中并从中加载。【参考方案12】:

由于源 URL 允许您调用全局函数,您可以使用它来设置自定义事件处理程序。

index.html

<script 
  type="text/javascript"
  src="http://www.bing.com/api/maps/mapcontrol?callback=onBingLoaded&branch=release"
  async defer
></script>
<script>
  function onBingLoaded() 
    const event = new CustomEvent("bingLoaded");
    window.dispatchEvent(event);
  
</script>

现在我们已经将自定义事件分派给了窗口对象,我们可以使用 Angular 在我们的组件中提供的装饰器 @HostListener 来监听它。

app.component.ts

export class AppComponent 
  @ViewChild('mapCanvas')
  mapCanvas!: ElementRef;
  private map!: Microsoft.Maps.Map;

  @HostListener('window:bingLoaded', ['$event'])
  defineMapCanvas() 
    this.map = new Microsoft.Maps.Map(
      this.mapCanvas.nativeElement,
      
        credentials: [YOUR API KEY HERE],
        ...other options
      
    );
  

参考:https://angular.io/api/core/HostListener

【讨论】:

以上是关于从 Angular 组件动态加载外部 javascript 文件的主要内容,如果未能解决你的问题,请参考以下文章

如何在作为 innerHtml 加载的文档的子部分中动态加载 Angular 组件?

angular 2组件无法从外部css皮肤加载相对背景图像url

如何将第 3 方脚本从 Web 动态加载到 Angular2 组件中

从外部源动态加载 React 组件/捆绑包

Angular2组件加载外部js lib文件

如何以角度动态删除组件