├── .gitattributes ├── .gitignore ├── LICENSE-MIT ├── README.md ├── WebAppCache.js ├── app.html ├── app.json ├── app.manifest └── demo ├── css ├── app.css └── demo.css ├── demo.html └── js ├── app.js └── demo.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Eclipse 3 | ################# 4 | 5 | *.pydevproject 6 | .project 7 | .metadata 8 | bin/ 9 | tmp/ 10 | *.tmp 11 | *.bak 12 | *.swp 13 | *~.nib 14 | local.properties 15 | .classpath 16 | .settings/ 17 | .loadpath 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # PDT-specific 29 | .buildpath 30 | 31 | 32 | ################# 33 | ## Visual Studio 34 | ################# 35 | 36 | ## Ignore Visual Studio temporary files, build results, and 37 | ## files generated by popular Visual Studio add-ons. 38 | 39 | # User-specific files 40 | *.suo 41 | *.user 42 | *.sln.docstates 43 | 44 | # Build results 45 | [Dd]ebug/ 46 | [Rr]elease/ 47 | *_i.c 48 | *_p.c 49 | *.ilk 50 | *.meta 51 | *.obj 52 | *.pch 53 | *.pdb 54 | *.pgc 55 | *.pgd 56 | *.rsp 57 | *.sbr 58 | *.tlb 59 | *.tli 60 | *.tlh 61 | *.tmp 62 | *.vspscc 63 | .builds 64 | *.dotCover 65 | 66 | ## TODO: If you have NuGet Package Restore enabled, uncomment this 67 | #packages/ 68 | 69 | # Visual C++ cache files 70 | ipch/ 71 | *.aps 72 | *.ncb 73 | *.opensdf 74 | *.sdf 75 | 76 | # Visual Studio profiler 77 | *.psess 78 | *.vsp 79 | 80 | # ReSharper is a .NET coding add-in 81 | _ReSharper* 82 | 83 | # Installshield output folder 84 | [Ee]xpress 85 | 86 | # DocProject is a documentation generator add-in 87 | DocProject/buildhelp/ 88 | DocProject/Help/*.HxT 89 | DocProject/Help/*.HxC 90 | DocProject/Help/*.hhc 91 | DocProject/Help/*.hhk 92 | DocProject/Help/*.hhp 93 | DocProject/Help/Html2 94 | DocProject/Help/html 95 | 96 | # Click-Once directory 97 | publish 98 | 99 | # Others 100 | [Bb]in 101 | [Oo]bj 102 | sql 103 | TestResults 104 | *.Cache 105 | ClientBin 106 | stylecop.* 107 | ~$* 108 | *.dbmdl 109 | Generated_Code #added for RIA/Silverlight projects 110 | 111 | # Backup & report files from converting an old project file to a newer 112 | # Visual Studio version. Backup files are not needed, because we have git ;-) 113 | _UpgradeReport_Files/ 114 | Backup*/ 115 | UpgradeLog*.XML 116 | 117 | 118 | 119 | ############ 120 | ## Windows 121 | ############ 122 | 123 | # Windows image file caches 124 | Thumbs.db 125 | 126 | # Folder config file 127 | Desktop.ini 128 | 129 | 130 | ############# 131 | ## Python 132 | ############# 133 | 134 | *.py[co] 135 | 136 | # Packages 137 | *.egg 138 | *.egg-info 139 | dist 140 | build 141 | eggs 142 | parts 143 | bin 144 | var 145 | sdist 146 | develop-eggs 147 | .installed.cfg 148 | 149 | # Installer logs 150 | pip-log.txt 151 | 152 | # Unit test / coverage reports 153 | .coverage 154 | .tox 155 | 156 | #Translations 157 | *.mo 158 | 159 | #Mr Developer 160 | .mr.developer.cfg 161 | 162 | # Mac crap 163 | .DS_Store 164 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 zawa 2 | http://www.zawaliang.com/ 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WebAppCache 1.0.3 2 | =========== 3 | 4 | 轻量化的WebApp缓存管理,使用Application Cache + localStorage(SessionStorage) 5 | intro:http://www.zawaliang.com/2013/04/277.html 6 | 7 | 8 | 9 | [如何使用] 10 | ---------------------- 11 | 基本只需要两步: 12 | 1. 按格式定义app.json 13 | 2. 定义入口文件(如app.html) 14 | 15 | 16 | [Demo] 17 | ---------------------- 18 | 目录结构如下: 19 | WebAppCache约定app.html与app.json处于相同的目录层级,其他的资源不作要求。 20 | ``` 21 | app/-- 22 | |---app.manifest 23 | |---app.json 24 | |---app.html 25 | |---WebAppCache.js 26 | |---page/ 27 | |---index.html 28 | |---inner/ 29 | |---demo.html 30 | |---js/ 31 | |---app.js 32 | |---demo.js 33 | |---css/ 34 | |---app.css 35 | |---inner/ 36 | |---demo.css 37 | ``` 38 | 39 | 40 | app.json应用配置文件 41 | 42 | ``` 43 | { 44 | // 配置文件过期时间(分钟) 45 | "expire": "10", 46 | // 离线时缓存命中失败的提示 47 | "networkError": "网络连接失败", 48 | // 核心加载的js文件 49 | "jsCore": ["app"], 50 | // 核心加载的css文件 51 | "cssCore": ["app"], 52 | // js配置 53 | "jsConfig": { 54 | // js基准路径 55 | "path": "app/js/", 56 | // js缺省后缀 57 | "suffix": ".js" 58 | }, 59 | // css配置 60 | "cssConfig": { 61 | // css基准路径 62 | "path": "app/css/", 63 | // css缺省后缀 64 | "suffix": ".css" 65 | }, 66 | // 页面配置 67 | "pageConfig": { 68 | // 页面基准路径 69 | "path": "app/page/", 70 | // 页面缺省后缀 71 | "suffix": ".html" 72 | }, 73 | // 声明应用js资源 74 | "js": { 75 | "app": { 76 | // 指定拉取路径,url为空时以基准路径+模块名拉取+缺省后缀 77 | "url": "app/js/app.js", 78 | // 版本号,-1时不作缓存 79 | "v": "1.0.0" 80 | }, 81 | "demo": { 82 | "url": "app/js/demo.js", 83 | "v": "1.0.0" 84 | } 85 | }, 86 | // 声明应用css资源 87 | "css": { 88 | "app": { 89 | "url": "app/css/app.css", 90 | "v": "1.0.0" 91 | }, 92 | "inner.demo": { 93 | "url": "app/css/inner/demo.css", 94 | "v": "1.0.0" 95 | } 96 | }, 97 | // 声明应用页面 98 | "page": { 99 | "demo": { 100 | "v": "1.0.0", 101 | // 声明除去核心加载外需要加载的资源 102 | "js": ["demo"], 103 | "css": ["inner.demo"] 104 | } 105 | } 106 | } 107 | ``` 108 | 109 | app.html缓存调度 110 | ``` 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | ``` 122 | 123 | 实际请求地址如下: 124 | ``` 125 | inner/demo.html -> app/app.html?v=inner.demo 126 | ``` 127 | 128 | [一些注意事项] 129 | ---------------------- 130 | 1. app.html?v=xxx中缺省为v=index 131 | 2. 核心css资源于所有其他css资源前渲染 132 | 3. 资源不可跨域访问 133 | 4. 由于document.write对脚本中的script敏感,WebAppCache采取动态生成脚本执行的方式,这就可能造成页面中的脚本于缓存中的脚本前执行的问题。所以对于依赖app.json中声明的js的脚本,建议通过配置到app.json里处理来解决,这个问题后续优化解决。 134 | -------------------------------------------------------------------------------- /WebAppCache.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WebAppCache v1.0.3 3 | * 2013, zawa, www.zawaliang.com 4 | * Licensed under the MIT license. 5 | */ 6 | 7 | ;(function(window, document, undefined) { 8 | var _local = localStorage, 9 | _session = sessionStorage, 10 | _pathname = location.pathname, 11 | _appName = _pathname, // App标识 12 | _fireQueue = [], // 最终需要执行的队列 13 | _onload = false, 14 | _renderReady = false, 15 | _cache = {}, 16 | _storage = null, 17 | _viewName = getParam('v'); 18 | 19 | 20 | function getParam(name) { 21 | var p = '&' + location.search.substr(1) + '&', 22 | re = new RegExp('&' + name + '=([^&]*)&'), 23 | r = p.match(re); 24 | 25 | return r ? r[1] : ''; 26 | } 27 | 28 | function get(url, callback, errorHandler) { 29 | var xhr = new XMLHttpRequest(); 30 | xhr.onreadystatechange = function() { 31 | if (xhr.readyState == 4) { 32 | var status = xhr.status; 33 | if ((status >= 200 && status < 300) || status == 304) { 34 | callback.call(null, xhr.responseText); 35 | } else { 36 | var msg = status ? 'Error:' + status : 'Abort'; 37 | (getType(errorHandler) == 'function') 38 | ? errorHandler(status, msg) 39 | : null; 40 | } 41 | } 42 | }; 43 | // xhr.withCredentials = true; 44 | xhr.open('get', url, true); 45 | xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 46 | xhr.send(null); 47 | } 48 | 49 | /** 50 | * 数据类型 51 | * @param {*} o 52 | * @return {String} string|array|object|function|number|boolean|null|undefined 53 | */ 54 | function getType(o) { 55 | var t = typeof(o); 56 | return (t == 'object' ? Object.prototype.toString.call(o).slice(8, -1) : t).toLowerCase(); 57 | } 58 | 59 | function each(item, callback) { 60 | if (getType(item) == 'array') { 61 | for (var i = 0, len = item.length; i < len; i++) { 62 | callback.apply(null, [i, item[i]]); 63 | } 64 | } else { 65 | for (var k in item) { 66 | if (item.hasOwnProperty(k)) { 67 | callback.apply(null, [k, item[k]]); 68 | } 69 | } 70 | } 71 | } 72 | 73 | function inArray(item, arr) { 74 | for (var i = 0, len = arr.length; i < len; i++) { 75 | if (arr[i] === item) { 76 | return i; 77 | } 78 | }; 79 | return -1; 80 | } 81 | 82 | /** 83 | * 优化的缓存设置, 溢出捕获以及队列管理 84 | */ 85 | function cache(n, v, prefix) { 86 | prefix = (getType(prefix) == 'string') ? prefix : _appName; 87 | if (getType(v) == 'undefined') { 88 | var r = _storage.getItem(prefix + n); 89 | 90 | if (r === null) { 91 | return null; 92 | } 93 | 94 | try { 95 | return JSON.parse(r); 96 | } catch (e) { 97 | return r; 98 | } 99 | } 100 | 101 | // 缓存当前应用的写操作key值(无序) 102 | if (prefix == _appName) { 103 | var cacheKey = cache('CacheKey') || []; 104 | cacheKey.push(n); 105 | cacheKey = uniq(cacheKey); 106 | _storage.setItem(_appName + 'CacheKey', JSON.stringify(cacheKey)); 107 | } 108 | 109 | if (getType(v) != 'string') { 110 | v = JSON.stringify(v); 111 | } 112 | 113 | try { 114 | _storage.setItem(prefix + n, v); 115 | } catch (e) { 116 | var appName = shiftAppCache(); 117 | 118 | if (appName !== false) { // 重新尝试缓存 119 | cache(n, v); 120 | } else { // 没有应用缓存可供删除时, 淘汰当前应用队列 121 | var cq = cache('Core') || [], 122 | sq = sourceQueue(); 123 | 124 | // 将Core与Source资源合并进行队列管理 125 | sq = sq.concat(cq); 126 | 127 | // 缓存区不足时,淘汰当前应用缓存重新发起请求 128 | if (sq.length < 1) { 129 | clearAppCache(_appName); 130 | location.reload(false); 131 | return; 132 | } 133 | 134 | var item = sq.shift(), 135 | key = _appName + item; 136 | 137 | // 删除最早的缓存 138 | _storage.removeItem(key); 139 | _storage.removeItem(key + '.Version'); 140 | // 更新队列 141 | sourceQueue(sq); 142 | // 重新尝试缓存 143 | cache(n, v); 144 | } 145 | } 146 | } 147 | 148 | /** 149 | * 清空应用缓存 150 | */ 151 | function clearAppCache(appName) { 152 | var cacheKey = cache('CacheKey', undefined, appName) || []; 153 | each(cacheKey, function(k, v) { 154 | _storage.removeItem(appName + v); 155 | }); 156 | _storage.removeItem(appName + 'CacheKey'); 157 | } 158 | 159 | /** 160 | * 按应用缓存队列清空应用缓存(跳过当前应用缓存) 161 | */ 162 | function shiftAppCache() { 163 | var appQueue = cache('App.Queue', undefined, '') || []; 164 | appQueue = arrDel(_appName, appQueue); // 跳过当前应用缓存 165 | 166 | if (appQueue.length > 0) { 167 | var appName = appQueue.shift(); 168 | clearAppCache(appName); 169 | cache('App.Queue', appQueue, ''); 170 | return appName 171 | } 172 | return false; 173 | } 174 | 175 | /** 176 | * 缓存非核心资源队列 177 | */ 178 | function sourceQueue(sq) { 179 | return (getType(sq) != 'undefined') 180 | ? cache('Source', sq) 181 | : cache('Source') || []; 182 | } 183 | 184 | /** 185 | * 格式化配置 186 | */ 187 | function formatAppConf(conf) { 188 | var source = {}; 189 | 190 | each(['js', 'css', 'page'], function(k, v) { 191 | conf[v] && each(conf[v], function(k2, v2) { 192 | var name = v + '_' + k2; 193 | v2.__name__ = k2; 194 | v2.__type__ = v; 195 | v2.v = v2.v || ''; // 版本号缺省为空 196 | source[name] = v2; 197 | }); 198 | 199 | // 删除格式化后的js css page配置 200 | conf[v] = null; 201 | delete conf[v]; 202 | }); 203 | conf.__source__ = source; 204 | return conf; 205 | } 206 | 207 | function concatArr(arr, type) { 208 | each(arr, function(k, v) { 209 | arr[k] = type + v; 210 | }); 211 | return arr; 212 | } 213 | 214 | /** 215 | * 删除数组某项 216 | */ 217 | function arrDel(n, arr) { 218 | var i = inArray(n, arr); 219 | if (i != -1) { 220 | arr.splice(i, 1); 221 | } 222 | return arr; 223 | } 224 | 225 | /** 226 | * 去重 227 | */ 228 | function uniq(arr) { 229 | var hash = {}, 230 | r = []; 231 | 232 | each(arr, function(k, v) { 233 | if (!hash[v]) { 234 | r.push(v); 235 | hash[v] = 1; 236 | } 237 | }); 238 | return r; 239 | } 240 | 241 | /** 242 | * 队列去重与依赖处理 243 | */ 244 | function handleFireQueue(config) { 245 | var view = 'page_' + _viewName, // 加载的视图 246 | source = config.__source__, 247 | view = source[view] ? view : 'page_index', 248 | c = source[view] || {}, 249 | jsCore = config.jsCore || [], 250 | cssCore = config.cssCore || [], 251 | jsQueue = jsCore.concat(c.js || []), 252 | cssQueue = cssCore.concat(c.css || []); 253 | 254 | jsQueue = concatArr(jsQueue, 'js_'); 255 | cssQueue = concatArr(cssQueue, 'css_'); 256 | 257 | // 去重 258 | jsQueue = uniq(jsQueue); 259 | cssQueue = uniq(cssQueue); 260 | 261 | // TODO: 依赖管理(暂时按数组顺序加载) 262 | 263 | // 保持css队列在前, 提升后续开始加载时间 264 | _fireQueue = cssQueue.concat(jsQueue); 265 | _fireQueue.push(view); 266 | } 267 | 268 | /** 269 | * 获取资源路径 270 | */ 271 | function getPath(config, c) { 272 | if (c.url) { 273 | return c.url; 274 | } 275 | 276 | var conf = config[c.__type__ + 'Config'] || {}; 277 | return conf.path + c.__name__.replace(/\./g, '/') + conf.suffix; 278 | } 279 | 280 | /** 281 | * 更新队列 282 | */ 283 | function updateQueue(queue) { 284 | var config = _cache['Config'], 285 | source = config.__source__, 286 | cq = cache('Core') || [], 287 | sq = sourceQueue(), 288 | len = queue.length, 289 | loaded = 0; 290 | 291 | // 并行加载资源 292 | each(queue, function(k, v) { 293 | var s = source[v], 294 | path = getPath(config, s), 295 | url = path + '?_=' + (s.v == -1 ? (+new Date()) : s.v); 296 | 297 | get(url, (function(n, c) { 298 | return function(data) { 299 | // 1) 核心资源不计入Source队列 300 | // 2) 非缓存资源不计入Source队列 301 | if (inArray(n, cq) == -1 && c.v != -1) { 302 | // 若缓存中已存在n的资源,则更新其队列位置 303 | if (inArray(n, sq) != -1) { 304 | sq = arrDel(n, sq); 305 | } 306 | 307 | sq.push(n); 308 | } 309 | 310 | // 缓存时方存储 311 | if (c.v != -1) { 312 | cache(n, data); 313 | cache(n + '.Version', c.v); 314 | } 315 | 316 | _cache[n] = data; 317 | loaded++; 318 | 319 | if (loaded == len) { 320 | // 按添加时间缓存队列 321 | sourceQueue(sq); 322 | 323 | render(); 324 | } 325 | }; 326 | })(v, s)); 327 | }); 328 | } 329 | 330 | /** 331 | * 版本比较 332 | */ 333 | function cmpVersion(config) { 334 | var source = config.__source__, 335 | jsCore = config.jsCore || [], 336 | cssCore = config.cssCore || []; 337 | 338 | handleFireQueue(config); 339 | 340 | // 缓存核心资源队列(按依赖权重升序排列,css在前,js在后, 用于缓存队列管理) 341 | var core = []; 342 | core[0] = concatArr(cssCore, 'css_').reverse(); 343 | core[1] = concatArr(jsCore, 'js_').reverse(); 344 | core = core[0].concat(core[1]); 345 | cache('Core', core); 346 | 347 | // 检查资源是否需要更新 348 | var udQueue = []; 349 | each(_fireQueue, function(k, v) { 350 | _cache[v] = cache(v); 351 | 352 | if (source[v].v == -1 // 不需缓存 353 | || _cache[v] === null // 本地不存在v的缓存 354 | || cache(v + '.Version') != source[v].v // 有新版本 355 | ) { 356 | 357 | udQueue.push(v); 358 | } 359 | }); 360 | 361 | if (udQueue.length > 0) { // 有更新队列 362 | updateQueue(udQueue); 363 | } else { // 版本没变化 364 | render(); 365 | } 366 | } 367 | 368 | // http://www.jspatterns.com/the-ridiculous-case-of-adding-a-script-element/ 369 | // http://www.whatwg.org/specs/web-apps/current-work/multipage/scripting-1.html#dom-script-text 370 | var appendJs = function(text) { 371 | var body = document.getElementsByTagName('body')[0], 372 | script = document.createElement('script'); 373 | 374 | script.type = 'text/javascript'; 375 | // script.charset = 'utf-8'; 376 | script.defer = 'true'; 377 | script.text = text; 378 | body.appendChild(script); // 插入到body下,防止defer不支持造成js于DOM加载完成前触发 379 | }; 380 | 381 | /** 382 | * 检查是否可渲染 383 | */ 384 | function render() { 385 | _renderReady = true; 386 | if (_onload) { 387 | goRender(); 388 | } 389 | } 390 | 391 | /** 392 | * 页面渲染 393 | */ 394 | function goRender() { 395 | var config = _cache['Config'], 396 | source = config.__source__ || {}, 397 | arrText = {}; 398 | 399 | each(_fireQueue, function(k, v) { 400 | var type = source[v].__type__; 401 | if (!arrText[type]) { 402 | arrText[type] = []; 403 | } 404 | arrText[type].push(_cache[v] || ''); 405 | }); 406 | 407 | var page = arrText.page.join(''); 408 | 409 | 410 | // css优先于页面其他样式渲染 411 | var css = '