Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(module:code-editor): load Monaco only once #7033

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 41 additions & 14 deletions components/code-editor/code-editor.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { DOCUMENT } from '@angular/common';
import { Inject, Injectable, OnDestroy } from '@angular/core';
import { BehaviorSubject, Observable, of as observableOf, Subject, Subscription } from 'rxjs';
import { BehaviorSubject, Observable, of, ReplaySubject, Subscription } from 'rxjs';
import { map, tap } from 'rxjs/operators';

import { CodeEditorConfig, NzConfigService } from 'ng-zorro-antd/core/config';
Expand All @@ -26,14 +26,22 @@ function tryTriggerFunc(fn?: (...args: NzSafeAny[]) => NzSafeAny): (...args: NzS
};
}

// Caretaker note: previously, these were `NzCodeEditorService` properties.
// They're kept as static variables because this will allow loading Monaco only once.
// This applies to micro frontend apps with multiple Angular apps or a single Angular app
// that can be bootstrapped and destroyed multiple times (e.g. using Webpack module federation).
// Root providers are re-initialized each time the app is bootstrapped. Platform providers aren't.
// We can't make the `NzCodeEditorService` to be a platform provider (`@Injectable({ providedIn: 'platform' })`)
// since it depends on other root providers.
const loaded$ = new ReplaySubject<boolean>(1);
let loadingStatus = NzCodeEditorLoadingStatus.UNLOAD;

@Injectable({
providedIn: 'root'
})
export class NzCodeEditorService implements OnDestroy {
private document: Document;
private firstEditorInitialized = false;
private loaded$ = new Subject<boolean>();
private loadingStatus = NzCodeEditorLoadingStatus.UNLOAD;
private option: JoinedEditorOptions = {};
private config: CodeEditorConfig;
private subscription: Subscription | null;
Expand Down Expand Up @@ -70,12 +78,12 @@ export class NzCodeEditorService implements OnDestroy {
}

requestToInit(): Observable<JoinedEditorOptions> {
if (this.loadingStatus === NzCodeEditorLoadingStatus.LOADED) {
if (loadingStatus === NzCodeEditorLoadingStatus.LOADED) {
this.onInit();
return observableOf(this.getLatestOption());
return of(this.getLatestOption());
}

if (this.loadingStatus === NzCodeEditorLoadingStatus.UNLOAD) {
if (loadingStatus === NzCodeEditorLoadingStatus.UNLOAD) {
if (this.config.useStaticLoading && typeof monaco === 'undefined') {
warn(
'You choose to use static loading but it seems that you forget ' +
Expand All @@ -87,7 +95,7 @@ export class NzCodeEditorService implements OnDestroy {
}
}

return this.loaded$.asObservable().pipe(
return loaded$.pipe(
tap(() => this.onInit()),
map(() => this.getLatestOption())
);
Expand All @@ -99,11 +107,11 @@ export class NzCodeEditorService implements OnDestroy {
return;
}

if (this.loadingStatus === NzCodeEditorLoadingStatus.LOADING) {
if (loadingStatus === NzCodeEditorLoadingStatus.LOADING) {
return;
}

this.loadingStatus = NzCodeEditorLoadingStatus.LOADING;
loadingStatus = NzCodeEditorLoadingStatus.LOADING;

const assetsRoot = this.config.assetsRoot;
const vs = assetsRoot ? `${assetsRoot}/vs` : 'assets/vs';
Expand All @@ -112,25 +120,44 @@ export class NzCodeEditorService implements OnDestroy {

loadScript.type = 'text/javascript';
loadScript.src = `${vs}/loader.js`;
loadScript.onload = () => {

const onLoad = (): void => {
cleanup();
windowAsAny.require.config({
paths: { vs }
});
windowAsAny.require(['vs/editor/editor.main'], () => {
this.onLoad();
});
};
loadScript.onerror = () => {

const onError = (): void => {
cleanup();
throw new Error(`${PREFIX} cannot load assets of monaco editor from source "${vs}".`);
};

const cleanup = (): void => {
// Caretaker note: we have to remove these listeners once the `<script>` is loaded successfully
// or not since the `onLoad` listener captures `this`, which will prevent the `NzCodeEditorService`
// from being garbage collected.
loadScript.removeEventListener('load', onLoad);
loadScript.removeEventListener('error', onError);
// We don't need to keep the `<script>` element within the `<body>` since JavaScript has
// been executed and Monaco is available globally. E.g. Webpack, always removes `<script>`
// elements after loading chunks (see its `LoadScriptRuntimeModule`).
this.document.documentElement.removeChild(loadScript);
};

loadScript.addEventListener('load', onLoad);
loadScript.addEventListener('error', onError);

this.document.documentElement.appendChild(loadScript);
}

private onLoad(): void {
this.loadingStatus = NzCodeEditorLoadingStatus.LOADED;
this.loaded$.next(true);
this.loaded$.complete();
loadingStatus = NzCodeEditorLoadingStatus.LOADED;
loaded$.next(true);
loaded$.complete();

tryTriggerFunc(this.config.onLoad)();
}
Expand Down
2 changes: 1 addition & 1 deletion components/code-editor/typings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export type JoinedEditorOptions = EditorOptions | DiffEditorOptions;

export type NzEditorMode = 'normal' | 'diff';

export enum NzCodeEditorLoadingStatus {
export const enum NzCodeEditorLoadingStatus {
UNLOAD = 'unload',
LOADING = 'loading',
LOADED = 'LOADED'
Expand Down