首页 > 代码库 > 1-7 basket.js localstorage.js缓存css、js

1-7 basket.js localstorage.js缓存css、js

basket.js 源码分析


api 使用文档:
http://t3n.de/news/basketjs-performance-localstorage-515119/



一、前言

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 - +new Date() < 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 value of ‘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. return true;
  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. return true;
  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 = new XMLHttpRequest();
  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( new Error( 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 = +new Date();
  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 - +new Date() < 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. } else if ( 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. return this;
  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. return false;
  279. }
  280. },
  281. // 批量清除缓存对象,传入true只清除过期对象
  282. clear: function( expired ) {
  283. var item, key;
  284. var now = +new Date();
  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. return this;
  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 );




来源: http://www.cnblogs.com/oadaM92/archive/2016/04/03/5348793.html#undefined


来自为知笔记(Wiz)


1-7 basket.js localstorage.js缓存css、js