UNPKG

@malagu/core

Version:
138 lines (120 loc) 4.96 kB
import { ApplicationShell } from '../shell'; import { FrontendApplicationStateService } from './frontend-application-state'; import { parseCssTime } from '../browser'; import { Component, Autowired, MaybePromise, ApplicationStateService, AbstractApplication, Application, Value } from '../../common'; @Component(Application) export class FrontendApplication extends AbstractApplication { @Autowired(ApplicationShell) protected readonly _shell: ApplicationShell; @Autowired(ApplicationStateService) protected readonly stateService: FrontendApplicationStateService; @Value('malagu.hostDomId') protected readonly hostDomId: string; get shell(): ApplicationShell { return this._shell; } async start(): Promise<void> { await this.doStart(); this.stateService.state = 'started'; const host = await this.getHost(); this._shell.attach(host); await new Promise<void>(resolve => requestAnimationFrame(() => resolve())); this.stateService.state = 'attached_shell'; await this.revealShell(host); this.registerEventListeners(); this.stateService.state = 'ready'; } /** * Return a promise to the host element to which the application shell is attached. */ protected getHost(): Promise<HTMLElement> { if (document.body) { return Promise.resolve(document.getElementById(this.hostDomId) || document.body); } return new Promise<HTMLElement>(resolve => window.addEventListener('load', () => resolve(document.getElementById(this.hostDomId) || document.body), { once: true }) ); } /** * Return an HTML element that indicates the startup phase, e.g. with an animation or a splash screen. */ protected getStartupIndicator(host: HTMLElement): HTMLElement | undefined { const startupElements = host.getElementsByClassName('malagu-preload'); return startupElements.length === 0 ? undefined : startupElements[0] as HTMLElement; } /** * Register global event listeners. */ protected registerEventListeners(): void { window.addEventListener('unload', () => { this.stateService.state = 'closing_window'; this.doStop(); }); } /** * If a startup indicator is present, it is first hidden with the `malagu-hidden` CSS class and then * removed after a while. The delay until removal is taken from the CSS transition duration. */ protected revealShell(host: HTMLElement): Promise<void> { const startupElem = this.getStartupIndicator(host); if (startupElem) { return new Promise(resolve => { window.requestAnimationFrame(() => { startupElem.classList.add('malagu-hidden'); const preloadStyle = window.getComputedStyle(startupElem); const transitionDuration = parseCssTime(preloadStyle.transitionDuration, 0); window.setTimeout(() => { const parent = startupElem.parentElement; if (parent) { parent.removeChild(startupElem); } resolve(); }, transitionDuration); }); }); } else { return Promise.resolve(); } } protected async doStart(): Promise<void> { for (const lifecycle of this.lifecycles) { if (lifecycle.initialize) { try { lifecycle.initialize(); } catch (error) { this.logger.error('Could not initialize lifecycle', error); } } } // TODO for (const lifecycle of this.lifecycles) { if (lifecycle.onStart) { try { await this.measure(lifecycle.constructor.name + '.onStart', () => lifecycle.onStart!(this) ); } catch (error) { this.logger.error('Could not start lifecycle', error); } } } } protected async measure<T>(name: string, fn: () => MaybePromise<T>): Promise<T> { const startMark = name + '-start'; const endMark = name + '-end'; performance.mark(startMark); const result = await fn(); performance.mark(endMark); performance.measure(name, startMark, endMark); for (const item of performance.getEntriesByName(name)) { if (item.duration > 100) { console.warn(item.name + ' is slow, took: ' + item.duration + ' ms'); } else { console.debug(item.name + ' took ' + item.duration + ' ms'); } } performance.clearMeasures(name); return result; } }