我的网站心得之缓存技术(前端篇)

在前端面试中,storage是面试官经常问的问题,我先问你几个问题,如果你回答不上来,那么你应该阅读一下:知道storage吗?storage存储的数据类型有什么?sessionStorage的生命周期?你都用localStorage做一些什么事?localStorage容量限制是多大?了解indexedDB跟WebSQL吗?如果你要在前端缓存视频或者图片怎么实现? 

目录

1、使用localStorage:

2、使用sessionStorage:

3、使用Cookie

4、使用IndexedDB跟Web SQL


在我做网站的时候,由于服务器带宽有限(仅仅3M),一开始打开视频页面和音乐页面会非常卡,经常造成接口请求超时,因此必须在前后台都加入缓存机制。其实我们使用的很多软件都有缓存机制,大部分人接触的基本都是后台的缓存中间件redis等等,它可以有效的减轻数据库和服务器的压力。前端缓存主要着重用户体验,实现一些特定功能(如记住上一次浏览),为用户节省不必要的流量浪费,减少QBS带给服务器的压力。本文章主要针对前端的浏览器缓存Storage(localStorage,SessionStorage)、IndexedDB(WebSQL)、Cookie,谈谈我个人的理解和实现,理解可能有误,请多指正。

本文主要内容有:localStorage存储,sessionStorage存储图片对象,IndexedDB存储音乐(实现听过的音乐0流量播放)。

注意:本篇描述的是手动管理缓存,因此不会使用下面给Header添加标签的缓存技术。

<meta http-equiv="Cache-Control" content="public" />
<meta http-equiv="Pragma" content="public" />
<meta http-equiv="Expires" content="36000" />

        这是因为,几乎所有网站都不会开启强缓存,甚至会禁用强制缓存...

<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />

        其实,目前浏览器是有自动缓存机制的,它都是存储的静态资源。在Tomcat和JSP的年代,这个自动缓存让前端简直吃屎,甚至手动清理浏览器缓存跟重启Tomcat都不一定清的掉。笔者之前在页面引了一张图片,然后发现不好看,就用另一张图片替换了,结果总是展示之前那张图片。其实在静态资源后面加入时间戳机制或一个方法等可以有效阻止此类缓存,这类机制将导致页面会重新请求静态资源,从而避免了使用缓存的静态资源,如:

<script src="js/zq.js?v=123"></script>
<script src="js/zq.js?time=<%=System.currentTimeMillis();%>"></script>

        这些自动缓存机制我们完全不必在意这些是如何实现的。下面我就介绍以下前台需要我们手动操作的缓存技术及API。

拿我音乐demo页面为例(demo链接):

        首先,Storage里面都是存储的键值对,必须注意这个值必须是字符串,如果你存的是1、true,那么取出来就是“1”跟“true”,而且不能将undefined存入,如果要存储对象必须要用JSON.stringify()强转字符串存入。目前localStorage在各个浏览器存储容量各有不同,对于谷歌通常一个值最大是5M,其他浏览器得用插入字符串的方式,用trycache获取溢出异常来获取最大存储容量。

1、使用localStorage:

        上图的内容都是存储在localStorage里的内容。localStorage应用最多最广泛,几乎所有的网站在localStorage都会满满的存进一堆东西。这是因为几乎很多的网站都是允许用户不登录就可以浏览的,那么由于用户没有登陆,这些网站的后台就记录不了该用户的行为。记录用户行为的重任就落在localStorage身上了。localStorage的生命周期是理论永久,而且是不可能被爬虫爬到的,如果用户或者代码不去手动清理它,它会一直存在。至于存储什么内容,得看你想怎么做了,据我观察,各大网站一个共识就是存储用户信息、主题(通常是夜间日间模式)、搜索历史、上次播放的位置等等,我的音乐网站就存储了音乐播放时间(就是视频的从上次观看的时间播放)、次数、播放历史、是否开启引导等等(有趣的是,百度文档就把浏览限制存到这里面,如果你浏览百度文档被限制继续查看,请把localStrage内容清空刷新页面)。

localStorage的API非常简单,通常会用到localStorage.setItem("key","value")设置键值对,用到localStorage.getItem("key")来获取key对应的value字符串,如果不存在就会返回null而不是undefined,使用localStorage.removeItem("key")去移除key对应的localStorage,使用localStorage.clear()去清空localStorage。例如:

<script>
			console.log("取localStorage的值name:"+localStorage.getItem("name"));//第一次取肯定不存在,此时返回null
			localStorage.setItem("name","hoppinzq.com");//存一个值
			console.log("取localStorage的值name:"+localStorage.getItem("name"));//存了才能取出来,返回hoppinzq.com
			localStorage.removeItem("name");//删除 或者localStorage.clear()
			console.log("取localStorage的值name:"+localStorage.getItem("name"));//已经被删了,返回null
            localStorage.setItem("name","hoppinzq.com");//存一个值
		</script>

 是不是很简单呢?看看我的网站是怎么写的吧

if (typeof(Storage) !== "undefined") { //浏览器支持Storage或隐私模式吗?

    if (localStorage.getItem("historySongs")) {  //先看看历史歌单的键值是否已创建
		historySongs = JSON.parse(localStorage.getItem("historySongs")); 
	} else { //历史歌单不存在时,一般用户第一次进到你页面(或者请过缓存)肯定不存在,所以需要加这个判断
        historySongs = [];
	}
	var isE = false;//这里是判断在听的歌曲是否包含在历史歌曲里,不用管
	for (let historySong of historySongs) {
		if (historySong.id === music_id) {
			isE = true;
			break;
		}
	}
	if (!isE) {
		historySongs.push(e.jPlayer.status.media);
		localStorage.setItem("historySongs", JSON.stringify(historySongs));//设置历史歌曲
    }
}

2、使用sessionStorage:

        上面的内容是我的网站的单页在sessionStorage存储的东西,有各种请求信息,图片等等。首先sessionStorage就像后台的session作用域,它的一次生命周期是一次临时会话,用户一旦刷新页面或者重新打开页面等执行路由刷新页面的操作都会导致sessionStorage内的内容清空 ,因此使用场景很受限,但是对于很多后台管理系统的前台来说简直是个宝。我观察了很多大型网站基本没有使用sessionStorage的,我主要是用来存储一些特定请求(比如分页,因为该请求不需要刷新页面)和媒体图片这样的资源,因为作为文件一般而言是最少被修改(忽略那些天天改头像的)。 

        如上面动图,在进入第二页的时候会请求拿到这些图片的路径,加载图片会花费大部分时间和占用很多带宽,而返回第一页的时候基本是秒加载,不使用任何流量,流畅到就好像做了个假分页。这样看来sessionStorage对于图片多的页面非常友好,因为图片大小很小,但对于大的媒体资源就好像无能为力了。

        sessionStorage的API很简单,跟localStroage一样。下面就看看我的网站是怎么写的吧。

/**
  * 缓存网易云图片
  * 注意:第一次加载的图片不建议直接使用缓存的图片,因为要花更多时间,第二次使用即可
  * @author zq
  * @param {Object} wyyUrl 图片url :格式 :
  * https://p3.music.126.net/t_Ax6p2zxfsBkymFbk6fMA==/109951165985230942.jpg
 */
function cacheNetBaseCloudImg(wyyUrl) {
    //这里解析了一下,为了保证sessionStorage的键不重复而且通过图片id能拼接出来
	let cacheImageNameIndex_1 = wyyUrl.lastIndexOf("/") + 1;
	let cacheImageNameIndex_2 = wyyUrl.lastIndexOf(".");
	let cacheImageName = "image_" + wyyUrl.substring(cacheImageNameIndex_1, cacheImageNameIndex_2);
	var StorageCacheImageUrl;
    //先看看要加入缓存的图片是否已经在缓存里了
	StorageCacheImageUrl = sessionStorage.getItem(cacheImageName);
	if (StorageCacheImageUrl != null) {
        //如果缓存里有该图片,直接返回缓存的图片URL对象
		return StorageCacheImageUrl;
	} else {
        //使用原生ajax获取图片URL对象(或者base64)
		var xhr = new XMLHttpRequest();
		var blob;
		xhr.open('GET', wyyUrl, true);
		xhr.responseType = 'blob';
		xhr.onload = function() {
		    var data = xhr.response;
			blob = new Blob([data]);
			// var reader = new window.FileReader();
			// reader.readAsDataURL(blob); 
			// reader.onloadend = function() {
			// 	localStorage.setItem(cacheImageName, reader.result);//reader.result就是图片的Base64码
			// }
			var blobUrl = window.URL.createObjectURL(blob);//创建图片的URL对象
			cacheName.push(cacheImageName);
			sessionStorage.setItem(cacheImageName, blobUrl);
			return blobUrl;
		};
		xhr.send();
	}
}

观察上面代码注意以下几点: 

1、我是保存的图片URL对象,就如同java一样,当页面刷新关闭的时候会执行dom卸载操作跟垃圾回收,这个URL对象就被垃圾回收了失去价值了,所以sessionStorage存的东西实际没什么用处了,因此必须给页面绑定beforeunload事件手动清除sessionStorage内的图片URL,其实页面刷新的时候会自动清除,但是。。讲究!!!

$(window).on("scroll", function() {//绑定滚动事件。。。
       }).on("beforeunload", function() { 
             //给页面刷新跟关闭绑定事件,手动清除缓存,并存储用户最后一次搜索的内容
		    localStorage.setItem("lastSearch", $("#search").val());
			sessionStorage.clear();//清除sessionStorage内容,防止出问题
});

2、在上面代码cacheNetBaseCloudImg方法的注释里有一个获取图片base64码的代码,你可以存储base64码到localStorage里面,下次直接丢到img标签也可以。但是我不建议这么干,因为一来图片太多不好管理,得加入时间戳机制去隔一段时间清理一下(比如允许用户一个月只能修改一次头像),不然的话很可能就导致头像不同步了;二来万一图片很大就会溢出导致存不进去。那么如何存储大文本文件、媒体文件(音乐视频)呢?

其实现有的Storage也是能实现的,思路就用链表的思路,我们只需要先创建一个存储地址的字符串,然后把大文本文件转成base64码分几批存入localStorage,每一批存储一定量base64码和指向上一个localStorage地址的字符串,读取的时候拼起来就行。这个思路有很多,反正你能把分割的base64码正确还原即可。由于代码太多就不展示,有兴趣可以访问demo链接,或者粘贴下载文件 到浏览器上下载源代码。

3、使用Cookie

        其实cookie这个东西在前端讲有点尴尬,因为cookie一般都是后台创建的并赋予它的生命周期(前台也能创建,有一个很好用的谷歌的cookie.js,但是本人前端目前没用过,以后会补充)。它的生命周期并不像localStoage一样是永久的,也不像sessionStoage是一次临时会话,你可以存一些东西而且完全不必手动去清理它。前端去创建cookie并存储数据我暂时没用过,因为我对于捕获用户行为跟喜好并没有太多思路,我都是是在后台操作。就是帮你记住你的用户名。大多数是用户登录后台会创建cookie去存储用户登录的唯一标识token。

        通过ajax可以携带cookie的特点,后台通过校验cookie的内容来辨别请求是登录用户发起的还或是伪造的。

//ajax
xhrFields: {
	withCredentials: true //这个配置项就是请求携带cookie
}
//axios
withCredentials: true
//Fetch
fetch(url, { credentials: 'include' })

4、使用IndexedDB跟Web SQL

        首先这两个东西可能需要前端稍微了解一下数据库表和一点点sql的知识,Web SQL底层是SQLlite,但是除了谷歌都不支持。indexedDB跟sql语句没什么关系,而且比较难用,但它的value可以存储任何数据类型包括undefined,基本没有容量限制(这取决电脑跟手机),因此我们用这个存储媒体blob对象或者base64比较好。所以为了照顾前端,就找到了一个开源的js组件localforage.js(下载链接:下载文件 )(中文文档地址:localForage 中文文档)。该组件将IndexedDB封装的很简单,API也很贴心的仿照了Storage的API。

下面我们用localforage.js将音乐存入IndexedDB中,真正实现一次加载以后就可以不用流量播放。

首先分析:网易云音乐的url是http://music.163.com/song/media/outer/url?id=22766042.mp3,也就是?id=音乐id.mp3。

可以看到,该链接返回302响应码,并重定向到了真正的音乐的接口。我们首先要获取的是音乐文件的blob对象,所以我们得用ajax去请求这个接口, 但是很蛋疼的,ajax请求不支持重定向,因为通过ajax捕获重定向的url是有一定风险的,有可能你接收的是流,抑或是另外的重定向(淘宝跟拼多多就很喜欢套娃),responseType就很难声明了。所以我们要在后台通过HttpCilent模拟这个请求,关闭重定向,并捕获302重定向响应码,将真正的音乐url返回给前台:

/**
 * 获取重定向后的URL
 * @author:ZhangQi
 **/
@RestController
@RequestMapping("/music")
public class MusicController {

    @GetMapping("/getMusicRealUrl")
    public ApiResponse getMusicRealUrl(String url){
        HttpClient client = new DefaultHttpClient();
        HttpParams params = client.getParams();
        params.setParameter(ClientPNames.HANDLE_REDIRECTS, false);//禁用自动重定向
        HttpContext context = new BasicHttpContext();
        HttpGet get = new HttpGet(url);
        try {
            HttpResponse response = client.execute(get,context);
            HttpHost host = (HttpHost) context.getAttribute(ExecutionContext.HTTP_TARGET_HOST);
            HttpUriRequest request =  (HttpUriRequest) context.getAttribute(ExecutionContext.HTTP_REQUEST);
            int code = response.getStatusLine().getStatusCode();
            if(code==302){
                HttpEntity entity = response.getEntity();
                if(null != entity){
                    Header[] headers = response.getHeaders("Location");
                    if(headers!=null && headers.length>0){
                        String redirectUrl = headers[0].getValue();
                        return ApiResponse.data(redirectUrl);
                    }
                }
            }else{
                return ApiResponse.fail(-1,"不是重定向的页面");
            }

        } catch (ClientProtocolException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

前台将音乐blob对象放入缓存代码:

        /**
		 * 缓存网易云音乐
		 * @author zq
		 * @param {Object} musicURL
         * url格式:http://music.163.com/song/media/outer/url?id=22766042.mp3
		 */
		function cacheNetBaseCloudMusic(musicURL) {
			let cacheMusicNameIndex_1 = musicURL.lastIndexOf("=") + 1;
			let cacheMusicNameIndex_2 = musicURL.lastIndexOf(".");
			let cacheMusicName = "music_" + musicURL.substring(cacheMusicNameIndex_1, cacheMusicNameIndex_2);
			localforage.getItem(cacheMusicName, function(err, value) {
				if (value == null) {
                    //获取真正的网易云播放url
					$.get("http://hoppinzq.com/music/getMusicRealUrl?url=" + musicURL,         
                      function(data) {
						if (data.code == 200) {
							let realUrl = data.data;
							var xhr = new XMLHttpRequest();
							var blob;
							xhr.open('GET', realUrl, true);
							xhr.responseType = 'blob';
							xhr.onload = function() {
								var data = xhr.response;
								blob = new Blob([data]);
								localforage.setItem(cacheMusicName, blob);
							};
							xhr.send();
						}
					})
				} else {
					return value;
				}
			});
		}

从代码看出,localforage的API跟Storage如出一辙(中文文档地址localForage 中文文档)也是getItem,setItem(),removeItem(),clear()。但是贴心的设计了回调函数如localforage.getItem("key",function(err, value){},

最后,将缓存里的blob对象解析成URL对象放到音乐播放列表就行了,大家可以在demo里断网体验一下哦:

        /**
		 * 动态设置播放列表
		 * 注意:在执行setPlaylist方法会执行init方法,该方法会把当前播放时间currentTime置为0,这会导致音乐重新从列表第一首开始重新播放
		 * 而原来你在听的歌曲会停掉,我的做法是:在播放时就建立一个被静音的播放源,在执行此法时将静音去掉,此时声音会增大,这是因为原有
		 * 的播放源的音量使用的系统音量乘以用户拖动的音量百分比,在切换播放源时会立即使用系统音量因此要手动×上一个比值。
		 * @param {Object} songList
		 */
		function setMusicList(songList) {
			timeout(1).then(function() {
                //遍历歌曲列表,将存在缓存的歌曲的URL对象替换网易云URL
				$.each(songList, function(index, value) {
					localforage.getItem("music_" + value.id, function(err, value_) {
						if (value_ != null) {
							var url = window.URL.createObjectURL(value_);
							value.mp3 = url;
						}
					});
				})
                //微微的缓冲
				return timeout(1000);
			}).then(function() {
				myAlert("切换声音源中。。。");
				myPlaylist.setPlaylist(songList);
				audio_hidden.muted = false;
				var wi = parseFloat($(".jp-volume-bar-value").width() / $(".jp-volume-bar").width()).toFixed(2);
				audio_hidden.volume = wi;
			});

		}

        这个地方作为一个纯前端是不太好理解的,因为现在直接访问文件资源的接口是一定要做重定向并且携带cookie、时间戳、加密的(网易云音乐真实URL是根据时间戳自动生成的,写错一个字母,你都会吃到网易云4xx黑名单套餐)。现在很少网站能做到让你免登录下载的(本站就是免登录>-<),因为这些接口会直接返回流,此类接口一旦被盗刷,频繁的IO操作会严重降低服务器性能并造成带宽堵塞。最后可能导致其他用户根本进不到我的网站,甚至可能导致服务器宕机。因此,必须要做一个重定向,并在重定向前对接口进行有效的管控。

具有很多手动缓存页面的源代码:链接

在线访问:链接

  • 4
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值