详述浏览器缓存机制

一、什么是缓存?

浏览器缓存就是浏览器保存通过HTTP获取的所有资源,是浏览器将网络资源保存到本地的一种行为。那么,为什么要缓存?一个优秀的缓存策略可以缩短网页请求资源的距离,减少等待时间;并且由于缓存文件可以重复利用,可以减少带宽,降低网络负荷;减少了对源服务器的请求,有效降低服务器的压力。

对于一个数据请求来说,可以分为发起网络请求、后端处理、浏览器响应三个步骤。浏览器缓存可以帮助我们在第一和第三步骤中优化性能。比如说直接使用缓存而不发起请求,或者发起了请求但后端存储的数据和前端一致,那么就没有必要再将数据回传回来,这样就减少了响应数据。

store.png

二、缓存的资源去哪里了呢?

如上图所示,缓存位置分为四种,并且各自有优先级,当依次查找缓存但没有找到时才会去请求网络。

1、Service Worker

Service Worker 是运行在浏览器背后的独立线程,一般可以用来实现缓存功能。使用Service Worker的话,传输协议必须是HTTPS,因为Service Worker中涉及到请求拦截。Service Worker的缓存与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。如果需要向服务器发起请求的就转给服务器,如果可以直接使用缓存的就直接返回缓存不再转给服务器。
Service Worker 实现缓存一般分为三个步骤:首先需要注册Service Worker ;然后监听到install事件以后就可以缓存需要的文件;在下次用户访问的时候就可以通过拦截请求的方式查询是否存在缓存,存在的话就直接读取,否则就去请求数据。
当Service Worker 没有命中缓存的时候,需要去调用fetch函数获取数据,也就是说,如果没有在Service Worker 命中缓存的话,会根据缓存查找优先级去查找数据,但是不管是从Memory Cache还是网络请求中获取的数据,浏览器都会显示是从Service Worker 获取的。

2、Memory Cache

Memory Cache就是将资源缓存到内存中,等下次访问时不需要重新下载资源,而直接从内存中获取,主要包含的是当前页面中已经抓取到的资源,例如页面上已经下载的样式、脚本、图片等。读取内存中的数据肯定比磁盘快,但是内存缓存持续性短,会随着进程的释放而释放,一旦关闭tab页面,内存中的缓存就释放了。
当我们访问过页面以后,再次刷新页面,可以发现很多数据都来自于内存缓存。内存缓存中有一块重要的缓存资源就是preloader指令(例如<link rel="prefetch">)下载的资源。众所周知preloader的相关指令已经是页面优化的常见手段之一,它可以一边解析js/css文件,一边网络请求下一个资源。
需要注意的是,内存缓存在缓存资源时不关心返回资源的HTTP缓存头Cache-Control是什么值,同时资源的匹配也并非仅仅是对URL做匹配,还可能会对Content-Type、CORS 等其他特征做校验。

3、Disk Cache

Disk Cache 就是将资源缓存在磁盘里,等待下次访问时不需要重新下载资源,直接从磁盘中获取,它的直接操作对象是CurlCacheManager。读取速度慢点,与Memory Cache相比,胜在容量和存储时效上。
在所有浏览器缓存中,Disk Cache覆盖面基本是最大的。它会根据HTTP Header 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,以及哪些资源已经过期需要重新请求。而且在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据,绝大部分的缓存都来自Disk Cache,关于HTTP的协议头中的缓存字段,会在下文中详细介绍。

缓存位置 Memory Cache Disk Cache
相同点 只能存储一些派生类资源文件 只能存储一些派生类资源文件
不同点 退出进程时数据会被清除 退出进程时数据不会被清除
存储资源 一般脚本、字体、图片会存在内存当中 一般非脚本会存在内存当中,如CSS等

因为CSS文件加载一次就可渲染出来,不需要频繁读取,所以它不适合缓存到内存中。但是js类脚本文件随时都会执行,如果脚本存在磁盘中,执行脚本的时候需要从磁盘中取到内存中,这样IO开销就会很大,有可能导致浏览器失去响应。

4、Push Cache

Push Cache,顾名思义,推送缓存,它是HTTP/2中的内容,当以上三种缓存都没有命中的时候,它才会被使用。它只在会话(Session)中存在,一旦回话结束即被释放,并且缓存时间也很短暂。在Chrome浏览器中只有5分钟左右,同时它也并非严格的执行HTTP头中的缓存指令。

如果以上四种缓存都没有命中的话,就需要发起网络请求资源了,请求获取的资源缓存到磁盘和内存中。

那么浏览器怎么确定一个资源该不该缓存呢?浏览器对缓存的处理是根据第一次请求资源时返回的响应头来决定的。如下图所示,


image.png

第一,浏览器每次发起请求时,都会先在浏览器缓存中查找该请求的结果和缓存标识;
第二,浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中。
以上就是浏览器缓存机制的关键,它确保了每个请求的缓存存入与读取。

三、缓存策略

出于性能考虑,大部分接口都应该做好缓存策略。通常浏览器缓存策略分为两种:强缓存和协商缓存,并且缓存策略是通过设置 HTTP Header 来实现的。浏览器在向服务器请求资源时,首先判断是否命中强缓存,再判断是否命中协商缓存。

1、强缓存

强缓存不会向服务器发送请求,直接从缓存中获取资源,在Chrome控制台的network可以看到该请求返回200的状态码,并且size显示from disk Cache 或者from memory cache。
HTTP header 中的信息指的是 Expires 和 Cache-Control。

- Expires

缓存过期时间,用来指定资源到期的时间,是服务器的具体的时间点。该字段是HTTP 1.0的规范,它的值为一个绝对时间的GMT格式的时间字符串,比如Expires:Mon,18 Oct 2020 23:59:59 GMT。这个时间代表着这个资源的失效时间,在此时间之前,即命中缓存。这种方式有一个明显的缺点,由于失效时间是一个绝对时间,所以当服务器与客户端时间偏差较大时或者修改了本地时间,就会导致缓存混乱。

- Cache-Control

Cache-Control 是HTTP 1.1时出现的header信息,主要是利用该字段的max-age值来判断,它是一个相对时间。例如Cache-Control:max-age=3600,代表着资源的有效期是3600秒,表示3600秒后就过期,需要重新请求。Cache-Control除了该字段外,还有下面几个比较常用的设置值:

  • no-cahce:需要进行协商缓存,发送请求到服务器确认是否使用缓存。
  • no-store:禁止使用缓存,每一次都要重新请求数据。
  • public:可以被所有用户缓存,包括终端用户和CDN等中间代理服务器。
  • private:只能被终端用户的浏览器缓存,不允许CDN等中间代理服务器对其缓存。
  • s-max-age=30:覆盖max-age,作用一样,只在代理服务器生效
  • max-stale=30:30秒内,即使缓存过期,也使用该缓存
  • min-fresh=30:希望在30秒内获取最新的响应。

Cache-Control与Expires可以在服务端配置同时启用,同时启用的时候Cache-Control优先级较高。在某些不支持HTTP 1.1的环境下,Expires就会发挥用处。所以Expires其实是过时的产物,现阶段它的存在只是一种兼容性的写法。

2、协商缓存

当强缓存没有命中的时候,浏览器会发送一个请求到服务器,服务器根据header中的部分信息来判断是否命中缓存。如果命中,返回304 Not Modified,告诉浏览器资源未更新,可以使用本地缓存。
HTTP header 中的信息指的是Last-Modify/If-Modify-Since 和 ETag/If-none-Match。

- Last-Modified/If-Modified-Since

浏览器第一次请求一个资源时,服务器返回的response header中会加上Last-Modified,Last-Modified是一个时间标识该资源的最后修改时间。

Last-Modified:Fri,22 Jul 2016 01:47:00 GMT

当浏览器再次请求该资源时,request的请求头中会包含If-Modified-Since,该值为缓存之前返回的Last-Modified。服务器收到If-Modified-Since后,根据资源的最后修改时间判断是否命中缓存。如果命中,返回304,并且不会返回资源内容,并且不会返回Last-Modified。

缺点:

  • 短时间内资源发生改变的话,Last-Modified 并不会发生变化,因为它以秒计时。
  • 周期性变化。如果这个资源在一个周期内修改回原来的样子,我们认为是可以使用缓存的,但是Last-Modified可不这样认为,因此便有了ETag。
- ETag/If-none-Match

ETag是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成)。ETag/If-none-Match返回的是一个校验码。ETag可以保证每一个资源都是唯一的,资源变化都会导致ETag变化。

浏览器在下一次请求资源时,会将上一次返回的ETag值放到request header中的If-none-Match里,服务器只需要比较客户端传来的If-none-Match跟自己服务器上该资源的ETag值是否一致,如果匹配不上,直接以常规get 200回包形式将新的资源以及新的ETag返回给客户端;如果匹配一致,则直接返回304给客户端直接使用本地缓存即可。

与Last-Modified不同的是,当服务器返回304 Not Modified 的响应时,由于ETag 重新生成过,response header 中还会把ETag返回,即使这个ETag与之前的没有变化。

二者的不同之处:

  • 首先在精确度上,Etag要优于Last-Modified。
    Last-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是Etag每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的Last-Modified也有可能不一致。
  • 第二在性能上,Etag要逊于Last-Modified,毕竟Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值。
  • 第三在优先级上,服务器校验优先考虑Etag。

Last-Modified 与 ETag 是可以一起使用的,服务器会优先验证ETag ,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。

四、缓存机制

强缓存优先于协商缓存进行,若强缓存生效则直接使用缓存,若不生效则进行协商缓存,协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,返回200,重新返回资源和缓存标识,再存入浏览器缓存中;若是生效则返回304,继续使用缓存。具体流程图如下:


image.png

如果浏览器没有设置缓存策略,对于这种情况,浏览器有个启发式的算法,通常会取响应头中的Date减去Last-Modified 值得10%作为缓存时间。

五、实际场景应用缓存策略

1、频繁变动的资源

Cache-Control:no-cache

对于频繁变动的资源,首先需要使用Cache-Control:no-cache 使浏览器每次都请求服务器,然后配合ETag或者Last-Modified来验证资源是否有效,这样的做法虽然不能节省请求数量,但是能显著减少响应数据大小。

2、不常变化的资源

Cache-Control:max-age=31536000

通常在处理这类资源时,给它们的Cache-Control:配置一个很大的max-age=31536000(一年),这样浏览器之后请求相同的URL会命中强制缓存。而为了解决更新的问题,就需要在文件名或路径中添加hash,版本号等动态字符,之后更改动态字符,从而达到更改引用URL的目的,让之前的强制缓存失效(其实不是失效,只是不再使用了而已)。在线提供的类库,如jquery-3.3.1.min.js,lodash.min.js等均采用这个模式。

六、用户行为对浏览器缓存的影响

所谓用户行为对浏览器缓存的影响,指的是用户在浏览器如何操作时,会触发怎样的缓存策略,主要分三种:

  • 打开网页,地址栏输入地址:查找disk cache中是否有匹配,如有则使用,如没有则发送网略请求。
  • 普通刷新(F5):跳过强缓存,会检查协商缓存。因为TAB页没有被关闭,因此memory cache 是可用的,会被优先使用,其次才是disk cache。
  • 强制刷新(Ctrl+F5):浏览器不使用缓存,直接从服务器加载,跳过强缓存和协商缓存,因此发送的请求头部均带有Cache-control:no-cache(为了兼容,还带了pragma:no-cache),服务器直接返回200和最新内容。

七、不会被缓存的情况

1、HTTP信息头中包含Cache-Control:no-cache ,pragma:no-cache(HTTP1.0),或Cache-Control: max-age=0 等告诉浏览器不用缓存的请求;
2、需要根据cookie,认证信息等决定输入内容的动态请求是不能被缓存的;
3、经过HTTPS安全加密的请求不能被缓存;
4、POST请求无法被缓存;
5、HTTP响应头中不包含Last-Modified/Etag,也不包含 Cache-Control/Expires 的请求无法被缓存;

八、缓存种类

我们常见且常用的存储方式主要有两种:cookie、webStorage(localstorage和sessionstorage)。
HTML5提供了两种客户端存储数据的新方法,localstorage和sessionstorage,挂载在Window对象下。webStorage 是本地存储,数据不是由服务器请求传递的,从而它可以存储大量的数据,而不影响网站的性能。webStorage的目的是为了克服由cookie带来的一些限制,当数据需要被严格控制在客户端时,无须持续的将数据发给服务器。比如客户端需要缓存的一些用户行为和数据,或者从接口获取的一些短期内不会更新的数据。

1、cookie
cookie是什么

cookie的本职工作并非本地存储,而是维护状态。因为HTTP协议是无状态的,HTTP协议自身不对请求和响应之间的通信状态进行保存,通俗来说,服务器不知道用户上一次做了什么,这严重阻碍了交互式web应用程序的实现,在典型的网上购物场景中,用户浏览了几个页面,买了一盒饼干和两瓶饮料,最后结账时,由于HTTP的无状态性,不通过额外的手段,服务器并不知道用户到底买了什么,于是就诞生了cookie,它就是用来绕开HTTP无状态的额外手段之一,服务器可以设置或读取cookie中包含信息,借此维护用户与服务器会话的状态。

cookie是服务器发送到浏览器的一小段数据,会在浏览器下一次请求的时候被携带并发送到服务器。我们可以把cookie理解为一个存储在浏览器中的一个小小的文本文件,它附着在HTTP请求上,在浏览器和服务器之间飞来飞去,它可以携带用户信息,当服务器检查cookie时,便可以获取到客户端的状态。

在刚才的购物场景中,当用户选购了第一个商品,服务器在向用户发送网页的同时,还发送了一段cookie,记录着那项商品的信息,当用户访问另一个页面,浏览器就会把cookie发送给服务器,于是服务器知道他选购了什么,用户继续选购饮料,服务器就在原来那段cookie里追加新的商品信息,结账时,服务器读取发送来的cookie就行。

使用场景
  • 会话状态管理(如用户登录状态、购物车、游戏分数等等)
  • 个性化设置(用户自定义设置、主题等)
  • 浏览器行为跟踪(如跟踪分析用户行为进行商品推荐等)
原理

第一次访问网站时,浏览器发出请求,服务器响应请求后,会在响应头里添加一个Set-Cookie选项,将cookie放入到响应请求中,在浏览器第二次发请求的时候,会通过cookie请求头部将cookie信息发送到服务器,服务器会辨认用户信息,另外,cookie的过期时间、域、路径、有效期、适用站点都可以根据需要来指定。cookie实际上存储在计算机的本地硬盘里。

cookie的缺陷
  • cookie不够大
    cookie的大小限制在4KB左右,对于复杂的存储需求来说是不够用的。当cookie超过4KB时,它将面临被裁的命运。这样看来,cookie只能用来存取少量的信息,此外很多浏览器对一个站点的cookie个数也是有限制的。
  • 过多的cookie会带来巨大的性能浪费
    cookie是紧跟域名的。同一个域名下的所有请求,都会携带cookie。如果仅仅是请求一个图片或一个css文件,也要携带一个cookie飞来飞去,而且cookie里携带的信息并不需要,随着请求的叠加,这样不必要的cookie越来越多,造成巨大的性能浪费。
    cookie是用来维护用户信息的,而域名下所有请求都会携带cookie,但对于静态文件的请求,携带cookie信息根本没有用,此时可以通过CDN的域名和主站的域名分开来解决。
  • 由于HTTP请求中的cookie是明文传递的,所以安全性成问题,很容易被用户篡改,除非用HTTPS。(session可以解决这个问题)
  • cookie默认有效期理论上在用户关闭页面后就失效,实际上在20分钟左右,不同浏览器策略不同,但是后端可以强制设置有效期。
读写操作
①、cookie的HttpOnly属性

为避免跨域脚本XSS攻击,通过JavaScript的Document.cookie API无法访问带有
HttpOnly标记的cookie,它们只应该发送给服务器。如果包含服务端session信息的cookie不想被客户端JavaScript脚本调用,那么就应该为其设置HttpOnly标记。

Set-cookie:id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly
②、如何设置cookie

在HTTP响应头中设置Set-cookie即可。具体参数如下:

Set-cookie:<cookie-name>=<cookie-value>
// 普通的cookie,所有参数默认
Set-cookie:<cookie-name>=<cookie-value>; Expires=<date>
// cookie的最长有效时间,形式为符合HTTP-date规范的时间戳
Set-cookie:<cookie-name>=<cookie-value>; Max-Age=<non-zero-digit>
// 在cookie失效之前需要经过的秒数。一位或多位非零数字。假如Expires和Max-Age都存在,那么Max-Age优先级更高。
Set-cookie:<cookie-name>=<cookie-value>; Domain=<domain-value>
// 指定cookie可以送达的主机名。假如没有指定,那么默认值为当前文档访问地址中的主机部分,但不包含子域名。域名之前的点号会被忽略,假如指定了域名,那么相当于各个子域名也包含在内了。
Set-cookie:<cookie-name>=<cookie-value>; Path=<path-value>
// 指定一个URL路径,这个路径必须出现在要请求的资源的路径中才可以发送Cookie首部。
Set-cookie:<cookie-name>=<cookie-value>; Secure
// 设置了HttpOnly属性的cookie不能使用JavaScript经由Document.cookie属性。
XmlHttRrequest和Request APIs 进行访问,以防范跨站脚本攻击(XSS)。
Set-cookie:<cookie-name>=<cookie-value>; SamSite=Strict
Set-cookie:<cookie-name>=<cookie-value>; SamSite=Lax
上面两个允许服务器设定一则cookie不随着跨域请求一起发送,这样可以在一定程度上防范跨站请求伪造攻击(CSRF)。
③、如何删除cookie

通过设置cookie的有效期在当前时间之前,就可以删除cookie了。

var delete_cookie = function (name) {
  document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:01 GMT;';
};
2、localstorage

localstorage用于存储一个域名下的需要永久存储在本地的数据,这些数据可以一直被访问,直到这些数据被删除。

特点:
  • 保存的数据长期存在,除非清除浏览器缓存。下一次访问该网站时,网页可以直接读取以前保存的数据,
  • 大小为5M左右
  • 仅在客户端使用,不和服务端通信
  • 接口封装较好
使用场景:

localstorage可以作为浏览器本地缓存方案,用来提升网页首屏渲染速度,根据第一次请求返回时,将一些不变的信息直接存储在本地。

读写操作
localstorage.setItem(key, value)  保存数据,key和value都必须是字符串类型
localstorage.getItem(key)  获取数据
localstorage.removeItem(key)  删除数据
localstorage.clear()  删除全部数据
3、sessionstorage

sessionstorage保存的数据用于浏览器的一次会话,当会话结束(通常是该窗口关闭),数据被清空;sessionstorage特别的一点在于,即便是相同域名的两个页面,只要他们不在同一个浏览器窗口打开,那么它们的sessionstorage内容便无法共享;localstorage在所有同源窗口都是共享的;cookie也是在所有同源窗口中都是共享的。除了保存的期限长短不同,sessionstorage与localstorage的属性和方法完全一样。

特点
  • 会话级别的浏览器存储
  • 大小为5M左右
  • 仅在客户端使用,不和服务端通信
  • 接口封装较好
使用场景

有效对表单信息进行维护,比如刷新时,表单信息不丢失。单页面应用较多,敏感账号一次性登录等。

读写操作
sessionstorage.setItem(key, value)  保存数据,key和value都必须是字符串类型
sessionstorage.getItem(key)  获取数据
sessionstorage.removeItem(key)  删除数据
sessionstorage.clear()  删除全部数据
4、三者区别
存储方式 作用与特性 存储大小 数据有效期 API
cookie a.在所有同源窗口中都是共享的。
b.存储用户信息,获取数据需要与服务器建立连接。
c.可存储的数据有限,且依赖于服务器,无需请求服务器的数据尽量不要存放在cookie中,以免影响页面性能。
d.可设置过期时间。
一般不超过4K 一般由服务器生成,可以设置失效时间;若没有设置失效时间,关闭浏览器cookie就失效;若设置了时间,cookie就会存放在硬盘里,过期才失效。 需要自己封装,原生的cookie接口不够友好
localstorage a.在所有同源窗口中都是共享的。
b.存储客户端信息,无需请求服务器。
c.数据永久保存,除非用户手动清理客户端缓存。
d.开发者可自行封装一个方法,设置失效时间。
5M或更大 永久有效,窗口或者浏览器关闭也会一直保存,除非手动清除缓存。 原生接口可以接受,可以封装来对object和array有更好的支持
sessionstorage a.在同一个浏览器窗口是共享的,不同浏览器同一页面也是不共享的。
b.存储客户端信息,无需请求服务器。
c.数据保存在当前会话,刷新页面数据不会被清除,结束会话(关闭浏览器、关闭页面、跳转页面)数据失效。
5M或更大 仅在浏览器窗口关闭之前有效,关闭页面或浏览器就会被清除 原生接口可以接受,可以封装来对object和array有更好的支持
5、indexedDB

indexedDB是一个运行在浏览器上的非关系型数据库,没有存储上限,一般不会小于250M,它不仅可以存储字符串,还可以存储二进制数据。

特点
  • 键值对存储
    indexedDB 内部采用对象仓库(object store)存放数据。所有类型的数据都可以直接存入,包括JavaScript对象,对象仓库中,数据以“键值对”形式保存,每一个数据记录都有对应的主键,主键是独一无二的。
  • 异步
    indexedDB 操作时不会锁死浏览器,用户依然可以进行其他操作,这与localstorage形成对比,后者的操作是同步的,异步设计是为了防止大量数据的读写,拖慢网页的表现。
  • 支持事务
    indexedDB 支持事务(transaction),这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。
  • 同源限制
    indexedDB 受到同源限制,每一个数据库对应创建它的域名,网页只能访问自身域名下的数据库,而不能访问跨域的数据库。
  • 储存空间大
    indexedDB 的储存空间比localstorage大得多,一般来说不少于250M,甚至没有上限。
  • 支持二进制储存
    indexedDB 不仅可以储存字符串,还可以储存二进制数据。
常见操作
  • 建立打开indexedDB --- window.indexedDB.open("testDB")
function openDB(name) {
    var request = window.indexedDB.open(name) //建立打开IndexedDB
    console.log('request', request)
    request.onerror = function (e) {
        console.log('open indexdb error')
    }
    request.onsuccess = function (e) {
        myDB.db = e.target.result //这是一个 IDBDatabase对象,这就是IndexedDB对象
        console.log(myDB.db) //此处就可以获取到db实例
    }
}
var myDB = {
    name: 'testDB',
    version: '1',
    db: null
}
    
openDB(myDB.name)
  • 关闭indexedDB --- indexedDB.close()
function closeDB(db){
    db.close();
}
  • 删除indexedDB --- window.indexedDB.deleteDatabase(indexdb)
function deleteDB(name) {
  indexedDB.deleteDatabase(name)
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容