Old situation
Previously, I used the following method to force the browser to reload my JavaScript file if there was a new version available.
<script src="common.js?version=1337"></script>
<script src="app.js?version=1337"></script>
My HTML is automatically generated (e.g. with PHP), so this is easy to automate.
New situation
Now I want to use ES6 modules and import my common code. My HTML becomes:
<script src="app.js?version=1337" type="module"></script>
And app.js
contains the import:
import {foo, bar} from './common.js';
Problem
Now my question: how do I influence the caching of common.js
in the new scenario?
I don't want to manually edit app.js
every time I edit common.js
. I also don't want to dynamically generate/pre-process any of my JavaScript files, if possible.
Old situation
Previously, I used the following method to force the browser to reload my JavaScript file if there was a new version available.
<script src="common.js?version=1337"></script>
<script src="app.js?version=1337"></script>
My HTML is automatically generated (e.g. with PHP), so this is easy to automate.
New situation
Now I want to use ES6 modules and import my common code. My HTML becomes:
<script src="app.js?version=1337" type="module"></script>
And app.js
contains the import:
import {foo, bar} from './common.js';
Problem
Now my question: how do I influence the caching of common.js
in the new scenario?
I don't want to manually edit app.js
every time I edit common.js
. I also don't want to dynamically generate/pre-process any of my JavaScript files, if possible.
- 1 A service worker could be used to process module requests. Native ES module implementations don't handle such things, that's why bundling tools still have no alternatives in production. – Estus Flask Commented Sep 6, 2017 at 21:40
- I wrote and answer you could be interested in at this similar question stackoverflow.com/a/56801858/210090 – Riccardo Galli Commented Jun 28, 2019 at 6:25
3 Answers
Reset to default 5You can use dynamic imports - if you're using ES6 modules (which seems to be the case) - or the Cache-Control response header if you're using any server that allows you to control your response headers through files like .htaccess
.
I know this question is 5 years old, but I faced this problem yesterday and bundlers are not always available for all types of projects.
Solution 1: Dynamic Imports
Dynamic imports can be used at the top level of a module just like the regular import
declaration. You just need to add the await
operator and assign its value to a constant.
The following example considers that you are using PHP and that you do not wish to emit your import line with it, only the version number.
index.php
<?php
// Add your dynamic version number somewhere accessible
// to JS (constant, element attribute, etc.).
echo '<script>const assetsVersion = "' . $assets_version . '";</script>';
app.js
// Add your import to the top-level of your module referencing
// the version number in the string.
const { MyImport } = await import("./my-import.js?v=" + assetsVersion);
This solution allows you to keep your current cache-bust technique - and also works regardless of the back-end technology used.
Solution 2: Cache-Control
This option requires you to modify your .htaccess
file. Setting the no-cache
response directive will make your browser validate your cached assets before actually using them (not the same as the no-store
directive).
The validation is done through the If-None-Match
and the If-Modified-Since
request headers. The server will respond with a 304 status code if the file wasn't modified or with a 200 status code if the asset was updated - causing your browser to re-download it.
.htaccess
<IfModule mod_headers.c>
<FilesMatch "\.(css|js)$">
Header set Cache-Control "no-cache"
</FilesMatch>
</IfModule>
If you're using Apache you might face issues with your server always returning 200 (due to a bug as far as I know). Removing the ETag
header will probably solve it (worked for me):
<IfModule mod_headers.c>
<FilesMatch "\.(css|js)$">
Header set Cache-Control "no-cache"
Header unset ETag
</FilesMatch>
</IfModule>
This solution affects both module imports and normal scripts.
Short Version:
Just use Webpack, and you can keep doing your trick because all the javascript will be in a single file.
Long Version:
You said you don't want to pre-process your javascript files. I would reconsider that stance. Most modern web applications (and their associated frameworks like React, Angular, Vue.js) are built using some sort of preprocessor/packager that bundles all the javascript for the app into one or more files.
There are many good reasons to do this that we don't need to re-iterate in full here, but briefly, you can do type-checking (e.g. TypeScript), linting, tree-shaking, optimization, obfuscation, and compacting easily.
Bundled javascript is often smaller (by a large margin), faster, and more correct. It leads to shorter loading times and less bandwidth usage, which is particularly important when so many more than 50% of web traffic is from mobile these days.
While it might be a bit of learning curve, it's the direction our industry is going, and with good reason. The reason there isn't an easy way to include a cache-busting query parameter in ES module important is probably because the designers of the language considered it an anti-pattern.
ES6 modules can import PHP files that output valid JS. As long as you also set the proper header you should be OK.
So your index.php
file would contain:
<script src="app.js.php?version=1337" type="module"></script>
And your app.js.php
file would contain:
<?php
header("Content-Type: application/javascript");
$version = 1337; // Probably imported form some config file;
?>
import {foo, bar} from './common.js.php?version=<?=$version?>';