22

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.

sebas
  • 1,283
  • 1
  • 12
  • 16
  • 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 Sep 06 '17 at 21:40
  • I wrote and answer you could be interested in at this similar question https://stackoverflow.com/a/56801858/210090 – Riccardo Galli Jun 28 '19 at 06:25

3 Answers3

1

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.

Yona Appletree
  • 8,801
  • 6
  • 35
  • 52
0

You 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.

Rafael
  • 33
  • 1
  • 8
-2

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?>';
  • 7
    This solution assumes you use php to emit js files. I am curious to see a solution for static js files. – daghan Mar 08 '18 at 12:47
  • 1
    There is no "importing of PHP files". PHP runs on remote HTTP server generating the HTTP response which is a valid JavaScript text, which is what ends up actually being imported. I know you probably didn't mean to imply ES6 actually allows importing PHP code, but it's important to note the distinction between what someone may read from your answer and what should be absolutely obvious to a HTTP developer (which Web developers should strive to be, in my opinion) -- that a HTTP server is potentially free to generate any response however they like, whether through invoking PHP or by other means. – Armen Michaeli Nov 10 '20 at 18:55