智能手机的发展
伴随着智能手机在国内的普及及其换代速度,HTML5技术在移动领域的发展终究比PC端来的更迅速些。
根据移动互联网第三方数据挖掘和整合营销机构艾媒咨询(iiMedia Research)发布的《2012中国智能手机市场年度研究报告》数据显示,截止2012Q4季度,中国智能手机用户数达到了3.8亿,同比增长72.7%;
报告指出,在操作系统占有率方面,Android份额到达68.6%,iOS有所下滑,占12.8%,Symbian则难抑下滑趋势,占12.4%,Windows Phone作为后起之秀,份额只占3.8%,但后续占有率会持续扩大;
在系统平台分布方面,Android上V2.x是主流版本,iOS上V5是主流版本
(以上数据来自iiMedia Research《2012中国智能手机市场年度研究报告》)
Application Cache的问题
回到正题,HTML5伴随着智能手机的发展,在手机应用开发方面的优势越发凸显,尤其是其跨平台、版本更新等优势;在一些对性能要求稍低及项目人员紧张的产品中,使用WebApp的形式(大多为Native App+WebApp的混搭方式)不失为一种好的解决方案。
排除WIFI,对于国内那昂贵的流量费用,而且是极不稳定的GPRS来说,WebApp的开发不得不考虑一个问题:缓存。
开始我很兴奋,知道HTML5给我们提供了Application Cache离线储存接口,通过manifest文件,我终于可以翱翔在离线数据的大海中。
理想很丰满,现实却总是很骨感;Application Cache这货可真不好管理,如果你还不清楚它的实际情况,可以参考下这篇文章《Application Cache is a Douchebag》,内容我就不翻译了,但标题我得翻译下:《Application Cache就是个人渣》。
关于Application Cache,有一个致命的缺点,那就是你不能选择更新哪些资源。你的manifest文件更新了,所有指定的资源都会给下载,对于流量是金的移动互联网来说,这不就是坑爹嘛。
localStorage
但上帝总在关了一扇门之后,给我们开启另一扇门,而这一扇门就是:localStorage。
localStorage的存储空间是按域名来计算的,不同平台容量不同,即使相同平台相同版本但由于手机厂商调教不一,造成实际使用中的大小也是 不一样的。就拿笔者的MX2(Android 4.1)自带的浏览器来说,测试出来的结果是64M。虽然不同平台及版本存在差异,但对于大部分WebApp来说,这样的存储空间已经可以派上大用场了。
UserAgent | Megabytes |
---|---|
Android 2.3 | 8 |
Android 4 | 58 |
iPhone 5 | 5 |
iPhone 6 | 26 |
(以上数据来自Browserscope,测试地址点这里)
在使用localStorage前,我们还需要清楚webview对localStorage的影响,特别是对于那些嵌套在不同客户端的 WebApp来说,webview对localStorage的支持与否也是不可忽视的一点。同时,对于业务较多的根域来说,不同WebApp之间可能会 出现空间上的使用管理混乱问题,这需要在前期规划时对存储做好队列管理工作。
解决方案:WebAppCache
考虑到Application Cache的维护麻烦问题,在我最近的项目中就基本放弃了manifest的方式,转而使用类MVC的方式(估且这么叫吧)。
WebAppCache方案由app.json配置应用的每个资源信息,app.html进行整个应用的调度,包括版本对比、更新以及缓存队列管理。由于使用了Ajax来拉取文件,所以受同源访问限制,对跨域请求有要求的同学,可以使用withCredentials,这里就不仔细展开了,只是提供其中一种简单的实现方式。
假设我们的目录结构如下:
app/-- |---app.manifest |---app.html |---WebAppCache.js |---page/ |---index.html |---inner/ |---demo.html |---js/ |---zepto.min.js |---touch.min.js |---app.js |---demo.js |---css/ |---global.css |---inner/ |---demo.css
WebAppCache约定app.html与app.json处于相同的目录层级,其他的资源不作要求。
则实际请求地址如下:
index.html -> /app/app.html 或 /app/app.html?v=index
inner/demo.html -> /app/app.html?v=inner.demo
app.json应用配置文件
{ // 配置app.json文件过期时间(分钟) "expire": "30", // 核心加载的js文件 "jsCore": ["zepto", "touch"], // 核心加载的css文件 "cssCore": ["global"], // js配置 "jsConfig": { // js基准路径 "path": "/app/js/", // js缺省后缀 "suffix": ".js" }, // css配置 "cssConfig": { // css基准路径 "path": "/app/css/", // css缺省后缀 "suffix": ".css" }, // 页面配置 "pageConfig": { // 页面基准路径 "path": "/app/page/", // 页面缺省后缀 "suffix": ".html" }, // 声明应用js资源 "js": { "zepto": { // 指定拉取路径,url为空时以"基准路径+模块名+缺省后缀"拉取 "url": "/app/js/zepto.min.js", // 版本号,-1时不作缓存 "v": "1.0.0" }, "touch": { "url": "/app/js/touch.min.js", "v": "1.0.0" }, "app": { "v": "1.0.0" }, "demo": { "v": "1.0.0" } }, // 声明应用css资源 "css": { "global": { "url": "/app/css/global.css", "v": "1.0.0" }, "app": { "v": "1.0.1" }, "inner.demo": { "v": "1.0.0" } }, // 声明应用页面 "page": { "index": { "v": "20130415", // 声明除去核心加载外需要加载的资源 "js": ["app"], "css": ["app"] }, "inner.demo": { "v": "20130415", "js": ["app", "demo"], "css": ["app", "inner.demo"] } } }
app.html缓存调度
<!DOCTYPE html> <html manifest="/app/app.manifest"> <head> <title></title> <meta charset="utf-8"> <script type="text/javascript" src="/app/WebAppCache.js"></script> </head> <body> </body> </html>
app.html我使用了Application Cache,这在不使用SPA方式对页面进行documwnt.write输出时,可以加快页面载入速度。当所有的资源处理完毕之后,会将内容渲染到当前页面输出。
WebAppCache.js之队列管理
为了兼容同一根域下多WebApp的场景,WebAppCache.js以应用为单位进行缓存管理,每次进行写操作时,都会缓存当前的key到队列 里;同时资源队列以”资源缓存时间先后 + css核心资源(按依赖权重由低到高排列)+ js核心资源(按依赖权重由低到高排列)“进行排列;
在溢出时,按App使用时间先后进行队列淘汰;当所有非当前App淘汰完毕后,再对当前App资源进行资源队列淘汰;在淘汰当前App资源队列后仍无法存储时,最后尝试清空当前App缓存再试。
/** * 优化的缓存设置, 溢出捕获以及队列管理 */ function cache(n, v, prefix) { prefix = (getType(prefix) == 'string') ? prefix : _appName; if (getType(v) == 'undefined') { var r = _storage.getItem(prefix + n); if (r === null) { return r; } try { return JSON.parse(r); } catch (e) { return r; } } // 缓存当前应用的写操作key值(无序) if (prefix == _appName) { var cacheKey = cache('CacheKey') || []; cacheKey.push(n); cacheKey = uniq(cacheKey); _storage.setItem(_appName + 'CacheKey', JSON.stringify(cacheKey)); } if (getType(v) != 'string') { v = JSON.stringify(v); } try { _storage.setItem(prefix + n, v); } catch (e) { var appName = shiftAppCache(); if (appName !== false) { // 重新尝试缓存 cache(n, v); } else { // 没有应用缓存可供删除时, 淘汰当前应用队列 var cq = cache('Core') || [], sq = sourceQueue(); // 将Core与Source资源合并进行队列管理 sq = sq.concat(cq); // 缓存区不足时,淘汰当前应用缓存重新发起请求 if (sq.length < 1) { clearAppCache(_appName); window.location.reload(false); return; } var item = sq.shift(), key = _appName + item; // 删除最早的缓存 _storage.removeItem(key); _storage.removeItem(key + '.Version'); // 更新队列 sourceQueue(sq); // 重新尝试缓存 cache(n, v); } } } /** * 清空应用缓存 */ function clearAppCache(appName) { var cacheKey = cache('CacheKey', undefined, appName) || []; each(cacheKey, function(k, v) { _storage.removeItem(appName + v); }); _storage.removeItem(appName + 'CacheKey'); } /** * 按应用缓存队列清空应用缓存(跳过当前应用缓存) */ function shiftAppCache() { var appQueue = cache('App.Queue', undefined, '') || []; appQueue = arrDel(_appName, appQueue); // 跳过当前应用缓存 if (appQueue.length > 0) { var appName = appQueue.shift(); clearAppCache(appName); cache('App.Queue', appQueue, ''); return appName } return false; }
对于同一根域下多WebApp的场景,当用户同时开启多个应用造成空间不足时,当前的解决方案在localStorage支持的情况下可能会出现数据缓存不久就被淘汰的情况,这种情况可以通过转换为sessionStorage来进行优化。
有一点需要注意,在使用document.write输出文档流时,要在window.onload触发后方可进行页面渲染,否则原文档流不会被覆盖。
最后附上GitHub地址:WebAppCache
评论