chrisvfritz/prerender-spa-plugin

On prerendered pages, Webpack injects async <script> that requires webpackJsonp to be defined

drewlustro opened this issue ยท 10 comments

I have a vue 1.x application that is using prerender-spa-plugin. When generating the index.html files for each route on build, webpack will sometimes inject <script src="/static/js/0.[hash].js" async=""></script> into the <head></head>.

This is normal and expected, as webpack optimizes for loading additional chunks on-demand.


A problem surfaces when visiting the endpoint, because the browser tries tries to evaluate the script 0.[hash].js before manifest.js or vendor.js chunks, and reliably causes an exception:

Uncaught ReferenceError: webpackJsonp is not defined.

Is there a flag or option to temporarily prevent webpack from injecting on-demand chunks? Or must we get hacky and scrub the <script /> tags from prerendered indexes? I cannot naively set the captureAfterTime: 0, as I rely on other aspects of the page asynchronously rendering.

It sounds like you just need to change where (or in what order) scripts are getting injected. If you're using html-webpack-plugin, you may find the inject or chunksSortMode options useful.

@chrisvfritz , your suggestion would address my problem if html-webpack-plugin was to blame. It is behaving correctly and as expected. Perhaps there's a misunderstanding of my problem.

To clarify:

After html-webpack-plugin:

index.html emitted with approximate structure:

<!DOCTYPE html>
<html>
<head>
    <title>Website</title>
    <link href="/static/css/app.65a8b165ef01db392fbe347dbb15474e.css" rel="stylesheet">
</head>
<body>
    <div id="app"></div>
    <script type="text/javascript" src="/static/js/manifest.678ccd30da482f0c1dfc.js"></script>
    <script type="text/javascript" src="/static/js/vendor.eed2bc873a3067e62761.js"></script>
    <script type="text/javascript" src="/static/js/app.7c3e217f3fa2b2b68deb.js"></script>
</body>
</html>

Everything works and is ๐Ÿ’ฏ .

After prerender-spa-plugin:

some-endpoint/index.html emitted with option captureAfterTime: 3000:

<!DOCTYPE html>
<html>
<head>
    <title>Website</title>
    <link href="/static/css/app.65a8b165ef01db392fbe347dbb15474e.css" rel="stylesheet">
    <script type="text/javascript" charset="utf-8" async="" src="/static/js/0.e62b0759da64f0dbfc4c.js"></script>
    <style type="text/css">
</head>
<body>
    <div id="app"></div>
    <script type="text/javascript" src="/static/js/manifest.678ccd30da482f0c1dfc.js"></script>
    <script type="text/javascript" src="/static/js/vendor.eed2bc873a3067e62761.js"></script>
    <script type="text/javascript" src="/static/js/app.7c3e217f3fa2b2b68deb.js"></script>
</body>
</html>

๐Ÿ‘Ž Broken due to the only pivotal difference: a new <script type="text/javascript" charset="utf-8" async="" src="/static/js/0.e62b0759da64f0dbfc4c.js"></script> injected into <head>.

My understanding of what's happening

  • PhantomJS client views a well-formed, vue+webpack powered endpoint and asks for more chunks, namely 0.e62b0759da64f0dbfc4c.js
  • Chunks get injected into <head> (as expected)
  • Resulting final HTML is captured by prerender-spa-plugin and saved to a destination some-endpoint/index.html
  • Final HTML for some-endpoint/index.html is unusable because it possess a rouge <script> tag in <head> that assumes it was injected after evaluating the manifest, app, and vendor JS.

Sidenote: I tried your suggestion for html-webpack-plugin and set inject: 'head'. It breaks the page before prerender-spa-plugin gets to it, most likely because the DOM is not guaranteed to be ready.

Thanks for clarifying. Unfortunately, Webpack hard-codes it in that split chunks get appended to the head rather than the body. I think your options are to either:

  • Fork Webpack and modify JsonpMainTemplate.prototype.renderRequireEnsure to append to the body instead.
  • Submit a PR to Webpack to add an option changing where the script gets injected.
  • Submit a PR to this repo to allow an arbitrary function to be run within the document directly before page capture. In that function, you could manually remove the script tag from the head - or move it to the body instead.

Damn. Thanks for listing those fine options, my friend.

Imma pray on this for a bit and decide on monday what I'll do.

@drewlustro Assuming you are using the HtmlWebpackPlugin and as an alternative, you can set inject: 'head' for the plugin to add all of your scripts (manifest, vendor, and app) into the <head>. Webpack will place it's async script right after yours, which will make the app work. If you end up using this, as I have, you'll also probably want to put your initial new Vue( inside a document.addEventListener('DOMContentLoaded', () => {, since the script in the head will run before the DOM is loaded.

bahahaha @nemtsov ๐Ÿ’ฏ that works and is much more elegant.

I had checked your (pre-edit) approach, and was stressed because my pages were still broken. The DOMContentLoaded event listener patched the issue easy. Are there any other drawbacks to this approach? The only minor thing I can think of is that <script> tags within <head> are blocking on super-legacy browsers (not important for me).

If no other drawbacks are known, it looks like my #10 PR is unnecessary.

@drewlustro If you don't find any other drawbacks, I'd still be happy to accept a PR documenting this approach. ๐Ÿ˜ƒ

@nemtsov I don't think that will fix the problem. async script will be executed immediately after the file be loaded. the run order is still unpredictable.

is this issue has been fixed?

Actually, the solution is easy , just go where u added the spa prerender plugin, and add ignoreJSErrors: true:
new PrerenderSpaPlugin(
// Path to compiled app
path.join(__dirname, '../dist'),
// List of endpoints you wish to prerender
[ '/'],{
ignoreJSErrors: true
}
)