/
githubmirror
/
amphtml
Обзор
Документация
Войти
/
githubmirror
/
amphtml
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
examples/pwa/pwa.js
509 строк
13 KB
Jake Fried
lint: support typescript (#37324)
12 янв 2022, 03:15
Не верифицирован
12 янв 2022, 03:15
b2e9a06
Код
Авторство
О чём код?
'use strict'; function log() { var var_args = Array.prototype.slice.call(arguments, 0); var_args.unshift('[SHELL]'); console /*OK*/.log .apply(console, var_args); } function startsWith(string, prefix) { return string.lastIndexOf(prefix, 0) == 0; } class Shell { constructor(win, useStreaming) { /** @private @const {!Window} */ this.win = win; /** @private @const {boolean} */ this.useStreaming_ = useStreaming; /** @private @const {!AmpViewer} */ this.ampViewer_ = new AmpViewer( win, win.document.getElementById('doc-container') ); /** @private {string} */ this.currentPage_ = win.location.pathname; this.sidebarCloseButton_ = document.querySelector('#sidebarClose'); win.addEventListener('popstate', this.handlePopState_.bind(this)); win.document.documentElement.addEventListener( 'click', this.handleNavigate_.bind(this) ); log('Shell created'); if (this.currentPage_ && !isShellUrl(this.currentPage_)) { this.navigateTo(this.currentPage_); } else if (this.win.location.hash) { const hashParams = parseQueryString(this.win.location.hash); const href = hashParams['href']; if (href) { this.currentPage_ = href; this.navigateTo(href); } } // Install service worker this.registerServiceWorker_(); } registerServiceWorker_() { if ('serviceWorker' in navigator) { log('Register service worker'); navigator.serviceWorker .register('/pwa/pwa-sw.js') .then((reg) => { log('Service worker registered: ', reg); }) .catch((err) => { log('Service worker registration failed: ', err); }); } } unregisterServiceWorker_() { if ('serviceWorker' in navigator) { log('Register service worker'); navigator.serviceWorker.getRegistration('/pwa/pwa-sw.js').then((reg) => { log('Service worker found: ', reg); reg.unregister(); log('Service worker unregistered'); }); } } /** * @param {!Event} e */ handleNavigate_(e) { if (e.defaultPrevented) { return false; } if (e.button) { return false; } let a = e.target; while (a) { if (a.tagName == 'A' && a.href) { break; } a = a.parentElement; } if (a) { const url = new URL(a.href); const {location} = this.win; if ( url.origin == location.origin && startsWith(url.pathname, '/pwa/') && url.pathname.indexOf('amp.html') != -1 ) { e.preventDefault(); const newPage = url.pathname + location.search; log('Internal link to: ', newPage); if (newPage != this.currentPage_) { this.closeSidebar().then(() => this.navigateTo(newPage)); } } } } handlePopState_() { const newPage = this.win.location.pathname; log('Pop state: ', newPage, this.currentPage_); if (newPage != this.currentPage_) { this.navigateTo(newPage); } } /** * @param {string} path * @return {!Promise} */ navigateTo(path) { log('Navigate to: ', path); const oldPage = this.currentPage_; this.currentPage_ = path; // Update URL. const push = !isShellUrl(path) && isShellUrl(oldPage); if (path != this.win.location.pathname) { if (push) { this.win.history.pushState(null, '', path); } else { this.win.history.replaceState(null, '', path); } } if (isShellUrl(path)) { log('Back to shell'); this.ampViewer_.clear(); return Promise.resolve(); } // Fetch. const url = this.resolveUrl_(path); log('Fetch and render doc:', path, url); // TODO(dvoytenko, #9490): Make `streamDocument` the only used API once // streaming is graduated out of experimental. if (this.useStreaming_) { log('Streaming started: ', url); return this.ampViewer_ .showAsStream(url) .then((shadowDoc) => streamDocument(url, shadowDoc.writer)); } return fetchDocument(url).then((doc) => { log('Fetch complete: ', doc); return this.ampViewer_.show(doc, url); }); } /** * @param {string} url * @return {string} */ resolveUrl_(url) { if (!this.a_) { this.a_ = this.win.document.createElement('a'); } this.a_.href = url; return this.a_.href; } closeSidebar() { if (this.sidebarCloseButton_) { return new Promise((resolve) => { this.sidebarCloseButton_.click(); // TODO implement a better method to detect when // closing sidebar has finished setTimeout(() => resolve(), 100); }); } else { return Promise.resolve(); } } } class AmpViewer { constructor(win, container) { /** @private @const {!Window} */ this.win = win; /** @private @const {!Element} */ this.container = container; win.AMP_SHADOW = true; const ampReadyPromise = new Promise((resolve) => { (window.AMP = window.AMP || []).push(resolve); }); ampReadyPromise.then((AMP) => { log('AMP LOADED:', AMP); }); const isShadowDomSupported = Element.prototype.attachShadow || Element.prototype.createShadowRoot; const shadowDomReadyPromise = new Promise((resolve) => { if (isShadowDomSupported) { resolve(); } else if ( this.win.document.querySelector('script[src*=webcomponents]') ) { this.win.addEventListener('WebComponentsReady', resolve); } else { // AMP polyfills small part of SD spec. It's functional, but some things // (e.g. slots) are not available. resolve(); } }); this.readyPromise_ = Promise.all([ ampReadyPromise, shadowDomReadyPromise, ]).then((results) => results[0]); /** @private @const {string} */ this.baseUrl_ = null; /** @private @const {?Element} */ this.host_ = null; /** @private @const {...} */ this.amp_ = null; // Immediately install amp-shadow.js. this.installScript_('/dist/shadow-v0.js', '/dist/amp-shadow.js'); } clear() { if (this.amp_) { this.amp_.close(); this.amp_ = null; } this.container.textContent = ''; } /** * @param {!Document} doc * @param {string} url */ show(doc, url) { log('Show document:', doc, url); // Cleanup the existing document if any. this.clear(); this.baseUrl_ = url; this.host_ = this.win.document.createElement('div'); this.host_.classList.add('amp-doc-host'); const hostTemplate = this.win.document.getElementById('amp-slot-template'); if (hostTemplate) { this.host_.appendChild(hostTemplate.content.cloneNode(true)); } this.container.appendChild(this.host_); return this.readyPromise_.then((AMP) => { this.amp_ = AMP.attachShadowDoc(this.host_, doc, url, {}); this.win.document.title = this.amp_.title || ''; this.amp_.onMessage(this.onMessage_.bind(this)); this.amp_.setVisibilityState('visible'); }); } /** * @param {string} url * @return {!Promise<!ShadowDoc>} */ showAsStream(url) { log('Show stream document:', url); // Cleanup the existing document if any. this.clear(); this.baseUrl_ = url; this.host_ = this.win.document.createElement('div'); this.host_.classList.add('amp-doc-host'); const hostTemplate = this.win.document.getElementById('amp-slot-template'); if (hostTemplate) { this.host_.appendChild(hostTemplate.content.cloneNode(true)); } this.container.appendChild(this.host_); return this.readyPromise_.then((AMP) => { this.amp_ = AMP.attachShadowDocAsStream(this.host_, url, {}); this.win.document.title = this.amp_.title || ''; this.amp_.onMessage(this.onMessage_.bind(this)); this.amp_.setVisibilityState('visible'); return this.amp_; }); } /** * @param {string} src * @param {string=} fallbackSrc * @param {string=} customElement * @param {string=} customTemplate */ installScript_(src, fallbackSrc, customElement, customTemplate) { const doc = this.win.document; const el = doc.createElement('script'); el.setAttribute('src', src); if (customElement) { el.setAttribute('custom-element', customElement); } if (customTemplate) { el.setAttribute('custom-template', customTemplate); } el.onload = () => { log('- script added: ', src, el); }; el.onerror = () => { log('- script failed to load: ', src, el); doc.head.removeChild(el); if (fallbackSrc) { this.installScript_( fallbackSrc, undefined, customElement, customTemplate ); } }; doc.head.appendChild(el); } /** * @param {string} url * @return {string} */ resolveUrl_(relativeUrlString) { return new URL(relativeUrlString, this.baseUrl_).toString(); } /** * @param {string} url * @return {string} */ getOrigin_(relativeUrlString) { return new URL(relativeUrlString, this.baseUrl_).origin; } onMessage_() {} } /** * @param {string} url * @return {boolean} */ function isShellUrl(url) { return url == '/pwa' || url == '/pwa/'; } /** * @param {string} url * @return {!Promise<!Document>} */ function fetchDocument(url) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.responseType = 'document'; xhr.setRequestHeader('Accept', 'text/html'); xhr.onreadystatechange = () => { if (xhr.readyState < /* STATUS_RECEIVED */ 2) { return; } if (xhr.status < 100 || xhr.status > 599) { xhr.onreadystatechange = null; reject(new Error(`Unknown HTTP status ${xhr.status}`)); return; } if (xhr.readyState == /* COMPLETE */ 4) { if (xhr.responseXML) { resolve(xhr.responseXML); } else { reject(new Error(`No xhr.responseXML`)); } } }; xhr.onerror = () => { reject(new Error('Network failure')); }; xhr.onabort = () => { reject(new Error('Request aborted')); }; xhr.send(); }); } /** * @param {string} url * @param {!WritableStreamDefaultWriter} writer * @return {!Promise} */ function streamDocument(url, writer) { // Try native first. if (window.fetch && window.TextDecoder && window.ReadableStream) { return fetch(url).then((response) => { // This should be a lot simpler with transforming streams and pipes, // but, TMK, these are not supported anywhere yet. const /** !ReadableStreamDefaultReader */ reader = response.body.getReader(); const decoder = new TextDecoder(); function readChunk(chunk) { const text = decoder.decode(chunk.value || new Uint8Array(), { stream: !chunk.done, }); if (text) { writer.write(text); } if (chunk.done) { writer.close(); } else { return reader.read().then(readChunk); } } return reader.read().then(readChunk); }); } // Polyfill via XHR. return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.setRequestHeader('Accept', 'text/html'); let pos = 0; xhr.onreadystatechange = () => { if (xhr.readyState < /* STATUS_RECEIVED */ 2) { return; } if (xhr.status < 100 || xhr.status > 599) { xhr.onreadystatechange = null; reject(new Error(`Unknown HTTP status ${xhr.status}`)); return; } if ( xhr.readyState == /* LOADING */ 3 || xhr.readyState == /* COMPLETE */ 4 ) { const s = xhr.responseText; const chunk = s.substring(pos); pos = s.length; writer.write(chunk); if (xhr.readyState == /* COMPLETE */ 4) { writer.close().then(resolve); } } }; xhr.onerror = () => { reject(new Error('Network failure')); }; xhr.onabort = () => { reject(new Error('Request aborted')); }; xhr.send(); }); } /** * Parses the query string of an URL. This method returns a simple key/value * map. If there are duplicate keys the latest value is returned. * @param {string} queryString * @return {!Object<string>} */ function parseQueryString(queryString) { const params = Object.create(null); if (!queryString) { return params; } if (startsWith(queryString, '?') || startsWith(queryString, '#')) { queryString = queryString.substr(1); } const pairs = queryString.split('&'); for (let i = 0; i < pairs.length; i++) { const pair = pairs[i]; const eqIndex = pair.indexOf('='); let name; let value; if (eqIndex != -1) { name = decodeURIComponent(pair.substring(0, eqIndex)).trim(); value = decodeURIComponent(pair.substring(eqIndex + 1)).trim(); } else { name = decodeURIComponent(pair).trim(); value = ''; } if (name) { params[name] = value; } } return params; } var shell = new Shell(window, /* useStreaming */ true); // eslint-disable-line @typescript-eslint/no-unused-vars