uni-app源码分析

通过脚手架创建uni-app并编译

vue create -p dcloudio/uni-preset-vue test-uni-app

使用脚手架创建的项目可以更清晰的看到它的架构,也可以直接阅读打包编译的源码。

我们可以看看uni-app的模板代码,App.vue并没有<template>代码,那它是怎么把元素挂载上去的呢?其实可以在它编译的过程中找到答案。我们后面遇到的问题,也都是在这个过程找到解决方案的。

创建出来的项目,package.json自带了一些编译脚本,来看其中一条:

cross-env NODE_ENV=development UNI_PLATFORM=h5 vue-cli-service uni-serve

cross-env 是一个用来设置环境变量的库,一般来说可以这样设置环境变量:

NODE_ENV_TEST=development node env-test.js

但windows不支持NODE_ENV=development的设置方式,使用cross-env就不必考虑平台差异。

再继续看这行命令,一般我们用vue的脚手架来编译,直接执行

vue-cli-service serve

这里的vue-cli-service就是一个自定义的node命令(自定义node命令)。

serve是vue-cli-service自带的插件,而uni-serve是uni-app自定义的一个插件。

这个插件是怎么被vue-cli-service识别出来并调用的呢?

App.vue

先看看vue-cli-service命令的执行

可以在node_modules的.bin文件夹下找到vue-cli-service命令,也是执行这段命令会运行的源码

const Service = require('../lib/Service')
const service = new Service(process.env.VUE_CLI_CONTEXT || process.cwd())

可以看到每次运行vue-cli-service都会创建一个service, 我们来看看service的构造函数,源码在node_modules/@vue/cli-service/lib/Service.js

可以看到一个很关键的代码:

    this.plugins = this.resolvePlugins(plugins, useBuiltIn)

这里就是解析我们在项目里引用的插件的。

resolvePlugins的源码:

      const projectPlugins = Object.keys(this.pkg.devDependencies || {})
        .concat(Object.keys(this.pkg.dependencies || {}))
        .filter(isPlugin)
        .map(id => {
          if (
            this.pkg.optionalDependencies &&
            id in this.pkg.optionalDependencies
          ) {
            let apply = () => {}
            try {
              apply = require(id)
            } catch (e) {
              warn(`Optional dependency ${id} is not installed.`)
            }

            return { id, apply }
          } else {
            return idToPlugin(id)
          }
        })
      plugins = builtInPlugins.concat(projectPlugins)

这里会读取所有的devDependenciesdependencies,取出其中的插件。isPlugin用来筛选插件。isPlugin的源码在node_modules/@vue/cli-shared-utils/lib/pluginResolution.js

const pluginRE = /^(@vue\/|vue-|@[\w-]+(\.)?[\w-]+\/vue-)cli-plugin-/

exports.isPlugin = id => pluginRE.test(id)

可以看到只要符合特定格式的就会被识别为插件。

再看idToPlugin的源码

    const idToPlugin = id => ({
      id: id.replace(/^.\//, 'built-in:'),
      apply: require(id)
    })

这里主要是把插件封装起来待后面调用。

再回到vue-cli-service命令的源码,可以看到最底下调用了service的run方法

service.run(command, args, rawArgv).catch(err => {
  error(err)
  process.exit(1)
})

在看看run的源码:

  async run (name, args = {}, rawArgv = []) {
    // resolve mode
    // prioritize inline --mode
    // fallback to resolved default modes from plugins or development if --watch is defined
    const mode = args.mode || (name === 'build' && args.watch ? 'development' : this.modes[name])

    // --skip-plugins arg may have plugins that should be skipped during init()
    this.setPluginsToSkip(args)

    // load env variables, load user config, apply plugins
    this.init(mode)
    ...

this.init(mode)是关键,从注释里面也可以看到,这里会运行插件。

init的源码:

    // apply plugins.
    this.plugins.forEach(({ id, apply }) => {
      if (this.pluginsToSkip.has(id)) return
      apply(new PluginAPI(id, this), this.projectOptions)
    })

这块代码会是插件正在执行的地方。这里利用解构直接取出插件的id和apply。

可以看看我们在项目里引用的uni-app插件,src/pages.json

    "@dcloudio/vue-cli-plugin-hbuilderx": "^2.0.0-26920200409002",
    "@dcloudio/vue-cli-plugin-uni": "^2.0.0-26920200409002",
    "@dcloudio/vue-cli-plugin-uni-optimize": "^2.0.0-26920200409002",

vue-cli-plugin-uni-optimize

这个插件定义了一些别名,在分析源码的时候我们需要用到:

  api.configureWebpack(webpackConfig => {
    return {
      watch: true,
      resolve: {
        alias: {
          ['uni-' + process.env.UNI_PLATFORM]: path.join(lib, `${process.env.UNI_PLATFORM}/main.js`),
          'uni-core': path.join(src, 'core'),
          'uni-view': path.join(src, 'core/view'),
          'uni-service': path.join(src, 'core/service'),
          'uni-shared': path.join(src, 'shared'),
          'uni-mixins': path.join(src, 'core/view/mixins'),
          'uni-helpers': path.join(src, 'core/helpers'),
          'uni-platform': path.join(src, 'platforms/' + process.env.UNI_PLATFORM),

          // tree shaking
          'uni-components': uniComponentsPath,
          'uni-invoke-api': uniInvokeApiPath,
          'uni-service-api': uniServiceApiPath,
          'uni-api-protocol': uniApiProtocolPath,
          'uni-api-subscribe': uniApiSubscribePath,
          // h5 components
          'uni-h5-app-components': uniH5AppComponentsPath,
          'uni-h5-app-mixins': uniH5AppMixinsPath,
          'uni-h5-system-routes': uniH5SystemRoutes
        }
      },

vue-cli-plugin-uni

来看看vue-cli-plugin-uni的源码,在/node_modules/@dcloudio/vue-cli-plugin-uni/index.js

前面插件的运行代码是:apply(new PluginAPI(id, this), this.projectOptions), apply就是apply: require(id)

再看看vue-cli-plugin-uni的源码

module.exports = (api, options) => {
  initServeCommand(api, options)

  initBuildCommand(api, options)
...

这样就和apply的调用对应起来了。api就是new PluginAPI(id, this)

先看下面的代码:

const type = ['app-plus', 'h5'].includes(process.env.UNI_PLATFORM)
    ? process.env.UNI_PLATFORM
    : 'mp'

  const platformOptions = require('./lib/' + type)

  let vueConfig = platformOptions.vueConfig

  if (typeof vueConfig === 'function') {
    vueConfig = vueConfig(options, api)
  }

  Object.assign(options, { // TODO 考虑非 HBuilderX 运行时,可以支持自定义输出目录
    outputDir: process.env.UNI_OUTPUT_TMP_DIR || process.env.UNI_OUTPUT_DIR,
    assetsDir
  }, vueConfig) // 注意,此处目前是覆盖关系,后续考虑改为webpack merge逻辑

  require('./lib/options')(options)

  api.configureWebpack(require('./lib/configure-webpack')(platformOptions, manifestPlatformOptions, options, api))
  api.chainWebpack(require('./lib/chain-webpack')(platformOptions, options, api))

这里先获取当前的编译类型,我们的是h5,取到的平台配置就是在./lib/h5中。源码在node_modules/@dcloudio/vue-cli-plugin-uni/lib/h5/index.js。可以看到这里导出了vueConfigwebpackConfigchainWebpack。然后再通过api.configureWebpackapi.chainWebpack运用到webpack中。(chainWebpack与configureWebpackvue-cli中chainWebpack的使用)

chainWebpack与configureWebpack用来修改webpack的配置 chainWebpack的粒度更细

h5配置的webpackConfig实际上定义了一个规则,在加载App.vue文件时插入了<template>代码块。

{
          test: /App\.vue$/,
          use: {
            loader: path.resolve(__dirname, '../../packages/wrap-loader'),
            options: {
              before: ['<template><App :keepAliveInclude="keepAliveInclude"/></template>']
            }
          }
        }

wrap-loader的用途:

Add custom content before and after the loaded source.

到这里我们就知道App.vue实际是挂载了一个App的自定义组件。那这个组件是什么时候注册到Vue当中的呢? 同样是这块代码:

const statCode = process.env.UNI_USING_STAT ? 'import \'@dcloudio/uni-stat\';' : ''
...
const beforeCode = (useBuiltIns === 'entry' ? 'import \'@babel/polyfill\';' : '') +
      `import 'uni-pages';import 'uni-${process.env.UNI_PLATFORM}';`
...
{
          test: path.resolve(process.env.UNI_INPUT_DIR, getMainEntry()),
          use: [{
            loader: path.resolve(__dirname, '../../packages/wrap-loader'),
            options: {
              before: [
                beforeCode + statCode + getGlobalUsingComponentsCode()
              ]
            }
          }]
        }

getMainEntry:

function getMainEntry () {
  if (!mainEntry) {
    mainEntry = fs.existsSync(path.resolve(process.env.UNI_INPUT_DIR, 'main.ts')) ? 'main.ts' : 'main.js'
  }
  return mainEntry
}

这里主要就是给main.js插入引用。我们这里是h5,所以这里引用了uni-h5。(node_modules文件夹查找规则)

引用uni-h5的源码路径在:node_modules/@dcloudio/uni-h5/dist/index.umd.min.js。这里面是经过编译压缩的。lib里面有源代码。
node_modules/@dcloudio/uni-h5/src/platforms/h5/components/index.js中就是注册App组件的源码:

Vue.component(App.name, App)
Vue.component(Page.name, Page)
Vue.component(AsyncError.name, AsyncError)
Vue.component(AsyncLoading.name, AsyncLoading)

到这里App组件的来源就清楚了。

App.vue的源码:node_modules/@dcloudio/uni-h5/src/platforms/h5/components/app/index.vue

Pages.json

webpack-uni-pages-loader

上面的流程中我们可以发现在调用api.configureWebpack之前还调用了另一个方法,把平台特有的webpack配置和公共的配置合并起来再返回,源码在node_modules/@dcloudio/vue-cli-plugin-uni/lib/configure-webpack.js

这里面的公共配置有个很重要的东西,涉及到怎么解析page.json的:

{
      test: path.resolve(process.env.UNI_INPUT_DIR, 'pages.json'),
      use: [{
        loader: 'babel-loader'
      }, {
        loader: '@dcloudio/webpack-uni-pages-loader'
      }],
      type: 'javascript/auto'
    }

Rule.type

webpack-uni-pages-loader的源码node_modules/@dcloudio/webpack-uni-pages-loader/lib/index.js

  if (
    process.env.UNI_USING_COMPONENTS ||
    process.env.UNI_PLATFORM === 'h5' ||
    process.env.UNI_PLATFORM === 'quickapp'
  ) {
    return require('./index-new').call(this, content)
  }

node_modules/@dcloudio/webpack-uni-pages-loader/lib/index-new.js

  if (process.env.UNI_PLATFORM === 'h5') {
    return require('./platforms/h5')(pagesJson, manifestJson)
  }

根据平台类型判断,最终来到node_modules/@dcloudio/webpack-uni-pages-loader/lib/platforms/h5.js,经过这层处理,pages.json变成了以下代码:

import Vue from 'vue'
global['________'] = true;
delete global['________'];
global.__uniConfig = {"globalStyle":{"navigationBarTextStyle":"black","navigationBarTitleText":"uni-app","navigationBarBackgroundColor":"#F8F8F8","backgroundColor":"#F8F8F8"}};
global.__uniConfig.router = {"mode":"hash","base":"/"};
global.__uniConfig['async'] = {"loading":"AsyncLoading","error":"AsyncError","delay":200,"timeout":60000};
global.__uniConfig.debug = false;
global.__uniConfig.networkTimeout = {"request":60000,"connectSocket":60000,"uploadFile":60000,"downloadFile":60000};
global.__uniConfig.sdkConfigs = {};
global.__uniConfig.qqMapKey = "XVXBZ-NDMC4-JOGUS-XGIEE-QVHDZ-AMFV2";
global.__uniConfig.nvue = {"flex-direction":"column"}

// 注册Page
Vue.component('pages-index-index', resolve=>{
const component = {
  component:require.ensure([], () => resolve(require('/Users/chenzhendong/Documents/WorkSpace/H5/test-uni-app/src/pages/index/index.vue')), 'pages-index-index'),
  delay:__uniConfig['async'].delay,
  timeout: __uniConfig['async'].timeout
}
if(__uniConfig['async']['loading']){
  component.loading={
    name:'SystemAsyncLoading',
    render(createElement){
      return createElement(__uniConfig['async']['loading'])
    }
  }
}
if(__uniConfig['async']['error']){
  component.error={
    name:'SystemAsyncError',
    render(createElement){
      return createElement(__uniConfig['async']['error'])
    }
  }
}
return component
})

// 定义路由
global.__uniRoutes=[
{
path: '/',
alias:'/pages/index/index',
component: {
  render (createElement) {
    return createElement(
      // 创建Page组件
      'Page',
      {
        props: Object.assign({
          isQuit:true,

          isEntry:true,

          
          
        },__uniConfig.globalStyle,{"navigationBarTitleText":"uni-app"})
      },
      [
        // 创建我们的页面,作为子组件插入到Page的slot中
        createElement('pages-index-index', {
          slot: 'page'
        })
      ]
    )
  }
},
meta:{
id:1,
  name:'pages-index-index',
  isNVue:false,
  pagePath:'pages/index/index',
isQuit:true,
isEntry:true,
  windowTop:44
}
},
{
path: '/preview-image',
component: {
  render (createElement) {
    return createElement(
      'Page',
      {
        props:{
          navigationStyle:'custom'
        }
      },
      [
        createElement('system-preview-image', {
          slot: 'page'
        })
      ]
    )
  }
},
meta:{
  name:'preview-image',
  pagePath:'/preview-image'
}
}
    ,
{
path: '/choose-location',
component: {
  render (createElement) {
    return createElement(
      'Page',
      {
        props:{
          navigationStyle:'custom'
        }
      },
      [
        createElement('system-choose-location', {
          slot: 'page'
        })
      ]
    )
  }
},
meta:{
  name:'choose-location',
  pagePath:'/choose-location'
}
}
    ,
{
path: '/open-location',
component: {
  render (createElement) {
    return createElement(
      'Page',
      {
        props:{
          navigationStyle:'custom'
        }
      },
      [
        createElement('system-open-location', {
          slot: 'page'
        })
      ]
    )
  }
},
meta:{
  name:'open-location',
  pagePath:'/open-location'
}
}
    ]

这里做的工作:
1、注册我们的页面;
2、定义路由,实际上是创建Page组件然后再把我们的控件插到Page的slot中;

修改路径

生成路由的时候,path是pageComponents的route,通过getPageComponents方法生成。这个值的路径如下:
page.json => pageJson => pageJson.pages => page.path => route

所有我们只需要修改getPageComponents的返回值即可

    return {
      name,
      route: page.routePath || page.path,
      path: pagePath,
      props,
      isNVue,
      isEntry,
      isTabBar,
      tabBarIndex,
      isQuit: isEntry || isTabBar,
      windowTop
    }

直接修改源码在重新npm install后会丢失修改,所以我们用到了一个工具:patch-package,它可以把我们的修改记录下来,在下一次npm install时再还原我们的修改。

page.json的引入node_modules/@dcloudio/vue-cli-plugin-uni/lib/configure-webpack.js:

    return merge({
      resolve: {
        alias: {
          '@': path.resolve(process.env.UNI_INPUT_DIR),
          './@': path.resolve(process.env.UNI_INPUT_DIR), // css中的'@/static/logo.png'会被转换成'./@/static/logo.png'加载
          'vue$': getPlatformVue(vueOptions),
          'uni-pages': path.resolve(process.env.UNI_INPUT_DIR, 'pages.json'),
          '@dcloudio/uni-stat': require.resolve('@dcloudio/uni-stat'),
          'uni-stat-config': path.resolve(process.env.UNI_INPUT_DIR, 'pages.json') +
            '?' +
            JSON.stringify({
              type: 'stat'
            })
        }

node_modules/@dcloudio/vue-cli-plugin-uni/lib/h5/index.js

    const beforeCode = (useBuiltIns === 'entry' ? `import '@babel/polyfill';` : '') +
      `import 'uni-pages';import 'uni-${process.env.UNI_PLATFORM}';`

路由

路由的基本使用:

import Vue from 'vue'
import VueRouter from 'vue-router'
import App from './App'

Vue.use(VueRouter)

// 1. 定义(路由)组件。
// 可以从其他文件 import 进来
const Foo = { template: '<div>foo</div>' }

// 2. 定义路由
// 每个路由应该映射一个组件。 其中"component" 可以是
// 通过 Vue.extend() 创建的组件构造器,
// 或者,只是一个组件配置对象。
const routes = [
  { path: '/foo', component: Foo }
]

// 3. 创建 router 实例,然后传 `routes` 配置
const router = new VueRouter({
  routes // (缩写)相当于 routes: routes
})

// 4. 创建和挂载根实例。
// 记得要通过 router 配置参数注入路由,
// 从而让整个应用都有路由功能
const app = new Vue({
  el: '#app',
  render(h) {
    return h(App)
  },
  router
})

Vue的构造函数最终来到如下代码src/core/instance/index.js

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

initMixin(Vue)
...

_init函数是在initMixin(Vue)里面定义的。传进来的options最终会保存在Vue实例的$options中。因此在根组件中可以通过this.$options.router拿到路由实例。那子组件是怎么拿到这个router的呢?

Vue.use(VueRouter)会来到VueRouter定义的install函数src/install.js

  Vue.mixin({
    beforeCreate () {
      if (isDef(this.$options.router)) {
        this._routerRoot = this
        this._router = this.$options.router
        this._router.init(this)
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
      }
      registerInstance(this, this)
    },
    destroyed () {
      registerInstance(this)
    }
  })

这里利用 Vue.mixin 去把 beforeCreate 和 destroyed 钩子函数注入到每一个组件中。根组件上的this.$options.router就是我们创建Vue实例的时候传进来的router,然后设置_routerRoot,初始化router。此外还定义了一个被监听的_route变量。

初始化函数src/index.js

  init (app: any /* Vue component instance */) {
    process.env.NODE_ENV !== 'production' && assert(
      install.installed,
      `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
      `before creating root instance.`
    )

    this.apps.push(app)

    // set up app destroyed handler
    // https://github.com/vuejs/vue-router/issues/2639
    app.$once('hook:destroyed', () => {
      // clean out app from this.apps array once destroyed
      const index = this.apps.indexOf(app)
      if (index > -1) this.apps.splice(index, 1)
      // ensure we still have a main app or null if no apps
      // we do not release the router so it can be reused
      if (this.app === app) this.app = this.apps[0] || null
    })

    // main app previously initialized
    // return as we don't need to set up new history listener
    if (this.app) {
      return
    }

    this.app = app

    const history = this.history

    if (history instanceof HTML5History) {
      history.transitionTo(history.getCurrentLocation())
    } else if (history instanceof HashHistory) {
      const setupHashListener = () => {
        history.setupListeners()
      }
      history.transitionTo(
        history.getCurrentLocation(),
        setupHashListener,
        setupHashListener
      )
    }

    history.listen(route => {
      this.apps.forEach((app) => {
        app._route = route
      })
    })
  }

history.listen其实只是把这个函数保存起来,当history监听到路径变化时就会调用这个函数,把最新的路径返回。这个app会更新自己的_route,就会引起router-view更新。

再看router的install函数:

  Object.defineProperty(Vue.prototype, '$router', {
    get () { return this._routerRoot._router }
  })

  Object.defineProperty(Vue.prototype, '$route', {
    get () { return this._routerRoot._route }
  })

给Vue的原型定义了$router$route,它们都是从_routerRoot拿到的,从前面可以知道在钩子函数beforeCreate里面已经给每个组件都设置了_routerRoot。组件调用route时其实是拿根组件的route。而router-view的render函数中调用了`route`来生成虚拟节点,我们可以把它类比成computed函数,当app._route变了就自然引起router-view重新计算。

uni-app是怎么注入router的呢?我们并没有在uni-app调用Vue.use(VueRouter),也没有创建router在创建Vue的时候传进去,这些uni-app帮我们实现了。

node_modules/@dcloudio/uni-h5/lib/h5/main.js

Vue.use(require('uni-service/plugins').default, {
  routes: __uniRoutes
})

Vue.use(require('uni-view/plugins').default, {
  routes: __uniRoutes
})

node_modules/@dcloudio/uni-h5/src/core/service/plugins/index.js

    Vue.mixin({
      beforeCreate () {
        const options = this.$options
        if (options.mpType === 'app') {
          options.data = function () {
            return {
              keepAliveInclude
            }
          }
          const appMixin = createAppMixin(routes, entryRoute)
          // mixin app hooks
          Object.keys(appMixin).forEach(hook => {
            options[hook] = options[hook] ? [].concat(appMixin[hook], options[hook]) : [
              appMixin[hook]
            ]
          })

          // router
          options.router = router

          // onError
          if (!Array.isArray(options.onError) || options.onError.length === 0) {
            options.onError = [function (err) {
              console.error(err)
            }]
          }
        }
        ...

uni-serve

再回到自定义命令的流程,可以看到 vue-cli-plugin-uni首先初始化了两个命令 build 和 serve。插件开发指南

先看看initServeCommand 的源码:

api.registerCommand('uni-serve', {
...

从前面的调用可以知道api就是 PluginAPI PluginAPI的registerCommand:

  /**
   * Register a command that will become available as `vue-cli-service [name]`.
   *
   * @param {string} name
   * @param {object} [opts]
   *   {
   *     description: string,
   *     usage: string,
   *     options: { [string]: string }
   *   }
   * @param {function} fn
   *   (args: { [string]: string }, rawArgs: string[]) => ?Promise
   */
  registerCommand (name, opts, fn) {
    if (typeof opts === 'function') {
      fn = opts
      opts = null
    }
    this.service.commands[name] = { fn, opts: opts || {}}
  }

到这里把命令注册完成了,保存在service的commands里面。

再回到service的run方法

    args._ = args._ || []
    let command = this.commands[name]
    if (!command && name) {
      error(`command "${name}" does not exist.`)
      process.exit(1)
    }
    if (!command || args.help || args.h) {
      command = this.commands.help
    } else {
      args._.shift() // remove command itself
      rawArgv.shift()
    }
    const { fn } = command
    return fn(args, rawArgv)

这里取出命令的执行函数来执行
也就是/node_modules/@dcloudio/vue-cli-plugin-uni/commands/serve.js中注册uni-serve命令时传进去的函数。

至此打包流程结束。

css

node_modules/@dcloudio/vue-cli-plugin-uni/index.js

  require('./lib/options')(options)

uni.scss

node_modules/@dcloudio/vue-cli-plugin-uni/lib/options.js

[Vue CLI 3] 插件开发之 registerCommand 到底做了什么
vue-cli 3学习之vue-cli-service插件开发(注册自定义命令)
vue-router工作原理概述和问题分析
Vue.js 技术揭秘

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