mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
Prior to this change `this.isStable.pipe(first((isStable) => isStable)).toPromise()` had to be done in multiple places across the framework and the Angular CLI see https://github.com/angular/angular-cli/pull/25856#discussion_r1328158846. In the majority of cases an Observable based `isStable` API is not needed. This also removes the need for RXJS operator imports. PR Close #51807
245 lines
10 KiB
TypeScript
245 lines
10 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.io/license
|
|
*/
|
|
|
|
import {APP_BOOTSTRAP_LISTENER, ApplicationRef, whenStable} from '../application_ref';
|
|
import {ENABLED_SSR_FEATURES} from '../application_tokens';
|
|
import {Console} from '../console';
|
|
import {ENVIRONMENT_INITIALIZER, EnvironmentProviders, Injector, makeEnvironmentProviders} from '../di';
|
|
import {inject} from '../di/injector_compatibility';
|
|
import {formatRuntimeError, RuntimeError, RuntimeErrorCode} from '../errors';
|
|
import {enableLocateOrCreateContainerRefImpl} from '../linker/view_container_ref';
|
|
import {enableLocateOrCreateElementNodeImpl} from '../render3/instructions/element';
|
|
import {enableLocateOrCreateElementContainerNodeImpl} from '../render3/instructions/element_container';
|
|
import {enableApplyRootElementTransformImpl} from '../render3/instructions/shared';
|
|
import {enableLocateOrCreateContainerAnchorImpl} from '../render3/instructions/template';
|
|
import {enableLocateOrCreateTextNodeImpl} from '../render3/instructions/text';
|
|
import {getDocument} from '../render3/interfaces/document';
|
|
import {isPlatformBrowser} from '../render3/util/misc_utils';
|
|
import {TransferState} from '../transfer_state';
|
|
import {NgZone} from '../zone';
|
|
|
|
import {cleanupDehydratedViews} from './cleanup';
|
|
import {IS_HYDRATION_DOM_REUSE_ENABLED, PRESERVE_HOST_CONTENT} from './tokens';
|
|
import {enableRetrieveHydrationInfoImpl, NGH_DATA_KEY, SSR_CONTENT_INTEGRITY_MARKER} from './utils';
|
|
import {enableFindMatchingDehydratedViewImpl} from './views';
|
|
|
|
/**
|
|
* Indicates whether the hydration-related code was added,
|
|
* prevents adding it multiple times.
|
|
*/
|
|
let isHydrationSupportEnabled = false;
|
|
|
|
/**
|
|
* Defines a period of time that Angular waits for the `ApplicationRef.isStable` to emit `true`.
|
|
* If there was no event with the `true` value during this time, Angular reports a warning.
|
|
*/
|
|
const APPLICATION_IS_STABLE_TIMEOUT = 10_000;
|
|
|
|
/**
|
|
* Brings the necessary hydration code in tree-shakable manner.
|
|
* The code is only present when the `provideClientHydration` is
|
|
* invoked. Otherwise, this code is tree-shaken away during the
|
|
* build optimization step.
|
|
*
|
|
* This technique allows us to swap implementations of methods so
|
|
* tree shaking works appropriately when hydration is disabled or
|
|
* enabled. It brings in the appropriate version of the method that
|
|
* supports hydration only when enabled.
|
|
*/
|
|
function enableHydrationRuntimeSupport() {
|
|
if (!isHydrationSupportEnabled) {
|
|
isHydrationSupportEnabled = true;
|
|
enableRetrieveHydrationInfoImpl();
|
|
enableLocateOrCreateElementNodeImpl();
|
|
enableLocateOrCreateTextNodeImpl();
|
|
enableLocateOrCreateElementContainerNodeImpl();
|
|
enableLocateOrCreateContainerAnchorImpl();
|
|
enableLocateOrCreateContainerRefImpl();
|
|
enableFindMatchingDehydratedViewImpl();
|
|
enableApplyRootElementTransformImpl();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Outputs a message with hydration stats into a console.
|
|
*/
|
|
function printHydrationStats(injector: Injector) {
|
|
const console = injector.get(Console);
|
|
const message = `Angular hydrated ${ngDevMode!.hydratedComponents} component(s) ` +
|
|
`and ${ngDevMode!.hydratedNodes} node(s), ` +
|
|
`${ngDevMode!.componentsSkippedHydration} component(s) were skipped. ` +
|
|
`Note: this feature is in Developer Preview mode. ` +
|
|
`Learn more at https://angular.io/guide/hydration.`;
|
|
// tslint:disable-next-line:no-console
|
|
console.log(message);
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns a Promise that is resolved when an application becomes stable.
|
|
*/
|
|
function whenStableWithTimeout(appRef: ApplicationRef, injector: Injector): Promise<void> {
|
|
const whenStablePromise = whenStable(appRef);
|
|
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
|
|
const timeoutTime = APPLICATION_IS_STABLE_TIMEOUT;
|
|
const console = injector.get(Console);
|
|
const ngZone = injector.get(NgZone);
|
|
|
|
// The following call should not and does not prevent the app to become stable
|
|
// We cannot use RxJS timer here because the app would remain unstable.
|
|
// This also avoids an extra change detection cycle.
|
|
const timeoutId = ngZone.runOutsideAngular(() => {
|
|
return setTimeout(() => logWarningOnStableTimedout(timeoutTime, console), timeoutTime);
|
|
});
|
|
|
|
whenStablePromise.finally(() => clearTimeout(timeoutId));
|
|
}
|
|
|
|
return whenStablePromise;
|
|
}
|
|
|
|
/**
|
|
* Returns a set of providers required to setup hydration support
|
|
* for an application that is server side rendered. This function is
|
|
* included into the `provideClientHydration` public API function from
|
|
* the `platform-browser` package.
|
|
*
|
|
* The function sets up an internal flag that would be recognized during
|
|
* the server side rendering time as well, so there is no need to
|
|
* configure or change anything in NgUniversal to enable the feature.
|
|
*/
|
|
export function withDomHydration(): EnvironmentProviders {
|
|
return makeEnvironmentProviders([
|
|
{
|
|
provide: IS_HYDRATION_DOM_REUSE_ENABLED,
|
|
useFactory: () => {
|
|
let isEnabled = true;
|
|
if (isPlatformBrowser()) {
|
|
// On the client, verify that the server response contains
|
|
// hydration annotations. Otherwise, keep hydration disabled.
|
|
const transferState = inject(TransferState, {optional: true});
|
|
isEnabled = !!transferState?.get(NGH_DATA_KEY, null);
|
|
if (!isEnabled && (typeof ngDevMode !== 'undefined' && ngDevMode)) {
|
|
const console = inject(Console);
|
|
const message = formatRuntimeError(
|
|
RuntimeErrorCode.MISSING_HYDRATION_ANNOTATIONS,
|
|
'Angular hydration was requested on the client, but there was no ' +
|
|
'serialized information present in the server response, ' +
|
|
'thus hydration was not enabled. ' +
|
|
'Make sure the `provideClientHydration()` is included into the list ' +
|
|
'of providers in the server part of the application configuration.');
|
|
// tslint:disable-next-line:no-console
|
|
console.warn(message);
|
|
}
|
|
}
|
|
if (isEnabled) {
|
|
inject(ENABLED_SSR_FEATURES).add('hydration');
|
|
}
|
|
return isEnabled;
|
|
},
|
|
},
|
|
{
|
|
provide: ENVIRONMENT_INITIALIZER,
|
|
useValue: () => {
|
|
// Since this function is used across both server and client,
|
|
// make sure that the runtime code is only added when invoked
|
|
// on the client. Moving forward, the `isPlatformBrowser` check should
|
|
// be replaced with a tree-shakable alternative (e.g. `isServer`
|
|
// flag).
|
|
if (isPlatformBrowser() && inject(IS_HYDRATION_DOM_REUSE_ENABLED)) {
|
|
verifySsrContentsIntegrity();
|
|
enableHydrationRuntimeSupport();
|
|
}
|
|
},
|
|
multi: true,
|
|
},
|
|
{
|
|
provide: PRESERVE_HOST_CONTENT,
|
|
useFactory: () => {
|
|
// Preserve host element content only in a browser
|
|
// environment and when hydration is configured properly.
|
|
// On a server, an application is rendered from scratch,
|
|
// so the host content needs to be empty.
|
|
return isPlatformBrowser() && inject(IS_HYDRATION_DOM_REUSE_ENABLED);
|
|
}
|
|
},
|
|
{
|
|
provide: APP_BOOTSTRAP_LISTENER,
|
|
useFactory: () => {
|
|
if (isPlatformBrowser() && inject(IS_HYDRATION_DOM_REUSE_ENABLED)) {
|
|
const appRef = inject(ApplicationRef);
|
|
const injector = inject(Injector);
|
|
return () => {
|
|
// Wait until an app becomes stable and cleanup all views that
|
|
// were not claimed during the application bootstrap process.
|
|
// The timing is similar to when we start the serialization process
|
|
// on the server.
|
|
//
|
|
// Note: the cleanup task *MUST* be scheduled within the Angular zone
|
|
// to ensure that change detection is properly run afterward.
|
|
whenStableWithTimeout(appRef, injector).then(() => {
|
|
NgZone.assertInAngularZone();
|
|
cleanupDehydratedViews(appRef);
|
|
|
|
if (typeof ngDevMode !== 'undefined' && ngDevMode) {
|
|
printHydrationStats(injector);
|
|
}
|
|
});
|
|
};
|
|
}
|
|
return () => {}; // noop
|
|
},
|
|
multi: true,
|
|
}
|
|
]);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param time The time in ms until the stable timedout warning message is logged
|
|
*/
|
|
function logWarningOnStableTimedout(time: number, console: Console): void {
|
|
const message =
|
|
`Angular hydration expected the ApplicationRef.isStable() to emit \`true\`, but it ` +
|
|
`didn't happen within ${
|
|
time}ms. Angular hydration logic depends on the application becoming stable ` +
|
|
`as a signal to complete hydration process.`;
|
|
|
|
console.warn(formatRuntimeError(RuntimeErrorCode.HYDRATION_STABLE_TIMEDOUT, message));
|
|
}
|
|
|
|
/**
|
|
* Verifies whether the DOM contains a special marker added during SSR time to make sure
|
|
* there is no SSR'ed contents transformations happen after SSR is completed. Typically that
|
|
* happens either by CDN or during the build process as an optimization to remove comment nodes.
|
|
* Hydration process requires comment nodes produced by Angular to locate correct DOM segments.
|
|
* When this special marker is *not* present - throw an error and do not proceed with hydration,
|
|
* since it will not be able to function correctly.
|
|
*
|
|
* Note: this function is invoked only on the client, so it's safe to use DOM APIs.
|
|
*/
|
|
function verifySsrContentsIntegrity(): void {
|
|
const doc = getDocument();
|
|
let hydrationMarker: Node|undefined;
|
|
for (const node of doc.body.childNodes) {
|
|
if (node.nodeType === Node.COMMENT_NODE &&
|
|
node.textContent?.trim() === SSR_CONTENT_INTEGRITY_MARKER) {
|
|
hydrationMarker = node;
|
|
break;
|
|
}
|
|
}
|
|
if (!hydrationMarker) {
|
|
throw new RuntimeError(
|
|
RuntimeErrorCode.MISSING_SSR_CONTENT_INTEGRITY_MARKER,
|
|
typeof ngDevMode !== 'undefined' && ngDevMode &&
|
|
'Angular hydration logic detected that HTML content of this page was modified after it ' +
|
|
'was produced during server side rendering. Make sure that there are no optimizations ' +
|
|
'that remove comment nodes from HTML enabled on your CDN. Angular hydration ' +
|
|
'relies on HTML produced by the server, including whitespaces and comment nodes.');
|
|
}
|
|
}
|