Reputation: 758
I'm partially updating an existing web application with new (react) code and am using webpack to bundle everything together for production. Because the existing HTML page (in fact it is XML converted to HTML) is already there, I can't use the index.html
that is generated by the HtmlWebpackPlugin
.
What I would like to achieve is that webpack generates a small runtime.bundle.js
which will dynamically load the other generated chunks (main.[contenthash]
and vendor.[contenthash]
), instead of adding these entries as script
tags to the index.html
. This way the runtime.bundle.js
can be set to nocache
whereas the large other chunks can be cached by the browser and correctly fetched on code changes.
As an example, here is the body block of the generated index.html
, note the comment:
<html>
<head>...</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="text/javascript" src="runtime.bundle.js"></script>
<!-- I want these two files below not injected as script tags,
but loaded from the runtime.bundle.js file above -->
<script type="text/javascript" src="vendors.31b8acd750477817012d.js"></script>
<script type="text/javascript" src="main.1e4a456d496cdd2e1771.js"></script>
</body>
</html>
The runtime file is already loading a different chunk that is imported dynamically from JS with the following code:
const App = React.lazy(() => import(/* webpackChunkName: "modulex" */ './App'));
This creates the following snippet somewhere within the runtime.bundle.js
a = document.createElement('script');
(a.charset = 'utf-8'),
(a.timeout = 120),
i.nc && a.setAttribute('nonce', i.nc),
(a.src = (function(e) {
return (
i.p +
'' +
({ 1: 'modulex' }[e] || e) +
'.' +
{ 1: '0e0c4000d075e81c1e5b' }[e] +
'.js'
);
So can the same be achieved for the vendors
and main
chunk?
The only other alternative solution that I can think of is to use the WebpackManifestPlugin
to generate the manifest.json
and use this to inject the chunks into the already existing HTML file.
Upvotes: 6
Views: 7152
Reputation: 4093
// https://github.com/webpack/webpack/issues/11816#issuecomment-716402552
declare var __webpack_get_script_filename__: any;
const oldFn = __webpack_get_script_filename__;
__webpack_get_script_filename__ = (chunkId) => {
const filename = oldFn(chunkId);
return filename + `?v=${environment.timestamp}`;
};
Barry run... RUNNNNN... with this super power script!
Upvotes: 0
Reputation: 476
HtmlWebpackPlugin offers a chunks
option that you could use to selectively include certain entries from your webpack config's entry
object. Using that, you could actually simplify most of the logic from your custom script by putting it into a separate src/dynamic-load.js
file, only adding it to the plugin config:
entry: {
runtimeLoader: './src/dynamic-load.js'
},
plugins: [
new HtmlWebpackPlugin({
// ...
chunks: [ 'runtimeLoader' ]
}),
]
(Another example of chunks
usage can be seen here).
It's possible that even their built-in templateParameters
would allow you to put the build output file names into a variable and read them in dynamic-load.js
. You'll have to make your own template for it, but that could be one route. You can even see how their suggested templateParameters
example did it.
If that doesn't work, you could always resort to getting the bundled output file names through webpack itself via the afterEmit
hook and then output those into a JSON file that dynamic-load.js
would call. The gist being something like below, but at that point, you're just doing the same thing WebpackManifestPlugin
does.
plugins: [
{
apply: compiler => {
compiler.hooks.afterEmit.tap('DynamicRuntimeLoader', compilation => {
const outputBundlePaths = Object.keys(compilation.assets)
// output to dist/files.json
saveToOutputDir('files.json', outputBundlePaths);
});
}
},
// ...
]
// dynamic-load.js
fetch('/files.json').then(res => res.json()).then(allFiles => {
allFiles.forEach(file => {
// document.createElement logic
});
});
One final note: WebpackManifestPlugin is actually an assets manifest and doesn't produce a correct manifest.json. They should update their default file name to assets-manifest.json
but I guess no one has pointed this out to them yet.
Upvotes: 1
Reputation: 758
I solved this problem in the end by creating a script that uses the manifest.json
(which is generated by the WebpackManifestPlugin
) to generate a runtime.js
script that will load the chunks dynamically on page load and insert this runtime.js
into the head of an index.html
. This callable from the npm scripts
section using tasksfile npm package.
In you webpack configuration, add the plugin into the plugin array:
{
// your other webpack config
plugins: [
new ManifestPlugin(),
// other webpack plugins you need
],
}
The I have the following external JS file that I can call from my npm scripts
using the tasksfile npm package, which is configured to call this function:
// The path where webpack saves the built files to (this includes manifest.json)
const buildPath = './build';
// The URL prefix where the file should be loaded
const urlPrefix = 'https://www.yourdomain.com';
function buildRuntime() {
const manifest = require(`${buildPath}/manifest`);
// Loop through each js file in manifest file and append as script element to the head
// Execute within an IIFE such that we don't pollute global namespace
let scriptsToLoad = Object.keys(manifest)
.filter(key => key.endsWith('.js'))
.reduce((js, key) => {
return (
js +
`
script = document.createElement('script');
script.src = urlPrefix + "/js/${manifest[key]}";
document.head.appendChild(script);`
);
}, `(function(){var script;`);
scriptsToLoad += '})()';
// Write the result to a runtime file that can be included in the head of an index file
const filePath = `${buildPath}/runtime.js`;
fs.writeFile(filePath, scriptsToLoad, err => {
if (err) {
return console.log('Error writing runtime.js: ', err);
}
console.log(`\n${filePath} succesfully built\n`);
});
}
The function basically loops over all JS entry files in the manifest.json
.
Script tags are then created with these entries as src
property and these script tags are then added to the document.head
as children (triggering the loading of the entries).
Finally this script is save to a runtime.js
file and stored in the build directory.
You can now include this runtime.js
file into you html file and if all paths are set correctly you chunks should be loaded.
Upvotes: 3