/
githubmirror
/
amphtml
Обзор
Документация
Войти
/
githubmirror
/
amphtml
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/multidoc-manager.js
558 строк
18 KB
Viktor Szépe
Fix typos in src/ (#39606)
12 авг 2025, 21:35
Не верифицирован
12 авг 2025, 21:35
fca692e
Код
Авторство
О чём код?
import {CommonSignals_Enum} from '#core/constants/common-signals'; import {VisibilityState_Enum} from '#core/constants/visibility-state'; import {isConnectedNode} from '#core/dom'; import {childElementsByTag} from '#core/dom/query'; import {setStyle} from '#core/dom/style'; import {isArray, isObject} from '#core/types'; import {Services} from '#service'; import {parseExtensionUrl} from '#service/extension-script'; import {dev, user} from '#utils/log'; import {getMode} from './mode'; import { disposeServicesForDoc, getServicePromiseOrNullForDoc, } from './service-helpers'; import { createShadowDomWriter, createShadowRoot, importShadowBody, } from './shadow-embed'; import {installStylesForDoc} from './style-installer'; import {parseUrlDeprecated} from './url'; /** @const @private {string} */ const TAG = 'multidoc-manager'; /** * A manager for documents in the multi-doc environment. */ export class MultidocManager { /** * @param {!Window} win * @param {!./service/ampdoc-impl.AmpDocService} ampdocService * @param {!./service/extensions-impl.Extensions} extensions * @param {!./service/timer-impl.Timer} timer */ constructor(win, ampdocService, extensions, timer) { /** @const */ this.win = win; /** @private @const */ this.ampdocService_ = ampdocService; /** @private @const */ this.extensions_ = extensions; /** @private @const */ this.timer_ = timer; /** @private @const {!Array<!ShadowRoot>} */ this.shadowRoots_ = []; } /** * Attaches the shadow root and calls the supplied DOM builder. * @param {!Element} hostElement * @param {string} url * @param {!{[key: string]: string}|undefined} params * @param {function(!Object, !ShadowRoot, * !./service/ampdoc-impl.AmpDocShadow):!Promise} builder * @return {!./runtime.ShadowDoc} * @private */ attachShadowDoc_(hostElement, url, params, builder) { params = params || Object.create(null); this.purgeShadowRoots_(); setStyle(hostElement, 'visibility', 'hidden'); const shadowRoot = createShadowRoot(hostElement); // TODO: closeShadowRoot_ is asynchronous. While this safety check is well // intentioned, it leads to a race between unlayout and layout of custom // elements. if (shadowRoot.AMP) { user().warn(TAG, "Shadow doc wasn't previously closed"); this.closeShadowRoot_(shadowRoot); } const amp = {}; shadowRoot.AMP = amp; amp.url = url; const {origin} = parseUrlDeprecated(url); const ampdoc = this.ampdocService_.installShadowDoc(url, shadowRoot, { params, }); /** @const {!./service/ampdoc-impl.AmpDocShadow} */ amp.ampdoc = ampdoc; dev().fine(TAG, 'Attach to shadow root:', shadowRoot, ampdoc); // Install runtime CSS. installStylesForDoc( ampdoc, AMP.combinedCss, /* callback */ null, /* opt_isRuntimeCss */ true ); // Instal doc services. AMP.installAmpdocServices(ampdoc); const viewer = Services.viewerForDoc(ampdoc); /** * Sets the document's visibility state. * @param {!VisibilityState_Enum} state */ amp['setVisibilityState'] = function (state) { ampdoc.overrideVisibilityState(state); }; // Messaging pipe. /** * Posts message to the ampdoc. * @param {string} eventType * @param {!JsonObject} data * @param {boolean} unusedAwaitResponse * @return {(!Promise<*>|undefined)} */ amp['postMessage'] = viewer.receiveMessage.bind(viewer); /** @type {?function(string, *, boolean):(!Promise<*>|undefined)|undefined} */ let onMessage; /** * Provides a message delivery mechanism by which AMP document can send * messages to the viewer. * @param {?function(string, *, boolean):(!Promise<*>|undefined)} callback */ amp['onMessage'] = function (callback) { onMessage = callback; }; viewer.setMessageDeliverer((eventType, data, awaitResponse) => { // Special messages. if (eventType == 'broadcast') { this.broadcast_(data, shadowRoot); return awaitResponse ? Promise.resolve() : undefined; } // All other messages. return onMessage?.(eventType, data, awaitResponse); }, origin); /** * Closes the document, resolving when visibility changes and services have * been cleaned up. The document can no longer be activated again. * @return {Promise} */ amp['close'] = () => { return this.closeShadowRoot_(shadowRoot); }; if (getMode().development) { amp.toggleRuntime = viewer.toggleRuntime.bind(viewer); amp.resources = Services.resourcesForDoc(ampdoc); } /** * Expose amp-bind getState * @param {string} name - Name of state or deep state * @return {Promise<*>} - Resolves to a copy of the value of a state */ amp['getState'] = (name) => { return Services.bindForDocOrNull(shadowRoot).then((bind) => { if (!bind) { return Promise.reject('amp-bind is not available in this document'); } return bind.getState(name); }); }; /** * Expose amp-bind setState * @param {(!JsonObject|string)} state - State to be set * @return {Promise} - Resolves after state and history have been updated */ amp['setState'] = (state) => { return Services.bindForDocOrNull(shadowRoot).then((bind) => { if (!bind) { return Promise.reject('amp-bind is not available in this document'); } if (typeof state === 'string') { return bind.setStateWithExpression( /** @type {string} */ (state), /** @type {!JsonObject} */ ({}) ); } else if (isObject(state) || isArray(state)) { return bind.setStateWithObject(/** @type {!JsonObject} */ (state)); } return Promise.reject('Invalid state'); }); }; // Start building the shadow doc DOM. builder(amp, shadowRoot, ampdoc).then(() => { // Document is ready. ampdoc.setReady(); ampdoc.signals().signal(CommonSignals_Enum.RENDER_START); setStyle(hostElement, 'visibility', 'visible'); }); // Store reference. if (!this.shadowRoots_.includes(shadowRoot)) { this.shadowRoots_.push(shadowRoot); } dev().fine(TAG, 'Shadow root initialization is done:', shadowRoot, ampdoc); return amp; } /** * Implementation for `attachShadowDoc` function. Attaches the shadow doc and * configures ampdoc for it. * @param {!Element} hostElement * @param {!Document} doc * @param {string} url * @param {!{[key: string]: string}=} opt_initParams * @return {!./runtime.ShadowDoc} */ attachShadowDoc(hostElement, doc, url, opt_initParams) { user().assertString(url); dev().fine(TAG, 'Attach shadow doc:', doc); // TODO(dvoytenko, #9490): once stable, port full document case to emulated // stream. return this.attachShadowDoc_( hostElement, url, opt_initParams, (amp, shadowRoot, ampdoc) => { // Install extensions. this.mergeShadowHead_(ampdoc, shadowRoot, doc); // Append body. if (doc.body) { const body = importShadowBody(shadowRoot, doc.body, /* deep */ true); body.classList.add('amp-shadow'); ampdoc.setBody(body); } // TODO(dvoytenko): find a better and more stable way to make content // visible. E.g. integrate with dynamic classes. In shadow case // specifically, we have to wait for stubbing to complete, which may // take awhile due to importNode. setTimeout(() => { ampdoc.signals().signal(CommonSignals_Enum.RENDER_START); setStyle(hostElement, 'visibility', 'visible'); }, 50); return Promise.resolve(); } ); } /** * Implementation for `attachShadowDocAsStream` function. Attaches the shadow * doc and configures ampdoc for it. * @param {!Element} hostElement * @param {string} url * @param {!{[key: string]: string}=} opt_initParams * @return {!Object} */ attachShadowDocAsStream(hostElement, url, opt_initParams) { user().assertString(url); dev().fine(TAG, 'Attach shadow doc as stream'); return this.attachShadowDoc_( hostElement, url, opt_initParams, (amp, shadowRoot, ampdoc) => { // Start streaming. let renderStarted = false; const writer = createShadowDomWriter(this.win); amp['writer'] = writer; writer.onBody((doc) => { // Install extensions. this.mergeShadowHead_(ampdoc, shadowRoot, doc); // Append shallow body. const body = importShadowBody( shadowRoot, dev().assertElement(doc.body), /* deep */ false ); body.classList.add('amp-shadow'); ampdoc.setBody(body); return body; }); writer.onBodyChunk(() => { // TODO(dvoytenko): find a better and more stable way to make // content visible. E.g. integrate with dynamic classes. In shadow // case specifically, we have to wait for stubbing to complete, // which may take awhile due to node importing. if (!renderStarted) { renderStarted = true; setTimeout(() => { ampdoc.signals().signal(CommonSignals_Enum.RENDER_START); setStyle(hostElement, 'visibility', 'visible'); }, 50); } }); return new Promise((resolve) => { writer.onEnd(() => { resolve(); amp.writer = null; }); }); } ); } /** * Processes the contents of the shadow document's head. * @param {!./service/ampdoc-impl.AmpDoc} ampdoc * @param {!ShadowRoot} shadowRoot * @param {!Document} doc * @private */ mergeShadowHead_(ampdoc, shadowRoot, doc) { if (doc.head) { shadowRoot.AMP.head = doc.head; const parentLinks = {}; const links = childElementsByTag( dev().assertElement(this.win.document.head), 'link' ); for (let i = 0; i < links.length; i++) { const href = links[i].getAttribute('href'); if (href) { parentLinks[href] = true; } } for (let n = doc.head.firstElementChild; n; n = n.nextElementSibling) { const {tagName} = n; const name = n.getAttribute('name'); const rel = n.getAttribute('rel'); switch (tagName) { case 'TITLE': shadowRoot.AMP.title = n.textContent; dev().fine(TAG, '- set title: ', shadowRoot.AMP.title); break; case 'META': if (n.hasAttribute('charset')) { // Ignore. } else if (name == 'viewport') { // Ignore. } else if (name) { // Store meta name/content pairs. ampdoc.setMetaByName(name, n.getAttribute('content') || ''); } else { // TODO(dvoytenko): copy other meta tags. dev().warn(TAG, 'meta ignored: ', n); } break; case 'LINK': /** @const {string} */ const href = n.getAttribute('href'); if (rel == 'canonical') { shadowRoot.AMP.canonicalUrl = href; dev().fine(TAG, '- set canonical: ', shadowRoot.AMP.canonicalUrl); } else if (rel == 'stylesheet') { // Must be a font definition: no other stylesheets are allowed. if (parentLinks[href]) { dev().fine(TAG, '- stylesheet already included: ', href); // To accommodate icon fonts whose stylesheets include // the class definitions in addition to the font definition, // we re-import the stylesheet into the shadow document. // Note: <link> in shadow mode is not yet fully supported on // all browsers, so we use <style>@import "url"</style> instead installStylesForDoc( ampdoc, `@import "${href}"`, /* callback */ null, /* isRuntimeCss */ false ); } else { parentLinks[href] = true; const el = this.win.document.createElement('link'); el.setAttribute('rel', 'stylesheet'); el.setAttribute('type', 'text/css'); el.setAttribute('href', href); this.win.document.head.appendChild(el); dev().fine(TAG, '- import font to parent: ', href, el); } } else { dev().fine(TAG, '- ignore link rel=', rel); } break; case 'STYLE': if (n.hasAttribute('amp-boilerplate')) { // Ignore. dev().fine(TAG, '- ignore boilerplate style: ', n); } else if (n.hasAttribute('amp-custom')) { installStylesForDoc( ampdoc, n.textContent, /* callback */ null, /* isRuntimeCss */ false, 'amp-custom' ); dev().fine(TAG, '- import style: ', n); } else if (n.hasAttribute('amp-keyframes')) { installStylesForDoc( ampdoc, n.textContent, /* callback */ null, /* isRuntimeCss */ false, 'amp-keyframes' ); dev().fine(TAG, '- import style: ', n); } break; case 'SCRIPT': if (n.hasAttribute('src')) { dev().fine(TAG, '- src script: ', n); const src = n.getAttribute('src'); const urlParts = parseExtensionUrl(src); // Note: Some extensions don't have [custom-element] or // [custom-template] e.g. amp-viewer-integration. const customElement = n.getAttribute('custom-element'); const customTemplate = n.getAttribute('custom-template'); const extensionId = customElement || customTemplate; if (!urlParts) { dev().fine(TAG, '- ignore runtime script: ', src); } else if (extensionId) { // This is an extension. this.extensions_.installExtensionForDoc( ampdoc, extensionId, urlParts.extensionVersion ); } else if (!n.hasAttribute('data-amp-report-test')) { user().error(TAG, '- unknown script: ', n, src); } } else { // Non-src version of script. const type = n.getAttribute('type') || 'application/javascript'; if (type.indexOf('javascript') == -1) { shadowRoot.appendChild(this.win.document.importNode(n, true)); dev().fine(TAG, '- non-src script: ', n); } else if (!n.hasAttribute('amp-onerror')) { // Don't error on amp-onerror script (https://github.com/ampproject/amphtml/issues/31966) user().error(TAG, '- unallowed inline javascript: ', n); } } break; case 'NOSCRIPT': // Ignore. break; default: user().error(TAG, '- UNKNOWN head element:', n); break; } } } ampdoc.setExtensionsKnown(); } /** * @param {*} data * @param {!ShadowRoot} sender * @private */ broadcast_(data, sender) { this.purgeShadowRoots_(); this.shadowRoots_.forEach((shadowRoot) => { if (shadowRoot == sender) { // Don't broadcast to the sender. return; } // Broadcast message asynchronously. const viewer = Services.viewerForDoc(shadowRoot.AMP.ampdoc); this.timer_.delay(() => { viewer.receiveMessage( 'broadcast', /** @type {!JsonObject} */ (data), /* awaitResponse */ false ); }, 0); }); } /** * @param {!ShadowRoot} shadowRoot * @return {Promise} * @private */ closeShadowRoot_(shadowRoot) { this.removeShadowRoot_(shadowRoot); const amp = shadowRoot.AMP; delete shadowRoot.AMP; const {ampdoc} = amp; ampdoc.overrideVisibilityState(VisibilityState_Enum.INACTIVE); disposeServicesForDoc(ampdoc); // There is a race between the visibility state change finishing and // resources.onNextPass firing, but this is intentional. closeShadowRoot_ // was traditionally introduced as a synchronous method, so PWAs in the wild // do not expect to have to wait for a promise to resolve before the shadow // is deemed 'closed'. Moving .overrideVisibilityState() and // disposeServicesForDoc inside a promise could adversely affect sites that // depend on at least the synchronous portions of those methods completing // before proceeding. The promise race is designed to be very quick so that // even if the pass callback completes before resources.onNextPass is called // below, we only delay promise resolution by a few ms. return this.timer_ .timeoutPromise( 15, // Delay for queued pass after visibility change is 10ms new this.win.Promise((resolve) => { getServicePromiseOrNullForDoc(ampdoc, 'resources').then( (resources) => { if (resources) { resources.onNextPass(resolve); } else { resolve(); } } ); }), 'Timeout reached waiting for visibility state change callback' ) .catch((error) => { user().info(TAG, error); }); } /** * @param {!ShadowRoot} shadowRoot * @private */ removeShadowRoot_(shadowRoot) { const index = this.shadowRoots_.indexOf(shadowRoot); if (index != -1) { this.shadowRoots_.splice(index, 1); } } /** * @param {!ShadowRoot} shadowRoot * @private */ closeShadowRootAsync_(shadowRoot) { this.timer_.delay(() => { this.closeShadowRoot_(shadowRoot); }, 0); } /** @private */ purgeShadowRoots_() { this.shadowRoots_.forEach((shadowRoot) => { // The shadow root has been disconnected. Force it closed. if (!shadowRoot.host || !isConnectedNode(shadowRoot.host)) { user().warn(TAG, "Shadow doc wasn't previously closed"); this.removeShadowRoot_(shadowRoot); this.closeShadowRootAsync_(shadowRoot); } }); } }