智能手机的发展
伴随着智能手机在国内的普及及其换代速度,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






评论