cordova源码文件cordova.js分析(3)

cordova/init module 分析

var channel = require('cordova/channel');
var cordova = require('cordova');
var modulemapper = require('cordova/modulemapper');
var platform = require('cordova/platform');
var pluginloader = require('cordova/pluginloader');
var utils = require('cordova/utils');

这是cordova/init所需要依赖的module.为了看懂这个模块,我们先把上述modules全部给分析了

var platformInitChannelsArray = [channel.onNativeReady, channel.onPluginsReady];

声明数组,保存channel的两个属性

function logUnfiredChannels (arr) {
    for (var i = 0; i < arr.length; ++i) {
        if (arr[i].state !== 2) {
            console.log('Channel not fired: ' + arr[i].type);
        }
    }
}

日志的打印函数

window.setTimeout(function () {
    if (channel.onDeviceReady.state !== 2) {
        console.log('deviceready has not fired after 5 seconds.');
        logUnfiredChannels(platformInitChannelsArray);
        logUnfiredChannels(channel.deviceReadyChannelsArray);
    }
}, 5000);

五秒后检查channel的onDeviceReady 状态

function replaceNavigator (origNavigator) {
    var CordovaNavigator = function () {};
    CordovaNavigator.prototype = origNavigator;
    var newNavigator = new CordovaNavigator();
    // This work-around really only applies to new APIs that are newer than Function.bind.
    // Without it, APIs such as getGamepads() break.
    if (CordovaNavigator.bind) {
        for (var key in origNavigator) {
            if (typeof origNavigator[key] === 'function') {
                newNavigator[key] = origNavigator[key].bind(origNavigator);
            } else {
                (function (k) {
                    utils.defineGetterSetter(newNavigator, key, function () {
                        return origNavigator[k];
                    });
                })(key);
            }
        }
    }
    return newNavigator;
}

替换导航

if (window.navigator) {
    window.navigator = replaceNavigator(window.navigator);
}

要是window有导航,替换成我们自己需要的

if (!window.console) {
    window.console = {
        log: function () {}
    };
}
if (!window.console.warn) {
    window.console.warn = function (msg) {
        this.log('warn: ' + msg);
    };
}

日志相关设置

channel.onPause = cordova.addDocumentEventHandler('pause');
channel.onResume = cordova.addDocumentEventHandler('resume');
channel.onActivated = cordova.addDocumentEventHandler('activated');
channel.onDeviceReady = cordova.addStickyDocumentEventHandler('deviceready');

这里需要注意,我们对channel 的 onPause onResume onActivated onDeviceReady 重新生成Channel.

onActivated 以前是没有的,在这个时候添加的

并且这四个事件的句柄在cordova对象的documentEventHandlers 有保存的

if (document.readyState === 'complete' || document.readyState === 'interactive') {
    channel.onDOMContentLoaded.fire();
} else {
    document.addEventListener('DOMContentLoaded', function () {
        channel.onDOMContentLoaded.fire();
    }, false);
}

这里我们需要知道document 有四种状态

  1. uninitialized - 还未开始载入
  2. loading - 载入中
  3. interactive - 已加载,文档与用户可以开始交互
  4. complete - 载入完成

当这个属性的值变化时,document 对象上的readystatechange 事件将被触发。

这里还需要知道

DOMContentLoaded顾名思义,就是dom内容加载完毕。那什么是dom内容加载完毕呢?我们从打开一个网页说起。当输入一个URL,页面的展示首先是空白的,然后过一会,页面会展示出内容,但是页面的有些资源比如说图片资源还无法看到,此时页面是可以正常的交互,过一段时间后,图片才完成显示在页面。从页面空白到展示出页面内容,会触发DOMContentLoaded事件。而这段时间就是HTML文档被加载和解析完成。

这时候问题又来了,什么是HTML文档被加载和解析完成。要解决这个问题,我们就必须了解浏览器渲染原理。

当我们在浏览器地址输入URL时,浏览器会发送请求到服务器,服务器将请求的HTML文档发送回浏览器,浏览器将文档下载下来后,便开始从上到下解析,解析完成之后,会生成DOM。如果页面中有css,会根据css的内容形成CSSOM,然后DOM和CSSOM会生成一个渲染树,最后浏览器会根据渲染树的内容计算出各个节点在页面中的确切大小和位置,并将其绘制在浏览器上。

746387-20170407181220066-2064922697.png

下面就是页面加载和解析过程中,浏览器的一个快照

上面我们看到在解析html的过程中,html的解析会被中断,这是因为javascript会阻塞dom的解析。当解析过程中遇到<script>标签的时候,便会停止解析过程,转而去处理脚本,如果脚本是内联的,浏览器会先去执行这段内联的脚本,如果是外链的,那么先会去加载脚本,然后执行。在处理完脚本之后,浏览器便继续解析HTML文档。

同时javascript的执行会受到标签前面样式文件的影响。如果在标签前面有样式文件,需要样式文件加载并解析完毕后才执行脚本。这是因为javascript可以查询对象的样式。

这里需要注意一点,在现在浏览器中,为了减缓渲染被阻塞的情况,现代的浏览器都使用了猜测预加载。当解析被阻塞的时候,浏览器会有一个轻量级的HTML(或CSS)扫描器(scanner)继续在文档中扫描,查找那些将来可能能够用到的资源文件的url,在渲染器使用它们之前将其下载下来。

在这里我们可以明确DOMContentLoaded所计算的时间,当文档中没有脚本时,浏览器解析完文档便能触发 DOMContentLoaded 事件;如果文档中包含脚本,则脚本会阻塞文档的解析,而脚本需要等位于脚本前面的css加载完才能执行。在任何情况下,DOMContentLoaded 的触发不需要等待图片等其他资源加载完成。

接下来,我们来说说load,页面上所有的资源(图片,音频,视频等)被加载以后才会触发load事件,简单来说,页面的load事件会在DOMContentLoaded被触发之后才触发。

小知识

最后我们来回答这个问题:我们为什么一再强调将css放在头部,将js文件放在尾部

在面试的过程中,经常会有人在回答页面的优化中提到将js放到body标签底部,原因是因为浏览器生成Dom树的时候是一行一行读HTML代码的,script标签放在最后面就不会影响前面的页面的渲染。那么问题来了,既然Dom树完全生成好后页面才能渲染出来,浏览器又必须读完全部HTML才能生成完整的Dom树,script标签不放在body底部是不是也一样,因为dom树的生成需要整个文档解析完毕。

746387-20170407181912191-1031407943.png

我们再来看一下chrome在页面渲染过程中的,绿色标志线是First Paint的时间。纳尼,为什么会出现firstpaint,页面的paint不是在渲染树生成之后吗?其实现代浏览器为了更好的用户体验,渲染引擎将尝试尽快在屏幕上显示的内容。它不会等到所有HTML解析之前开始构建和布局渲染树。部分的内容将被解析并显示。也就是说浏览器能够渲染不完整的dom树和cssom,尽快的减少白屏的时间。假如我们将js放在header,js将阻塞解析dom,dom的内容会影响到First Paint,导致First Paint延后。所以说我们会将js放在后面,以减少First Paint的时间,但是不会减少DOMContentLoaded被触发的时间。

从上面的知识,我们知道.当文档渲染或者加载完毕.我们需要channel的onDOMContentLoaded 属性执行fire方法.

if (window._nativeReady) {
    channel.onNativeReady.fire();
}

不知道干啥用,该window在哪添加的该属性

modulemapper.clobbers('cordova', 'cordova');
modulemapper.clobbers('cordova/exec', 'cordova.exec');
modulemapper.clobbers('cordova/exec', 'Cordova.exec');

这里我们看出来了.我们向modulemapper 中添加数据的样本了

modulemapper->symbolList 结构体

image-20190611162318243.png
platform.bootstrap && platform.bootstrap();
define("cordova/platform", function(require, exports, module) {

module.exports = {
    id: 'ios',
    bootstrap: function () {
        // Attach the console polyfill that is iOS-only to window.console
        // see the file under plugin/ios/console.js
        require('cordova/modulemapper').clobbers('cordova/plugin/ios/console', 'window.console');

        require('cordova/channel').onNativeReady.fire();
    }
};

});

上面看出来,我们在modulemapper->symbolList 又加入了新的数据,打印数据

并且这里更新了channel对象onNativeReady属性的状态

setTimeout(function () {
    pluginloader.load(function () {
        channel.onPluginsReady.fire();
    });
}, 0);

相当于立即执行pluginloader的load方法.要是load方法执行完毕,就更新channel 属性onPluginsReady 状态.

这里是加载插件

这里如何解析插件的cordova/plugin_list.在后面章节分析

channel.join(function () {
    modulemapper.mapModules(window);

    platform.initialize && platform.initialize();

    // Fire event to notify that all objects are created
    channel.onCordovaReady.fire();

    // Fire onDeviceReady event once page has fully loaded, all
    // constructors have run and cordova info has been received from native
    // side.
    channel.join(function () {
        require('cordova').fireDocumentEvent('deviceready');
    }, channel.deviceReadyChannelsArray);

}, platformInitChannelsArray);
  1. platformInitChannelsArray 数组内容 var platformInitChannelsArray = [channel.onNativeReady, channel.onPluginsReady];

  2. join方法

     join: function (h, c) {
            var len = c.length;
            var i = len;
            var f = function () {
                if (!(--i)) h();
            };
            for (var j = 0; j < len; j++) {
                if (c[j].state === 0) {
                    throw Error('Can only use join with sticky channels.');
                }
                c[j].subscribe(f);
            }
            if (!len) h();
        },
    

join方法,我们知道c = [channel.onNativeReady, channel.onPluginsReady]

这里就好理解join方法了.只有当channel的onNativeReady, 和channel.onPluginsReady 都fire之后,才能调用函数f

分析函数f

  1. 调用 modulemapper.mapModules(window);
  2. 平台需要如果需要初始化就调用初始化函数
  3. 跟新channel状态
  4. 当channel的onCordovaReady 和 onDOMContentLoaded 都加载完毕,执行5
  5. require('cordova').fireDocumentEvent('deviceready');

这里第一步,上面的章节由于没有具体的context.因此没有具体分析mapModules 这里具体分析

exports.mapModules = function (context) {
    var origSymbols = {};
    context.CDV_origSymbols = origSymbols;
    for (var i = 0, len = symbolList.length; i < len; i += 3) {
        var strategy = symbolList[I];
        var moduleName = symbolList[i + 1];
        var module = require(moduleName);
        // <runs/>
        if (strategy === 'r') {
            continue;
        }
        var symbolPath = symbolList[i + 2];
        var lastDot = symbolPath.lastIndexOf('.');
        var namespace = symbolPath.substr(0, lastDot);
        var lastName = symbolPath.substr(lastDot + 1);

        var deprecationMsg = symbolPath in deprecationMap ? 'Access made to deprecated symbol: ' + symbolPath + '. ' + deprecationMsg : null;
        var parentObj = prepareNamespace(namespace, context);
        var target = parentObj[lastName];

        if (strategy === 'm' && target) {
            builder.recursiveMerge(target, module);
        } else if ((strategy === 'd' && !target) || (strategy !== 'd')) {
            if (!(symbolPath in origSymbols)) {
                origSymbols[symbolPath] = target;
            }
            builder.assignOrWrapInDeprecateGetter(parentObj, lastName, module, deprecationMsg);
        }
    }
};

context 是window

这里symbolList 中的数据有是["c", "cordova", "cordova", "c","cordova/exec", "cordova.exec", "c","cordova/exec", "Cordova.exec","c", "cordova/plugin/ios/console", "window.console"]

  1. 给window的CDV_origSymbols 赋值对象{}

  2. 遍历symbolList 数组,三个数据一组处理

  3. 获取类型和module的名字

  4. 这里require module (相当于将module中没有转换成exports的进行转换成exports,cordova/exec和cordova/plugin/ios/console 需要加载)

  5. 对cordova , cordova.exec,"Cordova.exec ,"window.console 进行处理赋值给相关变量

  6. 调用prepareNamespace 就是判断是否window 是否有属性namespace(symbolPath以点分割的第一部分,这里)

  7. 这里就是动态添加属性到相关变量下,根是window.

    假设symbolPath=cordova ,那么我就需要把require的module 赋值到window的属性cordova下面

    假设symbolPath=cordova.exec ,那么我们就需要把require的module值赋值给window->cordova->exec下面

    假设symbolPath=Cordova.exec,由于window没有属性,因此这个就给抛弃了

    假设symbolPath=window.console 我们就更新window->console的属性

到此为止cordova分析完毕

cordova/plugin_list 的exports

cordova.define('cordova/plugin_list', function(require, exports, module) {
  module.exports = [];
  module.exports.metadata = {
    "cordova-plugin-whitelist": "1.3.3"
  };
});

这里没啥可说的.

cordova/exec 从factory到exports

我们知道该module加载时在cordova/init->modulemapper.mapModules(window)加载的

我们看看到底加载了啥

var cordova = require('cordova'),
    utils = require('cordova/utils'),
    base64 = require('cordova/base64'),
    execIframe,
    commandQueue = [], // Contains pending JS->Native messages.
    isInContextOfEvalJs = 0,
    failSafeTimerId = 0;

加载exec依赖模块和声明属性

function massageArgsJsToNative(args) {
    if (!args || utils.typeName(args) != 'Array') {
        return args;
    }
    var ret = [];
    args.forEach(function(arg, i) {
        if (utils.typeName(arg) == 'ArrayBuffer') {
            ret.push({
                'CDVType': 'ArrayBuffer',
                'data': base64.fromArrayBuffer(arg)
            });
        } else {
            ret.push(arg);
        }
    });
    return ret;
}

  1. 参数必须是数组,不是直接返回
  2. 单独处理数组中的值是ArrayBuffer的对象. 返回数组(这里用到了base64,不影响这里逻辑功能,不分析)
function massageMessageNativeToJs(message) {
    if (message.CDVType == 'ArrayBuffer') {
        var stringToArrayBuffer = function(str) {
            var ret = new Uint8Array(str.length);
            for (var i = 0; i < str.length; i++) {
                ret[i] = str.charCodeAt(i);
            }
            return ret.buffer;
        };
        var base64ToArrayBuffer = function(b64) {
            return stringToArrayBuffer(atob(b64));
        };
        message = base64ToArrayBuffer(message.data);
    }
    return message;
}

把ArrayBuffer base64 还原 这里不探讨具体还原过程

function convertMessageToArgsNativeToJs(message) {
    var args = [];
    if (!message || !message.hasOwnProperty('CDVType')) {
        args.push(message);
    } else if (message.CDVType == 'MultiPart') {
        message.messages.forEach(function(e) {
            args.push(massageMessageNativeToJs(e));
        });
    } else {
        args.push(massageMessageNativeToJs(message));
    }
    return args;
}

处理message,返回一个数组

function iOSExec() {

    var successCallback, failCallback, service, action, actionArgs;
    var callbackId = null;
    if (typeof arguments[0] !== 'string') {
        // FORMAT ONE
        successCallback = arguments[0];
        failCallback = arguments[1];
        service = arguments[2];
        action = arguments[3];
        actionArgs = arguments[4];

        // Since we need to maintain backwards compatibility, we have to pass
        // an invalid callbackId even if no callback was provided since plugins
        // will be expecting it. The Cordova.exec() implementation allocates
        // an invalid callbackId and passes it even if no callbacks were given.
        callbackId = 'INVALID';
    } else {
        throw new Error('The old format of this exec call has been removed (deprecated since 2.1). Change to: ' +
            'cordova.exec(null, null, \'Service\', \'action\', [ arg1, arg2 ]);'
        );
    }

    // If actionArgs is not provided, default to an empty array
    actionArgs = actionArgs || [];

    // Register the callbacks and add the callbackId to the positional
    // arguments if given.
    if (successCallback || failCallback) {
        callbackId = service + cordova.callbackId++;
        cordova.callbacks[callbackId] =
            {success:successCallback, fail:failCallback};
    }

    actionArgs = massageArgsJsToNative(actionArgs);

    var command = [callbackId, service, action, actionArgs];

    // Stringify and queue the command. We stringify to command now to
    // effectively clone the command arguments in case they are mutated before
    // the command is executed.
    commandQueue.push(JSON.stringify(command));

    // If we're in the context of a stringByEvaluatingJavaScriptFromString call,
    // then the queue will be flushed when it returns; no need for a poke.
    // Also, if there is already a command in the queue, then we've already
    // poked the native side, so there is no reason to do so again.
    if (!isInContextOfEvalJs && commandQueue.length == 1) {
        pokeNative();
    }
}

  1. 获取函数传入的参数.
  2. 变量actionArgs 赋值,是空的话,设置成[]
  3. 生成callBackId ,并将callBackId 保存到cordova的变量callbacks 中,还有回调函数
  4. 对actionArgs 中的数据进行处理.让其不含有ArrayBuffer数据类型的数据
  5. 生成变量command
  6. 将 command 转换成json对象存入commandQueue数组
  7. 如果isInContextOfEvalJs == 0 并且数组长度是1 ,那么调用pokeNative 函数

这个函数就是简单的对传入的参数进行处理,保存callbackId到cordova->callbacks中

function execProxy() {
    cordovaExec().apply(null, arguments);
};

execProxy.nativeFetchMessages = function() {
    return cordovaExec().nativeFetchMessages.apply(null, arguments);
};

execProxy.nativeEvalAndFetch = function() {
    return cordovaExec().nativeEvalAndFetch.apply(null, arguments);
};

execProxy.nativeCallback = function() {
    return cordovaExec().nativeCallback.apply(null, arguments);
};

module.exports = execProxy;

我们看出该模块返回的exports 结构是一个函数.该函数增加了一个属性nativeFetchMessages,nativeEvalAndFetch,nativeCallback.

我们知道cordova/exec exports绑定在window->cordova->exec命令下

当我们调用cordova.exec 相当于调用了execProxy()函数接着调用cordovaExec()函数

function cordovaExec() {
    var cexec = require('cordova/exec');
    var cexec_valid = (typeof cexec.nativeFetchMessages === 'function') && (typeof cexec.nativeEvalAndFetch === 'function') && (typeof cexec.nativeCallback === 'function');
    return (cexec_valid && execProxy !== cexec)? cexec : iOSExec;
}

该函数在ios中cexec_valid 是true,但是execProxy==cexec.因此这里返回的是iOSExec

接下来就调用到iOSExec()函数中

function proxyChanged() {
    var cexec = cordovaExec();
       
    return (execProxy !== cexec && // proxy objects are different
            iOSExec !== cexec      // proxy object is not the current iOSExec
            );
}

判断执行命令对象是否发生改变

function handleBridgeChange() {
    if (proxyChanged()) {
        var commandString = commandQueue.shift();
        while(commandString) {
            var command = JSON.parse(commandString);
            var callbackId = command[0];
            var service = command[1];
            var action = command[2];
            var actionArgs = command[3];
            var callbacks = cordova.callbacks[callbackId] || {};
            
            execProxy(callbacks.success, callbacks.fail, service, action, actionArgs);
            
            commandString = commandQueue.shift();
        };
        return true;
    }
    
    return false;
}

如果对象发生改变了,那么需要重新将队列里面的命令给执行一遍.

function pokeNative() {
    // CB-5488 - Don't attempt to create iframe before document.body is available.
    if (!document.body) {
        setTimeout(pokeNative);
        return;
    }
    
    // Check if they've removed it from the DOM, and put it back if so.
    if (execIframe && execIframe.contentWindow) {
        execIframe.contentWindow.location = 'gap://ready';
    } else {
        execIframe = document.createElement('iframe');
        execIframe.style.display = 'none';
        execIframe.src = 'gap://ready';
        document.body.appendChild(execIframe);
    }
    // Use a timer to protect against iframe being unloaded during the poke (CB-7735).
    // This makes the bridge ~ 7% slower, but works around the poke getting lost
    // when the iframe is removed from the DOM.
    // An onunload listener could be used in the case where the iframe has just been
    // created, but since unload events fire only once, it doesn't work in the normal
    // case of iframe reuse (where unload will have already fired due to the attempted
    // navigation of the page).
    failSafeTimerId = setTimeout(function() {
        if (commandQueue.length) {
            // CB-10106 - flush the queue on bridge change
            if (!handleBridgeChange()) {
                pokeNative();
             }
        }
    }, 50); // Making this > 0 improves performance (marginally) in the normal case (where it doesn't fire).
}

  1. 判断文档没有body,那么等到document生成body接着执行
  2. 要是设置了execIframe.那么调用 execIframe.contentWindow.location = 'gap://ready';(第一次是调用不到,到else中设置好了才能调用)
  3. 要是没有设置execIframe,那么生成execlframe.并且设置该元素不展示,并且设置src
  4. 50ms再次执行pokeNative 函数(这个时候就可以调用到2 步骤了)

当 execIframe.contentWindow.location =gap://ready 这个时候就是和原生应用做交互的时候了

iOSExec.nativeFetchMessages = function() {
    // Stop listing for window detatch once native side confirms poke.
    if (failSafeTimerId) {
        clearTimeout(failSafeTimerId);
        failSafeTimerId = 0;
    }
    // Each entry in commandQueue is a JSON string already.
    if (!commandQueue.length) {
        return '';
    }
    var json = '[' + commandQueue.join(',') + ']';
    commandQueue.length = 0;
    return json;
};

iOSExec.nativeCallback = function(callbackId, status, message, keepCallback, debug) {
    return iOSExec.nativeEvalAndFetch(function() {
        var success = status === 0 || status === 1;
        var args = convertMessageToArgsNativeToJs(message);
        function nc2() {
            cordova.callbackFromNative(callbackId, success, status, args, keepCallback);
        }
        setTimeout(nc2, 0);
    });
};

iOSExec.nativeEvalAndFetch = function(func) {
    // This shouldn't be nested, but better to be safe.
    isInContextOfEvalJs++;
    try {
        func();
        return iOSExec.nativeFetchMessages();
    } finally {
        isInContextOfEvalJs--;
    }
};

这三个函数调用时机暂时不明.应该是在应用内部调用的

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

推荐阅读更多精彩内容