前端静态文件缓存优化采坑记录 

前端静态文件缓存,大家已经耳熟能详,近期对项目进行优化又去详细看了下相关资料,在此整理。


1、PWA,目前比较火的 PWA 方案 :Progressive Web Apps Google 提出的用前沿的 Web 技术为网页提供 App 般使用体验的一系列方案。

一个 PWA 应用首先是一个网页, 可以通过 Web 技术编写出一个网页应用. 随后添加上 App Manifest Service Worker 来实现 PWA 的安装和离线等功能。

关于什么是service work 和 关于它的生命周期,注册卸载以及跟服务端客户端交互的相关内容在此就不详细介绍了。这里主要介绍如何给web app 加上 service worker 支持,网上资料已经比较多了,我尝试的是使用webpack 的一个插件 offline-plugin(https://github.com/NekR/offline-plugin) 使用比较方便,api比较齐全,可以自动生成manifest文件。在webpack端配置:

new OfflinePlugin({
      safeToUseOptionalCaches: true,

      caches: {
        main: [
          'main.js',
          'main.css',
          'index.html'
        ],
        additional: [
          '*.woff',
          '*.woff2'
        ],
        optional: [
          ':rest:'
        ]
      },

      ServiceWorker: {
        events: true
      },
      AppCache: {
        events: true
      }
    })

在入口js中添加

require('offline-plugin/runtime').install();

ES6/Babel/TypeScript

import * as OfflinePluginRuntime from 'offline-plugin/runtime';
OfflinePluginRuntime.install();

之后用webpack 构建就可以了。


network 中可以看到,再次请求的静态资源请求的是service worker:

service worker1.jpg


值得注意的是,感觉service worker 并不太适合多页面应用,何为多页面,即请求先走的服务端,通过服务端直接渲染的页面,service worker 效果不明显,它比较适合单页面应用,比如vuejs 或者 react spa类型的应用,另外,service worker 虽然被google和前端爱好者所推崇,然而,兼容性还不是那么理想,ios 目前是不支持的。

service worker2.png


2、尝试用独立的manifest 。虽然官方目前不是很推荐用manifest,而且也停止了manifest的更新,但是目前绝大多数的现代浏览器都是支持的,也是比较成熟的缓存方案,在w3上面对它的优点做了简单介绍:

  • 离线浏览 - 用户可在应用离线时使用它们

  • 速度 - 已缓存资源加载得更快

  • 减少服务器负载 - 浏览器将只从服务器下载更新过或更改过的资源。


于是开始尝试,manifestwebpack插件也比较多,试用了几个,感觉都很不爽,不能满足业务需求,比如我想:

1、开发环境,uat环境,生产环境 manifest 各不相同,特别是生产是cdn

2、我还需要额外添加一些其他静态资源


没有合适的就自己写一个(MakeManifest.js):

/**
 * manifest生成
 * 根据环境变量生成manifest文件做静态文件预缓存
 *
 * 参数: options = {
 *  path: './build/assets/manifest.appcache' //生成静态资源缓存文件
 * }
 *
 * @param options
 * @constructor
 */
var fs = require('fs'),NODE_ENV = process.env.NODE_ENV;
var pkg = require('../package.json');
function MakeManifest(output, options) {
    this.output = output
    this.options = options
}

function getExtraSource() {
    var sourceArr = [];
    sourceArr.push('https://m.163.com/static/common/js/NativeAPI.js');
    sourceArr.push('https://m.163.com/omm/mobile/sdk/product/1.0.0/js/product.js');
    sourceArr.push('https://m.163.com/omm/mobile/sdk/product/1.0.0/css/style.css');
    // a/b Test资源文件
    sourceArr.push('https://sdk.appadhoc.com/ab.plus.js');
    return sourceArr;
}
function getNowFormatDate() {
    var date = new Date();
    var seperator1 = "-";
    var seperator2 = ":";
    var month = date.getMonth() + 1;
    var strDate = date.getDate();
    if (month >= 1 && month <= 9) {
        month = "0" + month;
    }
    if (strDate >= 0 && strDate <= 9) {
        strDate = "0" + strDate;
    }
    var currentdate = date.getFullYear() + seperator1 + month + seperator1 + strDate
        + " " + date.getHours() + seperator2 + date.getMinutes()
        + seperator2 + date.getSeconds();
    return currentdate;
}    

MakeManifest.prototype.apply = function (compiler) {
    var baseOutPath = this.output;
    var outPutManiFile = this.output + '/manifest.appcache'
    var outPutManiHtmlFile = this.output + '/manifest.html'
    var options = this.options

    compiler.plugin('emit', function (compilation, done) {
        var results = compilation.getStats().toJson(options), cateFile = [];

        for (var i = 0; i < results.chunks.length; i++) {
            var chunk = results.chunks[i];
            chunk.files.forEach(function (item) {
                if (!/\.map$/.test(item)) {
                    var firstPath = NODE_ENV == 'production' ? 'https://cdn.m.163.com/oas/static/assets/' : '/oas/static/assets/';
                    cateFile.push(firstPath + item);
                }
            }, this);
        }
        var newFiles = cateFile.concat(getExtraSource());
        var currentDate = getNowFormatDate();
        var TAG = '#ver:' + currentDate + ' ' + pkg.version;
        var FALLBACK = '';
        var NETWORK = 'NETWORK:\r\n      *';
        var CONTENT = '';
        var maniText = ('\r\n      CACHE MANIFEST\r\n      ' + TAG + '\r\n\r\n      CACHE:\r\n      ' + newFiles.join('\r\n') + '\r\n\r\n      ' + NETWORK + '\r\n\r\n      ' + FALLBACK + '\r\n    ').trim().replace(/^      */gm, '');
        var maniHtml = ('\n      <!doctype html>\n      <html manifest="manifest.appcache">' + (CONTENT || '') + '</html>\n    ').trim().replace(/^      */gm, '');

        fs.exists(baseOutPath, function (exists) {
            if (exists) {
                addCacheFile(maniText, maniHtml);
            }
            else {
                   
                fs.mkdir(baseOutPath, function () {
                    addCacheFile(maniText, maniHtml);
                });
            }
            done();
        });

        function addCacheFile(maniText, maniHtml) {
            fs.writeFileSync(outPutManiFile, maniText);
            fs.writeFileSync(outPutManiHtmlFile, maniHtml);
        }

    });
}

module.exports = MakeManifest;


webpack.config.js 中添加:

var   plugins = require('./plugins'),
new plugins.MakeManifest('./build/assets', {
            exclude: [/node_modules[\\\/]react/],
            hash: true,
            assets: true,
            chunks: true,
            chunkModules: true
})

通过webpack 构建后可以看到静态文件目录生成两个文件:

manifest1.png

之后,我们可以通过后台打开iframe的方式在第一个页面加载时打开这个静态页面:

export function openUrlByIframe(url) {
    return new Promise(function (resolve, reject) {
        let rdm = Math.random().toString().substr(2);
        let newIframe = document.createElement('iframe');
        newIframe.id = "openUrl" + rdm;
        newIframe.src = url;
        let style = {
            "margin": 0,
            "padding": 0,
            "border": "none",
            "height": "0px",
            "width": "0px",
            "position": "absolute"
        };
        for (let key in style) {
            newIframe.style[key] = style[key];
        }
                let body = document.getElementsByTagName('body')[0];
        if (newIframe.attachEvent) {
            newIframe.attachEvent("onload", function () {
                console.log('预加载完成', url);
                resolve();
            });
        } else {
            newIframe.onload = function () {
                console.log('预加载完成', url);
                resolve();
            };
        }
        body.insertBefore(newIframe, body.firstChild);
    })
}

async storeStaticSource() {
   …
      let sessionPage = '/oas/static/assets/manifest.html';
      await native.openUrlByIframe(sessionPage);
      localStorage.setItem('hadSessionStatic', '1');
      console.info('静态文件缓存完成!');
   …
},

调用:

storeStaticSource()


使用缓存后对比:

不使用缓存:

① 弱网条件下(slow 3G)

弱网1.png

现象:页面打开正常,静态文件加载缓慢。

② 离线条件下  

离线1.png

现象:页面样式异常,静态文件加载异常。

加入缓存后:

① 离线情况下

被缓存2.png

现象:打开页面后,被缓存的静态资源从disk cache中请求,页面显示正常,打开速度很快

manifest 方式也是有坑的,它有一些缺陷:

  • 更新的资源,需要二次刷新才会被页面采用

  • 不支持增量更新,只有manifest发生变化,所有资源全部重新下载一次

  • 缺乏足够容错机制,当清单中任意资源文件出现加载异常,都会导致整个manifest策略运行异常

不过对于目前的项目来讲,这些问题不大,可以考虑使用。


3、使用iframe预加载页面。通过预加载网页的形式,我们可以在第一个页面加载完成时,预加载可能需要跳转的页面,预加载的方式和上面类似,可以通过一个隐藏的iframe来实现,这样,不仅可以提高打开速度,也可以缓存静态资源到disk cache 中,后面静态文件不用再请求服务器。

PS: 缓存的作用主要是让网页有更快的响应速度,增强体验,减少服务器响应时间,减少负载,缓存静态资源的方法很多,主要还是根据实际的场景来选择适合项目的方案来满足需求。关于浏览器的缓存机制,我在百度里扒了张图,介绍的比较详细了:


  • 浏览器第一次请求流程图:


http缓存.png


  • 浏览器再次请求时:

http缓存1.png


在测试缓存过程中,我发现有两种缓存 from disk cache , from memory cache 。顾名思义:磁盘缓存,内存缓存。这有什么区别呢,什么时候存内存,什么时候存磁盘缓存呢?

200 from disk cache

不访问服务器,直接读缓存,从磁盘中读取缓存,当kill进程时,数据还是存在。

这种方式也只能缓存派生资源

304 Not Modified

访问服务器,发现数据没有

更新,服务器返回此状态码。然后从缓存中读取数据。

对于memory cache的使用,浏览器主要是去存储一些当前获取到的资源,对于dist的缓存,浏览器启动的时候就会创建一个curl打头的对象,然后创建一个文件夹,读取本地缓存文件放进去。

发表评论

登录 后参与评论

评论列表 (1条)

  • w178191520
    3 个月前
    好好好好好好