Nuxt 自適應(yīng) SSR 方案: SEO 和首屏最小化優(yōu)化

20190922173229.png

目前項(xiàng)目采用 Nuxt SSR 來完成服務(wù)端渲染 荠列,為滿足 SEO 需求载城,將非首屏內(nèi)容也進(jìn)行了請(qǐng)求和服務(wù)端直出,導(dǎo)致首屏?xí)r間變長(非首屏的資源請(qǐng)求和組件的渲染都會(huì)帶來額外開銷)诉瓦。對(duì)于海量的用戶來說,少量的爬蟲訪問需求反而影響了正常用戶的訪問固额,導(dǎo)致 SEO 和用戶體驗(yàn)提升存在很大的矛盾煞聪。

為了解決這個(gè)問題对雪,我們?cè)O(shè)計(jì)和實(shí)踐了自適應(yīng) SSR 方案米绕,來同時(shí)滿足這兩種場(chǎng)景的需求馋艺。今天會(huì)分享這個(gè)方案的技術(shù)細(xì)節(jié)、設(shè)計(jì)思路以及在實(shí)施該方案過程中遇到的一些相關(guān)的子問題的實(shí)踐踩坑經(jīng)驗(yàn)捐祠,歡迎大家一起交流。

分享大綱

  • 問題來源和背景
  • 問題解決思路
  • 自適應(yīng) SSR 方案介紹
  • 采用自適應(yīng) SSR 優(yōu)化前后數(shù)據(jù)
  • Vue SSR client side hydration 踩坑實(shí)踐
  • 使用 SVG 生成骨架屏踩坑實(shí)踐

問題來源和背景

目前項(xiàng)目采用 Nuxt SSR 來完成服務(wù)端渲染窿给,為滿足 SEO 需求率拒,將非首屏資源也進(jìn)行了請(qǐng)求和服務(wù)端直出,導(dǎo)致首屏?xí)r間變長(非首屏的資源請(qǐng)求和組件的渲染都會(huì)帶來額外開銷)

優(yōu)化前的加載流程圖

20190808160403.png

目前我們的 Nuxt 項(xiàng)目采用 fetch 來實(shí)現(xiàn) SSR 數(shù)據(jù)預(yù)取猬膨,fetch 中會(huì)處理所有關(guān)鍵和非關(guān)鍵請(qǐng)求

Nuxt 生命周期圖

20190808160623.png

對(duì)于海量的用戶來說,少量的爬蟲訪問需求反而影響了正常用戶的訪問谒所,導(dǎo)致 SEO 和用戶體驗(yàn)提升存在很大的矛盾。

為了解決這個(gè)問題劣领,我們希望能區(qū)分不同的場(chǎng)景進(jìn)行不同的直出,SEO 場(chǎng)景全部直出尖淘,其他場(chǎng)景只直出最小化的首屏,非關(guān)鍵請(qǐng)求放在前端異步拉取

解決思路

計(jì)劃通過統(tǒng)一的方式來控制數(shù)據(jù)加載歇攻,將數(shù)據(jù)加載由專門的插件來控制梆造,插件會(huì)根據(jù)條件來選擇性的加載數(shù)據(jù)缴守,同時(shí)懶加載一部分?jǐn)?shù)據(jù)

  • 判斷是 SEO 情況镇辉,fetch 階段執(zhí)行所有的數(shù)據(jù)加載邏輯
  • 非 SEO 場(chǎng)景,fetch 階段只執(zhí)行最小的數(shù)據(jù)加載邏輯村砂,等到頁面首屏直出后,通過一些方式來懶加載另一部分?jǐn)?shù)據(jù)

優(yōu)化后的項(xiàng)目影評(píng)頁加載流程圖

20190808162208.png

自適應(yīng) SSR 方案介紹

Gitlab CI Pipeline

20190808160912.png

自研 Nuxt Fetch Pipeline

借鑒 Gitlab CI 持續(xù)集成的概念和流程础废,將數(shù)據(jù)請(qǐng)求設(shè)計(jì)為不同的階段 (Stage )罕模,每個(gè)階段執(zhí)行不同的異步任務(wù)(Job),所有的階段組成了數(shù)據(jù)請(qǐng)求的管線(Pipeline)

預(yù)置的 Stage

  • seoFetch : 面向 SEO 渲染需要的 job 集合淑掌,一般要求是全部數(shù)據(jù)請(qǐng)求都需要,盡可能多的服務(wù)端渲染內(nèi)容
  • minFetch:首屏渲染需要的最小的 job 集合
  • mounted: 首屏加載完之后芋绸,在 mounted 階段異步執(zhí)行的 job 集合
  • idle: 空閑時(shí)刻才執(zhí)行的 job 集合

每一個(gè)頁面的都有一個(gè) Nuxt Fetch Pipeline 的實(shí)例來控制担敌,Nuxt Fetch Pipeline 需要配置相應(yīng)的 job 和 stage摔敛,然后會(huì)自適應(yīng)判斷請(qǐng)求的類型全封,針對(duì)性的處理異步數(shù)據(jù)拉瓤嘟础:

  • 如果是 SEO 場(chǎng)景给猾,則只會(huì)執(zhí)行 seoFetch 這個(gè) stage 的 job 集合
  • 如果是真實(shí)用戶訪問,則會(huì)在服務(wù)端先執(zhí)行 minFetch 這個(gè) stage 的 job 集合敢伸,然后立即返回,客戶端可以看到首屏內(nèi)容及骨架屏尾序,然后在首屏加載完之后,會(huì)在 mounted 階段異步執(zhí)行 mounted stage 的 job 集合每币,另外一些優(yōu)先級(jí)更低的 job琢歇,則會(huì)在 idle stage 也就是空閑的時(shí)候才執(zhí)行兰怠。

Nuxt Fetch Pipeline 使用示例

page 頁面 index.vue

import NuxtFetchPipeline, {
  pipelineMixin,
  adaptiveFetch,
} from '@/utils/nuxt-fetch-pipeline';
import pipelineConfig from './index.pipeline.config';

const nuxtFetchPipeline = new NuxtFetchPipeline(pipelineConfig);

export default {
  mixins: [pipelineMixin(nuxtFetchPipeline)],

  fetch(context) {
    return adaptiveFetch(nuxtFetchPipeline, context);
  },
};

配置文件 index.pipeline.config.js

export default {
  stages: {
    // 面向SEO渲染需要的 job 集合李茫,一般要求是全部
    seoFetch: {
      type: 'parallel',
      jobs: [
        'task1'
      ]
    },
    // 首屏渲染需要的最小的 job 集合
    minFetch: {
      type: 'parallel',
      jobs: [
      ]
    },
    // 首屏加載完之后,在 mounted 階段異步執(zhí)行的 job 集合
    mounted: {
      type: 'parallel',
      jobs: [
      ]
    },
    // 空閑時(shí)刻才執(zhí)行的 job 集合
    idle: {
      type: 'serial',
      jobs: [
      ]
    }
  },
  pipelines: {
    // 任務(wù)1
    task1: {
      task: ({ store, params, query, error, redirect, app, route }) =&gt {
        return store.dispatch('action', {})
      }
    }
  }
}

并發(fā)控制

Stage 執(zhí)行 Job 支持并行和串行 Stage 配置 type 為 parallel 時(shí)為并行處理秸侣,會(huì)同時(shí)開始每一個(gè) job 等待所有的 job 完成后宠互,這個(gè) stage 才完成 Stage 配置 type 為 serial 時(shí)為串行處理,會(huì)依次開始每一個(gè) job予跌,前一個(gè) job 完成后,后面的 job 才開始匕得,最后一個(gè) job 完成后巾表,這個(gè) stage 才完成

Job 嵌套

可以將一些可以復(fù)用的 job 定義為自定義的 stage,然后考阱,在其他的 Stage 里按照如下的方式來引用鞠苟,減少編碼的成本

{
  seoFetch: {
    type: 'serial',
    jobs:
    [
      'getVideo',
      { jobType: 'stage', name: 'postGetVideo' }
    ]
  },
  postGetVideo: {
    type: 'parallel',
    jobs: [
      'anyjob',
      'anyjob2'
    ]
  }
}

Job 的執(zhí)行上下文

為了方便編碼秽之,以及減少改動(dòng)成本吃既,每一個(gè) job 執(zhí)行上下文和 Nuxt fetch 類似考榨,而是通過一個(gè) context 參數(shù)來訪問一些狀態(tài)鹦倚,由于 fetch 階段還沒有組件實(shí)例,為了保持統(tǒng)一震叙,都不可以通過 this 訪問實(shí)例

目前支持的 nuxt context 有

  • app
  • route
  • store
  • params
  • query
  • error
  • redirect

Stage 的劃分思路

Stage 適合的 Job 是否并行
seoFetch 全部,SEO 場(chǎng)景追求越多越好 最好并行
minFetch 關(guān)鍵的乐尊,比如首屏內(nèi)容划址、核心流程需要的數(shù)據(jù),頁面的主要核心內(nèi)容(例如影評(píng)頁面是影評(píng)的正文猴鲫,短視頻頁面是短視頻信息,帖子頁面是帖子正文)的數(shù)據(jù) 最好并行
mounted 次關(guān)鍵內(nèi)容的數(shù)據(jù)拂共,例如側(cè)邊欄,第二屏等 根據(jù)優(yōu)先成都考慮是否并行
idle 最次要的內(nèi)容的數(shù)據(jù)势告,例如頁面底部抚恒,標(biāo)簽頁被隱藏的部分 盡量分批進(jìn)行,不影響用戶的交互

使用 SVG 生成骨架屏踩坑實(shí)踐

由于服務(wù)端只拉取了關(guān)鍵數(shù)據(jù)俭驮,部分頁面部分存在沒有數(shù)據(jù)的情況,因此需要骨架屏來提升體驗(yàn)

20190808163542.png
20190808163628.png

Vue Content Loading 使用及原理

例子

<script>
  import VueContentLoading from 'vue-content-loading';

  export default {
    components: {
      VueContentLoading,
    },
  };
</script>

<template>
  <vue-content-loading :width="300" :height="100">
    <circle cx="30" cy="30" r="30" />
    <rect x="75" y="13" rx="4" ry="4" width="100" height="15" />
    <rect x="75" y="37" rx="4" ry="4" width="50" height="10" />
  </vue-content-loading>
</template>

Vue Content Loading 核心代碼

<template>
  <svg :viewBox="viewbox" :style="svg" preserveAspectRatio="xMidYMid meet">
    <rect
      :style="rect.style"
      :clip-path="rect.clipPath"
      x="0"
      y="0"
      :width="width"
      :height="height"
    />

    <defs>
      <clipPath :id="clipPathId">
        <slot>
          <rect x="0" y="0" rx="5" ry="5" width="70" height="70" />
          <rect x="80" y="17" rx="4" ry="4" width="300" height="13" />
          <rect x="80" y="40" rx="3" ry="3" width="250" height="10" />
          <rect x="0" y="80" rx="3" ry="3" width="350" height="10" />
          <rect x="0" y="100" rx="3" ry="3" width="400" height="10" />
          <rect x="0" y="120" rx="3" ry="3" width="360" height="10" />
        </slot>
      </clipPath>

      <linearGradient :id="gradientId">
        <stop offset="0%" :stop-color="primary">
          <animate
            attributeName="offset"
            values="-2; 1"
            :dur="formatedSpeed"
            repeatCount="indefinite"
          />
        </stop>

        <stop offset="50%" :stop-color="secondary">
          <animate
            attributeName="offset"
            values="-1.5; 1.5"
            :dur="formatedSpeed"
            repeatCount="indefinite"
          />
        </stop>

        <stop offset="100%" :stop-color="primary">
          <animate
            attributeName="offset"
            values="-1; 2"
            :dur="formatedSpeed"
            repeatCount="indefinite"
          />
        </stop>
      </linearGradient>
    </defs>
  </svg>
</template>

<script>
  const validateColor = color =>
    /^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/.test(color);
  export default {
    name: 'VueContentLoading',
    props: {
      rtl: {
        default: false,
        type: Boolean,
      },
      speed: {
        default: 2,
        type: Number,
      },
      width: {
        default: 400,
        type: Number,
      },
      height: {
        default: 130,
        type: Number,
      },
      primary: {
        type: String,
        default: '#f0f0f0',
        validator: validateColor,
      },
      secondary: {
        type: String,
        default: '#e0e0e0',
        validator: validateColor,
      },
    },
    computed: {
      viewbox() {
        return `0 0 ${this.width} ${this.height}`;
      },
      formatedSpeed() {
        return `${this.speed}s`;
      },
      gradientId() {
        return `gradient-${this.uid}`;
      },
      clipPathId() {
        return `clipPath-${this.uid}`;
      },
      svg() {
        if (this.rtl) {
          return {
            transform: 'rotateY(180deg)',
          };
        }
      },
      rect() {
        return {
          style: {
            fill: 'url(#' + this.gradientId + ')',
          },
          clipPath: 'url(#' + this.clipPathId + ')',
        };
      },
    },
    data: () => ({
      uid: null,
    }),
    created() {
      this.uid = this._uid;
    },
  };
</script>

SVG 動(dòng)畫卡頓

使用了 Vue content loading 做骨架屏之后,發(fā)現(xiàn)在 js 加載并執(zhí)行的時(shí)候動(dòng)畫會(huì)卡住逸嘀,而 CSS 動(dòng)畫大部分情況下可以脫離主線程執(zhí)行,可以避免卡頓

CSS animations are the better choice. But how? The key is that as long as the properties we want to animate do not trigger reflow/repaint (read CSS triggers for more information), we can move those sampling operations out of the main thread. The most common property is the CSS transform. If an element is promoted as a layer, animating transform properties can be done in the GPU, meaning better performance/efficiency, especially on mobile. Find out more details in OffMainThreadCompositing. https://developer.mozilla.org/en-US/docs/Web/Performance/CSS_JavaScript_animation_performance

測(cè)試 Demo 地址

https://jsbin.com/wodenoxaku/1/edit?html,css,output

看起來瀏覽器并沒有對(duì) SVG 動(dòng)畫做這方面的優(yōu)化翼岁,最終,我們修改了 Vue content loading 的實(shí)現(xiàn)琅坡,改為了使用 CSS 動(dòng)畫來實(shí)現(xiàn)閃爍的加載效果

<template>
  <div :style="style">
    <svg :viewBox="viewbox" preserveAspectRatio="xMidYMid meet">
      <defs :key="uid">
        <clipPath :id="clipPathId" :key="clipPathId">
          <slot>
            <rect x="0" y="0" rx="5" ry="5" width="70" height="70" />
            <rect x="80" y="17" rx="4" ry="4" width="300" height="13" />
            <rect x="80" y="40" rx="3" ry="3" width="250" height="10" />
            <rect x="0" y="80" rx="3" ry="3" width="350" height="10" />
            <rect x="0" y="100" rx="3" ry="3" width="400" height="10" />
            <rect x="0" y="120" rx="3" ry="3" width="360" height="10" />
          </slot>
        </clipPath>
      </defs>
    </svg>
  </div>
</template>

<script>
  const validateColor = color =>
    /^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/.test(color);

  export default {
    name: 'VueContentLoading',
    props: {
      rtl: {
        default: false,
        type: Boolean,
      },
      speed: {
        default: 2,
        type: Number,
      },
      width: {
        default: 400,
        type: Number,
      },
      height: {
        default: 130,
        type: Number,
      },
      primary: {
        type: String,
        default: '#F0F0F0',
        validator: validateColor,
      },
      secondary: {
        type: String,
        default: '#E0E0E0',
        validator: validateColor,
      },
      uid: {
        type: String,
        required: true,
      },
    },
    computed: {
      viewbox() {
        return `0 0 ${this.width} ${this.height}`;
      },
      formatedSpeed() {
        return `${this.speed}s`;
      },
      clipPathId() {
        return `clipPath-${this.uid || this._uid}`;
      },
      style() {
        return {
          width: `${this.width}px`,
          height: `${this.height}px`,
          backgroundSize: '200%',
          backgroundImage: `linear-gradient(-90deg, ${this.primary} 0, ${this.secondary} 20%, ${this.primary} 50%,  ${this.secondary} 75%,  ${this.primary})`,
          clipPath: 'url(#' + this.clipPathId + ')',
          animation: `backgroundAnimation ${this.formatedSpeed} infinite linear`,
          transform: this.rtl ? 'rotateY(180deg)' : 'none',
        };
      },
    },
  };
</script>

<style lang="scss">
  @keyframes backgroundAnimation {
    0% {
      background-position-x: 100%;
    }

    50% {
      background-position-x: 0;
    }

    100% {
      background-position-x: -100%;
    }
  }
</style>

Vue SSR client side hydration 踩坑實(shí)踐

一個(gè)例子

<template>
  <div :id="id"> text: {{ id }}</div>
</template>
<script>
  export default {
    data () {
       return {
         id: Math.random()
       }
    }
  }
</script>

client side hydration 的結(jié)果會(huì)是如何呢脑蠕?

  • A. id 是 client 端隨機(jī)數(shù), text 是 client 端隨機(jī)數(shù)
  • B. id 是 client 端隨機(jī)數(shù)谴仙, text 是 server 端隨機(jī)數(shù)
  • C. id 是 server 端隨機(jī)數(shù)排截, text 是 client 端隨機(jī)數(shù)
  • D. id 是 server 端隨機(jī)數(shù), text 是 server 端隨機(jī)數(shù)

為什么要問這個(gè)問題 炼蹦?

Vue content loading 內(nèi)部依賴了 this._uid 來作為 svg defs 里的 clippath 的 id凌盯,然而 this._uid 在客戶端和服務(wù)端并不一樣烹玉,實(shí)際跟上面隨機(jī)數(shù)的例子差不多。

client side hydration 的結(jié)果是 C

也就是說 id 并沒有改變二打,導(dǎo)致的現(xiàn)象在我們這個(gè)場(chǎng)景就是骨架屏閃了一下就沒了

為什么會(huì)出現(xiàn)這個(gè)情況?

初始化 Vue 到最終渲染的整個(gè)過程


20190808172826.png

來源:https://ustbhuangyi.github.io/vue-analysis/data-driven/update.html#%E6%80%BB%E7%BB%93

所謂客戶端激活症杏,指的是 Vue 在瀏覽器端接管由服務(wù)端發(fā)送的靜態(tài) HTML瑞信,使其變?yōu)橛?Vue 管理的動(dòng)態(tài) DOM 的過程厉颤。

在 entry-client.js 中凡简,我們用下面這行掛載(mount)應(yīng)用程序:

// 這里假定 App.vue template 根元素的 `id="app"`
app.$mount('#app');

由于服務(wù)器已經(jīng)渲染好了 HTML,我們顯然無需將其丟棄再重新創(chuàng)建所有的 DOM 元素秤涩。相反,我們需要"激活"這些靜態(tài)的 HTML,然后使他們成為動(dòng)態(tài)的(能夠響應(yīng)后續(xù)的數(shù)據(jù)變化)状植。

如果你檢查服務(wù)器渲染的輸出結(jié)果怨喘,你會(huì)注意到應(yīng)用程序的根元素上添加了一個(gè)特殊的屬性:

<div id="app" data-server-rendered="true"></div>

data-server-rendered 特殊屬性振定,讓客戶端 Vue 知道這部分 HTML 是由 Vue 在服務(wù)端渲染的,并且應(yīng)該以激活模式進(jìn)行掛載后频。注意,這里并沒有添加 id="app"卑惜,而是添加 data-server-rendered 屬性:你需要自行添加 ID 或其他能夠選取到應(yīng)用程序根元素的選擇器,否則應(yīng)用程序?qū)o法正常激活露久。

注意,在沒有 data-server-rendered 屬性的元素上征峦,還可以向 $mount 函數(shù)的 hydrating 參數(shù)位置傳入 true,來強(qiáng)制使用激活模式(hydration):

// 強(qiáng)制使用應(yīng)用程序的激活模式
app.$mount('#app', true);

在開發(fā)模式下栏笆,Vue 將推斷客戶端生成的虛擬 DOM 樹 (virtual DOM tree)臊泰,是否與從服務(wù)器渲染的 DOM 結(jié)構(gòu) (DOM structure) 匹配。如果無法匹配因宇,它將退出混合模式,丟棄現(xiàn)有的 DOM 并從頭開始渲染察滑。在生產(chǎn)模式下,此檢測(cè)會(huì)被跳過贺辰,以避免性能損耗。

vue 對(duì)于 attrs,class,staticClass,staticStyle,key 這些是不處理的

list of modules that can skip create hook during hydration because they are already rendered on the client or has no need

uid 解決方案

根據(jù)組件生成唯一 UUID

  • props 和 slot 轉(zhuǎn)換為字符串
  • hash 算法

太重了莽鸭,放棄

最終解決方案

干脆讓用戶自己傳 ID

<vue-content-loading
  uid="circlesMediaSkeleton"
  v-bind="$attrs"
  :width="186"
  :height="height"
>
  <template v-for="i in rows">
    <rect
      :key="i + '_r'"
      x="4"
      :y="getYPos(i, 4)"
      rx="2"
      ry="2"
      width="24"
      height="24"
    />
    <rect
      :key="i + '_r'"
      x="36"
      :y="getYPos(i, 6)"
      rx="3"
      ry="3"
      width="200"
      height="18"
    />
  </template>
</vue-content-loading>

優(yōu)化效果

  • 通過減少 fetch 階段的數(shù)據(jù)拉取的任務(wù)吃靠,減少了數(shù)據(jù)拉取時(shí)間
  • 同時(shí)減少了服務(wù)端渲染的組件數(shù)和開銷,縮短了首字節(jié)時(shí)間
  • 首屏大小變小也縮短了下載首屏所需的時(shí)間

綜合起來巢块,首字節(jié)巧号、首屏?xí)r間都將提前姥闭,可交互時(shí)間也會(huì)提前

本地?cái)?shù)據(jù)

類型 服務(wù)響應(yīng)時(shí)間 首頁大小 未 Gzip
首頁修改前 0.88s 561 KB
首頁(最小化 fetch 請(qǐng)求) 0.58s 217 KB

在本地測(cè)試,服務(wù)端渲染首頁只請(qǐng)求關(guān)鍵等服務(wù)器接口請(qǐng)求時(shí)棚品,服務(wù)響應(yīng)時(shí)間縮短 0.30s降低 34%铜跑,首頁 html 文本大小降低 344 KB,減少 60%

線上數(shù)據(jù)

file

首頁的首屏可見時(shí)間中位數(shù)從 2-3s 降低到了 1.1s 左右,加載速度提升 100%+

總結(jié)

本文分享了如何解決 SEO 和用戶體驗(yàn)提升之間存在矛盾的問題薪缆,介紹了我們?nèi)绾谓梃b Gitlab CI 的 pipeline 的概念伞广,在服務(wù)端渲染時(shí)兼顧首屏最小化和 SEO拣帽,分享了自適應(yīng) SSR 的技術(shù)細(xì)節(jié)嚼锄、設(shè)計(jì)思路以及在實(shí)施該方案過程中遇到的一些相關(guān)的子問題的實(shí)踐踩坑經(jīng)驗(yàn),希望對(duì)大家有所啟發(fā)和幫助区丑。


關(guān)于我

binggg(Booker Zhao) @騰訊

- 先后就職于迅雷、騰訊等可霎,個(gè)人開源項(xiàng)目有 mrn.js 等
- 創(chuàng)辦了迅雷內(nèi)部組件倉庫 XNPM ,參與幾個(gè)迅雷前端開源項(xiàng)目的開發(fā)
- 熱衷于優(yōu)化和提效癣朗,是一個(gè)奉行“懶惰使人進(jìn)步”的懶人工程師

社交資料

微信公眾號(hào) binggg_net, 歡迎關(guān)注

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末旺罢,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子扁达,更是在濱河造成了極大的恐慌,老刑警劉巖炉旷,帶你破解...
    沈念sama閱讀 218,451評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異窘行,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)抽高,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門透绩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人帚豪,你說我怎么就攤上這事±瓿迹” “怎么了?”我有些...
    開封第一講書人閱讀 164,782評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵诈泼,是天一觀的道長煤禽。 經(jīng)常有香客問我铐达,道長檬果,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,709評(píng)論 1 294
  • 正文 為了忘掉前任杭抠,我火速辦了婚禮,結(jié)果婚禮上偏灿,老公的妹妹穿的比我還像新娘。我一直安慰自己菩混,他們只是感情好扁藕,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,733評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著亿柑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上呼畸,一...
    開封第一講書人閱讀 51,578評(píng)論 1 305
  • 那天颁虐,我揣著相機(jī)與錄音,去河邊找鬼另绩。 笑死,一個(gè)胖子當(dāng)著我的面吹牛笋籽,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播车海,決...
    沈念sama閱讀 40,320評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼研铆!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蚜印,我...
    開封第一講書人閱讀 39,241評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤留量,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后楼熄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,686評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡可岂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,878評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了稚茅。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片平斩。...
    茶點(diǎn)故事閱讀 39,992評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖绘面,靈堂內(nèi)的尸體忽然破棺而出侈沪,到底是詐尸還是另有隱情,我是刑警寧澤亭罪,帶...
    沈念sama閱讀 35,715評(píng)論 5 346
  • 正文 年R本政府宣布歼秽,位于F島的核電站,受9級(jí)特大地震影響燥筷,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜荆责,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,336評(píng)論 3 330
  • 文/蒙蒙 一亚脆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧濒持,春花似錦、人聲如沸柑营。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至惋嚎,卻和暖如春站刑,著一層夾襖步出監(jiān)牢的瞬間另伍,已是汗流浹背绞旅。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評(píng)論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留因悲,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,173評(píng)論 3 370
  • 正文 我出身青樓臼朗,卻偏偏與公主長得像邻寿,于是被迫代替她去往敵國和親视哑。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,947評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容