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

IT技术 javascript angular typescript ecmascript-6
2021-02-03 21:08:31

我有这个module将外部库与附加逻辑一起组件化,而无需将<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 转译期间而不是在运行时解析。

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

6个回答

您可以使用以下技术在 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));
这非常有效。将初始加载大小减少超过 1MB - 我有很多库!
2021-03-27 21:08:31
我修改了您的加载功能以进行顺序加载。这是我所做的。load(...scripts: string[]) {let promise = Promise.resolve(); scripts.forEach(script => { promise = promise.then(() => this.loadScript(script)).then(res => console.log(res));});}谢谢您的回答。
2021-03-28 21:08:31
你如何调用js文件中的函数?它们是否附加到全局窗口?
2021-03-31 21:08:31
看来我说得太早了。此解决方案在 iOS Safari 中不起作用。
2021-04-01 21:08:31
加载脚本后检查它是否附加到你的 dom 的头部
2021-04-06 21:08:31

如果你使用 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();
     }); 
  }
}
似乎我需要在组件中专门使用module加载器。嗯,这很好,因为这System是装载机的未来,我想。
2021-03-18 21:08:31
据我所知,这些选项不适用于 Internet 上的脚本,仅适用于本地文件。这似乎并没有回答原来的问题。
2021-03-23 21:08:31
TypeScript Plugin for Sublime TextSystem对 TypeScript 代码不满意Cannot find name 'System'但在编译和运行期间没有错误。这两个Angular2System脚本文件已经加入index.html反正到importSystem,并把这个插件幸福吗?
2021-03-28 21:08:31
但是如果你没有使用 system.js 怎么办!
2021-03-31 21:08:31
@drewmoore我不记得我为什么这么说,require()/import现在应该可以作为你的答案,+1
2021-04-02 21:08:31

这可能会奏效。此代码在单击按钮时动态地将<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);
    }
}
谢谢男人为我工作。您现在也可以使用 ngonit 方法在页面加载时加载它。所以不需要点击按钮。
2021-03-11 21:08:31
您是在单击还是在页面加载时尝试执行此操作?你能告诉我们更多关于结果或错误的信息吗?
2021-03-11 21:08:31
你好,我已经尝试实现这个并且工作正常但new Promise没有解决。你能告诉我Promise我需要进口什么吗?
2021-03-25 21:08:31
嗨,我在使用 Promise 时没有导入任何特别的东西。
2021-03-27 21:08:31
我正在使用相同的方式..但对我不起作用。你能帮我解决同样的问题吗??this.loadJs("javascript文件路径"); public loadjs(file) { let node = document.createElement('script'); node.src = 文件;node.type = '文本/javascript'; node.async = true; node.charset = 'utf-8'; document.getElementsByTagName('head')[0].appendChild(node); }
2021-04-04 21:08:31

我修改了@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
}
我有一个问题,当我们附加脚本并在路由之间导航时,假设我在 home 上加载脚本并浏览到 about 并返回 home,有没有办法重新初始化当前脚本?意味着我想删除以前加载的一个并重新加载它?非常感谢你
2021-03-12 21:08:31
@d123546 你应该只初始化一次服务。使用@Injectable({providedIn: 'root'})和添加该服务 providers:[]数组app.module.ts
2021-03-12 21:08:31
行中有错误private scripts: {ScriptModel}[] = [];它应该是private scripts: ScriptModel[] = [];
2021-03-21 21:08:31
@d123546 是的,如果您愿意的话,这是可能的。但是您必须编写一些删除脚本标记的逻辑。看看这里:stackoverflow.com/questions/14708189/...
2021-03-26 21:08:31
您忘记将 设置asynctrue
2021-04-08 21:08:31

另一种选择是使用scriptjs来解决这个问题

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

例子

安装软件包:

npm i scriptjs

类型定义scriptjs

npm install --save @types/scriptjs

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

import { get } from 'scriptjs';

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

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

演示

很好,但不是 npm install --save @types/scriptjs,它应该是 npm install --save-dev @types/scriptjs(或只是 npm i -D @types/scriptjs)
2021-03-10 21:08:31