Android與JS之JsBridge使用與源碼分析

本篇文章已授權(quán)微信公眾號(hào) guolin_blog (郭霖)獨(dú)家發(fā)布

在Android開(kāi)發(fā)中龄章,由于Native開(kāi)發(fā)的成本較高,H5頁(yè)面的開(kāi)發(fā)更靈活,修改成本更低,因此前端網(wǎng)頁(yè)JavaScript(下面簡(jiǎn)稱JS)與Java之間的互相調(diào)用越來(lái)越常見(jiàn)惧浴。

JsBridge就是一個(gè)簡(jiǎn)化Android與JS通信的框架,源碼:https://github.com/lzyzsd/JsBridge
我們今天通過(guò)一個(gè)簡(jiǎn)單栗子來(lái)分析下開(kāi)源框架JsBridge的源碼奕剃。栗子的代碼我也放在Github衷旅,有需要的可以seesee:
https://github.com/juexingzhe/Android_JS
栗子很簡(jiǎn)單,隨便輸入信息登陸纵朋,會(huì)加載一個(gè)H5頁(yè)面柿顶,在H5界面點(diǎn)擊按鈕,Java執(zhí)行g(shù)etUserInfo()然后將UserInfo回傳給JS操软,H5頁(yè)面再顯示UserInfo嘁锯。

1.png
2.png
3.png

JS調(diào)用Android基本有下面三種方式

webView.addJavascriptInterface()
WebViewClient.shouldOverrideUrlLoading()
WebChromeClient.onJsAlert()/onJsConfirm()/onJsPrompt() 方法分別回調(diào)攔截JS對(duì)話框alert()、confirm()聂薪、prompt()消息

Android調(diào)用JS

webView.loadUrl();
webView.evaluateJavascript()

常用方法的使用后面栗子中會(huì)用到家乘,更細(xì)節(jié)的介紹各位同學(xué)可以去網(wǎng)上搜搜看看。

1.JsBridge使用

我們先來(lái)看下Java層的代碼
首先引入依賴和倉(cāng)庫(kù)

dependencies {
   ……
    compile 'com.github.lzyzsd:jsbridge:1.0.4'
    compile 'com.google.code.gson:gson:2.7'
}
repositories {
    jcenter()
    maven { url "https://jitpack.io" }
}

準(zhǔn)備工作就是這樣藏澳,下面可以開(kāi)始擼代碼烤低,首先就是點(diǎn)擊按鈕登陸,這個(gè)簡(jiǎn)單:

Intent intent = new Intent(LoginActivity.this, WebActivity.class);
intent.putExtra("email", mEmailView.getText().toString());
startActivity(intent);

布局文件中要使用BridgeWebView:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.github.lzyzsd.jsbridge.BridgeWebView
        android:id="@+id/web_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>

在跳轉(zhuǎn)后的頁(yè)面,獲取登陸信息并存儲(chǔ),再通過(guò)loadUrl加載H5頁(yè)面:

Intent intent = this.getIntent();
String email = intent.getStringExtra("email");

 mUserInfo = new UserInfo(email);

mBridgeWebView = (BridgeWebView) findViewById(R.id.web_view);
mBridgeWebView.setDefaultHandler(new DefaultHandler());
mBridgeWebView.loadUrl("file:///android_asset/getuserinfo.html");

registerHandler();

主要是要注冊(cè)Handler笆载,供JS調(diào)用,

getUserInfo就是注冊(cè)供JS調(diào)用的Handler的id
data是JS傳過(guò)來(lái)的參數(shù)
CallBackFunction 函數(shù)中需要把JS需要的response返回給JS

private void registerHandler() {
        mBridgeWebView.registerHandler("getUserInfo", new BridgeHandler() {
            @Override
            public void handler(String data, CallBackFunction function) {
                Log.i(TAG, "handler = getUserInfo, data from web = " + data);
                function.onCallBack(new Gson().toJson(mUserInfo));
            }
        });
}

Java層的代碼就這么簡(jiǎn)單,下面看下JS層工作:
首先需要一個(gè)js文件涯呻,我們寫一個(gè)getuserinfo.html文件放在assets目錄下凉驻,文件內(nèi)容,不建議把js代碼直接放在html文件中,我為了方便直接就寫在這了复罐。代碼放了兩個(gè)段落涝登,一個(gè)類似于TextView用來(lái)顯示用戶信息,一個(gè)Button效诅。點(diǎn)擊按鈕會(huì)調(diào)用callHandler胀滚,三個(gè)參數(shù)和Java層一一對(duì)應(yīng)趟济,在Java層返回的時(shí)候,會(huì)調(diào)用function(responseData)函數(shù)咽笼,顯示用于信息顷编。

<html>
<head>
    <meta content="text/html; charset=utf-8" http-equiv="content-type">
    <title>
        js調(diào)用java
    </title>
</head>
<body>
<p>
    <xmp id="show">
    </xmp>
</p>
<div align="center">
    <p>
        <input type="button" id="enter" value="獲取用戶信息" onclick="getUserInfo();"
        />
    </p>
</div>
</body>
<script>
    function getUserInfo(){
        window.WebViewJavascriptBridge.callHandler(
            'getUserInfo',
            {'info': 'I am JS, want to get UserInfo from Java'},
            function(responseData) {
                document.getElementById("show").innerHTML = "repsonseData from java,\ndata = " + responseData;
            }
        )
    }
</script>
</html>

使用基本就是這樣了,可以看出來(lái)JsBridge通過(guò)封裝剑刑,JS和Java之間的通信只需要實(shí)現(xiàn)兩個(gè)步驟媳纬,使用起來(lái)很方便。

我們來(lái)看下源碼是怎么個(gè)玩法施掏,先來(lái)個(gè)華麗麗的分割線



2.JsBridge源碼分析

分析之前我把JS調(diào)用Java畫了個(gè)簡(jiǎn)易交互圖钮惠,Java調(diào)用JS的過(guò)程類似:

4.png

是不是感覺(jué)反而更復(fù)雜了?七芭?素挽?其實(shí)只要捉住主要的三點(diǎn),JsBridge就原形畢露:

1.Android調(diào)用JS是通過(guò)loadUrl(url),url中可以拼接要傳給JS的對(duì)象
2.JS調(diào)用Android是通過(guò)shouldOverrideUrlLoading
3.JsBridge將溝通數(shù)據(jù)封裝成Message狸驳,然后放進(jìn)Queue,再將Queue進(jìn)行傳輸

接下來(lái)我們來(lái)一步一步跟蹤上面栗子的調(diào)用過(guò)程:

  • JS層點(diǎn)擊按鈕調(diào)用callHandler

handlerName预明,Java和JS要一致,
data是Java層handlerName函數(shù)執(zhí)行的參數(shù)
responseCallback是Java執(zhí)行完handlerName返回時(shí)锌历,JS回調(diào)的接口贮庞,是JS執(zhí)行

onclick="getUserInfo();"

function getUserInfo(){
    window.WebViewJavascriptBridge.callHandler(
            'getUserInfo',
            {'info': 'I am JS, want to get UserInfo from Java'},
            function(responseData) {
                document.getElementById("show").innerHTML = "repsonseData from java,\ndata = " + responseData;
            }
    )
}

callHandler會(huì)調(diào)用_doSend

如果JS需要回調(diào),就將回調(diào)的callbackId放進(jìn)message中究西,Java執(zhí)行完會(huì)傳回callbackId窗慎,這里是cb_1_1495182409011
構(gòu)造完message放進(jìn)隊(duì)列sendMessageQueue
通過(guò)iframe屬性給Java發(fā)送通知消息,消息結(jié)構(gòu)yy://QUEUE_MESSAGE/

function callHandler(handlerName, data, responseCallback) {
    _doSend({
        handlerName: handlerName,
        data: data
    }, responseCallback);
}

function _doSend(message, responseCallback) {
        if (responseCallback) {
            var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
            responseCallbacks[callbackId] = responseCallback;
            message.callbackId = callbackId;
        }

        sendMessageQueue.push(message);
        messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}
  • Java收到通知消息
    WebView在shouldOverrideUrlLoading攔截到url:yy://QUEUE_MESSAGE/
    然后會(huì)執(zhí)行webView.flushMessageQueue()卤材,在主線程執(zhí)行l(wèi)oadUrl通知JS層推送隊(duì)列到Java;

JS_FETCH_QUEUE_FROM_JAVA = "javascript:WebViewJavascriptBridge._fetchQueue();"
調(diào)用JS層的_fetchQueue遮斥,通知JS層發(fā)送隊(duì)列到Java層
在responseCallbacks中注冊(cè)回調(diào)接口,接口id是函數(shù)名_fetchQueue扇丛,在JS推送消息隊(duì)列時(shí)進(jìn)行回調(diào)

void flushMessageQueue() {
          if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
               loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {
                    @Override
                    public void onCallBack(String data) {
                         //
                    });
          }
}

public void loadUrl(String jsUrl, CallBackFunction returnCallback) {
          this.loadUrl(jsUrl);
          responseCallbacks.put(BridgeUtil.parseFunctionName(jsUrl), returnCallback);
}
  • JS 發(fā)送Request Queue
    執(zhí)行_fetchQueue

將sendMessageQueue轉(zhuǎn)化成JSON
通過(guò)iframe屬性給Java發(fā)送通知消息术吗,消息結(jié)構(gòu):yy://return/_fetchQueue/消息隊(duì)列的內(nèi)容

function _fetchQueue() {
    var messageQueueString = JSON.stringify(sendMessageQueue);
    sendMessageQueue = [];
    //android can't read directly the return data, so we can reload iframe src to communicate with java
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
}
  • Java收到調(diào)用通知,進(jìn)行處理并發(fā)送Response Queue到JS
    WebView在shouldOverrideUrlLoading會(huì)攔截到url:
yy://return/_fetchQueue/[{"handlerName":"getUserInfo","data":{"info":"I am JS, want to get UserInfo from Java"},"callbackId":"cb_1_1495180503779"}]

執(zhí)行webView.handlerReturnData(url);

根據(jù)函數(shù)名_fetchQueue拿到之前注冊(cè)的回調(diào)函數(shù)CallBackFunction returnCallback
執(zhí)行回調(diào)函數(shù)帆精,并且從注冊(cè)中移除

void handlerReturnData(String url) {
          String functionName = BridgeUtil.getFunctionFromReturnUrl(url);
          CallBackFunction f = responseCallbacks.get(functionName);
          String data = BridgeUtil.getDataFromReturnUrl(url);
          if (f != null) {
               f.onCallBack(data);
               responseCallbacks.remove(functionName);
               return;
          }
}

接下來(lái)就是對(duì)Request Queue的解析然后找到JS希望調(diào)用Handler并且執(zhí)行较屿,代碼中我寫了注釋,可以直接看:

//回調(diào)接口執(zhí)行onCallBack函數(shù)
//其中data [{"handlerName":"getUserInfo","data":{"info":"I am JS, want to get UserInfo from Java"},"callbackId":"cb_1_1495180503779"}]
void flushMessageQueue() {
          if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
               loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {

                    @Override
                    public void onCallBack(String data) {
                         // deserializeMessage
                         List<Message> list = null;
                         try {
                              //將JSON數(shù)組轉(zhuǎn)化成Java list
                              list = Message.toArrayList(data);
                         } catch (Exception e) {
                        e.printStackTrace();
                              return;
                         }
                         if (list == null || list.size() == 0) {
                              return;
                         }
                         for (int i = 0; i < list.size(); i++) {
                              //從list中取出Message
                              Message m = list.get(i);
                              //在我們的栗子中沒(méi)有responseId卓练,因此到else分支
                              String responseId = m.getResponseId();
                              // 是否是response
                              if (!TextUtils.isEmpty(responseId)) {
                                   CallBackFunction function = responseCallbacks.get(responseId);
                                   String responseData = m.getResponseData();
                                   function.onCallBack(responseData);
                                   responseCallbacks.remove(responseId);
                              } else {
                                   CallBackFunction responseFunction = null;
                                   // if had callbackId
                                   //如果有callbackId就說(shuō)明JS需要回調(diào)隘蝎,因此Java層需要構(gòu)造responseMsg
                                   //從message中取出callbackId,放進(jìn)responseMsg
                                   final String callbackId = m.getCallbackId();
                                   if (!TextUtils.isEmpty(callbackId)) {
                                        responseFunction = new CallBackFunction() {
                                             @Override
                                             public void onCallBack(String data) {
                                                  Message responseMsg = new Message();
                                                  responseMsg.setResponseId(callbackId);
                                                  responseMsg.setResponseData(data);
                                                  queueMessage(responseMsg);
                                             }
                                        };
                                   } else {
                                        responseFunction = new CallBackFunction() {
                                             @Override
                                             public void onCallBack(String data) {
                                                  // do nothing
                                             }
                                        };
                                   }
                                   BridgeHandler handler;
                                   //從message中取出Handler名字襟企,再?gòu)膍essageHandlers中取
                                   //如果沒(méi)有就使用默認(rèn)的Handler
                                   if (!TextUtils.isEmpty(m.getHandlerName())) {
                                        handler = messageHandlers.get(m.getHandlerName());
                                   } else {
                                        handler = defaultHandler;
                                   }
                                   if (handler != null){
                                        //執(zhí)行handler
                                        handler.handler(m.getData(), responseFunction);
                                   }
                              }
                         }
                    }
               });
          }
}

那么這個(gè)handler是什么嘱么?就是Java調(diào)用registerHandler注冊(cè)的getUserInfo

private void registerHandler() {
        mBridgeWebView.registerHandler("getUserInfo", new BridgeHandler() {
            @Override
            public void handler(String data, CallBackFunction function) {
                Log.i(TAG, "handler = getUserInfo, data from web = " + data);
                function.onCallBack(new Gson().toJson(mUserInfo));
            }
}

上面的function就是在flushMessageQueue 解析時(shí)構(gòu)造的responseFunction,在message中包括JS層需要回調(diào)的函數(shù)Id顽悼,然后就是getUserInfo執(zhí)行的結(jié)果
調(diào)用queueMessage

responseFunction = new CallBackFunction() {
          @Override
          public void onCallBack(String data) {
                    Message responseMsg = new Message();
                    responseMsg.setResponseId(callbackId);
                    responseMsg.setResponseData(data);
                    queueMessage(responseMsg);
          }
};

queueMessage調(diào)用dispatchMessage發(fā)送message給JS

通過(guò)構(gòu)造String指令曼振,然后loadUrl執(zhí)行JS代碼,注意對(duì)象也是通過(guò)這樣方式傳遞過(guò)去的几迄,就類似調(diào)用本地函數(shù),不發(fā)起網(wǎng)絡(luò)請(qǐng)求

void dispatchMessage(Message m) {
        String messageJson = m.toJson();
        //escape special characters for json string
        messageJson = messageJson.replaceAll("(\\\\)([^utrn])", "\\\\\\\\$1$2");
        messageJson = messageJson.replaceAll("(?<=[^\\\\])(\")", "\\\\\"");
        String javascriptCommand = String.format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson);
        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
            this.loadUrl(javascriptCommand);
        }
}

其中

BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA ="javascript:WebViewJavascriptBridge._handleMessageFromNative('%s');"
javascriptCommand = javascript:WebViewJavascriptBridge._handleMessageFromNative('{\"responseData\":\"{\\\"email\\\":\\\"jue@126.com\\\"}\",\"responseId\":\"cb_1_1495182558893\"}');
//data = {\"responseData\":\"{\\\"email\\\":\\\"jue@126.com\\\"}\",\"responseId\":\"cb_1_1495182409011\"}
  • JS收到Response JSON
    來(lái)到_handleMessageFromNative冰评,
function _handleMessageFromNative(messageJSON) {
        console.log(messageJSON);
        if (receiveMessageQueue && receiveMessageQueue.length > 0) {
            receiveMessageQueue.push(messageJSON);
        } else {
            _dispatchMessageFromNative(messageJSON);
        }
}

最后都會(huì)調(diào)用到_dispatchMessageFromNative映胁,由于是JS主動(dòng)調(diào)用Java,因此有responseId集索,執(zhí)行registerHandler時(shí)傳入的CallBack屿愚,也就是顯示用戶信息。我在代碼里加了注釋务荆,很容易看懂妆距。

function _dispatchMessageFromNative(messageJSON) {
        setTimeout(function() {
             //將數(shù)據(jù)解析成JSON
            var message = JSON.parse(messageJSON);
            var responseCallback;
            //java call finished, now need to call js callback function
            //根據(jù)responseId:cb_1_1495182409011拿到responseCallback,就是我們前門注冊(cè)的alert
            if (message.responseId) {
                responseCallback = responseCallbacks[message.responseId];
                if (!responseCallback) {
                    return;
                }
                responseCallback(message.responseData);
                delete responseCallbacks[message.responseId];
            } else {
                //直接發(fā)送
                if (message.callbackId) {
                    var callbackResponseId = message.callbackId;
                    responseCallback = function(responseData) {
                        _doSend({
                            responseId: callbackResponseId,
                            responseData: responseData
                        });
                    };
                }

                var handler = WebViewJavascriptBridge._messageHandler;
                if (message.handlerName) {
                    handler = messageHandlers[message.handlerName];
                }
                //查找指定handler
                try {
                    handler(message.data, responseCallback);
                } catch (exception) {
                    if (typeof console != 'undefined') {
                        console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception);
                    }
                }
            }
        });
}

源碼的分析就到這結(jié)束了函匕,代碼不多娱据,但是封裝的接口很是好用。最后再來(lái)個(gè)分割線~~



3.總結(jié)

最后總結(jié)下盅惜,使用上很方便主要兩個(gè)步驟

被調(diào)用方注冊(cè)Handler

registerHandler(String handlerName, BridgeHandler handler) 

調(diào)用方調(diào)用Handler

callHandler(String handlerName, String data, CallBackFunction callBack)

原理上還是那三句話中剩,請(qǐng)?jiān)徫覐纳厦嬷苯觕opy過(guò)來(lái):

1.Android調(diào)用JS是通過(guò)loadUrl(url),url中可以拼接要傳給JS的對(duì)象
2.JS調(diào)用Android是通過(guò)shouldOverrideUrlLoading
3.JsBridge將溝通數(shù)據(jù)封裝成Message,然后放進(jìn)Queue,再將Queue進(jìn)行傳輸

好了抒寂,今天我們JsBridge的使用和源碼分析就到這了结啼,謝謝!

文中栗子的鏈接:
https://github.com/juexingzhe/Android_JS

歡迎關(guān)注公眾號(hào):JueCode

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末屈芜,一起剝皮案震驚了整個(gè)濱河市郊愧,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌井佑,老刑警劉巖属铁,帶你破解...
    沈念sama閱讀 222,104評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異躬翁,居然都是意外死亡焦蘑,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門盒发,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)例嘱,“玉大人,你說(shuō)我怎么就攤上這事宁舰〉溃” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 168,697評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵明吩,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我殷费,道長(zhǎng)印荔,這世上最難降的妖魔是什么低葫? 我笑而不...
    開(kāi)封第一講書人閱讀 59,836評(píng)論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮仍律,結(jié)果婚禮上嘿悬,老公的妹妹穿的比我還像新娘。我一直安慰自己水泉,他們只是感情好善涨,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著草则,像睡著了一般钢拧。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上炕横,一...
    開(kāi)封第一講書人閱讀 52,441評(píng)論 1 310
  • 那天源内,我揣著相機(jī)與錄音,去河邊找鬼份殿。 笑死膜钓,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的卿嘲。 我是一名探鬼主播颂斜,決...
    沈念sama閱讀 40,992評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼拾枣!你這毒婦竟也來(lái)了沃疮?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 39,899評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤放前,失蹤者是張志新(化名)和其女友劉穎忿磅,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體凭语,經(jīng)...
    沈念sama閱讀 46,457評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡葱她,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評(píng)論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了似扔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吨些。...
    茶點(diǎn)故事閱讀 40,664評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖炒辉,靈堂內(nèi)的尸體忽然破棺而出豪墅,到底是詐尸還是另有隱情,我是刑警寧澤黔寇,帶...
    沈念sama閱讀 36,346評(píng)論 5 350
  • 正文 年R本政府宣布偶器,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏屏轰。R本人自食惡果不足惜颊郎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評(píng)論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望霎苗。 院中可真熱鬧姆吭,春花似錦、人聲如沸唁盏。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,511評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)厘擂。三九已至昆淡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間驴党,已是汗流浹背瘪撇。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,611評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留港庄,地道東北人倔既。 一個(gè)月前我還...
    沈念sama閱讀 49,081評(píng)論 3 377
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像鹏氧,于是被迫代替她去往敵國(guó)和親渤涌。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評(píng)論 2 359

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