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閱讀 229,836評論 6 540
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,275評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,904評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,633評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,368評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,736評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,740評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,919評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,481評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,235評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,427評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,968評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,656評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,055評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,348評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,160評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,380評論 2 379

推薦閱讀更多精彩內容