可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I have this module which componentize the external library together with additional logic without adding the <script>
tag directly into the 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 {...}
I notice the import
by ES6 spec is static and resolved during TypeScript transpiling rather than at runtime.
Anyway to make it configurable so the file.js will be loading either from CDN or local folder?
How to tell Angular 2 to load a script dynamically?
回答1:
If you\'re using system.js, you can use System.import()
at runtime:
export class MyAppComponent {
constructor(){
System.import(\'path/to/your/module\').then(refToLoadedModule => {
refToLoadedModule.someFunction();
}
);
}
If you\'re using webpack, you can take full advantage of its robust code splitting support with require.ensure
:
export class MyAppComponent {
constructor() {
require.ensure([\'path/to/your/module\'], require => {
let yourModule = require(\'path/to/your/module\');
yourModule.someFunction();
});
}
}
回答2:
You can use following technique to
dynamically load JS scripts and libraries on demand in your Angular project.
script.store.ts will contain the path of the script either locally or on a remote server and a name that will be used to load the script dynamically
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 is an injectable service that will handle the loading of script, copy script.service.ts
as it is
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);
}
});
}
}
Inject this ScriptService
wherever you need it and load js libs like this
this.script.load(\'filepicker\', \'rangeSlider\').then(data => {
console.log(\'script loaded \', data);
}).catch(error => console.log(error));
回答3:
This might work. This Code dynamically appends the <script>
tag to the head
of the html file on button clicked.
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);
}
}
回答4:
I have modified @rahul kumars answer, so that it uses Observables instead:
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
}
回答5:
You can load multiple scripts dynamically like this in your component.ts
file:
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);
}
}
and call this method inside the constructor,
constructor() {
this.loadScripts();
}
Note : For more scripts to be loaded dynamically, add them to dynamicScripts
array.
回答6:
Yet another option would be to utilize scriptjs
package for that matter which
allows you to load script resources on-demand from any URL
Example
Install the package:
npm i scriptjs
and type definitions for scriptjs
:
npm install --save @types/scriptjs
Then import $script.get()
method:
import { get } from \'scriptjs\';
and finally load script resource, in our case Google Maps library:
export class AppComponent implements OnInit {
ngOnInit() {
get(\"https://maps.googleapis.com/maps/api/js?key=\", () => {
//Google Maps library has been loaded...
});
}
}
Demo
回答7:
In my case, I\'ve loaded both the js
and css
visjs
files using the above technique - which works great. I call the new function from ngOnInit()
Note: I could not get it to load by simply adding a <script>
and <link>
tag to the html template file.
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);
}
回答8:
I have done this code snippet with the new renderer 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;
}
And then call it like this
this.addJsToElement(\'https://widgets.skyscanner.net/widget-server/js/loader.js\').onload = () => {
console.log(\'SkyScanner Tag loaded\');
}
StackBlitz
回答9:
@d123546
I faced the same issue and got it working now using ngAfterContentInit (Lifecycle Hook) in the component like this :
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));
}
回答10:
@rahul-kumar \'s solution works good for me, but i wanted to call my javascript function in my typescript
foo.myFunctions() // works in browser console, but foo can\'t be used in typescript file
I fixed it by declaring it in my typescript :
import { Component } from \'@angular/core\';
import { ScriptService } from \'./script.service\';
declare var foo;
And now, i can call foo anywhere in my typecript file
回答11:
a sample can be
script-loader.service.ts file
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});
}
});
}
}
and usage
first inject
constructor(
private _script: ScriptLoaderService) {
}
then
ngAfterViewInit() {
this._script.loadScripts(\'app-wizard-wizard-3\',
[\'assets/demo/default/custom/crud/wizard/wizard.js\']);
}
or
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();
});
回答12:
I have a good way to dynamically load scripts!
Now I use ng6, echarts4 (>700Kb ) ,ngx-echarts3 in my project. when I use them by ngx-echarts\'s docs, I need import echarts in angular.json :
\"scripts\":[\"./node_modules/echarts/dist/echarts.min.js\"]
thus in the login module, page while loading scripts.js, this is big file! I don\'t want it.
So, I think angular loads each module as a file, I can insert a router resolver to preload js, then begin the module loading!
// 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);
}
}
Then in the submodule-routing.module.ts ,import this PreloadScriptResolver:
const routes: Routes = [
{
path: \"\",
component: DashboardComponent,
canActivate: [AuthGuardService],
canActivateChild: [AuthGuardService],
resolve: {
preloadScripts: PreloadScriptResolver
},
data: {
preloadScripts: [\"echarts\"] // important!
},
children: [.....]
}
This code works well, and its promises that: After js file loaded, then module begin load! this Resolver can use in many routers