whatwg / xhr

XMLHttpRequest Standard
https://xhr.spec.whatwg.org/
Other
314 stars 131 forks source link

Synchronous XMLHttpRequest should be allowed in prerender scripts (JS in <head>) #253

Closed clshortfuse closed 3 years ago

clshortfuse commented 5 years ago

So, I'm dealing with an issue with bridging older browsers with the new manifest.json spec. Due to the way the pages are served, manifest.json is served based on certain server criteria (hostname) while index.html is a static file from a CDN.

Basically, before the UI events start, and any interaction is possible, I use a prerender.js script in <head> to style the document before it's rendered. This could include things like setting different content header padding if the page is rendered as an iOS PWA. Also, I use this pre-render state to set some colors based on prefers-color-scheme:dark.

Some properties I want to grab straight from manifest.json. This means it has to be intentionally synchronous. For example, let's say I want to style the background-color of the document (<html>) to be the same as what's in manifest.json. Let's say I have the color as #000 (black). When a PWA loads, it will load a black background, but, if I don't use block the render before changing the background-color in <html>and, instead do this asynchronously, then the background will flash white (browser default), and then go black.

Here's some short snippets of this configuration:

manifest.json:

{
  "name": "Full App Name",
  "short_name": "App",
  "start_url": "/",
  "background_color": "#000000",
  "display": "standalone",
  "theme_color": "#2196F3",
}

index.html:

<html> 
  <head>
    <link rel="manifest" href="manifest.json" />
    <script src="prerender.js">
    <!-- Content snip --!>

prerender.js

import * as Document from '@shortfuse/materialdesignweb/core/document/index';

/** @return {Object} */
function getManifestObject() {
  let manifestObject = {};
  const xhr = new XMLHttpRequest();

  xhr.onreadystatechange = () => {
    if (xhr.readyState === 4) {
      manifestObject = JSON.parse(xhr.responseText);
    }
  };
  xhr.open('GET', 'manifest.json', false);

  try {
    xhr.send();
  } catch (e) {
    console.error('Could not load manifest.json');
  }

  return manifestObject;
}

/**
 * @param {!string} name
 * @param {string} content
 * @return {void}
 */
function setMeta(name, content) {
  let attr = document.head.getElementsByTagName('meta').namedItem(name);
  if (attr) {
    if (content == null) {
      attr.parentElement.removeChild(attr);
    } else {
      attr.setAttribute('content', content);
    }
  } else if (content != null) {
    attr = document.createElement('meta');
    attr.setAttribute('content', content);
    document.head.appendChild(attr);
  }
}

/** @return {void} */
function onDOMContentLoaded() {
  const manifest = getManifestObject();
  setMeta('apple-mobile-web-app-title', manifest.short_name);
  setMeta('apple-mobile-web-app-status-bar-style', manifest.theme_color);
  setMeta('theme-color', manifest.theme_color);
  document.documentElement.style.setProperty('background-color', manifest.background_color);
  document.removeEventListener('DOMContentLoaded', onDOMContentLoaded);
}

Document.onPrerender();
document.addEventListener('DOMContentLoaded', onDOMContentLoaded);

I will add, I already have a service worker in place to serve manifest.json from cache, so ideally speaking, there's as little load-up time lag as possible here. (I'd imagine first fetch could experience a slight uptick in delay, but I could always add a check to see if the application is running as a PWA before doing the synchronous request.)

Right now, I'm wary about using the synchronous request because of the deprecated nature. The alternative I currently have is rewriting the index.html file, which complicates the back-end.

clshortfuse commented 5 years ago

I realized that Chrome will not emit this as a warning if the code is executing in the beforeunload block.

https://chromium.googlesource.com/chromium/src/+/1d328b0e963f7af9e63a4ceeb6bab908b566c848/third_party/blink/renderer/core/xmlhttprequest/xml_http_request.cc#699

Thus, I think it's reasonable to extend this as well to the DOMContentLoaded block.

annevk commented 3 years ago

I'm not sure why Chrome does that, but that's not a good enough reason to change the standard here. Duplicating this into #20.