mini-css-extract-plugin icon indicating copy to clipboard operation
mini-css-extract-plugin copied to clipboard

link element load event not fired on old browser.

Open lili21 opened this issue 6 years ago • 9 comments

MCEP use load event to resolve the promise, while the event is not supported well on some old browser, like Android 4.3 webview.

It breaks the webpack dynamic import feature, cause the promise will never be resolved.

// index.js
import('./app.js').then(() => {
  // the function will never be called
  console.log('App Loaded')
})
// app.js
import './app.css'
console.log('Hello, app')

Reproduce repo

lili21 avatar Oct 11 '18 06:10 lili21

@lili21 webpack everywhere uses Promises for loading async chunk, please use Promise polyfills, also your example about loading async js file, it is not related to this repo. If you think webpack doing something wrong please create issue in webpack repo

alexander-akait avatar Oct 11 '18 10:10 alexander-akait

@evilebottnawi I don't think you understand the issue. maybe it's my fault, my english isn't very good.

It's not about Promise polyfills, and it's not the async js file issue. If you remove the import './app.css' code, it will works as expecting.

and I think the issue is relate to this repo. as I mentioned before, the plugin using load event to detect the async css file be loaded and resolve the promise, but load event isn't supported well.

the example code logic is something like below,

const p1 = loadAsyncJs()
const p2 = loadAsyncCss()

Promise.all([p1, p2]).then(() => {
  // the function will never be called
  console.log('App Loaded')
})

and p2 will never be resolved.

lili21 avatar Oct 11 '18 15:10 lili21

@lili21 can you create minimum reproducible test repo and affected browsers?

alexander-akait avatar Oct 11 '18 15:10 alexander-akait

Yeah. here you go

I only tested it on Android mobile. Android 4.4+ is good. Android 4.3 isn't.

lili21 avatar Oct 11 '18 16:10 lili21

@lili21 Looks related https://github.com/webpack-contrib/mini-css-extract-plugin/pull/134, but solution is not very good, wee need search better, feel free to investigate

alexander-akait avatar Oct 11 '18 16:10 alexander-akait

same error here. When use webpack dynamic import(), all created css is not loaded in old android 4.3 browsers, which lead to async module not resolved.

moyus avatar Oct 16 '18 15:10 moyus

You can extract all css in a single file to prevent this, But it's bad for performance.

lili21 avatar Oct 17 '18 02:10 lili21

@lili21 I wouldn't say it's necessarily bad for performance as we have a single CSS chunk that contains the entire applications CSS at 7kb. Depending on the final size the argument can be made that the additional requests may not be necessary. CSS Modules and a well groomed base styleguide can do wonders 🎉

visormatt avatar Oct 17 '18 19:10 visormatt

I present to you this horribleness as a workaround, which does work on Chrome 14, one of the affected browsers according to https://pie.gd/test/script-link-events/:

image

I did not actually test this with mini-css-extract-plugin, but I did write the same code as the one mini-css-extract-plugin would try to run when you import() an extracted file. The important code is inside function polyfill():

// @ts-check

// Used for the test code
import 'core-js/fn/promise'

// Needed for the polyfill itself
// Chrome 14 didn't have this either
import MutationObserver from 'mutation-observer'

polyfill()
// Fun fact: can't import this over https, github's https is too new for Chrome 14
testImport('http://necolas.github.io/normalize.css/8.0.1/normalize.css').then(
  function() {
    console.log('Promise resolved!')
  }
)

/**
 *
 * @param {string} url
 */
function testImport(url) {
  // this code is the same as the one the webpack runtime will emit
  return new Promise(function(resolve) {
    var link = document.createElement('link')
    link.rel = 'stylesheet'
    link.type = 'text/css'
    link.crossOrigin = 'anonymous'
    link.onload = resolve
    link.href = url
    document.getElementsByTagName('head')[0].appendChild(link)
  })
}

function polyfill() {
  var webkitVersionMatch = navigator.userAgent.match(
    /AppleWebKit\/(\d+(?:\.\d+)+)/
  )
  var webkitVersion = webkitVersionMatch && parseFloat(webkitVersionMatch[1])
  // 535.24 is the first version listed as working
  // in https://pie.gd/test/script-link-events/
  if (webkitVersion === null || webkitVersion >= 535.24) {
    return
  }
  var copyCrossOrigin = document.createElement('link').crossOrigin !== undefined

  var mo = new MutationObserver(function(events) {
    events.forEach(function(event) {
      event.addedNodes.forEach(function(node) {
        // prettier-ignore
        if (/** @type {Element} */ (node).tagName === 'LINK') {
          handleLinkTag(/** @type {HTMLLinkElement} */ (node))
        }
      })
    })
  })
  mo.observe(document.getElementsByTagName('head')[0], {
    childList: true
  })

  /**
   * @param {HTMLLinkElement} tag
   */
  function handleLinkTag(tag) {
    if (
      tag.rel !== 'stylesheet' ||
      tag.type !== 'text/css' ||
      tag.onload === null
    ) {
      // don't care
      return
    }
    setTimeout(dispatch)

    function dispatch() {
      var img = new Image()
      // webpack doesn't care about the resolved value, only that it resolved
      img.onerror = /** @type {() => void} */ (tag.onload)
      img.onload = tag.onload
      // NOTE: only copy crossOrigin if the browser supports crossOrigin to begin with
      if (copyCrossOrigin) {
        img.crossOrigin = tag.crossOrigin
      }
      img.src = tag.href
    }
  }
}

This does, of course, make it impossible to detect real errors when loading the script; the Promise will always resolve.

You can replace the contents of the dispatch function with your favorite alternative hack if the Image tag hack isn't good enough 😉

Jessidhia avatar Jan 18 '19 10:01 Jessidhia