使用小程序·云开发构建多媒体小程序

小程序·云开发

什么是小程序的云开发?一句话就是能够使开发者省去搭建服务器、申请域名的成本,从开发到运维提供整套解决方案的小程序开发方式。
官方定义是,小程序·云开发为开发者提供完整的云端支持,弱化后端和运维概念,无需搭建服务器,使用平台提供的 API 进行核心业务开发,即可实现快速上线和迭代。

相对于传统小程序开发,云开发新提供了三大基础支持:

  • 云函数:在云端运行的代码,微信私有协议天然鉴权,开发者只需编写自身业务逻辑代码
  • 数据库:一个既可在小程序前端操作,也能在云函数中读写的 JSON 数据库
  • 存储:在小程序前端直接上传/下载云端文件,在云开发控制台可视化管理

由此开发者只需在小程序端调用 API 即可搭建简单的后端服务,无需考虑搭建服务器、申请合法域名带来的成本。云开发环境免费版对资源有一定限制,但在用户量不大的情况下可以提供稳定的服务。相对于普通后端服务,云开发环境还集成了控制台,提供了运维的有效手段。

云开发·多媒体服务

初始化小程序·云开发

注册小程序

登录微信公众平台,使用邮箱注册并激活小程序。进入小程序管理后台首页填写小程序信息,并添加开发者,详情见官方文档

创建小程序项目

  • 进入微信web开发者工具下载页,下载开发工具并安装。
  • 进入小程序管理后台-设置-开发设置,获取小程序的AppID,新建小程序项目,选择 建立云开发快速启动模板,详情见小程序·云开发文档。

建立云开发环境

  • 在开发者工具工具栏左侧,点击 云开发 开通云开发功能。每个小程序免费提供两个环境,首先创建名为 test 的云环境。复制自动生成的环境ID,编辑 miniprogram 文件夹下的 app.js,添加 env 参数。
  wx.cloud.init({
    traceUser: true,
    env: 'test-xxx'
  })
  • 选择 cloudfunctions 文件夹,设置当前环境为 test
  • 编译小程序,如控制台无报错,则环境连接正常。

云开发环境

云开发提供了一整套云服务及简单、易用的 API 和管理界面,以尽可能降低后端开发成本,让开发者能够专注于核心业务逻辑的开发、尽可能轻松的完成后端的操作和管理。这套云环境包括数据库、存储和云函数。

云开发控制台

云开发控制台提供了云开发的管理界面和运维工具,开发者可以通过操作控制台来查看环境使用情况、操作数据库、存储、管理云函数等。

云开发控制台
云环境统计分析

数据库

云开发环境提供一个文档数据库,其
API 和功能类似于 MongoDB。进入控制台的数据库选项,新建一个集合。集合中每条数据都是 JSON格式的。

数据库管理
权限控制

小程序端操作数据库时,读写数据受权限控制限制。写入的记录会默认增加写入用户的 _openid,如果需要所有用户对数据有读权限,需要更改权限为 所有用户可读,仅创建者及管理员可写

数据库权限控制
对数据库API进行封装
// 云db对象
const db = wx.cloud.database()

module.exports = {

  /**
   * 新增记录
   *
   * @param data 数据
   * @param collection 集合
   * @return {"_id": String, "errMsg": String}
   */
  add: function(data, collection) {
    return db.collection(collection).add({
      data: data
    })
  },

  /**
   * 查询记录
   * 
   * @param collection 集合
   * @param where 查询条件
   * @param skip 查询起始位置
   * @param limit 查询数量
   * @return {"data": Array, "errMsg": String}
   */
  query: function(collection, where, skip, limit) {
    where = where || {}
    skip = skip || 0
    limit = limit || 10
    return db.collection(collection)
      .where(where).orderBy('time', 'desc')
      .skip(skip).limit(limit).get()
  },

  /**
   * 查询记录数量
   *
   * @param collection 集合
   * @param where 查询条件
   * @return {"total": Number, "errMsg": String}
   */
  count: function(collection, where) {
    where = where || {}
    return db.collection(collection).where(where).count()
  },

  /**
   * 新增/全部更新文档
   *
   * @param collection 集合
   * @param doc 文档_id
   * @param data 数据
   * @return {"_id": String, "errMsg": String}
   */
  addDoc: function(collection, doc, data) {
    collection = collection || lovc
    return db.collection(collection).doc(doc).set({
      data: data
    })
  },

  /**
   * 查询文档
   *
   * @param collection 集合
   * @param doc 文档_id
   * @return {"data": Object, "errMsg": String}
   */
  getDoc: function(collection, doc) {
    collection = collection || lovc
    return db.collection(collection).doc(doc).get()
  },

  /**
   * 部分更新文档
   * 
   * @param collection 集合
   * @param doc 文档_id   
   * @param data 数据
   * @return {"stats": Object, "errMsg": String}
   */
  update: function(collection, doc, data) {
    collection = collection || lovc
    return db.collection(collection).doc(doc).update({
      data: data
    })
  },

  /**
   * 删除文档
   *
   * @param collection 集合
   * @param doc 文档_id
   * @return {"stats": Object, "errMsg": String}
   */
  remove: function(collection, doc) {
    collection = collection || lovc
    return db.collection(collection).doc(doc).remove()
  }
}

存储

云环境提供了免费的 5G存储空间,文件上传后每个文件会生成一个 fileID 和一个 https 下载地址,在小程序中,srcposter 等属性中都可以直接使用。

存储管理

对存储API进行封装
module.exports = {

  /**
   * 上传文件
   * 
   * @param fileName 文件名
   * @param filePath 文件路径
   * @return {"errMsg": String, "fileID": String, "statusCode": Number}
   */
  upload: function(fileName, filePath) {
    return wx.cloud.uploadFile({
      cloudPath: fileName,
      filePath: filePath
    })
  },

  /**
   * 下载文件
   * 
   * @param fileID 文件名
   * @return {"tempFilePath": String, "statusCode": Number}
   */
  download: function(fileID) {
    return wx.cloud.downloadFile({
      fileID: fileID
    })
  }
}

云函数

云函数即在云端(服务器端)运行的函数。通过使用云函数,开发者无需购买、搭建服务器,只需编写函数代码并部署到云端即可在小程序端调用,同时云函数之间也可互相调用。云函数本身是一个本地定义的 JS 方法,可以上传并部署到指定云环境,并运行在云端的 nodejs 中。云函数调用时用户的 openid 会作为请求参数进行调用,从而能够与微信鉴权无缝结合。

创建云函数
本地环境

云函数依赖本地 nodejs 环境,可以通过 npm 对服务进行扩展。

REST 调用

传统小程序开发的后端服务需要合法域名,而云函数通过 http 调用服务是无需鉴别域名的,因此可以通过小程序端调用云函数,云端调用远程服务的方式拓展低成本的 REST服务

tcb-router

tcb-router是由小程序官方提供的基于 koa 风格的云函数轻量级路由库,主要用于优化服务端函数处理逻辑。云函数允许创建的数量是有限的,通过 tcb-router 可以对服务数量进行拓展。

新建云函数
  • 选择小程序中的 cloudfunctions 文件夹,右键选择 新建Node.js云函数,本地会自动生成云函数模板文件和 package.json,并安装 wx-server-sdk 依赖。
创建云函数
  • 选择云函数所在文件夹,进入终端,通过 npm 安装所需依赖。

  • 编辑云函数(REST 服务)

// 云函数入口文件
const cloud = require('wx-server-sdk')
const rp = require('request-promise')

cloud.init()

// 云函数入口函数
exports.main = async(event, context) => {
  if (event.method === 'GET') {
    return get(event.url, event.data)
  } else if (event.method === 'POST') {
    return post(event.url, event.data)
  }
}

/**
 * GET请求
 * 
 * @param url REST服务地址
 * @param data 请求参数
 */
function get(url, data) {
  if (undefined !== data && '' !== data) {
    let queryString = '?'
    for (let key in data) {
      if (!queryString.endsWith('?')) {
        queryString = queryString.concat('&')
      }
      queryString = queryString.concat(key).concat('=').concat(data[key])
    }
    url = url.concat(queryString)
  }
  return rp(url)
}

/**
 * POST请求
 * 
 * @param url REST服务地址
 * @param data 请求参数
 */
function post(url, data) {
  return rp({
    uri: url,
    method: 'POST',
    body: data,
    json: true
  })
}
  • 编辑云函数(tcb-router
// 云函数入口文件
const cloud = require('wx-server-sdk')
const TcbRouter = require('tcb-router')

cloud.init()

// 云函数入口函数
exports.main = async(event, context) => {
  const app = new TcbRouter({
    event
  });

  // app.use 表示该中间件会适用于所有的路由
  app.use(async(ctx, next) => {
    // 创建返回data对象
    ctx.data = {}
    // 执行下一中间件
    await next()
  })

  // 路由为数组表示,该中间件适用于多个路由
  // app.router(['x', 'y'], async (ctx, next) => {
  //   ctx.data.from = 'cloud';
  //   await next();
  // });

  app.router('router', async(ctx, next) => {
    ctx.data.openId = event.userInfo.openId
    await next();
  }, async(ctx) => {
    ctx.data.other = event.other;
    // ctx.body 返回数据到小程序端
    ctx.body = {
      code: 0,
      data: ctx.data
    }
  })

  return app.serve()
}
  • 选择云函数所在文件夹,右键选择 上传并部署 云函数,在小程序端可以通过云函数调用访问服务。

  • 对云函数API进行封装

module.exports = {

  /**
   * 通过云函数访问服务
   * 
   * @param name 云函数名
   * @param params 参数
   * @return {"errMsg": String, "result": Object, "requestID": String}
   */
  call: function(name, params) {
    return wx.cloud.callFunction({
      name: name,
      data: params
    })
  },

  /**
   * 通过tcb-router访问服务
   *
   * @param url router路径
   * @param params 参数
   * @return {"errMsg": String, "result": String, "requestID": String}
   */
  tcbRouter: function(url, params) {
    params.$url = url
    return this.call('router', params)
  },

  /**
   * 云函数-REST访问服务
   *
   * @param url REST服务地址
   * @param 请求方法
   * @param params 请求参数
   * @return {"errMsg": String, "result": String, "requestID": String}
   */
  rest: function(url, method, params) {
    return this.call('rest', {
      url: url,
      method: method,
      data: params
    })
  }
}

开发多媒体服务

引入 iView Webapp

引入iView Webapp作为小程序的前端框架。

  • 进入的 iView WebappGitHub,下载项目
# clone iView Weapp
git clone https://github.com/TalkingData/iview-weapp.git
cd iview-weapp
# 安装依赖
npm install
# 编译组件,便于打开到模拟器查看
npm run dev
  • dist 文件夹拷贝到小程序项目的 miniprogram 文件夹下
  • 通过开发工具打开 iView Webapp 项目下的 examples 目录,预览 iView Webapp 提供的组件。

封装媒体API

云开发原生支持 Promise ,但是小程序 API 只能通过回调获取返回值,对于项目中使用的相关小程序的媒体 API 进行封装,避免多层回调。

module.exports = {

  /**
   * 选择照片
   * 
   * @return {"errMsg": String, "tempFilePaths": Array(String), "tempFiles": Array(Obejct)}
   */
  chooseImage: function() {
    return new Promise(function(resolve) {
      wx.chooseImage({
        count: 9,
        sizeType: ['original', 'compressed'],
        sourceType: ['album'],
        success: res => {
          resolve(res)
        }
      })
    })
  },

  /**
   * 全屏预览照片
   * 
   * @param current 当前显示图片的链接
   * @param urls 需要预览的图片链接列表(云文件ID @since 2.2.3)
   */
  previewImage: function(current, urls) {
    return new Promise(function(resolve, reject) {
      wx.previewImage({
        current: current,
        urls: urls,
        success: res => {
          resolve(res)
        },
        fail: err => {
          reject(err)
        }
      })
    })
  },

  /**
   * 选择视频
   * 
   * @return {"errMsg": String, "tempFilePath": String, "thumbTempFilePath": String, "duration": Number, "width": Number, "height": Number, "size": Number}
   */
  chooseVideo: function() {
    return new Promise(function(resolve, reject) {
      wx.chooseVideo({
        sourceType: ['album'],
        compressed: true,
        maxDuration: 60,
        success: res => {
          resolve(res)
        },
        fail: err => {
          reject(err)
        }
      })
    })
  },

  /**
   * 保存video到本地
   * 
   * @param filePath 文件路径
   * @return {"errMsg": String}
   */
  saveVideo: function(filePath) {
    return new Promise(function(resolve, reject) {
      wx.saveVideoToPhotosAlbum({
        filePath: filePath,
        success: res => {
          resolve(res)
        },
        fail: err => {
          reject(err)
        }
      })
    })
  },
  
  /**
   * 构造媒体存储标记
   * 
   * @param index 文件类型 0:视频,1:声音,2:照片
   * @param fileID 云文件ID
   * @param author 上传者
   * @return Object
   */
  mark: function(index, fileID, author) {
    return {
      index: index,
      fileID: fileID,
      author: author
    }
  }
}

页面布局

底部标签栏

新建一个 Page,在 json 配置文件中引入TabBar组件:

"usingComponents": {
    "i-tab-bar": "../../dist/tab-bar/index",
    "i-tab-bar-item": "../../dist/tab-bar-item/index"
}

新建4个 tab 页和一个增加按钮区域,指定每个 tab-itemkey,绑定 TabBarbindchange 事件来监听点击标签页切换事件,指定 current 属性可以切换各标签的 icon。这4个标签实际是写在同一个 Page 中的,当切换标签时,通过 wx:if 控制各个区域是否显示。

<i-tab-bar i-class="bar-high" fixed="true" current="{{ bar }}" bindchange="clickTabBar">
  <i-tab-bar-item key="video" icon="live" current-icon="live_fill" title="视频"></i-tab-bar-item>
  <i-tab-bar-item key="audio" icon="play" current-icon="play_fill" title="声音"></i-tab-bar-item>
  <i-tab-bar-item key="add" icon="add" current-icon="add" color="#ffff00"></i-tab-bar-item>
  <i-tab-bar-item key="notice" icon="remind" current-icon="remind_fill" title="通知"></i-tab-bar-item>
  <i-tab-bar-item key="mine" icon="mine" current-icon="mine_fill" title="我的"></i-tab-bar-item>
</i-tab-bar>

底部标签栏

TabBar 组件的高度为 50px,内容区域可以设置 padding-bottom: 50px; 来避免底部标签页遮挡内容。

用户登录

button 组件的 open-type (微信开放能力)提供了用户主动登录的方式。

  <button open-type="getUserInfo" bindgetuserinfo="onGetUserInfo" class="user-avatar" style="background-image: url({{avatarUrl}})"></button>

处理用户登录事件,获取用户openid用于判断是否登录,查询时作为条件等:

  /**
   * 用户登录获取用户信息
   */
  onGetUserInfo: function(e) {
    let userInfo = e.detail.userInfo
    // 项目模板中默认提供的云函数,可用于获取用户openid
    cloud.call('login', {}).then(res => {
      let openid = res.result.openid
      userInfo.openid = openid
      // 将用户信息写入本地缓存
      wx.setStorage({
        key: 'userInfo',
        data: userInfo,
      })
      // 设置openid
      this.setData({
        openid: openid
      })
    }).catch(err => {
      console.log(err)
    })
    // 设置登录头像
    this.setData({
      avatarUrl: userInfo.avatarUrl
    })
  }
遮罩层组件

上传文件等场景中,如果不希望用户在当前操作完成前有其它动作,可以弹出遮罩层保护用户界面。

  • 新增小程序组件:在 miniprogram 目录下新建 components/mask 文件夹,选择 新增Component,默认生成类似 Page 的4个文件。
  • 编辑遮罩层组件

mask.wxml

<view class='mask' hidden="{{mask}}"></view>

mask.wxss

.mask {
  width: 100%;
  height: 100%;
  position: fixed;
  background-color: #999;
  z-index: 999;
  top: 0;
  left: 0;
  opacity: 0.1;
}

mask.js

  /**
   * 组件的属性列表
   */
  properties: {
    hidden: {
      type: Boolean,
      value: true
    }
  }
  • 引入组件

编辑 Pagejson 配置文件:

  "usingComponents": {
    "mask": "../../components/mask/mask"
  }

编辑 Page 的wxml文件:

<!-- 遮罩对象 -->
<mask hidden="{{!mask}}"></mask>

js 文件中通过指定 mask 属性来打开/关闭遮罩层。

消息提示

json 配置文件中引入Toast组件。
引入 $Toast 对象:

const {
  $Toast
} = require('../../dist/base/index');
  /**
   * 弹出toast提示
   * 
   * @param content loading显示内容
   * @param type toast类型 default、success、warning、error、loading
   * @param modal 遮罩层是否打开
   * @param duration 持续时间,单位s,0为不自动关闭,需调用 $Toast.hide() 方法手动关闭
   * @param mask toast是否可关闭
   */
  popToast: function(content, type, modal, duration, mask) {
    duration = duration || 0
    mask = mask || false
    modal = modal || false
    $Toast({
      content: content,
      type: type,
      duration: duration,
      mask: mask
    })
    // 打开遮罩层
    this.setData({
      hidden: modal
    })
  },

  /**
   * 关闭toast提示
   */
  hideToast: function() {
    $Toast.hide()
    // 关闭遮罩层
    this.setData({
      hidden: true
    })
  }
处理多媒体上传

为了将上传文件与上传用户相关联、便于查找上传的文件,将数据库和存储结合使用。即将文件上传到存储后,获取返回的 fileID,与上传者信息、文件类型、文件描述等一起写入数据库。
在json配置文件中引入ActionSheet组件。

  <i-action-sheet visible="{{ showAdd }}" actions="{{ addActions }}" show-cancel bind:cancel="cancelAdd" bind:click="handleChooseMedia" />

处理 TabBar 的点击事件,当选择的是新增按钮时,弹出 ActionSheet 选项。

  /**
   * TabBar点击事件
   */
  clickTabBar({
    detail
  }) {
    let key = detail.key
    if ('add' === key) {
      // 点击添加按钮弹出上传选项
      this.setData({
        showAdd: true
      })
    } else {
      this.setData({
        bar: key
      })
    }
  }
弹出新增选项

ActionSheet 点击事件绑定了 handleChooseMedia 函数:

  handleChooseMedia({
    detail
  }) {
    // ActionSheet隐藏
    this.cancelAdd()
    // 登录提示
    if (this.data.openid === '') {
      this.popToast('请先登录~', 'warning', true, 3, true)
      return
    }
    // actions选项的索引,从0开始
    const index = detail.index;
    let that = this;
    if (0 === index) {
      // 视频
      media.chooseVideo().then(res => {
        that.popToast('上传中...', 'loading')
        // 文件
        let tempFilePath = res.tempFilePath
        let tempFilename = that.splitFileName(tempFilePath)
        // 缩略图文件(目前微信开发工具有这个字段,真机无)
        let thumbTempFilePath = res.thumbTempFilePath
        // 上传视频文件
        cloud.upload(tempFilename, tempFilePath).then(res => {
          let mark = media.mark(index, res.fileID, that.data.nickName)
          if (thumbTempFilePath) {
            // 缩略图文件
            let thumbTempFilename = that.splitFileName(thumbTempFilePath)
            // 上传缩略图文件
            cloud.upload(thumbTempFilename, thumbTempFilePath).then(res => {
              let thumb = res.fileID
              mark.thumb = thumb
              // 写入数据库
              cloud.add(mark).then(res => {
                let id = res._id
                // 跳转到编辑视频详情页面
                wx.navigateTo({
                  url: '../editVideo/editVideo'.concat('?thumb=').concat(thumb)
                    .concat('&id=').concat(id)
                })
                that.hideToast()
              }).catch(err => {
                console.log(err)
              })
            })
          } else {
            // 未生成缩略图,使用默认图片
            let thumb = 'cloud://test-d518bb.7465-test-d518bb/system/default-poster.jpg'
            mark.thumb = thumb
            // 写入数据库
            cloud.add(mark).then(res => {
              that.hideToast()
              let id = res._id
              wx.navigateTo({
                url: '../editVideo/editVideo'.concat('?thumb=').concat(thumb)
                  .concat('&id=').concat(id)
              })
            }).catch(err => {
              console.log(err)
            })
          }
        }).catch(err => {
          console.log(err)
        })
      }).catch(err => {
        console.log(err)
      })
    } else if (1 === index) {
      // 照片
      media.chooseImage().then(res => {
        res.tempFilePaths.forEach(function(filePath, i) {
          that.popToast('上传中...', 'loading')
          let filename = that.splitFileName(filePath)
          cloud.upload(filename, filePath).then(res => {
            let mark = media.mark(index, res.fileID, that.data.nickName)
            // 写入数据库
            cloud.add(mark).then(res => {
              that.loadAlbum()
              that.hideToast()
            }).catch(err => {
              console.log(err)
            })
          }).catch(err => {
            console.log(err)
          })
        })
      }).catch(err => {
        console.log(err)
      })
    }
  }
视频上传.png
视频列表页

在小程序中,video 是原生组件,层级很高,作为列表元素时会遮挡底部标签栏,因此视频列表以缩略图列表的形式呈现,列表区域设置 padding-bottom: 50px; 来保证能够完全呈现。
点击缩略图,可以跳转到新页面观看视频和具体信息。

  • 视频列表区域
<view wx:for="{{videoFiles}}" wx:key="{{item.id}}" class='video-image-warpper'>
  <image class='video-image' mode='aspectFill' src='{{item.thumb}}' bindtap='playVideo' data-index="{{item.id}}"></image>
  <view class="video-like-icon">
    <i-icon type="like_fill" size="28" color="#ff5050" />
    <text>99k+</text>
  </view>
  <i-icon class="play-icon" type="play" size="28" color="#fff" />
</view>
</view>
  • 加载视频列表
  /**
   * 加载视频
   */
  loadVideo: function() {
    let that = this
    cloud.query({
      // 查询该用户上传的文件
      // _openid: that.data.openid,
      index: 0
    }).then(res => {
      let files = []
      res.data.forEach(function(e, i) {
        files.push({
          id: e._id,
          fileID: e.fileID,
          thumb: e.thumb,
          author: e.author,
          desc: e.desc
        })
      })
      that.setData({
        videoFiles: files
      })
    }).catch(err => {
      console.log(err)
    })
  }
  • 视频列表页效果图
视频列表
视频详情页
  • 点击缩略图,跳转到视频详情页
    跳转页面传递参数,包括视频和缩略图的 fileID、视频信息等。
playVideo: function(e) {
    let video
    files.forEach(function(element, i) {
      if (e.currentTarget.dataset['index'] === element.id) {
        video = element
        return
      }
    })
    if (undefined !== video) {
      wx.navigateTo({
        url: '../video/video?fileID='.concat(video.fileID)
          .concat('&thumb=').concat(video.thumb)
          .concat('&author=').concat(video.author)
          .concat('&desc=').concat(video.desc == undefined ? '' : video.desc)
      })
    }
  }
  • 编辑详情页

视频参数详见官方文档

  <video id='video' src='{{fileID}}' poster='{{thumb}}' object-fit='cover' direction='0' bindended="onVideoEnd"></video>
  • 保存视频
  /**
   * 保存视频
   */
  saveVideo: function() {
    let that = this
    cloud.download(this.data.fileID).then(res => {
      // 临时文件路径
      let tempFilePath = res.tempFilePath
      media.saveVideo(tempFilePath).then(res => {}).catch(err => {
        console.log(err)
      })
    }).catch(err => {
      console.log(err)
    })
  }
  • 视频详情页效果图
视频详情页
相册
  • 编辑相册展示区域

图片参数详见官方文档
图片裁剪、缩放模式选择 mode='aspectFill',从而保持纵横比缩放图片,只保证图片的短边能完全显示出来。

  <image class='album-image' mode='aspectFill' wx:for="{{albumFiles}}" wx:key="{{item.id}}" src='{{item.fileID}}' bindtap='imageToPreview' data-current="{{item.fileID}}" />
  • 加载相册
  /**
   * 初始化album
   * 
   * TODO 分页加载
   */
  loadAlbum: function() {
    let that = this
    cloud.query({
      index: 1
    }).then(res => {
      let files = []
      res.data.forEach(function(e, i) {
        files.push({
          id: e._id,
          fileID: e.fileID
        })
      })
      that.setData({
        albumFiles: files
      })
    }).catch(err => {
      console.log(err)
    })
  }
  • 相册效果图
相册列表
  • 预览图片

图片绑定点击事件,生成预览图片:

  /**
   * 相册图片点击全屏预览
   */
  imageToPreview: function(e) {
    // 被点击图片云文件ID
    let current = e.currentTarget.dataset['current']
    // 所有图片云文件ID
    let fileIDs = []
    this.data.albumFiles.forEach(function(e, i) {
      fileIDs.push(e.fileID)
    })
    media.previewImage(current, fileIDs)
  }
  • 预览效果图
图片预览
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,274评论 25 707
  • 用两张图告诉你,为什么你的 App 会卡顿? - Android - 掘金 Cover 有什么料? 从这篇文章中你...
    hw1212阅读 12,672评论 2 59
  • 日期:2018.2.12姓名:黄晓明 单位:温州市博奕成套设备工程有限公司 组别:反省组 ...
    黄晓明_f18e阅读 182评论 0 0
  • 字迹一直这样,没啥长进,也不知道该怎么提高。大家给我出出主意。
    yayaluoshuiming阅读 177评论 1 1