认识 Webpack (三)

Plugins


plugin 顾名思义为插件的意思,那么我们很容易就能想到 plugins 应该为插件集合。我们先看官方文档对 Plugins 给出的定义:webpack 有着丰富的插件接口,且 webpack 自身的多数功能都使用这个插件接口,这个插件接口使 webpack 变得极其灵活。这里先不做过多的解释,拿我们前面的项目先来看看使用场景。

在之前的操作中每次使用 npm run build 打包的时候都会生成一个 dist 文件夹,但是并不会主动在 dist 文件夹中生成对应的 index.html 页面,所以前面我们每次使用的时候都是先提前在 dist 文件夹中手动建一个 index.html 页面用来加载打包出来的 bundle.js。如果我们希望每次打包构建后,不仅能帮我们生成 bundle.js,同时也能帮我们生成 index.html,那么我们就需要在 webpack.config.js 中进行 Plugins 的相关配置。

HtmlWebpackPlugin

依照官方文档,我们首先需要安装该插件

npm install --save-dev html-webpack-plugin

结合官网给出的基本用法,我们改造下 webpack.config.js,毕竟里面很多内容都是前面配置 loader 写的代码,如果都放到这里可能太长了,不利于阅读理解。这里我将 module 里面的整体内容先删除了,但是该内容还是要有的啊,前面的文章里面都有该部分的整体代码,因为这里主要是配置 plugins ,所以以免内容过多影响学习。

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: {
    main: './src/index.js'
  }, // 项目打包的入口文件
  plugins: [new HtmlWebpackPlugin()],
  output: { // 打包的最终文件放在哪里
    filename: 'bundle.js', // 打包生成的文件名
    path: path.resolve(__dirname, 'dist') // 打包目录
  }
}

我们在构建代码之前先把 dist 文件夹整个删除,使用上面的代码之后就会发现 webpack 帮我们构建了 bundle.js 的同时也帮我们构建了 index.html,当然它构建的 index.html 虽然导入了 bundle.js ,但是 body 里面却没有我们的根节点 <div id="root"></div> 这一项。

我们回想一下使用 vue-cli 帮我们搭建的项目基础模板中好像 index.html 页面在根目录中一个叫 public 的文件夹中,那么我们可不可以也在 src 目录中新建一个 index.html 的模板文件,如果使用 webpack 自动构建的话就以这个 index.html 里面的内容为模板,而我们在打包的 dist 目录中的 index.html 可以直接整合外部模板里的内容。

有了思路我们就现在 src 目录下创建项目的模板文件 index.html,然后修改 webpack.config.js 中的配置内容。

module.exports = {
  entry: {
    main: './src/index.js'
  }, // 项目打包的入口文件
  plugins: [new HtmlWebpackPlugin({
    template: 'src/index.html'
  })],
  output: { // 打包的最终文件放在哪里
    filename: 'bundle.js', // 打包生成的文件名
    path: path.resolve(__dirname, 'dist') // 打包目录
  }
}

这里我们先总结一下 HtmlWebpackPlugin 的作用

HtmlWebpackPlugin 会在打包结束后,自动生成一个 html 文件,并把打包生成的 js 自动引入到这个 html 文件中。

此时回过头我们再来看看 plugin 的作用

plugin 可以在 webpack 运行到某个时刻的时候,帮你做一些事情。

回过头来再看问题,我们原来定义的打包生成的文件名为 bundle.js ,但是如果我们将其改为 dist.js 之后再次进行打包我们就会发现打包生成的 dist 目录下既有 bundle.js 也有 dist.js,虽然这并不影响我们使用,但是极客精神告诉我们,如果我们每次打包之前都能先清理掉生成的 dist 文件夹,然后在重新生成,是不是会更好一点呢?

CleanWebpackPlugin

还是老规矩,先安装该插件

npm install clean-webpack-plugin -D

接着就是对 webpack.config.js 进行配置,同时记得修改打包输出文件的文件名进行验证哦~~~

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require("clean-webpack-plugin")

module.exports = {
  entry: {
    main: './src/index.js'
  }, // 项目打包的入口文件
  plugins: [new HtmlWebpackPlugin({
    template: 'src/index.html'
  }), new CleanWebpackPlugin()],
  output: { // 打包的最终文件放在哪里
    filename: 'dist.js', // 打包生成的文件名
    path: path.resolve(__dirname, 'dist') // 打包目录
  }
}

通过上述代码的执行我们就可以发现 cleanWebpackPlugin 会在每次打包运行前,自动删除 dist 目录。同时这个栗子也再次验证了我们先前得出的结论:plugin 可以在 webpack 运行到某个时刻的时候,帮你做一些事情。

EntryOutput


前面我们说过,在 entry 中做如下配置

module.exports = {
  entry: {
    main: './src/index.js'
  }, // 项目打包的入口文件
  output: { // 打包的最终文件放在哪里
    filename: 'dist.js', // 打包生成的文件名
    path: path.resolve(__dirname, 'dist') // 打包目录
  }
}

其实等价于下面这种:

module.exports = {
  entry: './src/index.js', // 项目打包的入口文件
  output: { // 打包的最终文件放在哪里
    filename: 'dist.js', // 打包生成的文件名
    path: path.resolve(__dirname, 'dist') // 打包目录
  }
}

如果我们不配置 output 中的 filename ,则默认生成的打包名为 main.js 。这里延伸出一个问题,如果我们希望对 index.js 进行多次打包,那么结果会怎么样呢?

module.exports = {
  entry: {
    main: './src/index.js',
    sub: './src/index.js'
  }, // 项目打包的入口文件
  output: { // 打包的最终文件放在哪里
    filename: 'dist.js', // 打包生成的文件名
    path: path.resolve(__dirname, 'dist') // 打包目录
  }
}

直接运行最后会报错,它会提示我们:打包生成的两个文件都使用了 dist.js 这个名字造成了命名冲突,所以此时我们需要在 output 中做一些配置修改:

module.exports = {
  entry: {
    main: './src/index.js',
    sub: './src/index.js'
  }, // 项目打包的入口文件
  output: { // 打包的最终文件放在哪里
    filename: '[name].js', // 打包生成的文件名,使用占位符匹配打包文件的 key 值
    path: path.resolve(__dirname, 'dist') // 打包目录
  }
}

运行上述代码,我们发现果然给我们在 dist 目录下生成了 main.jssub.js 两个文件,我们来瞅一瞅生成的 index.html 文件

<body>
  <div id="root"></div>
  <script src="main.js"></script>
  <script src="sub.js"></script>
</body>

body 的闭合处正确引入了我们想生成的两个 js 脚本,但是这里我们如果希望引入的脚本地址是一个线上 cdn 的地址如 https://www.baidu.com/main.js 这种,又应该怎么办呢?

module.exports = {
  entry: {
    main: './src/index.js',
    sub: './src/index.js'
  }, // 项目打包的入口文件
  output: { // 打包的最终文件放在哪里
    publicPath: 'https://www.baidu.com', // 配置打包资源的前缀路径
    filename: '[name].js', // 打包生成的文件名,使用占位符匹配打包文件的 key 值
    path: path.resolve(__dirname, 'dist') // 打包目录
  }
}

我们可以在 output 对象中配置 publicPath 属性,写成我们想要加入的前缀地址即可,此时我们再来瞅瞅 dist 目下下的 index.html 中的内容:

<body>
  <div id="root"></div>
  <script src="https://www.baidu.com/main.js"></script>
  <script src="https://www.baidu.com/sub.js"></script>
</body>

此处延伸学习文档:管理输出 | webpack

SourceMap


在了解它之前,我们先把代码结构清理一下,删除 dist 目录,删除 src 目录中除了 index.htmlindex.js 的所有其它文件,同时我们将 index.js 中随便写入一段错误的代码:

// index.js
consele.log('hello world') // console 打错了,故意留坑

接着我们在 webpack.config.js 进行 sourceMap 的相关配置

module.exports = {
  mode: 'development',
  devtool: 'none',
  entry: {
    main: './src/index.js',
  }, // 项目打包的入口文件
  output: { // 打包的最终文件放在哪里
    filename: '[name].js', // 打包生成的文件名,使用占位符匹配打包文件的 key 值
    path: path.resolve(__dirname, 'dist') // 打包目录
  }
}

明明说的是配置 sourceMap, 为啥只看到了一个 devtool 的配置呢?官方文档中 devtool 此选项控制是否生成以及如何生成 source map,那么我们配置了 development: 'none' 有什么用呢?此时我们打开打包出来的 index.html 页面,发现我们故意留坑的报错在 main.js 的第 96 行,虽然说确实提示出来了,但是开发中我们可能更希望报错提示出现在打包前的脚本文件中,并精确告诉我们是哪一行,此时我们就可以修改 devtool 的基础配置:

module.exports = {
  mode: 'development',
  devtool: 'source-map',
  entry: {
    main: './src/index.js',
  }, // 项目打包的入口文件
  output: { // 打包的最终文件放在哪里
    filename: '[name].js', // 打包生成的文件名,使用占位符匹配打包文件的 key 值
    path: path.resolve(__dirname, 'dist') // 打包目录
  }
}

devtool 的值设置为 source-map 之后我们发现报错信息确实出现在了 index.js 的第一行,也就是直接提示源代码中的错误地址,同时 dist 目录下也新增了一个 main.js.map 的文件,此时我们初步总结一下 sourceMap 的作用:

sourceMap 它是一个映射关系,它知道 dist 目录下 main.js 文件 96 行实际上对应的是 src 目录下 index.js 文件中的第一行。同时所有对应的映射关系都会生成在一个 .map 的脚本文件中。

官方文档中给出 devtool 的值多大十几个,不同的值打包速度不同,这里我们引用了官方给出的相关表格信息:

devtool 的值.png

因为表格中的可选项太多了,我们拿几个有代表性的来说一说:

  • none:不建立映射关系,报错主要体现在打包后的代码位置

  • source-map:建立映射关系,同时生成一个 .map 的映射文件,报错主要体现在源代码中,打包速度慢

  • inline-source-map:建立映射关系,不生成 .map 映射文件,而是直接以 base64 链接的形式被引入到打包后的 man.js 中,报错主要体现在源代码中,会告诉你在多少行多少列,打包速度慢

  • inline-cheap-source-map:只跟你写的业务代码建立映射关系,和 inline-source-map 差不多,但是只会告诉你报错在源代码中的第几行,而不会告诉你对应的列数,打包速度较快。

  • inline-cheap-module-source-map:不仅跟你写的业务代码建立映射关系,同时也会对你引入的 loaderplugins 等进行校验映射,打包速度较慢。

  • eval:打包速度快,但是遇到复杂逻辑代码,报错提示不明显。

更多配置关系可能更复杂,我们大概了解,然后在 development 开发环境下尽量使用:

devtool: 'cheap-module-eval-source-map',

而在 production 线上环境中尽量使用:

devtool: 'cheap-module-source-map',

WebpackDevServer


前面我们每一次更改了本地代码,都需要使用 npm run build 打包之后才能看到效果,说实话体验感非常糟糕,如果我们希望每次更改代码之后不用手动打包就能自动帮我们打包好,而且能够自动将浏览器打开等等,那我们该怎么办呢?

写代码之前吐槽一下,照着官网文档弄得,总是报错,各种版本问题,我太难了!!!查了半天最后发现要想照着官方文档的栗子实现代码 webpack-cli 的版本不要超过 4.0,最好只好卸载了 webpack-cli 重新安装,请小伙伴本牢记此坑~~~

如果你的 webpack-cli 版本超过 4.0.0 ,第一步请卸载

npm uninstall webpack-cli

安装 webpack-cli@3.3.3 版本

npm install webpack-cli@3.3.2 -D

修改 package.json 文件中的 scripts

"scripts": {
    "watch": "webpack --watch", // 观察者模式
    "start": "webpack-dev-server", // 打包运行 npm run start 即可
    "build": "webpack"
  },

接着在 webpack.config.js 中配置开启的 web 服务器

module.exports = {
  mode: 'development',
  devtool: 'cheap-module-eval-source-map',
  entry: {
    main: './src/index.js',
  }, 
  devServer: { // 相关配置
    contentBase: './dist', // 服务器启动在那个文件夹下
  },
  output: { 
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist')
  }
}

更多 devServer 的配置移步官方文档:开发中 Server(devServer)

模块热替换


感觉这是一个好高大上的词,有没有小伙伴好奇热模块是啥意思?官方给出的英文单词 Hot Module Replacement 简称 HMR,高大上的气息铺面而来,咱们先不纠结具体什么意思,先来看个栗子:

  • css 模块热替换

现在 index.js 随便编写一段代码

import './index.styl'
var btn = document.createElement('button')
btn.innerHTML = '新增'
btn.onclick = function () {
  var div = document.createElement('div')
  div.innerHTML = 'item'
  document.body.appendChild(div)
}
document.body.appendChild(btn)

然后新建一个 index.styl 文件,将所有添加的偶数的 div 块给个背景颜色

div:nth-child(odd)
  background: orange

此时我们运行 npm run start 之后其实就会有一个问题,就是如果我们每次修改 index.styl 的颜色值,页面就会刷新一次,而我们前面所有添加的按钮都要重新在添加一次。如果我们希望保留修改前的状态,css 在浏览器不刷新的情况下直接应用到已有的按钮上,这时候我们就需要用到这个高大上的名词了:Hot Module Replacement

修改 webpack.config.js 文件,进行相关配置:

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { CleanWebpackPlugin } = require("clean-webpack-plugin")
const webpack = require('webpack')

module.exports = {
  mode: 'development',
  devtool: 'cheap-module-eval-source-map',
  entry: {
    main: './src/index.js',
  }, 
  devServer: { // 相关配置
    contentBase: './dist', // 服务器启动在那个文件夹下
    hot: true, // 开启 hot module replacement
    hotOnly: true // 即便上面的 hmr 未生效也阻止浏览器的自动刷新
  },
  plugins: [new HtmlWebpackPlugin({
    template: 'src/index.html'
  }), new CleanWebpackPlugin(), new webpack.HotModuleReplacementPlugin()],
  output: { 
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist')
  }
}

上述代码中,我们新增了 devServer 配置中的 hot 属性和 hotOnly 属性,同时对 plugins 新增了 new webpack.HotModuleReplacementPlugin() 。然后我们运行 npm run start 即可。此时即使我们已经在页面新增了 100 个按钮后再去修改 index.styl ,页面也不会自动刷新而是直接在当前状态下响应 css 的变化,有没有觉得很神奇!!!

  • js 模块热替换

我们针对 js 文件同样也来尝试使用一下,我们在 src 目录中新建一个 counter.js 和一个 number.js

// counter.js
function counter() {
  const div = document.createElement('div')
  div.setAttribute('id', 'counter')
  div.innerHTML = 1
  div.onclick = function () {
    div.innerHTML = parseInt(div.innerHTML, 10) + 1
  }
  document.body.appendChild(div)
}
export default counter

// number.js
function number() {
  const div = document.createElement('div')
  div.setAttribute('id', 'number')
  div.innerHTML = 3000
  document.body.appendChild(div)
}

export default number

然后在 index.js 中引入这两个脚本文件:

import counter from './counter'
import number from './number'
counter()
number()

运行代码后首先将 counter.js 中的数值增加到 10,然后修改 number.jsdiv.innerHTML 的值为 2000,奇怪的事情发生了,我们发现页面并没有响应变化!!!那么我们该如何监听这种变化呢,此处查阅官方文档可以看到官方有下面这段代码,我们修改一下引入进来:

import counter from './counter'
import number from './number'
counter()
number()
if (module.hot) { // 如果当前代码开启了 hmr 功能
  // 监听 number.js 文件是否发生变化,如果发生变化执行后面的回调函数
  module.hot.accept('./number', () => { 
    number()
  })
}

加了如上代码之后确实能够直接监听到 number.js 的响应,但是这种响应并不完美,它会在页面的结尾处再次输出 number 的结果,也就是每修改一次都会叠加一次输出结果。这不是我们想要的功能,我们只好再来修改一下:

import counter from './counter'
import number from './number'
counter()
number()
if (module.hot) { // 如果当前代码开启了 hmr 功能
  module.hot.accept('./number', () => {
    // 回调每次执行前删除原先的节点元素
    document.body.removeChild(document.getElementById('number'))
    number()
  })
}

好吧,感觉虽然完成了功能,但是有没有觉得 js 实现 HMR 感觉好繁琐的样子,为什么他不能像 css 一样自己完成监听和响应文件的变化呢?为什么我们平时写 vue 都是自动监听文件的响应和变化,而不需要手动实现这种繁琐的过程呢?

其实所有 HMR 功能的实现都是依据这个原理来做的,但是 vue-loadercss-loader 已经帮我们将这些功能封装起来了,所以我们可以直接使用,但是它们底层的实现原理基本也离不开这种模式。

此处延伸扩展学习文档:模块热替换 | API模块热替换 | 指南模块热替换 | 概念

Babel


Babel 是什么,相信大家都有基本的概念,我看先看官方给出的定义:Babel 是一个工具链,主要用于将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。

我们清空前面写的代码,在 index.js 中写入一段 ES6 语法的代码:

const arr = [
  new Promise(() => { }),
  new Promise(() => { })
]

arr.map(item => {
  console.log(item)
})

此时打包我们就不使用 npm run start,因为通过 devServer 为了保证更快的打包效率会将代码直接打包到内存中,我们看不到实际打包过后的代码,所以我们还是使用老方法 npm run build,通过生成的 dist 文件夹中 main.js 文件我们可以清楚的看到 ES6 语法有没有被转译。

上述代码如果在谷歌浏览器中直接运行,一般是不会出现啥问题,因为谷歌对于开发者来说是兼容性最好的浏览器,但是如果直接运行在 IE 11 以下的浏览器中,就会不认识 Promisemap(item => {}) 这些语法,此时我们就需要借助到 Babel

因为我们使用 Babel 的场景是 webpack,所以大家可以参考这里的官方文档:Webpack 中使用 Babel 步骤

首先是安装:

npm install --save-dev babel-loader @babel/core

看到 loader 我们就应该知道,babel-loader 肯定是帮 webpack 用来打包的一个工具。babel/core 是什么呢?它是 babel 的一个核心库,它能够让 babel 去识别 js 代码的内容,然后把 js 代码转化成抽象语法树,然后再把抽象语法树转化成一些新的语法。

然后在 module => rules 中进行相关配置:

module: {
  rules: [
    { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" }
  ]
}

如果检测到你的文件是 js 文件,就使用 babel-loader 去分析一下 js 文件中的语法到底是什么情况。exclude 表示 node_modules 中的 js 文件不需要使用 babel-loader 检测。

接下来文档提示我们继续安装 @babel/preset-env

npm install @babel/preset-env --save-dev

这又是个什么东西呢?当我们使用 babel-loader 处理文件的时候,实际上 babel-loader 只是 babelwebpack 做通信的一个桥梁,但是实际上 babel-loader 并不会帮你把 js 中的 ES6 语法翻译成 ES5 语法,我们还需要借助一些其它的模块,而 babel/preset-env 就是这样一个模块,这个文件包含了所有 ES6 转换 ES5 的一些规则。

babel-loader 下进行相关配置

rules: [{
  test: /\.js$/,
  exclude: /node_modules/,
  loader: "babel-loader",
  options: {
    presets: ["@babel/preset-env"]
  }
}]

然后我们再次打包代码,这是我们打开 main.js 就会发现所有的 const 都变成了 var,箭头函数也都转化成了 function(){} 。但是 Promisemap 并没有进行转换,它们俩如果想在低版本浏览器中运行也是不行的,所以这个时候我们不仅要使用 babel/preset-env 做语法转换,还需要把它缺失的变量或者函数补充到低版本浏览器里。怎么补充呢?我们需要借助到 babel-polyfill 帮我们做这些变量或者函数在低版本浏览器中的补充。

我们首先安装 babel-polyfill

npm install --save @babel/polyfill

接下来我们只需要在所有代码引入之前引入 @babel/polyfill 即可,此时我们可以在 src 目录下的 index.js 最顶部引入如下代码:

import "@babel/polyfill"

此时我们可以再次进行打包,虽然完美解决了问题,但是又曝光出了一些新的问题。前面我们打包 main.js 的大小只有 30kb,但是引入 @babel/polyfill 之后 main.js 的大小接近 1000kb,我们明明只写了几行代码,确要引入接近 1M 的脚本文件。

这里我们可以做一些优化补充,如果我们这里只需要它帮我们实现 mappromise 的语法补充,不需要补充其他差异化语法,那我们又可以做如下优化:

rules: [{
  test: /\.js$/,
  exclude: /node_modules/,
  loader: "babel-loader",
  options: {
    presets: [['@babel/preset-env', {
      useBuiltIns: 'usage'
    }]]
  }
}]

useBuiltIns: 'usage' 这个配置代表什么意思了?它是告诉 @babel/polyfill 去往低版本浏览器增加特性的时候不是把所有特性内容都加进来,而是根据你业务代码里面需要进行补充声明的语法特性来进行添加。此时我们再次打包发现 main.js 的大小只有 200kb 左右,最终完美实现所有效果。

当然上述代码如果在我们写入库或者 UI 组件的时候引用,使用 @babel/polyfill 会污染全局环境,所以此时我们不能使用上面的方法,此时我们应该怎么办呢?查看官方文档 transform-runtime

首先安装:

npm install --save-dev @babel/plugin-transform-runtime

接着安装:

 npm install --save @babel/runtime

此时我们可以根据官方文档来做一些 options 的配置,但是如果在 webpack.config.js 中写相关配置项,会将这个文件下的代码拉扯的超级长,所以其实我们可以新建一个 .babelrc 文件用来管理 babel-loader 中配置的 options

首先我们删除 webpack.config.js 中关于 module => rulesbabel-loader 配置的 options

rules: [{
  test: /\.js$/,
  exclude: /node_modules/,
  loader: 'babel-loader',
  // options: { 整体删除
    // presets: [['@babel/preset-env', {
    //   useBuiltIns: 'usage'
    // }]]
  // }
},

然后根目录下新建一个 .babelrc 文件,配置如下代码

{
  "plugins": [
    [
      "@babel/plugin-transform-runtime",
      {
        "absoluteRuntime": false,
        "corejs": 2,
        "helpers": true,
        "regenerator": true,
        "useESModules": false
      }
    ]
  ]
}

当然此时需要我们额外安装 runtime-corejs2

npm install --save @babel/runtime-corejs2

此时在运行 npm run build 其实跟之前没有啥区别,只是我们在构建 UI 组件库,或者造轮子的时候,可以使用 transform-runtime 这种方法。同时如果我们不想在 babel-loader 下的 options 对象中填写过多的配置代码,可以直接在项目根目录下新建 .babelrc 文件用来存放 options 里面的内容。

不知不觉又写的有点多了,结尾还是那句老话:此整理仅供记录学习,如果文中有不对的地方或者理解有误的地方欢迎大家提出并指正。每一天都要相对前一天进步一点,加油!!!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351