谷歌插件

谷歌插件开发笔记

插件开发相关及问题记录。

开发目标:具有登录功能及可以展示频道列表信息。
延伸问题:可以生成二维码及提取当前网页的主要内容。
  1. 第一阶段,原生chrome extension阶段,调用chorme的API

    谷歌程序插件的本质就是一个网页展示的小窗口。html搭建界面,css完成布局,js完成交互操作。外加一些chrome的API,去特定的实现一些功能。

    1. 插件的配置文件。
    manifest.json 文件(必须有的)
    更多此文件的属性设置参考http://open.chrome.360.cn/extension_dev/manifest.html
    此文件就是插件的配置文件。定义插件名称,插件图片,插件的出现位置等等
    
         {
            "manifest_version": 2, //默认设置,必有
            "name": "TR-iOS", //扩展程序的名字
            "version": "1.0", //扩展程序的当前版本号
            "icons": { //扩展程序的在设置一栏显示的图片
            "16": "images/tr_icon16.png",
            "48": "images/tr_icon48.png",
            "128": "images/tr_icon128.png"
            },
            "browser_action": { //扩展程序类型,即显示在浏览器输入url框外,而不是内
            "default_title": "ios extension", //当把鼠标放到浏览器上此程序的图标上时显示的名字
            "default_popup": "popup.html", //点击图标时出现的界面,与点击响应事件不同时响应。设置此项则不响应点击事件
            "default_icon": { //在浏览器框显示的图片
                "19": "images/tr_icon19.png",
                "38": "images/tr_icon38.png"
                }
            },
            "permissions": [ //权限设置
            "activeTab",
            "tabs" ,
            "http://*/*", //默认在任何http://开始的网页都能启动此程序
            "https://*/*" //默认在任何https://开始的网页都能启动此程序
            ],
            "background": { //当程序扩展开启时,就会在后台运行的
                "scripts": ["js/background.js"], //运行的js文件
                "persistent": true //false为按需运行,true是一直运行
            },
            //更多此属性介绍参考http://open.chrome.360.cn/extension_dev/content_scripts.html
            "content_scripts": [ //在Web页面内运行的javascript脚本
                {
                    "matches": ["http://*/*","https://*/*"],  //满足什么条件执行该js脚本
                    "js": ["js/Readability.js" ,"js/contentJS.js"] //js文件 写入当前界面的js文件
                    "run_at": "document_idle"          //控制content script注入的时机。可以是document_start, document_end或者document_idle。缺省时是document_idle。
                    //如果是document_start, 文件将在所有CSS加载完毕,但是没有创建DOM并且没有运行任何脚本的时候注入。
                    //如果是document_end,则文件将在创建完DOM之后,但还没有加载类似于图片或frame等的子资源前立刻注入。
                    //如果是document_idle,浏览器会在document_end和发出window.onload事件之间的某个时机注入。具体的时机取决与文档加载的复杂度,为加快页面加载而优化。
                }
            ]
        }
    
    

    此文件定义了,插件的基本属性。主要有标题,图标,权限。及popup.html,点击出现的界面。以及启动时默认运行的background.js文件。
    以及在什么时间,在什么页面下,注入到当前网页的js文件 设置。完成了最基本的设置。可以自己根据需求添加或者减少部分字段。

    1. 配置文件可以根据实际需求不断修改。根据manifest.json文件。可以具体的在不同的文件里处理实际问题。
    popup.html
    
    <!DOCTYPE html>
    <html>
        <head>
            <meta charset="utf-8">
            <title></title>
        </head>
        <body style="width: 320px; height: 400px;">
            <script type="text/javascript" src="js/popup.js"></script>
        </body>
    </html>
    **chrome不允许扩展中的HTML页面内直接内嵌js脚本,而要求所有的脚本都作为外部src来引入**
    定义了界面的大小和导入了popup.js文件。
    此界面是一个空的界面,根据popup.js文件进行判断出现登录界面还是频道信息界面
    。在点击进行处理的时间内,给了此界面一个大小,是为了让界面的切换更加顺畅(如果不给会先出现一个小白块,在出现具体的界面(popup会自适应大小))。
    
    > popup.js
    
    // 获得背景页对象
    var backgroundPage = chrome.extension.getBackgroundPage();
    // 将背景页的属性cur_url作为将要展示的界面的值
    // location.href 可以改变当前出现的界面
    location.href = backgroundPage.cur_url;
    
    > backgroud.js 作为一个中间界面存储着当前网页的URL和title
    
    // 定义属性
    var cur_url;
    var cur_title;
    // 接收信息 chrome的API
    chrome.runtime.onMessage.addListener(function(request, sender, sendResponse){
        cur_url = request.cur_url;
        cur_title = request.cur_title;
    });
    
    // 判断出现的网页 根据本地对象localStorage.success属性 此属性会在登录成功的时候被设置
    var success = localStorage.success;
    if (success == "true") {
        cur_url = "main.html";
    } else {
        cur_url = "login.html";
    }
    
    > contentJS.js 文件是运行在当前网页中的脚本文件 部分代码如下
    
    // 发送信息
    chrome.runtime.sendMessage({
        cur_url: docment.URL,
        cur_title: docment.title
    });
    
    // 获取网页正文
    var loc = document.location;
    var url = {
        spec: loc.href,
        host: loc.host,
        prePath: loc.protocol + "//" + loc.host,
        scheme: loc.protocol.substr(0, loc.protocol.indexOf(":")),
        pathBase: loc.protocol + "//" + loc.host + loc.pathname.substr(0, loc.pathname.lastIndexOf("/") + 1)
    };
    
    // clone一个document对象,因为Readability是通过对当前网页的DOM的修改来进行解析的。这会删除当前网页的一些元素。
    // 克隆一个新的对象,就是对新的对象的操作,则不会影响当前网页的正常展示
    var documentClone = document.cloneNode(true);
    var article = new Readability(url, documentClone).parse();
    if (article) { // 如果读取到内容,则处理,没有则提示
        console.log(article.textContent);
    } else {
        // alert("读取失败");
    }
    
    // clone一个document对象,因为Readability是通过对当前网页的DOM的修改来进行解析的。这会删除当前网页的一些元素。
    // 克隆一个新的对象,就是对新的对象的操作,则不会影响当前网页的正常展示
    var documentClone = document.cloneNode(true);
    var article = new Readability(url, documentClone).parse();
    if (article) { // 如果读取到内容,则处理,没有则提示
        console.log(article.textContent);
    } else {
        // alert("读取失败");
    }
    解释下,正文提取的算法是Readability,其他网页提取正文相关请看下面的资料参考。Readability.js默认是直接在当前网页加载完毕之后注入的。
    
    > login.js 中有两点。
    1. 获取当前页面的属性时,曾考虑过contentJS.js中获取,然后发送给background.js。
    但是就会局限在,插件的开启在网页打开之前。且如果网页已存在,在打开插件,必须刷新界面才可以获得。因此抛弃了。
    正确打开姿势为
    chrome.tabs.getSelected(null, function(tab) {
        qrcode.makeCode(tab.url);
    });
    2. 生成二维码
    导入qrcode.js, qrcode.min.js文件
    // 生二维码图片
    function makeCode () {
      // 创建QRCode 对象
      var qrcode = new QRCode(document.getElementById("qrcode"), {
           width : 150,
           height : 150
       });
    
       chrome.tabs.getSelected(null, function(tab) {
            qrcode.makeCode(tab.url);
        });
    }
    
    **插件通信相关**
    这个时候出现一个问题。插件本身与注入到当前网页的js文件的通信问题,以及插件本身各个界面的通信问题。
    一般会转化为
    1. 插件与注入到网页的js文件的通信
        即background.js与contentJS.js直接的通信。
        消息传递分为两种,一种是单次的消息请求,另外一种是长连接。
        一,单次的消息请求
            > contenJS.js
                // 发送信息
                chrome.runtime.sendMessage({
                    greeting: "您好",
                    cur_url: docment.URL,
                    cur_title: docment.title
                });
                
            > backgroud.js
                old method getSelected在chrome.33后弃用
                chrome.tabs.getSelected(null, function(tab) {
                    chrome.tabs.sendMessage(tab.id, {greeting: "您好"}, function(response) {
                    console.log(response.farewell);
                    });
                });
                
                new method chrome.33后替代getSelected方法
                    chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
                        chrome.tabs.sendMessage(tabs[0].id, {greeting: "您好"}, function(response) {
                            console.log(response.farewell);
                        });
                    });
            
            
            > contenJS.js 与 background.js 接收信息
                // 接收信息
                chrome.runtime.onMessage.addListener(function(request, sender, sendResponse){
                    // 可以根据sender.tab存在ze则是background.js 否则contentJS.js
                    console.log(sender.tab ?
                                "来自内容脚本:" + sender.tab.url :
                                "来自扩展程序");
                    // greenting 是参数字段即上例子的cur_url
                    if (request.greeting == "您好")
                        // 接收到还可以回复一条简单信息
                        sendResponse({farewell: "再见"});
                });
                如果有多个页面同时监听 onMessage 事件,那么只有第一个调用 sendResponse() 的页面可以成功返回响应信息,其它的都会被忽略。
        二,长连接
            > 与短连接类似 contenJS.js
                // 创建长连接
                var port = chrome.runtime.connect({name: "敲门"});
                port.postMessage({joke: "敲门"});
                port.onMessage.addListener(function(msg) {
                    if (msg.question == "是谁?") {
                        port.postMessage({answer: "女士"});
                    } else if (msg.question == "哪位女士?") {
                        port.postMessage({answer: "Bovary 女士"});
                    }
                });
            > background.js
                由于这个文件在插件开启时就会存在,所以他发送信息时,contentJS.js还没有注入到当前网页中。
                所以测试方式为:
                    如果没有popup.html界面,则为点击图标的时候
                    chrome.browserAction.onClicked.addListener(function(tab) {
                        postMessage(tab.id);
                    });
                    如果有的话
                    在popup.js中调用背景页的这个方法即可
                    function sendMessage() {
                        chrome.tabs.getSelected(null, function(tab) {
                            postMessage(tab.id);
                        });
                    }
                    
                    function postMessage(tabID) {
                        // 长连接
                        var port = chrome.tabs.connect(tabID, {name: "敲门"});
                        port.postMessage({joke: "敲门"});
                        port.onMessage.addListener(function(msg) {
                            if (msg.question == "是谁?") {
                                port.postMessage({answer: "女士"});
                            } else if (msg.question == "哪位女士?") {
                                port.postMessage({answer: "Bovary 女士"});
                            }
                        });
                    }
                    问题是:请求发出了,但是调试时postMessage的信息没有监听到。原因未知
            监听的方法为
                chrome.runtime.onConnect.addListener(function(port) {
                    if (port.name == "敲门") {
                        port.onMessage.addListener(function(msg) {
                            if (msg.joke == "敲门") {
                                port.postMessage({question: "是谁?"});
                            } else if (msg.answer == "女士") {
                                port.postMessage({question: "哪位女士?"});
                            } else if (msg.answer == "Bovary 女士") {
                                port.postMessage({question: "我没听清楚。"});
                            }
                        });
                    }
                });
        三,更多解析请参考
            https://crxdoc-zh.appspot.com/extensions/messaging(需翻墙)
            http://open.chrome.360.cn/extension_dev/messaging.html#simple
            
    2. 插件内部之间的通信
        即background.js与其他各个界面的通信。对于 background 和 popup ,其实都是运行在同一个进程中的,所以background 和 popup 之间可以直接相互调用对方的方法,不需要消息传递
        >   popup.js
            var test = "textStatus";
            // 获得背景页对象
            var backgroundPage = chrome.extension.getBackgroundPage();
            // 将背景页的属性cur_url作为将要展示的界面的值
            // location.href 可以改变当前出现的界面
            location.href = backgroundPage.cur_url;
            // 调用背景页的方法
            backgroundPage.getPopupPage();
            
        > backgroud.js 中
            // 调用popup.js
            function getPopupPage() {
                // 获得popup.html的相关属性及方法
                var popupPage = chrome.extension.getViews({type:'popup'}); // 获取popup页面
                bgTest = popupPage[0].test;
                alert(bgTest);
            }
            即可弹出警告框内容是textStatus。
            注意一定要指明type,如果没有指定,则获取Background Page之外的所有Extension Page的window对象。
            然后就是background是一个运行在扩展进程中的HTML页面。它在你的扩展的整个生命周期都存在,
            而popup是在你点击了图标之后才存在,所以,在获取popup变量时,请确认popup已打开。
    
    css相关
    @charset "UTF-8";
    /* body标签的属性 */ 即html一些元素属性的设置
    body {
        background-color:#2c3335;
        color:#f5f5f5;
        /*text-align:center;*/
        font-family:"Lucida Grande", "DejaVu Sans", Verdana, sans-serif;
        width: 320px;
        height: 400px;
    }
    
    /* id = loginform 的属性 */ 自己设置的元素的id的属性的设置
    #loginform {
        margin-top:20px;
        margin-left:auto;
        margin-right:auto;
        width:300px;
    }
    
    /* class = input 的属性设置 */ 自己设置的元素的class的属性的设置
    .input {
        width:300px;
        background: #f5f5f5;
        border:none;
        border-radius: 4px;
        color: #333;
        font-size: 14px;
        margin-top:10px;
    }
    
    
    javaScript相关 只说我遇到的
    1. 添加对某个元素的点击事件,添加对这些事件的监听
    $(document).ready(function(){
        // 对form表单的处理
        $("form").submit(function(e){
            return false;
        });
    
        // 退出按钮响应事件 id = mainLogoutClick
        $("#mainLogoutClick").bind("click", function(){
           
        });
    
        // 对整个div元素的监听点击事件
        $(document).on('click', 'div', function () {
            
        });
    
        // class = loginbutton 点击事件
        $('.loginbutton').on('click', function(){
        
        });
    });
    
    2. 网络请求
    我用到的是ajax
    $.ajax(
    {
        url: requestAddArticle, // 请求的网络地址
        type: "POST", // 请求的网络类型
        data: JSON.stringify({"channelIDs": selectArray, "source": $.base64.encode(locationURL)}), // 请求的参数json序列化
        contentType: "application/json", // 设置返回的参数为json对象
            dataType: "json", // 设置请求的参数为json对象
        beforeSend: function(xhr) { // 请求的header设置
            xhr.setRequestHeader("Authorization", "Token " + localStorage.Token);
        },
        success:function(data, textStatus, jqXHR){ // 成功回调
            document.getElementById("mainAddBtn").value = "添加成功";
        },
        error: function(jqXHR, textStatus, errorThrown){ // 失败回调
            console.log(JSON.stringify(jqXHR));
        }
    });
    
    3. 其他
    js 是一个很讲究顺序的脚本语言。如果a.js是基于b.js使用的,那么b.js一定要在a.js之前导入。
    如果你的js文件将要对html中的某些元素的id或者class进行操作,请在这些元素创建完成之后,导入你的js文件。
    
    插件其他相关
    1. chrome extension 应该是谷歌程序扩展,不是插件。但是貌似都是这么叫,所以也默认为插件开发了。
    还有一个chrome app。貌似是谷歌扩展应用。
    2. 问题。
        1)由于界面是两个之间切换,所以点击出现时并不总是很流畅。
        2)由于界面的一些元素是根据网络数据动态计算的,所以界面不够稳定。也许可以尝试出现加载等待界面(没尝试)或者固定界面的大小(我的效果,也许我设置错了)
        3)由于要加入第三方登录pc端授权。授权回调必须是一个正常的网页,担心插件不具有这个权限(未尝试)。
           同时授权回调会跳转到另一界面,成功之后再做一些事,会直接导致本界面的消失。
        4)其他
    3. 插件开发的可能性选择。
        1)有道云笔记的注入iframe,同时具有第三方登录和网页内容提取功能,是一个选择。
        2)印象笔记剪藏版插件。一个很棒的插件功能很强大且高效。同样是注入iframe的方式。
        3)Asana 插件。一个很简洁的插件。在同一个界面中,写入很多元素布局,根据需要显示隐藏部分元素达到不同的界面显示。登录直接在新窗口进行操作,猜测使用cookies方式达到数据共享。
    
  2. 第二阶段,把一个iframe注入到当前的网页中。

    看到有道云笔记的插件之后。知道了第三方登录可以融入插件中且界面统一,因此在此改变开发方向,进入iframe注入的研究。
    使用iframe后,去掉manifest.json文件中default_popup字段。同时在backgroud.js中实现点击响应事件。
     //当点击的时候 注入js文件
    chrome.browserAction.onClicked.addListener(function(tab) {
        chrome.tabs.executeScript(tab.id, {file: "js/jquery.min.js"});
        // insert.js文件依赖于jQuery库。所以先导入jquery.min.js文件
        chrome.tabs.executeScript(tab.id, {file: "js/insert.js"});
    });
    
    > insert.js
    // 判断是否存在id = TR-IOS 的元素
    if (document.getElementById("TR-IOS")) { // 存在则移除
        $('#TR-IOS').remove();
    } else { // 不存在则添加
        var URL = "main.html";
        var totalURL = URL + '?URL=' + document.URL;
        // iframe元素设置
        var iframe = '<iframe src="http://kpoints.cn/zsk/chrome-Extension/' + totalURL + '" frameborder="10px" name="TR-IOS" id="TR-IOS" style="border: 10px; border-radius: 5px; border-color: #2c3335; visibility: visible; z-index: 2147483647; position: fixed; width: 340px; right: 20px; top: 20px; height: 520px; display: block!important;"></iframe>';
        $("body").append(iframe);  //添加iframe
        document.getElementById('TR-IOS').src = 'http://kpoints.cn/zsk/chrome-Extension/main.html?URL=' + "wwww";
    }
    
    // 添加监听事件
    addEventListener('message', function(ev) {
        // 当收到此事件时移除此插件
        if (ev.data === 'closeIframe') {
            $('#TR-IOS').remove();
        }
    });
    
    // 通知父窗口 删除本窗口 放到点击按钮的事件中处理 于上面的监听事件正好搭配使用
    // parent.postMessage('closeIframe', '*');
    
    > 第三方登录(稍候)
        1) QQ 
        2) sina 
        3) wxchat 暂未通过审核,所以暂未处理。
    插件基本上到此。完毕。
    其他为server端处理。例如:html, css, js. 文件存储在服务器端。
    
    第二种方式的问题:
        1)无法获取当前网页的URL和title。
            解决方式:在注入iframe时直接在url后面拼接上当前网页的URL,title,favIconURL
            > insert.js
                // 获得当前页的favIconUrl
                var getFavicon = function(){
                    var favicon = undefined;
                    var nodeList =             document.getElementsByTagName("link");
                    for (var i = 0; i < nodeList.length; i++)
                    {
                        if((nodeList[i].getAttribute("rel") == "icon")||(nodeList[i].getAttribute("rel") == "shortcut icon"))
                        {
                            favicon = nodeList[i].getAttribute("href");
                        }
                    }
                    return favicon;
                }
    
                var favIconUrl = getFavicon();
    
                // 判断是否存在id = TR-IOS 的元素
                if (document.getElementById("TR-IOS")) { // 存在则移除
                    $('#TR-IOS').remove();
                } else { // 不存在则添加
                    var URL = "main.html";
                    // 拼接上当前网页的URL, title, favIconUrl
                    var totalURL = URL + '?URL=' + document.URL + '&title=' + document.title + '&favIconUrl=' + favIconUrl;
                    // iframe元素设置
                    var iframe = '<iframe src="http://kpoints.cn/zsk/chrome-Extension/' + totalURL + '" frameborder="10px" name="TR-IOS" id="TR-IOS" style="border: 10px; border-radius: 5px; border-color: #2c3335; visibility: visible; z-index: 2147483647; position: fixed; width: 340px; right: 20px; top: 20px; height: 520px; display: block!important;"></iframe>';
                    $("body").append(iframe);  //添加iframe
                }
                在你输入的那个网页中heade中导入处理的js文件。我的是main.html,我导入的是otherMain.js
                > otherMain.js
                // 先存储URL和title的值
                var url = getParameterByName('URL');
                var title = getParameterByName('title');
                var favIconUrl = getParameterByName('favIconUrl');
                if (url) {
                    localStorage.baseURL = url;
                    localStorage.baseTitle = title;
                    localStorage.favIconUrl = favIconUrl;
                }
    
                // 处理网络地址后缀 我们人为添加的
                function getParameterByName(name, url) {
                    if (!url) {
                        url = window.location.href;
                    }
                    name = name.replace(/[\[\]]/g, "\\$&");
                    var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
                    results = regex.exec(url);
                    if (!results) return null;
                    if (!results[2]) return '';
                    return decodeURIComponent(results[2].replace(/\+/g, " "));
                }
                然后在正常有需求的网页使用即可。
                favIconUrl的获取函数不太完美,有些网站的写法不是正规写法就没法获得。而网站icon的设置也并不统一。
        2)注入iframe这种方式会被拒绝(貌似有反注入)。特别是在https的网站上,直接被阻拦。
           有道云笔记的方式貌似是http的网站注入http://youdao...之类,https网站注入https://youdao...之类。暂未测试,只是猜测。
    
  3. 调试与安装相关(稍候)

  4. 查看源码(学习别人的插件)

    image
    • 然后在文件路径下找到这个ID文件 /Users/zsk/Library/Application\ Support/Google/Chrome/Default/Extensions
    • .crx的扩展文件后缀,直接解压就可以看到里面的文件

extension参考资料


二维码生成


网页正文提取

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

推荐阅读更多精彩内容