zoukankan      html  css  js  c++  java
  • 1-7 basket.js localstorage.js缓存css、js

    basket.js 源码分析

     
    api 使用文档:
     
     
     

    一、前言

    basket.js 可以用来加载js脚本并且保存到 LocalStorage 上,使我们可以更加精准地控制缓存,即使是在 http 缓存过期之后也可以使用。因此可以使我们防止不必要的重新请求 js 脚本,提升网站加载速度。

    可以到 basket.js 的 Github 上查看更多的相关信息。

    由于之前在工作中使用过 basket.js ,好奇它的实现原理,因此就有了这篇分析 basket.js 源码的文章。

    二、简单的使用说明

    basket.js 的使用非常简单,只要引入相应的js脚本,然后使用 basket 的 require 方法加载就可以了,例子:

    <!DOCTYPE html><html><head><title>basket.js demo</title><script src="basket.full.min.js"></script></head><body><script>basket.require({url:'helloworld.js'});</script></body></html>

    第一次加载,由于helloworld.js 只有一行代码alert('hello world');, 所以运行该demo时就会弹出 "hello world"。并且对应的 js 会被保存到 LocalStorage:

    此时对应的资源加载情况:

    刷新一次页面,再查看一次资源的加载情况:

    可以看到已经没有再发送 helloworld.js 相关的请求,因为 LocalStorage 上已经有对应的缓存了,直接从本地获取即可。

    三、实现流程

    流程图

    细节说明

    处理参数

    参数处理就是根据已有的参数初始化未指定的参数。例如 require 方法支持 once 参数用来表示是否只执行一次对应 JS,execute 参数标示是否加载完该 JS 之后立刻执行。所以参数处理这一步骤就会根据是否执行过该 JS 和 once 参数是否为 ture 来设置execute参数。

    获取缓存

    调用 localStorage.getItem方法获取缓存。存入的 key 值为 basket- 加上 JS 文件的 URL。以上面加载 helloworld.js 为例,key 值为:basket-helloworld.js获取的缓存为一个缓存对象,里面包含 JS 代码和相关的一些信息,例如:

    1. {
    2. "url":"helloworld.js?basket-unique=123",
    3. "unique":"123",
    4. "execute":true,
    5. "key":"helloworld.js",
    6. "data":"alert('hello world');",
    7. "originalType":"application/javascript",
    8. "type":"application/javascript",
    9. "skipCache":false,
    10. "stamp":1459606005108,
    11. "expire":1477606005108
    12. }
     

    其中 data 属性对应的值就是 JS 代码。

    判断缓存是否有效

    判断比较简单,根据缓存对象里面的版本号 unique 和过期时间 expire 等来判断。这和浏览器使用 Expire 和 Etag 头部来判断 HTTP 缓存是否有效相似。最大的不同就是缓存完全由 JS 控制!这也就是 basket.js 最大的作用。让我们更好的控制缓存。默认的过期时间为5000小时,也就是208.33天。

    判断代码:

    /** * 判断ls上的缓存对象是否过期 * @param{object} source 从ls里取出的缓存对象 * @param{object} obj 传入的参数对象 * @returns {Boolean} 过期返回true,否则返回false */var isCacheValid =function(source, obj) {return!source ||// 没有缓存数据返回truesource.expire-+newDate() <0||// 超过过期时间返回trueobj.unique!==source.unique||// 版本号不同的返回true (basket.isValidItem&&!basket.isValidItem(source, obj));// 自定义验证函数不成功的返回true};

    Ajax获取JS

    普通的利用 XMLHttpRequest 请求。

    缓存到LocalStorage

    调用localStorage.setItem方法保存缓存对象。一般来说,只要这一行代码就能完成本步骤。但是LocalStorage保存的数据是有大小限制的!我利用 chrome 做了一个小测试,保存500KB左右的东西就会令��� Resources 面板变卡,2M 几乎可以令到 Resources 基本卡死,到了 5M 就会超出限制,浏览器抛出异常:

     

    OMException: Failed to execute'setItem'on'Storage': Setting the valueof'basket-http://file.com/ykq/wap/v3Templates/timeout/timeout/large.js' exceeded the quota

     

    因此需要使用 try catch 对localStorage.setItem方法进行异常捕获。当没容量不足时就需要根据保存时间逐一删除 LocalStorage 的缓存对象。

    相关代码:

     

    1. /**
    2. * 把缓存对象保存到localStorage中
    3. * @param {string} key ls的key值
    4. * @param {object} storeObj ls的value值,缓存对象,记录着对应script的对象、有url、execute、key、data等属性
    5. * @returns {boolean} 成功返回true
    6. */
    7. var addLocalStorage =function( key, storeObj ){
    8. // localStorage对大小是有限制的,所以要进行try catch
    9. // 500KB左右的东西保存起来就会令到Resources变卡
    10. // 2M左右就可以令到Resources卡死,操作不了
    11. // 5M就到了Chrome的极限
    12. // 超过之后会抛出如下异常:
    13. // DOMException: Failed to execute 'setItem' on 'Storage': Setting the value of 'basket-http://file.com/ykq/wap/v3Templates/timeout/timeout/large.js' exceeded the quota
    14. try{
    15. localStorage.setItem( storagePrefix + key, JSON.stringify( storeObj ));
    16. returntrue;
    17. }catch( e ){
    18. // localstorage容量不够,根据保存的时间删除已缓存到ls里的js代码
    19. if( e.name.toUpperCase().indexOf('QUOTA')>=0){
    20. var item;
    21. var tempScripts =[];
    22. // 先把所有的缓存对象来出来,放到 tempScripts里
    23. for( item in localStorage ){
    24. if( item.indexOf( storagePrefix )===0){
    25. tempScripts.push( JSON.parse( localStorage[ item ]));
    26. }
    27. }
    28. // 如果有缓存对象
    29. if( tempScripts.length ){
    30. // 按缓存时间升序排列数组
    31. tempScripts.sort(function( a, b ){
    32. return a.stamp - b.stamp;
    33. });
    34. // 删除缓存时间最早的js
    35. basket.remove( tempScripts[0].key );
    36. // 删除后在再添加,利用递归完成
    37. return addLocalStorage( key, storeObj );
    38. }else{
    39. // no files to remove. Larger than available quota
    40. // 已经没有可以删除的缓存对象了,证明这个将要缓存的目标太大了。返回undefined。
    41. return;
    42. }
    43. }else{
    44. // some other error
    45. // 其他的错误,例如JSON的解析错误
    46. return;
    47. }
    48. }
    49. };

    生成script标签注入到页面

    生成 script 标签,append 到 document.head:

    1. var injectScript =function( obj ){
    2. var script = document.createElement('script');
    3. script.defer =true;
    4. // Have to use .text, since we support IE8,
    5. // which won't allow appending to a script
    6. script.text = obj.data;
    7. head.appendChild( script );
    8. };

    四、异步编程

    basket.js 是一个典型的需要大量异步编程的库,所以稍有不慎,代码将会高度耦合,臃肿难看。。。

    所以 basket.js 引入遵从 Promises/A+ 标准的异步编程库 RSVP.js 来这个问题。

    (遵从 Promises/A+ 标准的还有 ES6 原生的 Promise 对象,jQuery 的$.Deferred 方法等)

    所以 basket.js 中涉及异步编程的方法都会返回一个 Promise 对象。很好地解决了异步编程问题。例如 basket.require 方法就是返回一个promise 对象,因此需要按顺序加载 JS 的时候可以这样子写:

    basket.require({url: 'helloworld.js'}).then(function() {basket.require({url: 'helloworld2.js'})
    });

    为了使代码更好看,basket.js 添加了一个方法 basket.thenRequire,现在代码就可以写成这样:

    basket.require({url: 'helloworld.js'}).thenRequire({url: 'helloworld2.js'});

    五、吐槽

    其实 basket.js 算是一种黑科技,使用起来有比较多的东西要注意。例如我们无法正常使用 chrome 的 Sources 面板断点调试,解决方法为手动在代码里面添加debugger设置断点。还有就是由于强制刷新页面也不能清除 localStorage 上的缓存,所以每次修改代码时我们都需要手动清除 localStorage,比较麻烦。当然调试时可以在 JS 文件的头部添加localStorage.clear()解决这个问题。

    还有就是 basket.js 已经好久没有更新了,毕竟黑科技,总会被时代淘汰。而且 api 文档也不齐全,例如上面的 thenRequire 方法是我查看源代码时才发现的,官方文档里面根本没有。

    最后,虽然 basket.js 应该不会在维护了,但是阅读其源码还是能有很多收获,推荐大家花点时间阅读一下。

    六、源码完整注释

    1. /*!
    2. * basket.js
    3. * v0.5.2 - 2015-02-07
    4. * http://addyosmani.github.com/basket.js
    5. * (c) Addy Osmani; License
    6. * Created by: Addy Osmani, Sindre Sorhus, Andrée Hansson, Mat Scales
    7. * Contributors: Ironsjp, Mathias Bynens, Rick Waldron, Felipe Morais
    8. * Uses rsvp.js, https://github.com/tildeio/rsvp.js
    9. */(function( window, document ){
    10. 'use strict';
    11. var head = document.head || document.getElementsByTagName('head')[0];
    12. var storagePrefix ='basket-';// 保存localStorage时的前缀
    13. var defaultExpiration =5000;// 默认过期时间为5000小时
    14. var inBasket =[];// 保存已经执行过的js的url。辅助设置参数的execute选项。
    15. /**
    16. * 把缓存对象保存到localStorage中
    17. * @param {string} key ls的key值
    18. * @param {object} storeObj ls的value值,缓存对象,记录着对应script的对象、有url、execute、key、data等属性
    19. * @returns {boolean} 成功返回true
    20. */
    21. var addLocalStorage =function( key, storeObj ){
    22. // localStorage对大小是有限制的,所以要进行try catch
    23. // 500KB左右的东西保存起来就会令到Resources变卡
    24. // 2M左右就可以令到Resources卡死,操作不了
    25. // 5M就到了Chrome的极限
    26. // 超过之后会抛出如下异常:
    27. // DOMException: Failed to execute 'setItem' on 'Storage': Setting the value of 'basket-http://file.com/ykq/wap/v3Templates/timeout/timeout/large.js' exceeded the quota
    28. try{
    29. localStorage.setItem( storagePrefix + key, JSON.stringify( storeObj ));
    30. returntrue;
    31. }catch( e ){
    32. // localstorage容量不够,根据保存的时间删除已缓存到ls里的js代码
    33. if( e.name.toUpperCase().indexOf('QUOTA')>=0){
    34. var item;
    35. var tempScripts =[];
    36. // 先把所有的缓存对象来出来,放到 tempScripts里
    37. for( item in localStorage ){
    38. if( item.indexOf( storagePrefix )===0){
    39. tempScripts.push( JSON.parse( localStorage[ item ]));
    40. }
    41. }
    42. // 如果有缓存对象
    43. if( tempScripts.length ){
    44. // 按缓存时间升序排列数组
    45. tempScripts.sort(function( a, b ){
    46. return a.stamp - b.stamp;
    47. });
    48. // 删除缓存时间最早的js
    49. basket.remove( tempScripts[0].key );
    50. // 删除后在再添加,利用递归完成
    51. return addLocalStorage( key, storeObj );
    52. }else{
    53. // no files to remove. Larger than available quota
    54. // 已经没有可以删除的缓存对象了,证明这个将要缓存的目标太大了。返回undefined。
    55. return;
    56. }
    57. }else{
    58. // some other error
    59. // 其他的错误,例如JSON的解析错误
    60. return;
    61. }
    62. }
    63. };
    64. /**
    65. * 利用ajax获取相应url的内容
    66. * @param {string} url 请求地址
    67. * @returns {object} 返回promise对象,解决时的参数为对象:{content:'', type: ''}
    68. */
    69. var getUrl =function( url ){
    70. var promise =new RSVP.Promise(function( resolve, reject ){
    71. var xhr =newXMLHttpRequest();
    72. xhr.open('GET', url );
    73. xhr.onreadystatechange =function(){
    74. if( xhr.readyState ===4){
    75. if(( xhr.status ===200)||
    76. (( xhr.status ===0)&& xhr.responseText )){
    77. resolve({
    78. content: xhr.responseText,
    79. type: xhr.getResponseHeader('content-type')
    80. });
    81. }else{
    82. reject(newError( xhr.statusText ));
    83. }
    84. }
    85. };
    86. // By default XHRs never timeout, and even Chrome doesn't implement the
    87. // spec for xhr.timeout. So we do it ourselves.
    88. // 自定义超时设置
    89. setTimeout(function(){
    90. if( xhr.readyState <4){
    91. xhr.abort();
    92. }
    93. }, basket.timeout );
    94. xhr.send();
    95. });
    96. return promise;
    97. };
    98. /**
    99. * 获取js,保存缓存对象到ls
    100. * @param {object} obj basket.require的参数对象(之前的处理过程中添加相应的属性)
    101. * @returns {object} promise对象
    102. */
    103. var saveUrl =function( obj ){
    104. return getUrl( obj.url ).then(function( result ){
    105. var storeObj = wrapStoreData( obj, result );
    106. if(!obj.skipCache){
    107. addLocalStorage( obj.key , storeObj );
    108. }
    109. return storeObj;
    110. });
    111. };
    112. /**
    113. * 进一步添加对象obj属性
    114. * @param {object} obj basket.require的参数(之前的处理过程中添加相应的属性)
    115. * @param {object} data 包含content和type属性的对象,content就是js的内容
    116. * @returns {object} 经过包装后的obj
    117. */
    118. var wrapStoreData =function( obj, data ){
    119. var now =+newDate();
    120. obj.data = data.content;
    121. obj.originalType = data.type;
    122. obj.type = obj.type || data.type;
    123. obj.skipCache = obj.skipCache ||false;
    124. obj.stamp = now;
    125. obj.expire = now +(( obj.expire || defaultExpiration )*60*60*1000);
    126. return obj;
    127. };
    128. /**
    129. * 判断ls上的缓存对象是否过期
    130. * @param {object} source 从ls里取出的缓存对象
    131. * @param {object} obj 传入的参数对象
    132. * @returns {Boolean} 过期返回true,否则返回false
    133. */
    134. var isCacheValid =function(source, obj){
    135. return!source ||// 没有缓存数据返回true
    136. source.expire -+newDate()<0||// 超过过期时间返回true
    137. obj.unique !== source.unique ||// 版本号不同的返回true
    138. (basket.isValidItem &&!basket.isValidItem(source, obj));// 自定义验证函数不成功的返回true
    139. };
    140. /**
    141. * 判断缓存是否还生效,获取js,保存到ls
    142. * @param {object} obj basket.require参数对象
    143. * @returns {object} 返回promise对象
    144. */
    145. var handleStackObject =function( obj ){
    146. var source, promise, shouldFetch;
    147. if(!obj.url ){
    148. return;
    149. }
    150. obj.key =( obj.key || obj.url );
    151. source = basket.get( obj.key );
    152. obj.execute = obj.execute !==false;
    153. shouldFetch = isCacheValid(source, obj);// 判断缓存是否还有效
    154. // 如果shouldFetch为true,请求数据,保存到ls(live选项意义不明,文档也没有说,这里当它一只是undefined)
    155. if( obj.live || shouldFetch ){
    156. if( obj.unique ){
    157. // set parameter to prevent browser cache
    158. obj.url +=(( obj.url.indexOf('?')>0)?'&':'?')+'basket-unique='+ obj.unique;
    159. }
    160. promise = saveUrl( obj );// 请求对应js,缓存到ls里
    161. if( obj.live &&!shouldFetch ){
    162. promise = promise
    163. .then(function( result ){
    164. // If we succeed, just return the value
    165. // RSVP doesn't have a .fail convenience method
    166. return result;
    167. },function(){
    168. return source;
    169. });
    170. }
    171. }else{
    172. // 缓存可用。
    173. source.type = obj.type || source.originalType;
    174. source.execute = obj.execute;
    175. promise =new RSVP.Promise(function( resolve ){
    176. // 下面的setTimeout用来解决结合requirejs使用时的加载问题。
    177. // setTimeout(function(){
    178. debugger;
    179. resolve( source );
    180. // },0);
    181. });
    182. }
    183. return promise;
    184. };
    185. /**
    186. * 把script插入到head中
    187. * @param {object} obj 缓存对象
    188. */
    189. var injectScript =function( obj ){
    190. var script = document.createElement('script');
    191. script.defer =true;
    192. // Have to use .text, since we support IE8,
    193. // which won't allow appending to a script
    194. script.text = obj.data;
    195. head.appendChild( script );
    196. };
    197. // 保存着特定类型的执行函数,默认行为是把script注入到页面
    198. var handlers ={
    199. 'default': injectScript
    200. };
    201. /**
    202. * 执行缓存对象对应回调函数,把script插入到head中
    203. * @param {object} obj 缓存对象
    204. * @returns {undefined} 不需要返回结果
    205. */
    206. var execute =function( obj ){
    207. // 执行类型特定的回调函数
    208. if( obj.type && handlers[ obj.type ]){
    209. return handlers[ obj.type ]( obj );
    210. }
    211. // 否则执行默认的注入script行为
    212. return handlers['default']( obj );// 'default' is a reserved word
    213. };
    214. /**
    215. * 批量执行缓存对象动作
    216. * @param {Array} resources 缓存对象数组
    217. * @returns {Array} 返回参数resources
    218. */
    219. var performActions =function( resources ){
    220. return resources.map(function( obj ){
    221. if( obj.execute ){
    222. execute( obj );
    223. }
    224. return obj;
    225. });
    226. };
    227. /**
    228. * 处理请求对象,不包括执行对应的动作
    229. * @param {object} 会把basket.require的参数传过来,也就是多个对象
    230. * @returns {object} promise对象
    231. */
    232. var fetch =function(){
    233. var i, l, promises =[];
    234. for( i =0, l = arguments.length; i < l; i++){
    235. promises.push( handleStackObject( arguments[ i ]));
    236. }
    237. return RSVP.all( promises );
    238. };
    239. /**
    240. * 包装promise的then方法实现链式调用
    241. * @returns {Object} 添加了thenRequire方法的promise实例
    242. */
    243. var thenRequire =function(){
    244. var resources = fetch.apply(null, arguments );
    245. var promise =this.then(function(){
    246. return resources;
    247. }).then( performActions );
    248. promise.thenRequire = thenRequire;
    249. return promise;
    250. };
    251. window.basket ={
    252. require:function(){// 参数为多个请求相关的对象,对象的属性:url、key、expire、execute、unique、once和skipCache等
    253. // 处理execute参数
    254. for(var a =0, l = arguments.length; a < l; a++){
    255. arguments[a].execute = arguments[a].execute !==false;// execute 默认选项为ture
    256. // 如果有只执行一次的选项once,并之前已经加载过这个js,那么设置execute选项为false
    257. if( arguments[a].once && inBasket.indexOf(arguments[a].url)>=0){
    258. arguments[a].execute =false;
    259. // 需要执行的请求的url保存到inBasket,
    260. }elseif( arguments[a].execute !==false&& inBasket.indexOf(arguments[a].url)<0){
    261. inBasket.push(arguments[a].url);
    262. }
    263. }
    264. var promise = fetch.apply(null, arguments ).then( performActions );
    265. promise.thenRequire = thenRequire;
    266. return promise;
    267. },
    268. remove:function( key ){
    269. localStorage.removeItem( storagePrefix + key );
    270. returnthis;
    271. },
    272. // 根据key值获取对应ls的value
    273. get:function( key ){
    274. var item = localStorage.getItem( storagePrefix + key );
    275. try{
    276. return JSON.parse( item ||'false');
    277. }catch( e ){
    278. returnfalse;
    279. }
    280. },
    281. // 批量清除缓存对象,传入true只清除过期对象
    282. clear:function( expired ){
    283. var item, key;
    284. var now =+newDate();
    285. for( item in localStorage ){
    286. key = item.split( storagePrefix )[1];
    287. if( key &&(!expired ||this.get( key ).expire <= now )){
    288. this.remove( key );
    289. }
    290. }
    291. returnthis;
    292. },
    293. isValidItem:null,// 可以自己扩展一个isValidItem函数,来自定义判断缓存是否过期。
    294. timeout:5000,// ajax 默认的请求timeout为5s
    295. // 添加特定类型的执行函数
    296. addHandler:function( types, handler ){
    297. if(!Array.isArray( types )){
    298. types =[ types ];
    299. }
    300. types.forEach(function( type ){
    301. handlers[ type ]= handler;
    302. });
    303. },
    304. removeHandler:function( types ){
    305. basket.addHandler( types,undefined);
    306. }
    307. };
    308. // delete expired keys
    309. // basket.js 加载时会删除过期的缓存
    310. basket.clear(true);
    311. })(this, document );
     
     
     





  • 相关阅读:
    java基础_面试题笔记
    ACM-ICPC 2018 Xuzhou Online Contest题解
    覆盖点问题总结
    2018icpc沈阳网络赛题解(转发)
    树链剖分
    树状数组
    线段树板子
    sdoi2016生成魔咒
    洛谷3804
    大佬博文收集
  • 原文地址:https://www.cnblogs.com/wujiaolong/p/eea65bd2995d33f502a9cd3275f50543.html
Copyright © 2011-2022 走看看