Suppressing 404s in retina.js library

2019-03-12 08:55发布

问题:

We use the js lib retina.js which swaps low quality images with "retina" images (size times 2). The problem is, that retina.js throws a 404 for every "retina" image which can't be found.

We own a site where users can upload their own pictures which are most likely not in a retina resolution.

Is there no way to prevent the js from throwing 404s?

If you don't know the lib. Here is the code throwing the 404:

http = new XMLHttpRequest;
http.open('HEAD', this.at_2x_path);
http.onreadystatechange = function() {
    if (http.readyState != 4) {
        return callback(false);
    }

    if (http.status >= 200 && http.status <= 399) {
        if (config.check_mime_type) {
            var type = http.getResponseHeader('Content-Type');
            if (type == null || !type.match(/^image/i)) {
                return callback(false);
            }
        }

        RetinaImagePath.confirmed_paths.push(that.at_2x_path);
        return callback(true);
    } else {
        return callback(false);
    }
}
http.send();

回答1:

There are a few options that I see, to mitigate this.

Enhance and persist retina.js' HTTP call results caching

For any given '2x' image that is set to swap out a '1x' version, retina.js first verifies the availability of the image via an XMLHttpRequest request. Paths with successful responses are cached in an array and the image is downloaded.

The following changes may improve efficiency:

  • Failed XMLHttpRequest verification attempts can be cached: Presently, a '2x' path verification attempt is skipped only if it has previously succeeded. Therefore, failed attempts can recur. In practice, this doesn't matter much beacuse the verification process happens when the page is initially loaded. But, if the results are persisted, keeping track of failures will prevent recurring 404 errors.

  • Persist '2x' path verification results in localStorage: During initialization, retina.js can check localStorage for a results cache. If one is found, the verification process for '2x' images that have already been encountered can be bypassed and the '2x' image can either be downloaded or skipped. Newly encounterd '2x' image paths can be verified and the results added to the cache. Theoretically, while localStorage is available, a 404 will occur only once for an image on a per-browser basis. This would apply across pages for any page on the domain.

Here is a quick workup. Expiration functionality would probably need to be added.

https://gist.github.com/4343101/revisions

Employ an HTTP redirect header

I must note that my grasp of "server-side" matters is spotty, at best. Please take this FWIW

Another option is for the server to respond with a redirect code for image requests that have the @2x characters and do not exist. See this related answer.

In particular:

If you redirect images and they're cacheable, you'd ideally set an HTTP Expires header (and the appropriate Cache-Control header) for a date in the distant future, so at least on subsequent visits to the page users won't have to go through the redirect again.

Employing the redirect response would get rid of the 404s and cause the browser to skip subsequent attempts to access '2x' image paths that do not exist.

retina.js can be made more selective

retinajs can be modified to exclude some images from consideration.

A pull request related to this: https://github.com/imulus/retinajs/commit/e7930be

Per the pull request, instead of finding <img> elements by tag name, a CSS selector can be used and this can be one of the retina.js' configurable options. A CSS selector can be created that will filter out user uploaded images (and other images for which a '2x' variant is expected not to exist).

Another possibility is to add a filter function to the configurable options. The function can be called on each matched <img> element; a return true would cause a '2x' variant to be downloaded and anything else would cause the <img> to be skipped.

The basic, default configuration would change from the current version to something like:

var config = {
  check_mime_type: true,
  retinaImgTagSelector: 'img',
  retinaImgFilterFunc: undefined
};

The Retina.init() function would change from the current version to something like:

Retina.init = function(context) {
  if (context == null) context = root;

  var existing_onload = context.onload || new Function;

  context.onload = function() {
    // uses new query selector
    var images = document.querySelectorAll(config.retinaImgTagSelector), 
        retinaImages = [], i, image, filter;

    // if there is a filter, check each image
    if (typeof config.retinaImgFilterFunc === 'function') {
      filter = config.retinaImgFilterFunc;
      for (i = 0; i < images.length; i++) {
        image = images[i];
        if (filter(image)) {
          retinaImages.push(new RetinaImage(image));
        }
      }
    } else {
      for (i = 0; i < images.length; i++) {
        image = images[i];
        retinaImages.push(new RetinaImage(image));
      }
    }
    existing_onload();
  }
};

To put it into practice, before window.onload fires, call:

window.Retina.configure({

  // use a class 'no-retina' to prevent retinajs
  // from checking for a retina version
  retinaImgTagSelector : 'img:not(.no-retina)',

  // or, assuming there is a data-owner attribute
  // which indicates the user that uploaded the image:
  // retinaImgTagSelector : 'img:not([data-owner])',

  // or set a filter function that will exclude images that have
  // the current user's id in their path, (assuming there is a
  // variable userId in the global scope)
  retinaImgFilterFunc: function(img) {
    return img.src.indexOf(window.userId) < 0;
  }
});

Update: Cleaned up and reorganized. Added the localStorage enhancement.



回答2:

Short answer: Its not possible using client-side JavaScript only

After browsing the code, and a little research, It appears to me that retina.js isn't really throwing the 404 errors.

What retina.js is actually doing is requesting a file and simply performing a check on whether or not it exists based on the error code. Which actually means it is asking the browser to check if the file exists. The browser is what gives you the 404 and there is no cross browser way to prevent that (I say "cross browser" because I only checked webkit).

However, what you could do if this really is an issue is do something on the server side to prevent 404s altogether.

Essentially this would be, for example, /retina.php?image=YOUR_URLENCODED_IMAGE_PATH a request to which could return this when a retina image exists...

{"isRetina": true, "path": "YOUR_RETINA_IMAGE_PATH"}}

and this if it doesnt...

{"isRetina": false, "path": "YOUR_REGULAR_IMAGE_PATH"}}

You could then have some JavaScript call this script and parse the response as necessary. I'm not claiming that is the only or the best solution, just one that would work.



回答3:

Retina JS supports the attribute data-no-retina on the image tag. This way it won't try to find the retina image.

Helpful for other people looking for a simple solution.

<img src="/path/to/image" data-no-retina />


回答4:

I prefer a little more control over which images are replaced.

For all images that I've created a @2x for, I changed the original image name to include @1x. (* See note below.) I changed retina.js slightly, so that it only looks at [name]@1x.[ext] images.

I replaced the following line in retina-1.1.0.js:

retinaImages.push(new RetinaImage(image));

With the following lines:

 if(image.src.match(/@1x\.\w{3}$/)) {
    image.src = image.src.replace(/@1x(\.\w{3})$/,"$1");
    retinaImages.push(new RetinaImage(image));
}

This makes it so that retina.js only replaces @1x named images with @2x named images.

(* Note: In exploring this, it seems that Safari and Chrome automatically replace @1x images with @2x images, even without retina.js installed. I'm too lazy to track this down, but I'd imagine it's a feature with the latest webkit browsers. As it is, retina.js and the above changes to it are necessary for cross-browser support.)



回答5:

One of solutions is to use PHP:

replace code from 1st post with:

        http = new XMLHttpRequest;
        http.open('HEAD', "/image.php?p="+this.at_2x_path);
        http.onreadystatechange = function() {
            if (http.readyState != 4) {
                return callback(false);
            }

            if (http.status >= 200 && http.status <= 399) {
                if (config.check_mime_type) {
                    var type = http.getResponseHeader('Content-Type');
                    if (type == null || !type.match(/^image/i)) {
                        return callback(false);
                    }
                }

                RetinaImagePath.confirmed_paths.push(that.at_2x_path);
                return callback(true);
            } else {
                return callback(false);
            }
        }
        http.send();

and in yours site root add file named "image.php":

<?php
 if(file_exists($_GET['p'])){
  $ext = explode('.', $_GET['p']);
  $ext = end($ext);
  if($ext=="jpg") $ext="jpeg";
  header("Content-Type: image/".$ext);
  echo file_get_contents($_GET['p']);
 }
?>


回答6:

retina.js is a nice tool for fixed images on static web pages, but if you are retrieving user uploaded images, the right tool is server side. I imagine PHP here, but the same logic may be applied to any server side language.

Provided that a nice security habit for uploaded images is to not let users reach them by direct url: if the user succeeds in uploading a malicious script to your server, he should not be able to launch it via an url (www.yoursite.com/uploaded/mymaliciousscript.php). So it is usually a good habit to retrieve uploaded images via some script <img src="get_image.php?id=123456" /> if you can... (and even better, keep the upload folder out of the document root)

Now the get_image.php script can get the appropriate image 123456.jpg or 123456@2x.jpg depending on some conditions.

The approach of http://retina-images.complexcompulsions.com/#setupserver seems perfect for your situation.

First you set a cookie in your header by loading a file via JS or CSS:

Inside HEAD:

<script>(function(w){var dpr=((w.devicePixelRatio===undefined)?1:w.devicePixelRatio);if(!!w.navigator.standalone){var r=new XMLHttpRequest();r.open('GET','/retinaimages.php?devicePixelRatio='+dpr,false);r.send()}else{document.cookie='devicePixelRatio='+dpr+'; path=/'}})(window)</script>

At beginning of BODY:

<noscript><style id="devicePixelRatio" media="only screen and (-moz-min-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min-device-pixel-ratio: 2)">#devicePixelRatio{background-image:url("/retinaimages.php?devicePixelRatio=2")}</style></noscript>

Now every time your script to retrieve uploaded images is called, it will have a cookie set asking for retina images (or not).

Of course you may use the provided retinaimages.php script to output the images, but you may also modify it to accomodate your needs depending on how you produce and retrieve images from a database or hiding the upload directory from users.

So, not only it may load the appropriate image, but if GD2 is installed, and you keep the original uploaded image on the server, it may even resize it and crop accordingly and save the 2 cached image sizes on the server. Inside the retinaimages.php sources you can see (and copy) how it works:

<?php
    $source_file = ...
    $retina_file = ....

    if (isset($_COOKIE['devicePixelRatio'])) {
        $cookie_value = intval($_COOKIE['devicePixelRatio']);
    }
    if ($cookie_value !== false && $cookie_value > 1) {
        // Check if retina image exists
        if (file_exists($retina_file)) {
            $source_file = $retina_file;
        }
    }
    ....



    header('Content-Length: '.filesize($source_file), true);
    readfile($source_file); // or read from db, or create right size.. etc..

?>

Pros: image is loaded only once (retina users on 3G at least won'tload 1x+2x images), works even without JS if cookies are enabled, can be switched on and off easily, no need to use apple naming conventions. You load image 12345 and you get the correct DPI for your device.

With url rewriting you may even render it totally transparent by redirecting /get_image/1234.jpg to /get_image.php?id=1234.jpg



回答7:

My suggestion is that you recognize the 404 errors to be true errors, and fix them the way that you are supposed to, which is to provide Retina graphics. You made your scripts Retina-compatible, but you did not complete the circle by making your graphics workflow Retina-compatible. Therefore, the Retina graphics are actually missing. Whatever comes in at the start of your graphics workflow, the output of the workflow has to be 2 image files, a low-res and Retina 2x.

If a user uploads a photo that is 3000x2400, you should consider that to be the Retina version of the photo, mark it 2x, and then use a server-side script to generate a smaller 1500x1200 non-Retina version, without the 2x. The 2 files together then constitute one 1500x1200 Retina-compatible image that can be displayed in a Web context at 1500x1200 whether the display is Retina or not. You don’t have to care because you have a Retina-compatible image and Retina-compatible website. The RetinaJS script is the only one that has to care whether a client is using Retina or not. So if you are collecting photos from users, your task is not complete unless you generate 2 files, both low-res and high-res.

The typical smartphone captures a photo that is more than 10x the size of the smartphone’s display. So you should always have enough pixels. But if you are getting really small images, like 500px, then you can set a breakpoint in your server-side image-reducing script so that below that, the uploaded photo is used for the low-res version and the script makes a 2x copy that is going to be no better than the non-Retina image but it is going to be Retina-compatible.

With this solution, your whole problem of “is the 2x image there or not?” goes away, because it is always there. The Retina-compatible website will just happily use your Retina-compatible database of photos without any complaints.