前端大文件分段上傳绝编;控制接口并發(fā)數(shù)量

說明:使用axios方式上傳僻澎,文件不能過大,因為過多的連續(xù)Ajax請求會使后臺崩潰十饥,接口報錯窟勃;所以使用分段上傳的方式,減輕服務(wù)器的壓力逗堵。其實就是將 文件變小秉氧,也就是通過 文件資源分塊 后再上傳。

問題 1:誰負責(zé)資源分塊蜒秤?誰負責(zé)資源整合汁咏?
前端負責(zé)分塊,服務(wù)端負責(zé)整合.

問題 2:前端怎么對資源進行分塊作媚?
首先是選擇上傳的文件資源梆暖,接著就可以得到對應(yīng)的文件對象 File,而 File.prototype.slice 方法可以實現(xiàn)資源的分塊掂骏,當(dāng)然也有人說是 Blob.prototype.slice 方法,因為 Blob.prototype.slice === File.prototype.slice.

問題 3:服務(wù)端怎么知道什么時候要整合資源厚掷?如何保證資源整合的有序性弟灼?
由于前端會將資源分塊级解,然后單獨發(fā)送請求,也就是說田绑,原來 1 個文件對應(yīng) 1 個上傳請求勤哗,現(xiàn)在可能會變成 1 個文件對應(yīng) n 個上傳請求,所以前端可以基于 Promise.all 將這多個接口整合掩驱,上傳完成在發(fā)送一個合并的請求芒划,通知服務(wù)端進行合并。

在發(fā)送請求資源時欧穴,前端會定好每個文件對應(yīng)的序號民逼,并將當(dāng)前分塊、序號以及文件 hash 等信息一起發(fā)送給服務(wù)端涮帘,服務(wù)端在進行合并時拼苍,通過序號進行依次合并即可。

此示例是純前端代碼调缨,不涉及后端疮鲫。
錄屏.gif
第一步:使用input或者antd_upload獲取文件
image.png
第二步:調(diào)接口獲取文件段數(shù),分段列表和分段尺寸弦叶;使用slice方法俊犯,分段讀取文件為blob
   let dataMsg = await createMultipart({
       fileSize: file.size, // 傳參數(shù)
       filename: file.name
   }).then(
       (rem) => {
           return rem.data;
       },
       (err) => {
           return upFailed(file, onUpload); // 如果接口報錯,使用upFailed方法處理
       }
   );
   let urlList = dataMsg?.parts || []; // 分段列表
   let DEFAULT_SIZE = dataMsg?.partSize; // 分段尺寸

   for (let i = 0; i < urlList.length; i++) {
       let url = urlList[i]['url'];
       let fname = encodeURIComponent(file.name);
       let start = i * DEFAULT_SIZE;
       let stepFile;
       if (i === urlList.length - 1) {
          // 使用slice方法伤哺,分段讀取文件為blob 
           stepFile = file.slice(start, -1); // 如果是最后一段燕侠,直接截取剩下的所有內(nèi)容
       } else {
           stepFile = file.slice(start, start + DEFAULT_SIZE); // 分割文件
       }
       urlList[i]['stepFile'] = stepFile;
       urlList[i]['fname'] = fname;
       urlList[i]['uid'] = file.uid;
   }

urlList已準備好

image.png

數(shù)據(jù)說明: {
fname: '使用encodeURIComponent 編碼過的文件名',
partNumber: '段數(shù)序號,合并時候使用',
stepFile: '截取的文件'
uid: 'antd組件生成的文件唯一值',
url: '上傳該段文件的路徑'
};

第三步:循環(huán)urlList,上傳每一段文件

準備工作:單個文件上傳方法

 const detalItem = ({ url, stepFile, fname, partNumber }) => {
        return new Promise((resolve, reject) => {
            fileAxios({
                url,
                method: 'PUT',
                data: stepFile,
                headers: {
                    'Content-Type': '',
                    'Content-disposition': `filename*=utf-8\'zh_cn\'${fname}`
                }
            })
                .then((res) => {
                    let str = res.headers.etag.split('"').join('');
                    resolve({ eTag: str, partNumber });
                })
                .catch((err) => {
                    reject({ eTag: '', partNumber });
                });
        });
    };


準備工作:并發(fā)上傳默责,控制每次上傳的接口數(shù)量贬循,防止上傳接口數(shù)量過多,瀏覽器崩潰桃序。

參數(shù)說明:
poolLimit(數(shù)字類型):表示限制的并發(fā)數(shù)杖虾;
array(數(shù)組類型):表示任務(wù)數(shù)組;
iteratorFn(函數(shù)類型):表示迭代函數(shù)媒熊,用于實現(xiàn)對每個任務(wù)項進行處理奇适,該函數(shù)會返回一個 Promise 對象或異步函數(shù);
onUpload: 進度條

async function asyncPool(poolLimit, array, iteratorFn, onUpload) {
    const ret = []; // 存儲所有的異步任務(wù)
    const executing = []; // 存儲正在執(zhí)行的異步任務(wù)
    for (const item of array) {
        // 結(jié)束運行
        if (endExecution.end && endExecution.uid === item?.uid) {
            return;
        }

       --------重點開始---------------
        // 調(diào)用iteratorFn函數(shù)創(chuàng)建異步任務(wù)
        const p = Promise.resolve().then(() => iteratorFn(item, array));
        ret.push(p); // 保存新的異步任務(wù)

        // 當(dāng)poolLimit值小于或等于總?cè)蝿?wù)個數(shù)時芦鳍,進行并發(fā)控制
        if (poolLimit <= array.length) {
            // 當(dāng)任務(wù)完成后嚷往,從正在執(zhí)行的任務(wù)數(shù)組中移除已完成的任務(wù)
            // e 是個promise 。其后續(xù)的then接受的回調(diào)是 “自殺”柠衅,給executing 這個數(shù)組騰出空位
            const e = p.then(() => executing.splice(executing.indexOf(e), 1));
            executing.push(e); // 保存正在執(zhí)行的異步任務(wù)
            if (executing.length >= poolLimit) {
                await Promise.race(executing); // 等待較快的任務(wù)執(zhí)行完成
            }
        }
       --------重點結(jié)束---------------

       // 進度條
        onUpload &&
            onUpload({
                loaded: ret.length < array.length ? ret.length : ret.length - 1, // 等到接口合并完成皮仁,再返回100%
                total: array.length,
                uid: item['uid'],
                endAction: endAction // 如果用戶刪除文件,調(diào)用此函數(shù),結(jié)束文件上傳
            }); // 進度條
    }
    return Promise.all(ret);  // 集合多個返回結(jié)果
}

整合方法贷祈,開始上傳

 let etags = [];
    try {
        etags = await asyncPool(5, urlList, detalItem, onUpload);  // 重點
    } catch {
        // 上傳失敗
        etags = [];
        endExecution.end = true;
        endExecution.uid = file?.uid;
        file['url'] = '';
        file['link'] = '';
        file['attachmentID'] = '';
        return upFailed(file, onUpload);
    }

請求中趋急,保證5條并發(fā)數(shù),如果5條中有請求結(jié)束了势誊,自動補上


image.png

創(chuàng)建請求呜达,請求全部發(fā)出,結(jié)束后合并文件


image.png

文件上傳完的結(jié)果粟耻,etags查近。
eTag是每段文件的唯一值,
partNumber: 文件順序挤忙。后端根據(jù)這個數(shù)據(jù)表來合并文件霜威,避免順序亂了。


image.png
第四步: 通知后端饭玲,合并文件
 let params = {
        attachmentID: dataMsg?.attachmentID,
        uploadID: dataMsg?.uploadID
    };

    if (endExecution.end && endExecution.uid === file?.uid) {
        console.log('刪除文件侥祭,結(jié)束上傳,調(diào)用結(jié)束上傳接口茄厘,后端清除已經(jīng)上傳的數(shù)據(jù)');
        cancelMultipart(params);
        file['url'] = '';
        file['link'] = '';
        file['attachmentID'] = '';
        return { file: file, upResult: '' };
    }
      --------重點開始---------------
      // 調(diào)接口 傳參數(shù)
      result = await completeMultipart({
            ...params,
            etags
        });
        if (etags) {
            onUpload &&
                onUpload({
                    loaded: 100,
                    total: 100,
                    uid: file['uid'],
                    endAction: endAction
                }); // 進度條
        }
        let presignedURL = result?.data?.presignedURL;
        // console.log('result', result, 'presignedURL', presignedURL);
        file['url'] = presignedURL;
        file['link'] = presignedURL;
        file['attachmentID'] = dataMsg?.attachmentID;
        file['status'] = 'done';

        --------重點結(jié)束---------------
        return { file: file, upResult: '' };

附加功能:
1 返回進度條onUpload矮冬,原理: 當(dāng)前發(fā)出去的請求數(shù),除以總條數(shù)
2 結(jié)束請求endAction次哈,應(yīng)用場景胎署,文件正在上傳中,刪除文件窑滞,結(jié)束接口調(diào)用

全部代碼:

React上傳組件:

import React, { Component } from 'react';
import { Upload, Progress, Tooltip, Modal } from 'antd';
const { Dragger } = Upload;
export default class List extends Component {
    constructor(props) {
        super(props);
        this.state = {
            fileList: [
                // {
                //     uid: '-1',
                //     name: 'image.png',
                //     status: 'done',
                //     url:
                //         'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
                // }
            ],
            visible: false,
            upLoading: false
        };
        this.formRef = React.createRef();
        this.littleRef = React.createRef();
        this.departmentRef = React.createRef();
        this.smalledepartmentRef = React.createRef();
    }
 // 進度條
  handleProgress = (progressEvent) => {
        const num = (progressEvent.loaded / progressEvent.total) * 100;
        let percent = num >= 100 ? 100 : num.toFixed(2) * 1;
        const { fileList } = this.state;

        this[`${progressEvent?.uid}_up`] = progressEvent;

        // if (progressEvent.loaded > 5) {
        //     progressEvent.endAction();
        // }

        this.setState({
            fileList: fileList.map((p) => {
                if (p?.uid === progressEvent?.uid) {
                    p['percent'] = percent;
                }
                return p;
            })
        });
    };
    // 刪除文件
    onRemove = (file) => {
        if (!file?.status) {
            // 刪除正在上傳的文件琼牧,結(jié)束調(diào)用
            this[`${file?.uid}_up`] &&
                this[`${file?.uid}_up`]?.endAction &&
                this[`${file?.uid}_up`]?.endAction(file?.uid);
        }
        const { fileList } = this.state;
        let newList = fileList.filter((p) => p.uid !== file.uid);
        let loading = false;
        for (let v of newList) {
            if (!v?.status) {
                loading = true;
                break;
            }
        }
        this.setState({
            fileList: newList,
            upLoading: loading
        });
    };
    beforeUpload = (file, fileLists) => {
        console.log('打印file:', file);
        let repeat = [...this.state.fileList, ...fileLists];
        let obj = {};
        let noRepeat = repeat.reduce((pur, item) => {
            if (!obj[item?.uid]) {
                obj[item?.uid] = true;
                pur.push(item);
            }
            return pur;
        }, []);
        this.setState({ fileList: noRepeat, upLoading: true });
        commonUpload({ file, onUpload: this.handleProgress })
            .then((rem) => {
                const { fileList } = this.state;
                var data = {};
                for (var key in rem.file) {
                    data[key] = rem.file[key];
                }

                let newFilelist = fileList
                    .map((p) => {
                        if (p) {
                            if (p?.uid === data?.['uid']) {
                                p = { ...p, ...data };
                            }
                            return p;
                        }
                    })
                    .filter((p) => p?.status !== 'error');
                if (isNotEmpty(rem.file)) {
                    this.setState({
                        fileList: newFilelist
                    });
                }
            })
            .finally(() => {
                const { fileList } = this.state;
                // 批量上傳完成,關(guān)閉loading
                let flag = true;
                for (let item of fileList) {
                    if (!item?.status) {
                        flag = false;
                        break;
                    }
                }
                flag && this.setState({ upLoading: false });
                // console.log('this.state.fqwFile', JSON.parse(this.state.fqwFile));
            });
        // 阻止默認上傳
        return false;
      };
    render(){
             <Dragger
                           fileList={fileList}
                          className="drag-uploader"
                            onPreview={this.handlePreview} // 點擊文件鏈接或預(yù)覽圖標時的回調(diào)
                            onRemove={this.onRemove}
                            multiple={true} // 支持多個文件一起上傳
                          // onChange={this.onfileChange}
                          itemRender={(originNode, file, currFileList) => (
                           <UploadListItem
                                            originNode={originNode}
                                            file={file}
                                            currFileList={currFileList}
                                            fileList={fileList}
                                        />
                                    )}
                                    beforeUpload={this.beforeUpload}
                                    showUploadList={{
                                        showPreviewIcon: false,
                                        downloadIcon: true
                                    }}
                          >
                                    {fileList.length >= 15 ? null : UploadButton}
                          </Dragger>
}


進度條uploadListItem.jsx文件

/*
 * @desc   文件上傳哀卫,自定義上傳列表項, 帶進度條
 * @author fqw
 */

import React, { Component } from 'react';
import { Progress, Tooltip } from 'antd';
import Cns from 'classnames';
import './index.scss';

const UploadListItem = ({ originNode, file, current, fileList }) => {
    const errorNode = <Tooltip title={file['response']}>{originNode.props.children}</Tooltip>;
    let have = file.percent < 100;
    return (
        <div
            className={Cns('ant-upload-draggable-list-item', have && 'progressIng')}
            style={{ cursor: 'move' }}
            key={file.percent}
        >
            {file.status === 'error' ? errorNode : originNode}
            {have && <Progress style={{ width: '100px' }} percent={file.percent} />}
        </div>
    );
};
export default UploadListItem;

fileAxios.js文件

import { message } from 'antd';
import axios from 'axios';
import { cancelMultipart } from './common';
import {
    getUserPresignedurl,
    submitFileMsg,
    createMultipart,
    completeMultipart
} from 'services/common';

// 結(jié)束運行
let endExecution = {
    end: false,
    uid: ''
};
let endAction = (uid) => {
    endExecution.end = true;
    endExecution.uid = uid;
};
let upFailed = (file, onUpload) => {
    endExecution.end = true;
    endExecution.uid = file?.uid;
    file['status'] = 'error';
    file['response'] = '上傳失敗巨坊,請重試';
    message.warning({
        content: `文件 ${file.name} 上傳失敗,請重試`,
        duration: 5
    });
    onUpload &&
        onUpload({
            loaded: 1, // 結(jié)束進度條此改,不顯示
            total: 1,
            uid: file['uid'],
            endAction: endAction
        }); // 進度條
    return { file, upResult: false };
};
// 普通上傳
const uploadFile = async (file, onUpload) => {
    // 獲取上傳接口的路徑
    let urlRest = await getUserPresignedurl({ filename: file.name, fileSize: file.size }).then(
        (rem) => {
            if (rem.status === 200) {
                // file['uid'] = rem.data['attachmentID'];
                file = Object.assign(file, rem.data);
                return rem.data;
            }
        }
    );
    // 獲取文件類型
    let fileType = file.name.split('.').slice(-1)[0];
    let typesObj = {
        jpg: 'image/jpeg',
        jpe: 'image/jpeg',
        jpeg: 'image/jpeg',
        png: 'image/png',
        gif: 'image/gif',
        bmp: 'application/x-bmp',
        wbmp: 'image/vnd.wap.wbmp',
        ico: 'image/x-icon',
        pdf: 'application/pdf',
        ppt: 'application/x-ppt',
        doc: 'application/msword',
        xls: 'application/vnd.ms-excel'
    };
    let url = urlRest.presignedURL;
    const fileAxios = axios.create();
    let fname = encodeURIComponent(file.name);
    let upBool = false;
    upBool = await fileAxios({
        url,
        method: 'PUT',
        data: file,
        headers: {
            'Content-Type': typesObj[fileType] || '',
            'Content-disposition': `filename*=utf-8\'zh_cn\'${fname}`
        },
        onUploadProgress: (arg) => {
            arg.uid = file.uid;
            onUpload(arg);
        }
    })
        .then((res) => {
            return res.status === 200;
        })
        .catch((err) => {
            return false;
        });
    // 上傳失敗趾撵,結(jié)束運行
    if (!upBool) {
        return upFailed(file, onUpload);
    }
    // 獲取文件下載或預(yù)覽鏈接
    let upResult = await submitFileMsg({
        filename: file.name,
        fileSize: file.size,
        attachmentID: urlRest.attachmentID
    }).then(
        (rem) => {
            file['url'] = rem.data['link'];
            file['status'] = 'done';
            file['attachmentID'] = urlRest.attachmentID;
            file = Object.assign(file, rem.data);
            return true;
        },
        (err) => {
            return false;
        }
    );
    let copy = JSON.parse(JSON.stringify(file));
    copy['name'] = file.name;
    return { file: copy, upResult };
};

// poolLimit(數(shù)字類型):表示限制的并發(fā)數(shù);
// array(數(shù)組類型):表示任務(wù)數(shù)組共啃;
// iteratorFn(函數(shù)類型):表示迭代函數(shù)占调,用于實現(xiàn)對每個任務(wù)項進行處理,該函數(shù)會返回一個 Promise 對象或異步函數(shù)
// onUpload: 進度條

async function asyncPool(poolLimit, array, iteratorFn, onUpload) {
    const ret = []; // 存儲所有的異步任務(wù)
    const executing = []; // 存儲正在執(zhí)行的異步任務(wù)
    for (const item of array) {
        // 結(jié)束運行
        if (endExecution.end && endExecution.uid === item?.uid) {
            console.log('結(jié)束上傳0');
            return;
        }
        // 調(diào)用iteratorFn函數(shù)創(chuàng)建異步任務(wù)
        const p = Promise.resolve().then(() => iteratorFn(item, array));
        ret.push(p); // 保存新的異步任務(wù)

        // 當(dāng)poolLimit值小于或等于總?cè)蝿?wù)個數(shù)時移剪,進行并發(fā)控制
        if (poolLimit <= array.length) {
            // 當(dāng)任務(wù)完成后究珊,從正在執(zhí)行的任務(wù)數(shù)組中移除已完成的任務(wù)
            const e = p.then(() => executing.splice(executing.indexOf(e), 1));
            executing.push(e); // 保存正在執(zhí)行的異步任務(wù)
            if (executing.length >= poolLimit) {
                await Promise.race(executing); // 等待較快的任務(wù)執(zhí)行完成
            }
        }

        onUpload &&
            onUpload({
                loaded: ret.length < array.length ? ret.length : ret.length - 1, // 等到接口合并完成,再返回100%
                total: array.length,
                uid: item['uid'],
                endAction: endAction
            }); // 進度條
    }
    return Promise.all(ret);
}

// 分段上傳
const multiPartUpload = async (file, onUpload = null) => {
    // 獲取段數(shù)
    let dataMsg = await createMultipart({
        fileSize: file.size, // 傳參數(shù)
        filename: file.name
    }).then(
        (rem) => {
            return rem.data;
        },
        (err) => {
            return upFailed(file, onUpload); // 如果接口報錯纵苛,使用upFailed方法處理
        }
    );
    let urlList = dataMsg?.parts || []; // 分段列表
    let DEFAULT_SIZE = dataMsg?.partSize; // 分段尺寸

    for (let i = 0; i < urlList.length; i++) {
        let url = urlList[i]['url'];
        let fname = encodeURIComponent(file.name);
        let start = i * DEFAULT_SIZE;
        let stepFile;
        if (i === urlList.length - 1) {
            stepFile = file.slice(start, -1); // 如果是最后一段的話剿涮,直接截取剩下的所有內(nèi)容
        } else {
            stepFile = file.slice(start, start + DEFAULT_SIZE); // 分割文件
        }
        urlList[i]['stepFile'] = stepFile;
        urlList[i]['fname'] = fname;
        urlList[i]['uid'] = file.uid;
    }

    const fileAxios = axios.create();

    const detalItem = ({ url, stepFile, fname, partNumber }) => {
        return new Promise((resolve, reject) => {
            fileAxios({
                url,
                method: 'PUT',
                data: stepFile,
                headers: {
                    'Content-Type': '',
                    'Content-disposition': `filename*=utf-8\'zh_cn\'${fname}`
                }
            })
                .then((res) => {
                    let str = res.headers.etag.split('"').join('');
                    resolve({ eTag: str, partNumber });
                })
                .catch((err) => {
                    reject({ eTag: '', partNumber });
                });
        });
    };

    let etags = [];
    try {
        etags = await asyncPool(5, urlList, detalItem, onUpload);
    } catch {
        // 上傳失敗
        etags = [];
        endExecution.end = true;
        endExecution.uid = file?.uid;
        file['url'] = '';
        file['link'] = '';
        file['attachmentID'] = '';
        return upFailed(file, onUpload);
    }
    let params = {
        attachmentID: dataMsg?.attachmentID,
        uploadID: dataMsg?.uploadID
    };

    if (endExecution.end && endExecution.uid === file?.uid) {
        cancelMultipart(params);
        file['url'] = '';
        file['link'] = '';
        file['attachmentID'] = '';
        return { file: file, upResult: '' };
    }

    let result = null;
    // console.log('etags', etags);
    // 上傳完合并文件
    try {
        result = await completeMultipart({
            ...params,
            etags
        });
        if (etags) {
            onUpload &&
                onUpload({
                    loaded: 100,
                    total: 100,
                    uid: file['uid'],
                    endAction: endAction
                }); // 進度條
        }
        let presignedURL = result?.data?.presignedURL;
        // console.log('result', result, 'presignedURL', presignedURL);
        file['url'] = presignedURL;
        file['link'] = presignedURL;
        file['attachmentID'] = dataMsg?.attachmentID;
        file['status'] = 'done';
    } catch {
        file['url'] = '';
        file['link'] = '';
        file['attachmentID'] = '';
        return upFailed(file, onUpload);
    }
    let copy = JSON.parse(JSON.stringify(file));
    copy['name'] = file.name;
    return { file: copy, upResult: '' };
};

export const commonUpload = ({ file, onUpload }) => {
    let fileSize = 100; // 100M
    if (file.size / 1024 / 1024 > fileSize) {
        // 當(dāng)文件大于100M采用分段上傳;
        return multiPartUpload(file, onUpload);
    } else {
        return uploadFile(file, onUpload);
    }
};

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末言津,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子幔虏,更是在濱河造成了極大的恐慌纺念,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件想括,死亡現(xiàn)場離奇詭異,居然都是意外死亡烙博,警方通過查閱死者的電腦和手機瑟蜈,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來渣窜,“玉大人铺根,你說我怎么就攤上這事∏撬蓿” “怎么了位迂?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長详瑞。 經(jīng)常有香客問我掂林,道長,這世上最難降的妖魔是什么坝橡? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任泻帮,我火速辦了婚禮,結(jié)果婚禮上计寇,老公的妹妹穿的比我還像新娘锣杂。我一直安慰自己,他們只是感情好番宁,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布元莫。 她就那樣靜靜地躺著,像睡著了一般蝶押。 火紅的嫁衣襯著肌膚如雪踱蠢。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天播聪,我揣著相機與錄音朽基,去河邊找鬼。 笑死离陶,一個胖子當(dāng)著我的面吹牛稼虎,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播招刨,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼霎俩,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起打却,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤杉适,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后柳击,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體猿推,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年捌肴,在試婚紗的時候發(fā)現(xiàn)自己被綠了蹬叭。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡状知,死狀恐怖秽五,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情饥悴,我是刑警寧澤坦喘,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站西设,受9級特大地震影響瓣铣,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜济榨,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一坯沪、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧擒滑,春花似錦腐晾、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至库车,卻和暖如春巨柒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背柠衍。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工洋满, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人珍坊。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓牺勾,卻偏偏與公主長得像,于是被迫代替她去往敵國和親阵漏。 傳聞我的和親對象是個殘疾皇子驻民,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容