需求及分析

项目是 web App.需兼容 ios, android.

  1. img 占位图显示

  2. url 被重定向的图片前端缓存

知乎有人提相似的问题: https://www.zhihu.com/question/60177714,但是没有好的答案.

占位图自然是为了体验.

前端缓存图片,也没啥稀奇,大部分浏览器自己都做好的.

但是考虑到 img url 是来自服务端 redirect 的 302 响应呢?

服务端之所以这么设计,是考虑到图片资源可能被其他 cdn 分发,总之来自不同的存储,但是考虑兼容这些存储,需要统一 url,就是下面的 origin url 的形式

origin url:     xxx/files/5:
redirect to local:     /storage/adsaf2qe12.jpg;
or redirect to cdn:     /xx.cdn.com/adsaf2qe12.jpg;

img 标签访问重定向 url 的过程

1.初次访问 /files/22

GET /files/705%22 HTTP/1.1
Host: xxx:8000                                                        Accept: image/png,image/svg+xml,image/*;q=0.8,video/*;q=0.8,*/*;q=0.5
Connection: keep-alive
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Faraday Futu      re
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate

2.返回 302

HTTP/1.1 302 Found
Server: nginx
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
X-Powered-By: PHP/7.2.19
Cache-Control: no-cache
Date: Mon, 12 Aug 2019 23:06:06 GMT
Location: http://xxxx:8000/storage/2019/07/26/0650/eWNojunNteHaADwujoHeBPSucm1Ac1DlTENiroXL.jpeg
Expires: Mon, 12 Aug 2019 23:06:05 GMT
X-matching: api_begin
X-matching: api_end

注意:

response body是1个页面,就是图片的展示,img的src实现里应该只关心302和Location的url.

3.访问真实地址/storage/2019/07/26/0650/xxx.jpeg

GET /storage/2019/07/26/0650/eWNojunNteHaADwujoHeBPSucm1Ac1DlTENiroXL.jpeg HTTP/1.1
Host: 54.223.41.252:8000
Accept: image/png,image/svg+xml,image/*;q=0.8,video/*;q=0.8,*/*;q=0.5
Connection: keep-alive
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Faraday Future
Accept-Language: zh-cn
Referer: http://54.223.41.252:8000/ff/news/79?lang=zh&follow=false
Accept-Encoding: gzip, deflate

4.返回 200

HTTP/1.1 200 OK
Server: nginx
Date: Mon, 12 Aug 2019 23:06:06 GMT
Content-Type: image/jpeg
Content-Length: 331683
Last-Modified: Fri, 26 Jul 2019 06:50:25 GMT
Connection: keep-alive
ETag: "5d3aa2b1-50fa3"
Expires: Wed, 11 Sep 2019 23:06:06 GMT
Cache-Control: max-age=2592000
X-matching: img_begin
X-matching: img_end
Accept-Ranges: bytes

静态资源由 nginx 返回,带了 cache 控制的 header,正常情况就应该按 http 缓存的规则来了.

即便是重定位,对 chrome 这样的强缓存策略的浏览器也没撒问题.

问题就在于,我们的项目是 web app. 对于 ios 的 WKWebview 的 img.src 引发的访问,在重定向的前提下是无视缓存的

必须自己来实现前端缓存机制.

占位图

首先,切换 url 来实现占位图的效果:

<img
  v-for="(image,index) in saved_images"
  :src="image.url"
  :class="layout(saved_images.length)"
  @click="clickImage(index)"
  style="object-fit: cover"
  @load="loadedImage(index)"
/>

vue 的核心思路:

watch: {
    //watch images数据,里面包含url
    'images': {
        handler(newVal) {
            // console.log(this.images);
            let that = this;
            //not video
            if( !this.hasVideo) {
                //newVal is a array, save url;
                newVal.forEach(val => {
                    // 缓存图片的api,核心在这里
                    that.apiImg.fetchImg(val.url,response=>{
                         // base64编码
                        //response来自cache 或者第1次访问的结果
                        val.url = response;
                    });
                    that.image_urls.push(val.url);
                    //place holder
                    val.url = resources.placeHolder;
                });
            }
            this.saved_images = newVal;
        },
        immediate: true,
        // deep: true,
    }

带前端缓存的 img api

前端缓存需要了解下 cookie->localstorage-->indexDB.

我最终选择了 localforage 库,结合axios-cache-adapter来做请求的缓存.

代码核心功能:

1. 配置localforage,使用INDEXEDDB;
2. 配置axios-cache-adapter
3. 图片转储base64
4. 调用localforage的setItem/getItem;

最终实现如下:

import { axios } from "../utils/http";
import localforage from "localforage";
import memoryDriver from "localforage-memoryStorageDriver";
import { setup } from "axios-cache-adapter";
import { setupCache } from "axios-cache-adapter";

// Register the custom `memoryDriver` to `localforage`
localforage.defineDriver(memoryDriver);
// Create `localforage` instance
const forageStore = localforage.createInstance({
  // List of drivers used
  driver: [
    localforage.INDEXEDDB,
    localforage.LOCALSTORAGE,
    memoryDriver._driver
  ],
  size: 10000000,
  // Prefix all storage keys to prevent conflicts
  name: "img-cache"
});

const cache = setupCache({
  //过期时间
  maxAge: 15 * 60 * 1000,
  store: forageStore
});

const api = axios.create({
  adapter: cache.adapter
});

export default {
  fetchImg: async function(url, fn) {
    const config = {
      responseType: "arraybuffer"
    };
    //get from store
    await forageStore.getItem(url, (err, value) => {
      if (!err && value) {
        console.log(forageStore);
        console.log("already cached");
        fn(value);
      } else {
        console.log("1st time");
        api
          .get(url, config)
          .then(async function(response) {
            //base64 format
            let bas64Url =
              "data:" +
              response.headers["content-type"] +
              ";base64," +
              btoa(
                new Uint8Array(response.data).reduce(
                  (data, byte) => data + String.fromCharCode(byte),
                  ""
                )
              );
            //store
            await forageStore.setItem(url, bas64Url, () => {
              console.log("set item ok");
              return fn(url);
            });
          })
          .catch(function(reason) {
            console.log(reason);
          });
      }
    });
  }
};

扩展

需要同步下和资源的 max-age.

还有啥资源缓存的,都可以拿来,比如 video, api data…但是前端做太多没必要

看下这篇文章: http://www.alloyteam.com/2012/03/web-cache-5-web-app-cache/

因为问题都来自重定向,可从服务端入手,不让 ajax 处理 302,而是直接拿到 url。

如:访问xxx/1?json=true,返回的 json 里就有 url.

       return $request->input('json') !== null
            ? $response->json(['url' => $url])->setStatusCode(200)
            : $response->redirectTo($url, 302);

这样 webview 可根据 http 缓存协议自行处理,这样当然最好.

思路调整为缓存上面的 api 就可以了.