前端监控

1.jpg

前端监控细分可为俩大类,技术监控和行为监控。

技术准备

此处目的是生成使用的sdk,sdk打包更适合rollup

  • npm创建项目
  • 安装依赖
 "devDependencies": {
    "@babel/core": "^7.11.1",
    "babel": "^6.23.0",
    "cross-env": "^7.0.2",
    "rollup": "^2.26.3",
    "rollup-plugin-babel": "^4.4.0"
  }
  • 配置文件
//rollup.config.js

import babel from "rollup-plugin-babel";
let isDev = (process.env.NODE_ENV === 'develop');
let babaelConfig = {
    "presets": [
        [
            "env", {
                "modules": false,
                "targets": {
                    "browers": ["chrome>40", "safari>=7"]
                }
            }
        ]
    ]
}

export default {
    input: 'index.js',
    watch: {
        exclude: 'node_modules/**'
    },
    output: {
        file: isDev ? '../website/client/js/eagle-monitor/bundle.umd.js' : '../dist/bundle.umd.js',
        name: 'EagleMonitor',
        format: 'umd',
        sourcemap: true
    },
    plugin: [
        babel({
            babelrc:false,
            presets:babaelConfig.presets,
            plugins:babaelConfig.plugin,
            exclude:'node_modules/**'
        })
    ]
}
  • 打包入口类
//index.js
import perf from "./perf";
import resource from "./resource";
import xhrHook from "./xhrHook";
import beh from "./beh";
import errorCatch from "./errorCatch";
perf.init(perfData=>{
    console.log('perf');
})

resource.init(item=>{
    console.log('resource');
})
beh.init(value=>{
    console.log('beh');
})
errorCatch.init(errorInfo=>{
    console.log('errorInfo');
})
xhrHook.init(xhrInfo=>{
    console.log('xhrInfo');
})

技术监控

  • 页面性能监控
  • performance.timing
export default {

    init: cb => {
        let isDOMReady=false;
        let isOnload=false;
        let Util = {
            getPerfData: p => {
                let data = {
                    //网络建连
                    prevPage: p.fetchStart - p.navigationStart,//上一个页面的时间
                    redirect: p.redirectEnd - p.redirectStart,//重定向时间
                    dns: p.domainLookupEnd - p.domainLookupStart,//DNS查找时间
                    connect: p.connectEnd - p.connectStart,//TCP建连时间
                    network: p.connectEnd - p.navigationStart,//网络总耗时

                    //网络接收
                    send: p.responseStart - p.requestStart,//前端从发送到接收的时间
                    receive: p.responseEnd - p.responseStart,//接收数据用时
                    request: p.responseEnd - p.requestStart,//请求页面的总耗时


                    //前端渲染
                    dom: p.domComplete - p.domLoading,//dom解析时间
                    loadEvent: p.loadEventEnd - p.loadEventStart,//loadEvent时间
                    frotend: p.loadEventEnd - p.domLoading,//前端总时间

                    //关键阶段
                    load: p.loadEventEnd - p.navigationStart,//页面完全加载的时间
                    domReady: p.domContentLoadedEventStart - p.navigationStart,//dom准备时间
                    interactive: p.domInteractive - p.navigationStart,//用户可操作的时间
                    ttfb: p.responseStart - p.navigationStart //首字节时间
                }
                return data;
            },
            //dom解析完成
            domready: (callback) => {
                if (isDOMReady===true) return
                let timer = null;

                //之所以这样做,而不是初始时候执行一次,是因为各个属性值计算的时候,可能还没就绪,例如dom相关,很容易出现默认值计算
                //然后结果是复数,所以需要这要递归,直到出现真实结果

                let runCheck = () => {
                    if (performance.timing.domInteractive) {
                        //停止循环检测  然后运行callback
                        clearTimeout(timer);
                        callback();
                        isDOMReady=true
                    } else {
                        //再去循环检测
                        timer = setTimeout(runCheck, 100);
                    }
                }
                if (document.readyState === 'interactive') {
                    callback();
                    return
                }
                document.addEventListener('DOMContentLoaded', () => {
                    //开始循环检测,是否DOMContentLoaded已经完成
                    runCheck();
                })
            },
            //页面加载完成
            onload: (callback) => {
                if (isOnload===true) return
                let timer = null;
                let runCheck = () => {
                    if (performance.timing.loadEventEnd) {
                        //停止循环检测  然后运行callback
                        clearTimeout(timer);
                        callback();
                        isOnload=true
                    } else {
                        //再去循环检测
                        timer = setTimeout(runCheck, 100);
                    }
                }
                if (document.readyState === 'interactive') {
                    callback();
                    return
                }
                window.addEventListener('load', () => {
                    //开始循环检测,是否DOMContentLoaded已经完成
                    runCheck();
                })
            }
        }
        let performance = window.performance; //兼容性问题,所以此处只是简化代码
        Util.domready(() => {
            let perfData = Util.getPerfData(performance.timing);
            perfData.type='domready';
            cb(perfData);
            debugger
        })

        Util.onload(() => {
            let perfData = Util.getPerfData(performance.timing);
            perfData.type='onload';
            cb(perfData);
            debugger
        })


        // window.addEventListener('load', () => {
            // setTimeout(() => {
            //     console.log(performance.timing);
            //     let perfData = Util.getPerfData(performance.timing);
            //     debugger
            // }, 100);
        // })
    }
}

/**
 * performance.timing
 *
 *
含义   默认值
connectEnd: 1597806127336    向服务器建立连接结束  fetchStart
connectStart: 1597806127335  向服务器建立连接开始  fetchStart
domComplete: 0  文档解析完成
domContentLoadedEventEnd: 0    ContentLoaded结束
domContentLoadedEventStart: 0   ContentLoaded开始  备注(只针对dom结构,而不针对里面图片等资源)
domInteractive: 0 解析dom结束     备注:document.readyState为字符串interactive
domLoading: 1597806127367  解析dom开始     备注:document.readyState为字符串loading
domainLookupEnd: 1597806127335 dns查询结束  fetchStart
domainLookupStart: 1597806127335  dns查询开始  fetchStart
fetchStart: 1597806127332  开始请求网页
loadEventEnd: 0   load事件发送后
loadEventStart: 0  load事件发送前
navigationStart: 1597806127331  前一个网页卸载的时间   fetchStart
redirectEnd: 0  重定向结束时间  0    需要同域
redirectStart: 0   重定向开始时间  0  需要同域
requestStart: 1597806127336  向服务器发送请求开始  无默认值
responseEnd: 1597806127361   服务器返回数据结束
responseStart: 1597806127360 服务器返回数据开始
secureConnectionStart: 0  安全握手开始  0  非https的没有
unloadEventEnd: 1597806127365   前一个网页的unload(关掉)时间结束   0
unloadEventStart: 1597806127365   前一个网页的unload(关掉)时间开始   0
 */

总之:注意属性值的触发问题(例如初始化执行时候对应周期还没有,值是默认值导致计算出错,解决方案是递归)

  • 静态资源性能监控
//util.js
export default{
    onload:(cb)=>{
        if ( document.readyState==='complete') {
            cb();
            return
        }
        window.addEventListener('load',()=>{
            cb();
        })
    }
}
import Util from "./util";
let resolvePerformanceResource = (resourceData) => {
    let r = resourceData;
    let o = {
        initiatorType: r.initiatorType,
        name: r.name,
        duration: parseInt(r.duration),


        //连接过程
        redirect: r.redirectEnd - r.redirectStart,//重定向
        dns: r.domainLookupEnd - r.domainLookupStart,//DNS查找时间
        connect: r.connectEnd - r.connectStart,//TCP建连时间 
        network: r.connectEnd - r.startTime,//网络总耗时

        //接收过程
        send: r.responseStart - r.requestStart,//前端从发送到接收的时间
        receive: r.responseEnd - r.responseStart,//接收数据用时
        request: r.responseEnd - r.requestStart,//请求页面的总耗时

        //核心指标
        ttfb: r.responseStart - r.requestStart //首字节时间
    }
    return o;
}
//帮助我们循环获得每一个资源的性能数据
let resolveEntries = (entries) => entries.map(_ => resolvePerformanceResource(_));



export default {
    init: cb => {
    //此处意义是使用新的api,通过回调触发,而不是else里面的什么请求都进
        if (window.PerformanceObserver) {
            //动态获得每一个资源信息
            let observer = new window.PerformanceObserver((list) => {
                try {
                    let entries = list.getEntries();
                } catch (error) {
                    console.error(error);
                }
            });
            observer.observe({entryTypes: ['resource']})
        } else {
            Util.onload(() => {
                //在onload之后获得所有的资源信息
                let entries = performance.getEntries('resource');
                let entriesData = resolveEntries(entries);
                cb(entriesData);
                // resolvePerformanceResource(entries[0])
                debugger
            });
        }
    }
}

总之:监控sdk的js尽量放在所有要加载的link,js,css上面,因为只有注册了才能监控,放在下面的话,可能导致监控不到之前的请求

  • 错误监控
  • window.onerror
let formatError=errorObj=>{
    debugger
    let col=errorObj.column||errorObj.columnNumber;//兼容不同浏览器
    let row=errorObj.line||errorObj.lineNumber;
    let errorType=errorObj.name;
    let message=errorObj.message;
    let {stack}=errorObj;
    if (stack) {
        //正则很多,所以理论上应该try不然很容i报错
        let matchUrl=stack.match(/https?:\/\/[^\n]+/);
        let urlFirstStack=matchUrl?matchUrl[0]:'';

        //获取真正的url
        let resourceUrl='';
        let regUrlCheck=/https?:\/\/(\S)*\.js/;
        if (resourceUrl.test(urlFirstStack)) {
            resourceUrl=urlFirstStack.match(regUrlCheck)[0];
        }

        //获取真正的行列信息
        let stackCol=null;
        let stackRow=null;
        //chrome只能通过正则匹配出来行列
        let posStack=urlFirstStack.match(/:(\d+):(\d+)/)
        if (posStack&&posStack.length>=3) {
            [,stack,stackRow]=posStack;
        }
        return {
            content:stack,
            col:Number(col||stackCol),
            row:Number(row||stackRow),
            errorType,
            message,
            resourceUrl
        };
    }
}


export default{
    init:cb=>{
        let _origin_error=window.onerror;
       window.onerror=function(message,source,lineno,colno,error) {
           /**
            colno: 5 列
            error: ReferenceError: b is not defined at http://127.0.0.1:3003/:17:5
            lineno: 17    行
            message: "Uncaught ReferenceError: b is not defined"
            source: "http://127.0.0.1:3003/"
            */

            //注意:一般项目都会压缩,如果压缩之后可能是第一行,xxxx列,完全无法调试

            /**
             * 之所以通过stack去解析正则匹配,就是因为比如说react项目,打包之后,直接lineno,colno出来的数据可能不对
             * 甚至source:定位报错的js都不对(例如报到vendor.js中的错误,没有任何意义),所以这些数据只是参考,主要还是上面formatError解析之后的信息,一定是正确的
             * 而且这些报错都是sourcemap的,需要利用服务端反解,然后返给bug统计界面,说明报错的代码是啥
             */
           let errorInfo=formatError(error);
           errorInfo._message=message;
           errorInfo._source=source;
           errorInfo._lineno=lineno;
           errorInfo._colno=colno;
           errorInfo.type='error';
           cb(errorInfo);
            _origin_error&&_origin_error.apply(window,arguments);
       }
    }
}

只有这些不够,因为针对vue,react这种压缩类型的项目,真实运行的都是mapresource资源,所以需要服务端配置,然后解析,最终定位错误代码的上下几行,然后返回给错误监控平台显示

需要注意的是:chrome的报错信息很多其实都是无效数据,因为无法精确定位行数等(例如单页面应用),所以一般都是解析stack的数据正则匹配,这里面取出的才是真实问题所在;还有错误统计代码本身也可能出错,这时候的出错要try处理,同时做特殊上班,例如分类,但是不能不做任何处理,否则直接回出现死循环

//服务端解析source-map
const fs = require('fs');
const path = require('path');
const SourceMap = require('source-map');

let sourceMapFilePath = path.join(__dirname, './main.bundle.js.map');
let sourceFileMap = {};
//替换不规则路径,此处是该map文件内部文件路径的替换
let fixPath = filePath => {
    return filePath.replace(/\.[\.\/]+/, '');
}


module.exports = async (ctx, next) => {
    //一般是把sourcemap文件在客户端上传上去,然后再此处再反解,
    //但是这只是案例,sourcemap文件直接放到服务端,只写反解逻辑


    if (ctx.path = '/sourcemap') {
        let sourceMapContent = fs.readFileSync(sourceMapFilePath, 'utf-8');
        let fileObj = JSON.parse(sourceMapContent);
        let { sources } = fileObj;

        sources.forEach(item => {
            sourceFileMap[fixPath(item)] = item;
        })

        let column = 554;//此处假设网络请求已经把报错位置上传上来
        let line = 17;
        const consumer = await new SourceMap.SourceMapConsumer(sourceMapContent);
        let result = consumer.originalPositionFor({
            line, column
        });
        /**
         * result
         * {
         *   source:"webpack:///react-app.js",
         *   line:10
         *   column:6
         *   name :vue    vue就是错误,未定义但是使用了
         * }
         */

        let originSource = sourceFileMap[result.source];
        //originSource:"webpack:///./react-app.js",

        //报错的代码-但是基本上是出错js的全部
        let sourceContent=fileObj.sourceContent[sources.indexOf(originSource)];
        //可以通过这个分行取出所需行数的上下几行,甚至标红出错的行
        let sourceContentArr=sourceContent.split('\n');
        ctx.body = {sourceContent, sourceContentArr,originSource, result };
    }
    //此处是koa的中间件,所以这么写,不需要特别在意
    return next();
}
  • 接口性能监控

核心就是类似代理模式,重写XMLHttpRequest的send和open函数,然后类似于面向切面的形式,在里面实现信息上报

export default {
    //TODO  自身SDK请求不需要拦截
    init: cb => {
        //xhr hook
        let xhr = window.XMLHttpRequest;
        //避免多次加载该hook,例如用户把sdk引用两次的情况
        if (xhr._eagle_monitor_flag === true) {
            return
        }
        xhr._eagle_monitor_flag = true;
        let _originOpen = xhr.prototype.open;
        //原生xhr有这几个参数
        xhr.prototype.open = function (method, url, async, user, password) {
            //此处是面向切面编程
            this._eagle_xhr_info = {
                url, method, status: null
            };
            return _originOpen.apply(this, arguments);
        }
        let _originSend = xhr.prototype.send;
        xhr.prototype.send = function (value) {
            let _self = this;
            this._eagle_start_time = Date.now();
            //注意此处,是多个箭头的高阶函数
            let ajaxEnd = eventType => () => {
                if (_self.response) {
                    let responseSize = null;
                    switch (_self.responseType) {
                        case 'json':
                            responseSize = JSON.stringify(_self.response).length;
                            break;
                        case 'arraybuffer':
                            responseSize = _self.response.byteLength;
                            break
                        default:
                            //注意这里:responseText和response区别
                            responseSize = _self.responseText.length;
                            break
                    }
                    _self._eagle_xhr_info.event = eventType;
                    _self._eagle_xhr_info.status = _self.status;
                    _self._eagle_xhr_info.success = _self.status === 200;
                    _self._eagle_xhr_info.duration = Date.now() - _self._eagle_start_time;
                    _self._eagle_xhr_info.responseSize = responseSize;
                    _self._eagle_xhr_info.requestSize = value ? value.length : 0;
                    _self._eagle_xhr_info.type = 'xhr';
                    cb(_self._eagle_xhr_info);
                }
                //注意:如果sdk也报错,如果不做任何处理会被sdk错误统计上报,然后上报继续报错,不一会CPU就是满,所以sdk错误需要捕获,然后特殊处理
            };
            //这三种状态都代表着请求已经结束了,需要统计一些信息并上报
            this.addEventListener('load', ajaxEnd('load'), false);
            this.addEventListener('error', ajaxEnd('error'), false);
            this.addEventListener('abort', ajaxEnd('abort'), false); //取消请求
            //上面是统计逻辑,这才是真实调用网络请求,例如json请求
            return _originSend.apply(this, arguments);
        }
        //ftech hook
        if (window.fetch) {
            let _origin_fetch = window.fetch;
            window.fetch = function () {
                let startTime = Date.now();
                let args = [].slice.call(arguments);

                let fetchInput = args[0];
                let method = 'GET';
                let url = null;
                if (typeof fetchInput === 'string') {
                    url = fetchInput
                } else if ('Request' in window && fetchInput instanceof window.Request) {
                    url = fetchInput.url;
                    if (fetchInput.method) {
                        method = fetchInput.method;
                    }
                } else {
                    url = '' + fetchInput;
                }

                let eagleFetchData = {
                    method, url, status: null
                }
                return _origin_fetch.apply(this,args).then(function(response){
                    eagleFetchData.status=response.status;
                    eagleFetchData.type='fetch';
                    eagleFetchData.status=response.status;
                    eagleFetchData.duration = Date.now() - startTime;
                    cb(eagleFetchData);
                    return response;
                })
            }
        }
    }
}
  1. 自身sdk请求不需要拦截,否则死循环
  2. 此处处理了两个一个是fetch一个是XMLHttpRequest

行为监控

行为监控重点只说一下用户行为路径,其他不做讨论,因为各种方式不同,其中打点监控,例如用户点击一下触发一次行为监控也算,方式很多。

核心是通过xpath形式,统计用户的行为,例如点击了什么

// /html/body/ul[1]/li[1]    xpath


/**
 * 当然xpath不能直接这么用,因为很多dom点击不需要上传
 * 但是比如说支付按钮,则肯定有特殊id或者类名,可以这样过滤需要的
 */



//获取我的元素是兄弟元素的第几个
let getIndex=ele=>{
    let children=[].slice.call(ele.parentNode.children);
    let myIndex=null;

    children=children.filter(node=>node.tagName===ele.tagName);
    for (let i = 0; i < children.length; i++) {
        if (ele===children[i]) {
            myIndex=i;
            break
        }
    }
    myIndex= `[${myIndex+1}]`;
    let tagName=ele.tagName.toLocaleLowerCase();
    let myLabel=tagName+myIndex;
    return myLabel;
}
let getXpath=ele=>{
    let xpath='';
    let currentEle=ele;
    while (currentEle!==document.body) {
        xpath=getIndex(currentEle)+'/'+xpath;
        currentEle=currentEle.parentNode;
    }

}
export default{
    init:cb=>{
        document.addEventListener('click',e=>{
            let target=e.target;
            let xpath=getXpath(target);

        },false);
    }
}

全局注册点击监控,然后通过递归遍历,查找到具体是点击什么,形成xpath类似于这种/html/body/ul[1]/li[1],然后可以通过不同类名或者id去过滤需要的行为。例如:支付按钮的样式肯定不同,而且一般不可能统计所有行为,因为数据量太大,如果完全不讲究全部上传也行,那样就需要后端处理逻辑去过滤行为,否则行为统计页面数据量太多,完全无法有效观看。

补充:前端页面打点为什么一般要用gif打点

参考-拷贝: https://blog.csdn.net/weixin_37719279/article/details/103476567?utm_medium=distribute.pc_aggpage_search_result.none-task-blog-2allfirst_rank_v2~rank_v25-7-103476567.nonecase&utm_term=%E6%89%93%E7%82%B9%E7%9A%84%E6%84%8F%E6%80%9D

所谓的前端监控,其实是在满足一定条件后,由Web页面将用户信息(UA/鼠标点击位置/页面报错/停留时长/etc)上报给服务器的过程。一般是将上报数据用url_encode(百度统计/CNZZ)或JSON编码(神策/诸葛io)为字符串,通过url参数传递给服务器,然后在服务器端统一处理。

这套流程的关键在于:

  • 能够收集到用户信息;
  • 能够将收集到的数据上报给服务器。也就是说,只要能上报数据,无论是请求GIF文件还是请求js文件或者是调用页面接口,服务器端其实并不关心具体的上报方式。

向服务器端上报数据,可以通过请求接口,请求普通文件,或者请求图片资源的方式进行。为什么所有系统都统一使用了请求GIF图片的方式上报数据呢?

  • 首先,为什么不能直接用GET/POST/HEAD请求接口进行上报?

这个比较容易想到原因。一般而言,打点域名都不是当前域名,所以所有的接口请求都会构成跨域。而跨域请求很容易出现由于配置不当被浏览器拦截并报错,这是不能接受的。所以,直接排除。

  • 其次,为什么不能用请求其他的文件资源(js/css/ttf)的方式进行上报?

这和浏览器的特性有关。通常,创建资源节点后只有将对象注入到浏览器DOM树后,浏览器才会实际发送资源请求。反复操作DOM不仅会引发性能问题,而且载入js/css资源还会阻塞页面渲染,影响用户体验。

但是图片请求例外。构造图片打点不仅不用插入DOM,只要在js中new出Image对象就能发起请求,而且还没有阻塞问题,在没有js的浏览器环境中也能通过img标签正常打点,这是其他类型的资源请求所做不到的。

  • 那还剩下最后一个问题,同样都是图片,上报时选用了1x1的透明GIF,而不是其他的PNG/JEPG/BMP文件

首先,1x1像素是最小的合法图片。而且,因为是通过图片打点,所以图片最好是透明的,这样一来不会影响页面本身展示效果,二者表示图片透明只要使用一个二进制位标记图片是透明色即可,不用存储色彩空间数据,可以节约体积。因为需要透明色,所以可以直接排除JEPG(BMP32格式可以支持透明色)。

  • 然后还剩下BMP、PNG和GIF,但是为什么会选GIF呢?

因为体积!,gif最小,最小的BMP文件需要74个字节,PNG需要67个字节,而合法的GIF,只需要43个字节。同样的响应,GIF可以比BMP节约41%的流量,比PNG节约35%的流量。这样比较一下,答案就很明显了。

总结

前端监控使用GIF进行上报主要是因为:

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