├── .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 = '