How do I cache bust imported modules in es6? - web
ES6 modules allows us to create a single point of entry like so:
// main.js
import foo from 'foo';
foo()
<script src="scripts/main.js" type="module"></script>
foo.js will be stored in the browser cache. This is desirable until I push a new version of foo.js to production.
It is common practice to add a query string param with a unique id to force the browser to fetch a new version of a js file (foo.js?cb=1234)
How can this be achieved using the es6 module pattern?
There is one solution for all of this that doesn't involve query string. let's say your module files are in /modules/. Use relative module resolution ./ or ../ when importing modules and then rewrite your paths in server side to include version number. Use something like /modules/x.x.x/ then rewrite path to /modules/. Now you can just have global version number for modules by including your first module with
<script type="module" src="/modules/1.1.2/foo.mjs"></script>
Or if you can't rewrite paths, then just put files into folder /modules/version/ during development and rename version folder to version number and update path in script tag when you publish.
HTTP headers to the rescue. Serve your files with an ETag that is the checksum of the file. S3 does that by default at example.
When you try to import the file again, the browser will request the file, this time attaching the ETag to a "if-none-match" header: the server will verify if the ETag matches the current file and send back either a 304 Not Modified, saving bandwith and time, or the new content of the file (with its new ETag).
This way if you change a single file in your project the user will not have to download the full content of every other module. It would be wise to add a short max-age header too, so that if the same module is requested twice in a short time there won't be additional requests.
If you add cache busting (e.g. appending ?x={randomNumber} through a bundler, or adding the checksum to every file name) you will force the user to download the full content of every necessary file at every new project version.
In both scenario you are going to do a request for each file anyway (the imported files on cascade will produce new requests, which at least may end in small 304 if you use etags). To avoid that you can use dynamic imports e.g if (userClickedOnSomethingAndINeedToLoadSomeMoreStuff) { import('./someModule').then('...') }
From my point of view dynamic imports could be a solution here.
Step 1)
Create a manifest file with gulp or webpack. There you have an mapping like this:
export default {
"/vendor/lib-a.mjs": "/vendor/lib-a-1234.mjs",
"/vendor/lib-b.mjs": "/vendor/lib-b-1234.mjs"
};
Step 2)
Create a file function to resolve your paths
import manifest from './manifest.js';
const busted (file) => {
return manifest[file];
};
export default busted;
Step 3)
Use dynamic import
import busted from '../busted.js';
import(busted('/vendor/lib-b.mjs'))
.then((module) => {
module.default();
});
I give it a short try in Chrome and it works. Handling relative paths is tricky part here.
I've created a Babel plugin which adds a content hash to each module name (static and dynamic imports).
import foo from './js/foo.js';
import('./bar.js').then(bar => bar());
becomes
import foo from './js/foo.abcd1234.js';
import('./bar.1234abcd.js').then(bar => bar());
You can then use Cache-control: immutable to let UAs (browsers, proxies, etc) cache these versioned URLs indefinitely. Some max-age is probably more reasonable, depending on your setup.
You can use the raw source files during development (and testing), and then transform and minify the files for production.
what i did was handle the cache busting in webserver (nginx in my instance)
instead of serving
<script src="scripts/main.js" type="module"></script>
serve it like this where 123456 is your cache busting key
<script src="scripts/123456/main.js" type="module"></script>
and include a location in nginx like
location ~ (.+)\/(?:\d+)\/(.+)\.(js|css)$ {
try_files $1/$2.min.$3 $uri;
}
requesting scripts/123456/main.js will serve scripts/main.min.js and an update to the key will result in a new file being served, this solution works well for cdns too.
Just a thought at the moment but you should be able to get Webpack to put a content hash in all the split bundles and write that hash into your import statements for you. I believe it does the second by default.
You can use an importmap for this purpose. I've tested it at least in Edge. It's just a twist on the old trick of appending a version number or hash to the querystring. import doesn't send the querystring onto the server but if you use an importmap it will.
<script type="importmap">
{
"imports": {
"/js/mylib.js": "/js/mylib.js?v=1",
"/js/myOtherLib.js": "/js/myOtherLib.js?v=1"
}
}
</script>
Then in your calling code:
import myThing from '/js/mylib.js';
import * as lib from '/js/myOtherLib.js';
You can use ETags, as pointed out by a previous answer, or alternatively use Last-Modified in relation with If-Modified-Since.
Here is a possible scenario:
The browser first loads the resource. The server responds with Last-Modified: Sat, 28 Mar 2020 18:12:45 GMT and Cache-Control: max-age=60.
If the second time the request is initiated earlier than 60 seconds after the first one, the browser serves the file from cache and doesn't make an actual request to the server.
If a request is initiated after 60 seconds, the browser will consider cached file stale and send the request with If-Modified-Since: Sat, 28 Mar 2020 18:12:45 GMT header. The server will check this value and:
If the file was modified after said date, it will issue a 200 response with the new file in the body.
If the file was not modified after the date, the server will issue a304 "not modified" status with empty body.
I ended up with this set up for Apache server:
<IfModule headers_module>
<FilesMatch "\.(js|mjs)$">
Header set Cache-Control "public, must-revalidate, max-age=3600"
Header unset ETag
</FilesMatch>
</IfModule>
You can set max-age to your liking.
We have to unset ETag. Otherwise Apache keeps responding with 200 OK every time (it's a bug). Besides, you won't need it if you use caching based on modification date.
A solution that crossed my mind but I wont use because I don't like it LOL is
window.version = `1.0.0`;
let { default: fu } = await import( `./bar.js?v=${ window.version }` );
Using the import "method" allows you to pass in a template literal string. I also added it to window so that it can be easily accessible no matter how deep I'm importing js files. The reason I don't like it though is I have to use "await" which means it has to be wrapped in an async method.
If you are using Visual Studio 2022 and TypeScript to write your code, you can follow a convention of adding a version number to your script file names, e.g. MyScript.v1.ts. When you make changes and rename the file to MyScript.v2.ts Visual Studio shows the following dialog similar to the following:
If you click Yes it will go ahead and update all the files that were importing this module to refer to MyScript.v2.ts instead of MyScript.v1.ts. The browser will notice the name change too and download the new modules as expected.
It's not a perfect solution (e.g. if you rename a heavily used module, a lot of files can end up being updated) but it is a simple one!
this work for me
let url = '/module/foo.js'
url = URL.createObjectURL(await (await fetch(url)).blob())
let foo = await import(url)
I came to the conclusion that cache-busting should not be used with ES Module.
Actually, if you have the versioning in the URL, the version is acting like a cache-busting. For instance https://unpkg.com/react#18.2.0/umd/react.production.min.js
If you don't have versioning in the URL, use the following HTTP header Cache-Control: max-age=0, no-cache to force the browser to always check if a new version of the file is available.
no-cache tells the browser to cache the file but to always perform a check
no-store tells the browser to don't cache the file. Don't use it!
Another approach: redirection
unpkg.com solved this problem with HTTP redirection.
Therefore it is not an ideal solution because it involves 2 HTTP requests instead of 1.
The first request is to get redirected to the latest version of the file (not cached, or cached for a short period of time)
The second request is to get the JS file (cached)
=> All JS files include the versioning in the URL (and have an aggressive caching strategy)
For instance https://unpkg.com/react#18.2.0/umd/react.production.min.js
=> Removing the version in the URL, will lead to a HTTP 302 redirect pointing to the latest version of the file
For instance https://unpkg.com/react/umd/react.production.min.js
Make sure the redirection is not cached by the browser, or cached for a short period of time. (unpkg allows 600 seconds of caching, but it's up to you)
About multiple HTTP requests: Yes, if you import 100 modules, your browser will do 100 requests. But with HTTP2 / HTTP3, it is not a problem anymore because all requests will be multiplexed into 1 (it is transparent for you)
About recursion:
If the module you are importing also imports other modules, you will want to check about <link rel="modulepreload"> (source Chrome dev blog).
The modulepreload spec actually allows for optionally loading not just the requested module, but all of its dependency tree as well. Browsers don't have to do this, but they can.
If you are using this technic in production, I am deeply interested to get your feedback!
Append version to all ES6 imports with PHP
I didn't want to use a bundler only because of this, so I created a small function that modifies the import statements of all the JS files in the given directory so that the version is at the end of each file import path in the form of a query parameter. It will break the cache on version change.
This is far from an ideal solution, as all JS file contents are verified by the server on each request and on each version change the client reloads every JS file that has imports instead of just the changed ones.
But it is good enough for my project right now. I thought I'd share.
$assetsPath = '/public/assets'
$version = '0.7';
$rii = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($assetsPath, FilesystemIterator::SKIP_DOTS) );
foreach ($rii as $file) {
if (pathinfo($file->getPathname())['extension'] === 'js') {
$content = file_get_contents($file->getPathname());
$originalContent = $content;
// Matches lines that have 'import ' then any string then ' from ' and single or double quote opening then
// any string (path) then '.js' and optionally numeric v GET param '?v=234' and '";' at the end with single or double quotes
preg_match_all('/import (.*?) from ("|\')(.*?)\.js(\?v=\d*)?("|\');/', $content, $matches);
// $matches array contains the following:
// Key [0] entire matching string including the search pattern
// Key [1] string after the 'import ' word
// Key [2] single or double quotes of path opening after "from" word
// Key [3] string after the opening quotes -> path without extension
// Key [4] optional '?v=1' GET param and [5] closing quotes
// Loop over import paths
foreach ($matches[3] as $key => $importPath) {
$oldFullImport = $matches[0][$key];
// Remove query params if version is null
if ($version === null) {
$newImportPath = $importPath . '.js';
} else {
$newImportPath = $importPath . '.js?v=' . $version;
}
// Old import path potentially with GET param
$existingImportPath = $importPath . '.js' . $matches[4][$key];
// Search for old import path and replace with new one
$newFullImport = str_replace($existingImportPath, $newImportPath, $oldFullImport);
// Replace in file content
$content = str_replace($oldFullImport, $newFullImport, $content);
}
// Replace file contents with modified one
if ($originalContent !== $content) {
file_put_contents($file->getPathname(), $content);
}
}
}
$version === null removes all query parameters of the imports in the given directory.
This adds between 10 and 20ms per request on my application (approx. 100 JS files when content is unchanged and 30—50ms when content changes).
Use of relative path works for me:
import foo from './foo';
or
import foo from './../modules/foo';
instead of
import foo from '/js/modules/foo';
EDIT
Since this answer is down voted, I update it. The module is not always reloaded. The first time, you have to reload the module manually and then the browser (at least Chrome) will "understand" the file is modified and then reload the file every time it is updated.
Related
Socket.io does not work on deploying to Heroku [duplicate]
Why am I getting this error in console? Refused to execute script from 'https://www.googleapis.com/customsearch/v1?key=API_KEY&q=flower&searchType=image&fileType=jpg&imgSize=small&alt=json' because its MIME type ('application/json') is not executable, and strict MIME type checking is enabled.
In my case it was a file not found, I typed the path to the javascript file incorrectly.
You have a <script> element that is trying to load some external JavaScript. The URL you have given it points to a JSON file and not a JavaScript program. The server is correctly reporting that it is JSON so the browser is aborting with that error message instead of trying to execute the JSON as JavaScript (which would throw an error). Odds are that the underlying reason for this is that you are trying to make an Ajax request, have hit a cross origin error and have tried to fix it by telling jQuery that you are using JSONP. This only works if the URL provides JSONP (which is a different subset of JavaScript), which this one doesn't. The same URL with the additional query string parameter callback=the_name_of_your_callback_function does return JavaScript though.
This result is the first that pops-up in google, and is more broad than what's happening here. The following will apply to an express server: I was trying to access resources from a nested folder. Inside index.html i had <script src="./script.js"></script> The static route was mounted at : app.use(express.static(__dirname)); But the script.js is located in the nested folder as in: js/myStaticApp/script.js I just changed the static route to: app.use(express.static(path.join(__dirname, "js"))); Now it works :)
Try to use express.static() if you are using Node.js. You simply need to pass the name of the directory where you keep your static assets, to the express.static middleware to start serving the files directly. For example, if you keep your images, CSS, and JavaScript files in a directory named public, you can do as below − i.e. : app.use(express.static('public')); This approach resolved my issue.
In my case, I was working on legacy code and I have this line of code <script type="text/javascript" src="js/i18n.js.php"></script> I was confused about how this supposed to work this code was calling PHP file not js despite it was working on the live server but I have this error on the stage sever and the content type was content-type: text/html; charset=UTF-8 even it is text/javascript in the script tag and after I added header('Content-Type: text/javascript'); at the beginning for file i18n.js.php the error is fixed
After searching for a while I realized that this error in my Windows 10 64 bits was related to JavaScript. In order to see this go to your browser DevTools and confirm that first. In my case it shows an error like "MIME type ('application/javascript') is not executable". If that is the case I've found a solution. Here's the deal: Borrowing user "ilango100" on https://github.com/jupyterlab/jupyterlab/issues/6098: I had the exact same issue a while ago. I think this issue is specific to Windows. It is due to the wrong MIME type being set in Windows registry for javascript files. I solved the issue by editing the Windows registry with correct content type: regedit -> HKEY_LOCAL_MACHINE\Software\Classes -> You will see lot of folders for each file extension -> Just scroll down to ".js" registry and select it -> On the right, if the "Content Type" value is other than application/javascript, then this is causing the problem. Right click on Content Type and change the value to application/javascript enter image description here Try again in the browser." After that I've realized that the error changes. It doesn't even open automatically in the browser anymore. PGAdmin, however, will be open on the side bar (close to the calendar/clock). By trying to open in the browser directly ("New PGAdmin 4 window...") it doesn't work either. FINAL SOLUTION: click on "Copy server URL" and paste it on your browser. It worked for me! EDIT: Copying server URL might not be necessary, as explained by Eric Mutta in the comment below.
I accidentally named the js file .min instead of .min.js ...
Python flask On Windows, it uses data from the registry, so if the "Content Type" value in HKCR/.js is not set to the proper MIME type it can cause your problem. Open regedit and go to the HKEY_CLASSES_ROOT make sure the key .js/Content Type has the value text/javascript C:\>reg query HKCR\.js /v "Content Type" HKEY_CLASSES_ROOT\.js Content Type REG_SZ text/javascript
In my case (React app), just force cleaning the cache and it worked.
I had my web server returning: Content-Type: application\javascript and couldn't for the life of me figure out what was wrong. Then I realized I had the slash in the wrong direction. It should be: Content-Type: application/javascript
In my case, while executing my typescript file, I wrote: <script src="./script.ts"></script> Instead of: <script src="./script.js"></script>
In my case Spring Security was looking for authentication before allowing calls to external libraries. I had a folder /dist/.. that I had added to the project, once I added the folder to the ignore list in my WebSecurityConfig class, it worked fine. web.ignoring().antMatchers("/resources/**", "/static/**", "/css/**", "/js/**", "/images/**", "/error", "/dist/**");
Check for empty src in script tag. In my case, i was dynamically populating src from script(php in my case), but in a particular case src remained empty, which caused this error. Out was something like this: <script src=""></script> //empty src causes error So instead of empty src in script tag, I removed the script tag all together. Something like this: if($src !== ''){ echo '<script src="'.$src.'"></script>'; }
You can use just Use type or which you are using you choose that file type
My problem was that I have been putting the CSS files in the scripts definition area just above the end of the Try to check the files spots within your pages
I am using SpringMVC+tomcat+React #Anfuca's answer does not work for me(force cleaning the browser's cache) I used Filter to forward specific url pattern to the React's index.html public class FrontFilter extends HttpFilter { #Override protected void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException { boolean startsWithApi = requestURI.startsWith("/api/"); boolean isFrontendUri = requestURI.startsWith("/index.html"); if (!startsWithApi && !isFrontendUri) { req.getRequestDispatcher("/index.html").forward(req, res); } super.doFilter(wrapped, res, chain); } } There is no Spring Security problem bcs my filter executes before Spring Security's but I still see the same error and find here Then I realized that I forgot adding one more condition for JS and CSS: boolean startsWithStatic = requestURI.startsWith(contextPath + "/static"); Add this to my if condition and problem solved, no more error with MIME type or ('text/html') with js and css Root cause is that I incorrectly forward JS and CSS type to HTML type
I got the same error. I realized my app.js was in another folder. I just moved it into that folder where my index.html file is and it resolved.
In Angular Development try this
Add the code snippet as shown below to the entry html. i.e "index.html" in reactjs <div id="wrapper"></div> <base href="/" />
If you have a route on express such as: app.get("*", (req, res) => { ... }); Try to change it for something more specific: app.get("/", (req, res) => { ... }); For example. Or else you just might find yourself recursively serving the same HTML template over and over...
In my case I had a symlink for the 404'd file and my Tomcat was not configured to allow symlinks. I know that it is not likely to be the cause for most people, but if you are desperate, check this possibility just in case.
I hade same problem then i fixed like this change "text/javascript" to type="application/json"
I solved my problem by adding just ${pageContext.request.contextPath} to my jsp path . in stead of : <script src="static/js/jquery-3.2.1.min.js"></script> I set : <script src="${pageContext.request.contextPath}/static/js/jquery-3.2.1.min.js"></script>
Renaming files with MD5
Was going through gulp packages on the npm website and came across this package called gulp-rename-md5. Is there a scenario where renaming a file using MD5 is useful and why?
I've used a similar tool for cache busting (called gulp-freeze which adds an MD5 hash of the file contents to the filename). When you update a CSS or JS file you want users to get the latest version of that file when they visit your site. If your file is named "app.min.js" and you update it, their browsers might not pull down the latest file. If you're using a CDN even clearing the browser cache probably won't request the new file. I've used gulp-freeze with gulp-filenames (to store the name of the cache busted file) and gulp-html-replace (to actually update the <link /> or <script /> tags with the name of this cache busted file in the html). It's really handy. CODE SAMPLE This will get your files, use gulp-freeze to build the hash, use gulp-filenames to store the name of that file, then write that to the html with gulp-html-replace. This is tested and working var gulp = require("gulp"), runSequence = require("run-sequence"), $ = require("gulp-load-plugins")(); gulp.task("build", () => { runSequence("js", "write-to-view"); }); gulp.task("js", () => { return gulp .src("./js/**/*.js") .pipe($.freeze()) .pipe($.filenames("my-javascript")) .pipe(gulp.dest("./")); }); gulp.task("write-to-view", () => { return gulp .src("index.html") .pipe( $.htmlReplace( { js: $.filenames.get("my-javascript") }, { keepBlockTags: true } ) ) .pipe(gulp.dest("./")); }); EDIT I should add that index.html just needs these comments so gulp-html-replace knows where to write the <script /> element <!--build:js--> <!-- endbuild -->
One of advantages is that you can setup your app to cache files with MD5 sum in their name (e.g. mystyle.a345fe.css) for long time (several months) because you know that this file will never be modified. This will save you some traffic and your web will be faster for returning visitors.
NodeJs web crawler file extension handling
I'm developing a web crawler in nodejs. I've created a unique list of the urls in the website crawle body. But some of them have extensions like jpg,mp3, mpeg ... I want to avoid crawling those who have extensions. Is there any simple way to do that?
Two options stick out. 1) Use path to check every URL As stated in comments, you can use path.extname to check for a file extension. Thus, this: var test = "http://example.com/images/banner.jpg" path.extname(test); // '.jpg' This would work, but this feels like you'll wind up having to create a list of file types you can crawl or you must avoid. That's work. Side note -- be careful using path. Typically, url is your best tool for parsing links because path is aimed at files/directories, not urls. On some systems (Windows), using path to manipulate a url can result in drama because of the slashes involved. Fair warning! 2) Get the HEAD for each link & see if content-type is set to text/html You may have reasons to avoid making more network calls. If so, this isn't an option. But if it is OK to make additional calls, you could grab the HEAD for each link and check the MIME type stored in content-type. Something like this: var headersOptions = { method: "HEAD", host: "http://example.com", path: "/articles/content.html" }; var req = http.request(headersOptions, function (res) { // you will probably need to also do things like check // HTTP status codes so you handle 404s, 301s, and so on if (res.headers['content-type'].indexOf("text/html") > -1) { // do something like queue the link up to be crawled // or parse the link or put it in a database or whatever } }); req.end(); One benefit is that you only grab the HEAD, so even if the file is a gigantic video or something, it won't clog things up. You get the HEAD, see the content-type is a video or whatever, then move along because you aren't interested in that type. Second, you don't have to keep track of file names because you're using a standard MIME type to differentiate html from other data formats.
How to load txt file in haxe in js project?
I've started a haxe js project in FlashDevelop, I need to load a local file, is this possible? how to to so?
The simple answer is use "resources". You add a path and an identifier to your hxml: -resource hello_message.txt#welcome And you use it in your code like this: var welcome = haxe.Resource.getString("welcome"); Note that the operation is performed at compile time so there is no runtime overhead. It is essentially equivalent to embed the file content in a quoted string. The complex answer is to use a macro. With them you can load, parse, process and do all the manipulation you might need. Pretty commonly, you can see macros to load a config file (say JSON or YAML) and use it as part of your application (again at compile time and not at runtime).
You could grab files with an XMLHttpRequest as long as you keep them somewhere public (if you're putting it online) and accessible to the script. Here's a quick example of grabbing a text file from the location assets/test.txt This is the sort of thing I usually do in the JS games I make, I find it a bit more flexible than just embedding them with -resource. If it's not exactly what you're looking for then Franco's answer should see you through. package ; import js.html.XMLHttpRequest; import js.html.Event; class Start { static function main() { var request = new XMLHttpRequest(); // using the GET method, get the file at this location, asynchronously request.open("GET", "assets/test.txt", true); // when loaded, get the response and trace it out request.onload = function(e:Event){ trace(request.response); }; // if there's an error, handle it request.onerror = function(e:Event) { trace("error :("); }; // send the actual request to the server request.send(); } }
Standalone PHP script using Expression Engine
Is there a way to make a script where I can do stuff like $this->EE->db (i.e. using Expression Engine's classes, for example to access the database), but that can be run in the command line? I tried searching for it, but the docs don't seem to contain this information (please correct me if I'm wrong). I'm using EE 2.4 (the link above should point to 2.4 docs).
The following article seems to have a possible approach: Bootstrapping EE for CLI Access Duplicate your index.php file and name it cli.php. Move the index.php file outside your DOCUMENT_ROOT. Now, technically, this isn’t required, but there’s no reason for prying eyes to see your hard work so why not protect it. Inside cli.php update the $system_path on line 26 to point to your system folder. Inside cli.php update the $routing['controller'] on line 96 to be cli. Inside cli.php update the APPPATH on line 96 to be $system_path.'cli/'. Duplicate the system/expressionengine directory and name it system/cli. Duplicate the cli/controllers/ee.php file and name it cli/controllers/cli.php. Finally, update the class name in cli/controllers/cli.php to be Cli and remove the methods. By default EE calls the index method, so add in an index method to do what you need.
#Zenbuman This was useful as a starting point although I would add I had issues with all of my requests going to cli -> index, whereas I wanted some that went to cli->task1, cli->task2 etc I had to update *system\codeigniter\system\core\URI.php*so that it knew how to extract the parameters I was passing via the command line, I got the code below from a more recent version of Codeigniter which supports the CLI // Is the request coming from the command line? if (php_sapi_name() == 'cli' or defined('STDIN')) { $this->_set_uri_string($this->_parse_cli_args()); return; } // Let's try the REQUEST_URI first, this will work in most situations and also created the function in the same file private function _parse_cli_args() { $args = array_slice($_SERVER['argv'], 1); return $args ? '/' . implode('/', $args) : ''; } Also had to comment out the following in my cli.php file as all routing was going to the index method in my cli controller and ignoring my parameters /* * ~ line 109 - 111 /cli.php * --------------------------------------------------------------- * Disable all routing, send everything to the frontend * --------------------------------------------------------------- */ $routing['directory'] = ''; $routing['controller'] = 'cli'; //$routing['function'] = ''; Even leaving $routing['function'] = ''; Will force requests to go to index controller In the end I felt this was a bit hacky but I really need to use the EE API library in my case. Otherwise I would have just created a separate application with Codeigniter to handle my CLI needs, hope the above helps others.
I found #Zenbuman's answer after solving my own variation of this problem. My example allows you to keep the cron script inside a module, so if you need your module to have a cron feature it all stays neatly packaged together. Here's a detailed guide on my blog.