前端静态文件缓存,大家已经耳熟能详,近期对项目进行优化又去详细看了下相关资料,在此整理。
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 worker 并不太适合多页面应用,何为多页面,即请求先走的服务端,通过服务端直接渲染的页面,service worker 效果不明显,它比较适合单页面应用,比如vuejs 或者 react 类 spa类型的应用,另外,service worker 虽然被google和前端爱好者所推崇,然而,兼容性还不是那么理想,ios 目前是不支持的。

2、尝试用独立的manifest 。虽然官方目前不是很推荐用manifest,而且也停止了manifest的更新,但是目前绝大多数的现代浏览器都是支持的,也是比较成熟的缓存方案,在w3上面对它的优点做了简单介绍:
离线浏览 - 用户可在应用离线时使用它们
速度 - 已缓存资源加载得更快
减少服务器负载 - 浏览器将只从服务器下载更新过或更改过的资源。
于是开始尝试,manifest的webpack插件也比较多,试用了几个,感觉都很不爽,不能满足业务需求,比如我想:
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 构建后可以看到静态文件目录生成两个文件:

之后,我们可以通过后台打开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)

现象:页面打开正常,静态文件加载缓慢。
② 离线条件下

现象:页面样式异常,静态文件加载异常。
加入缓存后:
① 离线情况下

现象:打开页面后,被缓存的静态资源从disk cache中请求,页面显示正常,打开速度很快
manifest 方式也是有坑的,它有一些缺陷:
更新的资源,需要二次刷新才会被页面采用
不支持增量更新,只有manifest发生变化,所有资源全部重新下载一次
缺乏足够容错机制,当清单中任意资源文件出现加载异常,都会导致整个manifest策略运行异常
不过对于目前的项目来讲,这些问题不大,可以考虑使用。
3、使用iframe预加载页面。通过预加载网页的形式,我们可以在第一个页面加载完成时,预加载可能需要跳转的页面,预加载的方式和上面类似,可以通过一个隐藏的iframe来实现,这样,不仅可以提高打开速度,也可以缓存静态资源到disk cache 中,后面静态文件不用再请求服务器。
PS: 缓存的作用主要是让网页有更快的响应速度,增强体验,减少服务器响应时间,减少负载,缓存静态资源的方法很多,主要还是根据实际的场景来选择适合项目的方案来满足需求。关于浏览器的缓存机制,我在百度里扒了张图,介绍的比较详细了:
浏览器第一次请求流程图:

浏览器再次请求时:

在测试缓存过程中,我发现有两种缓存 from disk cache , from memory cache 。顾名思义:磁盘缓存,内存缓存。这有什么区别呢,什么时候存内存,什么时候存磁盘缓存呢?
200 from disk cache
不访问服务器,直接读缓存,从磁盘中读取缓存,当kill进程时,数据还是存在。
这种方式也只能缓存派生资源
304 Not Modified
访问服务器,发现数据没有
更新,服务器返回此状态码。然后从缓存中读取数据。
对于memory cache的使用,浏览器主要是去存储一些当前获取到的资源,对于dist的缓存,浏览器启动的时候就会创建一个curl打头的对象,然后创建一个文件夹,读取本地缓存文件放进去。