koa@2学习笔记

前言:站在巨人的肩膀上,感谢前辈们的付出与贡献

安装 koa 模块

koa 需要 node v7.6.0 及以上版本,提供 ES6 和 async 函数支持

$ npm install koa

新建 hello.js

const Koa = require('koa');
const app = new Koa();

// response
app.use(ctx => {
  ctx.body = 'Hello Koa';
});

app.listen(3000);

中间件

普通函数

app.use((ctx, next) => {
  const start = Date.now();
  return next().then(() => {
    const ms = Date.now() - start;
    console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
  });
});

async 函数(node v7.6.0+)

app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});

中间件开发

/* ./middleware/logger-async.js */

function log( ctx ) {
    console.log( ctx.method, ctx.header.host + ctx.url )
}

module.exports = function () {
  return async function ( ctx, next ) {
    log(ctx);
    await next()
  }
}
/* index.js */
const Koa = require('koa')
const loggerAsync = require('./middleware/logger-async')
var app = new Koa()

app.use(loggerAsync())

app.use((ctx) => {
    ctx.body = 'hello world'
})
app.listen(3000, 'localhost', () => {
    console.log('starting on port: ', 3000)
})
//控制台
PS D:\workspace\koa2demo> node .\index.js
starting on port:  3000
GET localhost:3000/

理解 async/await

function getSyncTime() {
    return new Promise((resolve, reject) => {
        try {
            let startTime = new Date().getTime()
            setTimeout(() => {
                let endTime = new Date().getTime()
                let data = endTime - startTime
                resolve(data)
            }, 500)
        } catch (err) {
            reject(err)
        }
    })
}
async function getSyncData() {
    let time = await getSyncTime()
    let data = `endTime - startTime = ${time}`
    return data
} async function getData() {
    let data = await getSyncData()
    console.log(data)
}
getData()
async/await

koa2特性

  • 利用ES7的async/await的来处理传统回调嵌套问题和代替koa@1的generator
  • 中间件只支持 async/await 封装,如果要使用koa@1基于generator中间件,需要通过中间件koa-convert封装一下才能使用

路由中间件 koa-router

npm install koa-router --save
/* index.js */
const Koa = require('koa')
const fs = require('fs')
const app = new Koa()

const Router = require('koa-router')

//子路由1
let home = new Router()
home.get('/', async (ctx) => {
    let html = `
        <ul>
            <li><a href="/page/helloworld">/page/helloworld</a></li>
            <li><a href="/page/404">/page/404</a></li>
        </ul>
    `
    ctx.body = html
})

//子路由2
let page = new Router()
page
    .get('/404', async (ctx) => {
        ctx.body = '404 page'
    })
    .get('/helloworld', async (ctx) => {
        ctx.body = 'helloworld page'
    })

//装载所有子路由
let router = new Router()
router.use('/', home.routes(), home.allowedMethods())
router.use('/page', page.routes(), page.allowedMethods())

//加载路由中间件
app.use(router.routes()).use(router.allowedMethods())

app.listen(3000, () => {
    console.log('[demo] koa-router is starting on port: 3000')
})
//console
PS D:\workspace\koa2demo> node .\index.js
[demo] koa-router is starting on port: 3000

请求获取数据

GET请求

const Koa = require('koa')
const app = new Koa()

app.use(async (ctx)=>{
    let url = ctx.url
    //从上下文的request对象中获取
    let request = ctx.request
    let req_query = request.query
    let req_querystring = request.querystring

    //从上下文直接获取
    let ctx_query = ctx.query
    let ctx_querystring = ctx.querystring

    ctx.body = {
        url,
        req_query,
        req_querystring,
        ctx_query,
        ctx_querystring
    }
})

app.listen(3000, () => {
    console.log('[demo] get request is starting on port: 3000')
})
GET

POST请求获取数据

const Koa = require('koa')
const app = new Koa()

app.use(async (ctx) => {
    if (ctx.url === '/' && ctx.method === 'GET') {
        //get请求时返回表单
        let html = `
            <h2>koa@2 post request</h2>
            <form action="/" method="POST">
                <p>userName</p>
                <input name="username" type="text"><br>
                <p>userPwd</p>
                <input name="userPwd" type="password"><br>
                <button type="submit">submit</button>
            </form>
        `
        ctx.body = html
    } else if (ctx.url === '/' && ctx.method === 'POST') {
        //post请求时,解析表单里数据,并显示
        let postData = await parsePostData(ctx)
        ctx.body = postData
    } else {
        //其他请求显示404
        ctx.body = '<h1>404 page</h1>'
    }
})

//解析上下文里node原生请求的post参数
function parsePostData(ctx) {
    return new Promise((resolve, reject) => {
        try {
            let postData = '';
            ctx.req.addListener('data', (data) => {
                postData += data
            })
            ctx.req.addListener('end', () => {
                let parseData = parseQueryStr(postData)
                resolve(parseData)
            })
        } catch (err) {
            reject(err)
        }
    })
}

//将post请求参数字符串解析成JSON
function parseQueryStr(queryStr) {
    let queryData = {}
    let queryStrList = queryStr.split('&')
    console.log(queryStrList)
    for (let [index, queryStr] of queryStrList.entries()) {
        let itemList = queryStr.split('=')
        queryData[itemList[0]] = decodeURIComponent(itemList[1])
    }
    return queryData
}

app.listen(3000, () => {
    console.log('[demo] post request is starting on port: 3000')
})
POST表单请求 请求响应结果
POST表单
提交结果

koa-bodyparser 中间件

const Koa = require('koa')
const app = new Koa()
const bodyParser = require('koa-bodyparser')

//使用ctx.body解析中间件
app.use(bodyParser())

app.use(async (ctx) => {
    if (ctx.url === '/' && ctx.method === 'GET') {
        //get请求时返回表单
        let html = `
            <h2>koa@2 post request</h2>
            <form action="/" method="POST">
                <p>userName</p>
                <input name="username" type="text"><br>
                <p>userPwd</p>
                <input name="userPwd" type="password"><br>
                <button type="submit">submit</button>
            </form>
        `
        ctx.body = html
    } else if (ctx.url === '/' && ctx.method === 'POST') {
        //post请求时,解析表单里数据,并显示
        let postData = ctx.request.body
        ctx.body = postData
    } else {
        //其他请求显示404
        ctx.body = '<h1>404 page</h1>'
    }
})

app.listen(3000, () => {
    console.log('[demo] koa-bodyparser is starting on port: 3000')
})

静态资源加载

koa-static中间件

const Koa = require('koa')
const path = require('path')
const static = require('koa-static')

const app = new Koa()

//静态资源相对路径
const staticPath = './public'

app.use(static(path.join(__dirname, staticPath)))

app.use(async (ctx) => {
    ctx.body = 'hello koa@2'
})

app.listen(3000, () => {
    console.log('[demo] koa-static middleware is starting on port: 3000')
})

koa2使用cookie

const Koa = require('koa')
const app = new Koa()

app.use(async (ctx) => {
    if (ctx.url === '/index') {
        ctx.cookies.set('cid', 'hello world', {
            domain: 'localhost',//cookie所在的域名
            path: '/index',//cookie所在的路径
            maxAge: 20 * 60 * 1000,//cookie有效时长
            expires: new Date('2018-10-24'),//cookie失效时间
            httpOnly: false,//是否只用于http请求中获取
            overwrite: false//是否允许重写
        })
        ctx.body = 'cookie is ok'
    } else {
        ctx.body = 'hello koa@2'
    }
})

app.use(async (ctx) => {
    ctx.body = 'hello koa@2'
})

app.listen(3000, () => {
    console.log('[demo] cookie is starting on port: 3000')
})
cookie

koa2实现session

存放mysql中

//创建mysql数据库名为koademo
CREATE DATABASE IF NOT EXISTS koademo DEFAULT CHARSET utf8 COLLATE utf8_general_ci;
const Koa = require('koa')
const session = require('koa-session-minimal')
const MysqlSession = require('koa-mysql-session')

const app = new Koa()

//配置存储session信息的mysql
let store = new MysqlSession({
    user: 'root',
    password: 'root',
    database: 'koademo',
    host: '127.0.0.1'
})

//存放sessionId的cookie配置
let cookie = {
    maxAge: '',
    expires: '',
    path: '',
    domain: '',
    httpOnly: '',
    overwrite: '',
    secure: '',
    sameSite: '',
    signed: ''
}

//使用session中间件
app.use(session({
    key: 'SESSION_ID',
    store: store,
    cookie: cookie
}))

app.use(async (ctx) => {
    //设置session
    if (ctx.url === '/set') {
        ctx.session = {
            user_id: Math.random().toString(36).substr(2),
            count: 0
        }
        ctx.body = ctx.session
    } else if (ctx.url === '/') {
        //读取session信息
        ctx.session.count = ctx.session.count + 1
        ctx.body = ctx.session
    }
})

app.listen(3000, () => {
    console.log('[demo] session is starting on port: 3000')
})

加载模板引擎

koa-views中间件

const Koa = require('koa')
const views = require('koa-views')
const path = require('path')

const app = new Koa()

//加载模板引擎
app.use(views(path.join(__dirname, './views'), {
    extension: 'ejs'
}))

app.use(async (ctx) => {
    let title = 'hello koa@2'
    await ctx.render('index', {
        title
    })
})

app.listen(3000, () => {
    console.log('[demo] koa-views ejs is starting on port: 3000')
})

文件上传

busboy模块

busboy模块是用来解析post请求,node原生req中的文件流

const inspect = require('util').inspect
const path = require('path')
const fs = require('fs')
const Busboy = require('busboy')

//req为node原生请求
const busboy = new Busboy({ headers: req.headers })

//监听文件解析事件
busboy.on('file', function (fieldname, file, filename, encoding, mimetype) {
    console.log(`File [${fieldname}]: filename: ${filename}`)

    //文件保存特定路径
    file.pipe(fs.createWriteStream('./upload'))

    //开始解析文件流
    file.on('data', (data) => {
        console.log(`File [${fieldname}] got ${data.length} bytes`)
    })

    //解析文件结束
    file.on('end', () => {
        console.log(`File [${fieldname}] finished`)
    })
})

//监听请求中的字段
busboy.on('field', (fieldname, val, fieldnameTruncated, valTruncated) => {
    console.log(`Field [${fieldname}]: value: ${inspect(val)}`)
})

//监听结束事件
busboy.on('finish', () => {
    console.log('Done parsing form!')
    res.writeHead(303, { Connection: 'close', Location: '/' })
    res.end()
})
req.pipe(busboy)

上传文件简单实现

封装上传文件到写入服务方法

const inspect = require('util').inspect
const path = require('path')
const os = require('os')
const fs = require('fs')
const Busboy = require('busboy')

/**
* 同步创建文件目录
* @param  {string} dirname 目录绝对地址
* @return {boolean}        创建目录结果
*/ function mkdirsSync(dirname) {
    if (fs.existsSync(dirname)) {
        return true
    } else {
        if (mkdirsSync(path.dirname(dirname))) {
            fs.mkdirSync(dirname)
            return true
        }
    }
}

/**
* 获取上传文件的后缀名
* @param  {string} fileName 获取上传文件的后缀名
* @return {string}          文件后缀名
*/
function getSuffixName(fileName) {
    let nameList = fileName.split('.')
    return nameList[nameList.length - 1]
}

     /**
* 上传文件
* @param  {object} ctx     koa上下文
* @param  {object} options 文件上传参数 fileType文件类型, path文件存放路径
* @return {promise}         
*/ function uploadFile(ctx, options) {
    let req = ctx.req
    let res = ctx.res
    let busboy = new Busboy({ headers: req.headers }) // 获取类型 
    let fileType = options.fileType || 'common'
    let filePath = path.join(options.path, fileType)
    let mkdirResult = mkdirsSync(filePath)
    return new Promise((resolve, reject) => {
        console.log('文件上传中...')
        let result = { success: false, formData: {}, } // 解析请求文件事件 
        busboy.on('file', function (fieldname, file, filename, encoding, mimetype) {
            let fileName = Math.random().toString(16).substr(2) + '.' + getSuffixName(filename)
            let _uploadFilePath = path.join(filePath, fileName)
            let saveTo = path.join(_uploadFilePath) // 文件保存到制定路径 
            file.pipe(fs.createWriteStream(saveTo)) // 文件写入事件结束 
            file.on('end', function () {
                result.success = true
                result.message = '文件上传成功'
                console.log('文件上传成功!')
                resolve(result)
            })
        }) // 解析表单中其他字段信息 
        busboy.on('field', function (fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) {
            console.log('表单字段数据 [' + fieldname + ']: value: ' + inspect(val));
            result.formData[fieldname] = inspect(val);
        }); // 解析结束事件 
        busboy.on('finish', function () {
            console.log('文件上结束')
            resolve(result)
        }) // 解析错误事件 
        busboy.on('error', function (err) {
            console.log('文件上出错')
            reject(result)
        })
        req.pipe(busboy)
    })
}

module.exports = { uploadFile }

入口文件

const Koa = require('koa')
const path = require('path')
const app = new Koa()
// const bodyParser = require('koa-bodyparser')
const { uploadFile } = require('./util/upload')
// app.use(bodyParser()) 
app.use(async (ctx) => {
    if (ctx.url === '/' && ctx.method === 'GET') {
        // 当GET请求时候返回表单页面
        let html = `
            <h1>koa2 upload demo</h1>
            <form method="POST" action="/upload.json" enctype="multipart/form-data">
            <p>file upload</p>
            <span>picName:</span><input name="picName" type="text" /><br/>
            <input name="file" type="file" /><br/><br/>
            <button type="submit">submit</button>
            </form>
        `
        ctx.body = html
    } else if (ctx.url === '/upload.json' && ctx.method === 'POST') {
        // 上传文件请求处理
        let result = {
            success: false
        }
        let serverFilePath = path.join(__dirname, 'upload-files')
        // 上传文件事件
        result = await uploadFile(ctx, {
            fileType: 'album', // common or album 
            path: serverFilePath
        })
        ctx.body = result
    } else {
        // 其他请求显示404 
        ctx.body = '<h1>404!!! o(╯□╰)o</h1>'
    }
})

app.listen(3000, () => {
    console.log('[demo] upload-simple is starting at port 3000')
})

异步上传图片

入口文件
const Koa = require('koa')
const views = require('koa-views')
const path = require('path')
const convert = require('koa-convert')
const static = require('koa-static')
const { uploadFile } = require('./util/upload')
const app = new Koa()

/**
* 使用第三方中间件 start 
*/
app.use(views(path.join(__dirname, './views'), {
    extension: 'ejs'
}))

// 静态资源目录对于相对入口文件index.js的路径 
const staticPath = './public'
// 由于koa-static目前不支持koa2 
// 所以只能用koa-convert封装一下 
app.use(convert(static(path.join(__dirname, staticPath))))

/**
* 使用第三方中间件 end 
*/
app.use(async (ctx) => {
    if (ctx.method === 'GET') {
        let title = 'upload pic async'
        await ctx.render('index', {
            title,
        })
    }
    else if (ctx.url === '/api/picture/upload.json' && ctx.method === 'POST') {
        // 上传文件请求处理 
        let result = { success: false }
        let serverFilePath = path.join(__dirname, 'public/image')
        // 上传文件事件 
        result = await uploadFile(ctx, {
            fileType: 'album',
            path: serverFilePath
        })
        ctx.body = result
    } else {
        // 其他请求显示404
        ctx.body = '<h1>404!!! o(╯□╰)o</h1>'
    }
})

app.listen(3000, () => {
    console.log('[demo] upload-async is starting at port 3000')
})
上传图片流写操作
const inspect = require('util').inspect
const path = require('path')
const os = require('os')
const fs = require('fs')
const Busboy = require('busboy')

/**
* 同步创建文件目录
* @param  {string} dirname 目录绝对地址
* @return {boolean}        创建目录结果
*/
function mkdirsSync(dirname) {
    if (fs.existsSync(dirname)) {
        return true
    } else {
        if (mkdirsSync(path.dirname(dirname))) {
            fs.mkdirSync(dirname)
            return true
        }
    }
}

/**
* 获取上传文件的后缀名
* @param  {string} fileName 获取上传文件的后缀名
* @return {string}          文件后缀名
*/ function getSuffixName(fileName) {
    let nameList = fileName.split('.')
    return nameList[nameList.length - 1]
}

/**
* 上传文件
* @param  {object} ctx     koa上下文
* @param  {object} options 文件上传参数 fileType文件类型, path文件存放路径
* @return {promise}         
*/ function uploadFile(ctx, options) {
    let req = ctx.req
    let res = ctx.res
    let busboy = new Busboy({ headers: req.headers })
    // 获取类型 
    let fileType = options.fileType || 'common'
    let filePath = path.join(options.path, fileType)
    let mkdirResult = mkdirsSync(filePath)
    return new Promise((resolve, reject) => {
        console.log('文件上传中...')
        let result = { success: false, message: '', data: null }
        // 解析请求文件事件
        busboy.on('file', function (fieldname, file, filename, encoding, mimetype) {
            let fileName = Math.random().toString(16).substr(2) + '.' + getSuffixName(filename)
            let _uploadFilePath = path.join(filePath, fileName)
            let saveTo = path.join(_uploadFilePath)
            // 文件保存到制定路径 
            file.pipe(fs.createWriteStream(saveTo))
            // 文件写入事件结束 
            file.on('end', function () {
                result.success = true
                result.message = '文件上传成功'
                result.data = {
                    pictureUrl: `//${ctx.host}/image/${fileType}/${fileName}`
                }
                console.log('文件上传成功!')
                resolve(result)
            })
        })
        // 解析结束事件 
        busboy.on('finish', function () {
            console.log('文件上结束')
            resolve(result)
        })
        // 解析错误事件 
        busboy.on('error', function (err) {
            console.log('文件上出错')
            reject(result)
        })
        req.pipe(busboy)
    })
}

module.exports = { uploadFile }
前端代码
<!DOCTYPE html>
<html lang="en">
    <head>
        <title><%= title%></title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
    </head>
    <body>
        <button class="btn" id="J_UploadPictureBtn">上传图片</button>
        <hr/>
        <p>上传进度<span id="J_UploadProgress">0</span>%</p>
        <p>上传结果图片</p>
        <div id="J_PicturePreview" class="preview-picture"> </div> 
        <script src="js/index.js"></script>
    </body>
</html>

上传操作代码

(function () {
    let btn = document.getElementById('J_UploadPictureBtn')
    let progressElem = document.getElementById('J_UploadProgress')
    let previewElem = document.getElementById('J_PicturePreview')
    btn.addEventListener('click', function () {
        uploadAction({
            success: function (result) {
                console.log(result)
                if (result && result.success && result.data && result.data.pictureUrl) {
                    previewElem.innerHTML = '![](' + result.data.pictureUrl + ')'
                }
            },
            progress: function (data) {
                if (data && data * 1 > 0) {
                    progressElem.innerText = data
                }
            }
        })
    })

    /**
    * 类型判断
    * @type {Object}
    */
    let UtilType = {
        isPrototype: function (data) {
            return Object.prototype.toString.call(data).toLowerCase();
        }, isJSON: function (data) {
            return this.isPrototype(data) === '[object object]';
        }, isFunction: function (data) {
            return this.isPrototype(data) === '[object function]';
        }
    }

    /**
    * form表单上传请求事件
    * @param  {object} options 请求参数
    */
    function requestEvent(options) {
        try {
            let formData = options.formData
            let xhr = new XMLHttpRequest()
            xhr.onreadystatechange = function () {
                if (xhr.readyState === 4 && xhr.status === 200) {
                    options.success(JSON.parse(xhr.responseText))
                }
            }
            xhr.upload.onprogress = function (evt) {
                let loaded = evt.loaded
                let tot = evt.total
                let per = Math.floor(100 * loaded / tot)
                options.progress(per)
            }
            xhr.open('post', '/api/picture/upload.json')
            xhr.send(formData)
        } catch (err) { options.fail(err) }
    }
/**
* 上传事件
* @param  {object} options 上传参数      
*/ function uploadEvent(options) {
        let file
        let formData = new FormData()
        let input = document.createElement('input')
        input.setAttribute('type', 'file')
        input.setAttribute('name', 'files')
        input.click()
        input.onchange = function () {
            file = input.files[0]
            formData.append('files', file)
            requestEvent({ formData, success: options.success, fail: options.fail, progress: options.progress })
        }
    }

    /**
    * 上传操作
    * @param  {object} options 上传参数     
    */
    function uploadAction(options) {
        if (!UtilType.isJSON(options)) {
            console.log('upload options is null')
            return
        }
        let _options = {}
        _options.success = UtilType.isFunction(options.success) ? options.success : function () { }
        _options.fail = UtilType.isFunction(options.fail) ? options.fail : function () { }
        _options.progress = UtilType.isFunction(options.progress) ? options.progress : function () { }
        uploadEvent(_options)
    }
})()

创建mysql数据库连接池

const mysql = require('mysql')

//创建数据连接池
const pool = mysql.createPool({
    host: '127.0.0.1',
    user: 'root',
    password: 'root',
    database: 'koademo'
})

//在数据池中进行会话操作
pool.getConnection((err, conn) => {
    conn.query('SELECT * FROM test', (err, rs, fields) => {
        //结束会话
        conn.release()

        if (err) throw err
    })
})

async/await封装使用mysql

/* ./async-db.js */
const msyql = require('mysql')
const pool = msyql.createPool({
    host: '127.0.0.1',
    user: 'root',
    password: 'root',
    database: 'koademo'
})

let query = (sql, values) => {
    return new Promise((resolve, reject) => {
        pool.getConnection((err, conn) => {
            if (err) {
                reject(err)
            } else {
                conn.query(sql, values, (err, rows) => {
                    if (err) {
                        reject(err)
                    } else {
                        resolve(rows)
                    }
                    conn.release()
                })
            }
        })
    })
}

module.exports = {
    query
}
/* index.js */
const { query } = require('./async-db')

async function selectAllData() {
    let sql = 'SELECT * FROM test'
    let dataList = await query(sql)
    return dataList
}

async function getData() {
    let dataList = await selectAllData()
    console.log(dataList)
}

getData()

jsonp

const Koa = require('koa')
const app = new Koa()

app.use(async (ctx) => {
    //如果JSONP的请求为GET
    if (ctx.method === 'GET' && ctx.url.split('?')[0] === '/getData.jsonp') {
        //获取JSONP的callback
        let callbackName = ctx.query.callback || 'callback'
        let returnData = {
            success: true,
            data: {
                text: 'this is a jsonp api',
                time: new Date().getTime()
            }
        }
        //JSONP的script字符串
        let jsonpStr = `;${callbackName}(${JSON.stringify(returnData)})`
        //用text/javascript,让请求支持跨域请求
        ctx.type = 'text/javascript'
        //输出jsonp字符串
        ctx.body = jsonpStr
    } else {
        ctx.body = 'hello jsonp'
    }
})
app.listen(3000, () => {
    console.log('[demo] jsonp is tarting on port 3000')
})

koa-jsonp中间件

const Koa = require('koa')
const jsonp = require('koa-jsonp')
const app = new Koa()

//使用中间件
app.use(jsonp())

app.use(async (ctx) => {
    let returnData = {
        success: true,
        data: {
            text: 'this is a jsonp api',
            time: new Date().getTime()
        }
    }
    //直接输出json
    ctx.body = returnData
})
app.listen(3000, () => {
    console.log('[demo] koa-jsonp is tarting on port 3000')
})

各章节代码存放在对应的分支中:所有源码

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

推荐阅读更多精彩内容