前端静态文件缓存优化采坑记录
前端静态文件缓存,大家已经耳熟能详,近期对项目进行优化又去详细看了下相关资料,在此整理。
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打头的对象,然后创建一个文件夹,读取本地缓存文件放进去。