優(yōu)秀開源代碼解讀之JS與iOS Native Code互調(diào)的優(yōu)雅實(shí)現(xiàn)方案

本篇為大家介紹一個(gè)優(yōu)秀的開源小項(xiàng)目:WebViewJavascriptBridge谎倔。

它優(yōu)雅地實(shí)現(xiàn)了在使用UIWebView時(shí)JS與ios 的ObjC nativecode之間的互調(diào)褂策,支持消息發(fā)送、接收粪般、消息處理器的注冊(cè)與調(diào)用以及設(shè)置消息處理的回調(diào)。

就像項(xiàng)目的名稱一樣恕刘,它是連接UIWebView和Javascript的bridge桑嘶。在加入這個(gè)項(xiàng)目之后屈尼,他們之間的交互處理方式變得很友好册着。

在native code中跟UIWebView中的js交互的時(shí)候,像下面這樣:

[cpp]view plaincopy

print?

//發(fā)送一條消息給UI端并定義回調(diào)處理邏輯

[_bridge?send:@"A?string?sent?from?ObjC?before?Webview?has?loaded."responseCallback:^(id?error,?id?responseData)?{

if(error)?{?NSLog(@"Uh?oh?-?I?got?an?error:?%@",?error);?}

NSLog(@"objc?got?response!?%@?%@",?error,?responseData);

}];

而在UIWebView中的js跟native code交互的時(shí)候也變得很簡(jiǎn)潔脾歧,比如在調(diào)用處理器的時(shí)候甲捏,就可以定義回調(diào)處理邏輯:

[javascript]view plaincopy

print?

//調(diào)用名為testObjcCallback的native端處理器,并傳遞參數(shù)鞭执,同時(shí)設(shè)置回調(diào)處理邏輯

bridge.callHandler('testObjcCallback',?{'foo':'bar'},function(response)?{

??log('Got?response?from?testObjcCallback',?response)

})

一起來(lái)看看它的實(shí)現(xiàn)吧摊鸡,它總共就包含了三個(gè)文件:

[plain]view plaincopy

print?

WebViewJavascriptBridge.h

WebViewJavascriptBridge.m

WebViewJavascriptBridge.js.txt

它們是以如下的模式進(jìn)行交互的:

很明顯:WebViewJavascriptBridge.js.txt主要用于銜接UIWebView中的web page绽媒,而WebViewJavascriptBridge.h/m則主要用于與ObjC的native code打交道。他們作為一個(gè)整體免猾,其實(shí)起到了一個(gè)“橋梁”的作用是辕,這三個(gè)文件封裝了他們具體的交互處理方式,只開放出一些對(duì)外的涉及到業(yè)務(wù)處理的API猎提,因此你在需要UIWebView與Native code交互的時(shí)候获三,引入該庫(kù),則無(wú)需考慮太多的交互上的問(wèn)題锨苏。整個(gè)的Bridge對(duì)你來(lái)說(shuō)都是透明的疙教,你感覺編程的時(shí)候,就像是web編程的前端和后端一樣清晰伞租。

簡(jiǎn)單地羅列一下它可以實(shí)現(xiàn)哪些功能吧:

出于表達(dá)上的需要贞谓,對(duì)于UIWebView相關(guān)的我就稱之為UI端,而objc那端的處理代碼稱之為Native端葵诈。

【1】UI端

(1)???UI端在初始化時(shí)支持設(shè)置消息的默認(rèn)處理器(這里的消息指的是從Native端接收到的消息)

(2)???從UI端向Native端發(fā)送消息裸弦,并支持對(duì)于Native端響應(yīng)后的回調(diào)處理的定義

(3)???UI端調(diào)用Native定義的處理器,并支持Native端響應(yīng)后的回調(diào)處理定義

(4)???UI端注冊(cè)處理器(供Native端調(diào)用)作喘,并支持給Native端響應(yīng)處理邏輯的定義

【2】 Native端

(1)???Native端在初始化時(shí)支持設(shè)置消息的默認(rèn)處理器(這里的消息指的是從UI端發(fā)送過(guò)來(lái)的消息)

(2)???從Native端向UI端發(fā)送消息理疙,并支持對(duì)于UI端響應(yīng)后的回調(diào)處理邏輯的定義

(3)???Native端調(diào)用UI端定義的處理器,并支持UI端給出響應(yīng)后在Native端的回調(diào)處理邏輯的定義

(4)???Native端注冊(cè)處理器(供UI端調(diào)用)泞坦,并支持給UI端響應(yīng)處理邏輯的定義

UI端以及Native端完全是對(duì)等的兩端窖贤,實(shí)現(xiàn)也是對(duì)等的。一段是消息的發(fā)送端贰锁,另一段就是接收端赃梧。這里為引起混淆,需要解釋一下我這里使用的“響應(yīng)”豌熄、“回調(diào)”在這個(gè)上下文中的定義:

(1)???響應(yīng):接收端給予發(fā)送端的應(yīng)答

(2)???回調(diào):發(fā)送端收到接收端的應(yīng)答之后在接收端調(diào)用的處理邏輯

下面來(lái)分析一下源碼:

WebViewJavascriptBridge.js.txt:

主要完成了如下工作:

(1) 創(chuàng)建了一個(gè)用于發(fā)送消息的iFrame(通過(guò)創(chuàng)建一個(gè)隱藏的ifrmae槽奕,并設(shè)置它的URL 來(lái)發(fā)出一個(gè)請(qǐng)求,從而觸發(fā)UIWebView的shouldStartLoadWithRequest回調(diào)協(xié)議)

(2) 創(chuàng)建了一個(gè)核心對(duì)象WebViewJavascriptBridge房轿,并給它定義了幾個(gè)方法,這些方法大部分是公開的API方法

(3) 創(chuàng)建了一個(gè)事件:WebViewJavascriptBridgeReady所森,并dispatch(觸發(fā))了它囱持。

代碼解讀

UI端實(shí)現(xiàn)

對(duì)于(1),相應(yīng)的代碼如下:

[javascript]view plaincopy

print?

/*

*創(chuàng)建一個(gè)iFrame焕济,設(shè)置隱藏并加入到DOM中

*/

function_createQueueReadyIframe(doc)?{

messagingIframe?=?doc.createElement('iframe')

messagingIframe.style.display?='none'

doc.documentElement.appendChild(messagingIframe)

}

對(duì)于(2)中的WebViewJavascriptBridge纷妆,其對(duì)象擁有如下方法:

[plain]view plaincopy

print?

window.WebViewJavascriptBridge?=?{

init:?init,

send:?send,

registerHandler:?registerHandler,

callHandler:?callHandler,

_fetchQueue:?_fetchQueue,

_handleMessageFromObjC:?_handleMessageFromObjC

}

方法的實(shí)現(xiàn):

[javascript]view plaincopy

print?

??/*

*初始化方法,注入默認(rèn)的消息處理器

*默認(rèn)的消息處理器用于在處理來(lái)自objc的消息時(shí)晴弃,如果該消息沒有設(shè)置處理器掩幢,則采用默認(rèn)處理器處理

*/

functioninit(messageHandler)?{

if(WebViewJavascriptBridge._messageHandler)?{thrownewError('WebViewJavascriptBridge.init?called?twice')?}

WebViewJavascriptBridge._messageHandler?=?messageHandler

varreceivedMessages?=?receiveMessageQueue

receiveMessageQueue?=null

//如果接收隊(duì)列有消息逊拍,則處理

for(vari=0;?i

_dispatchMessageFromObjC(receivedMessages[i])

}

}

??/*

*發(fā)送消息并設(shè)置回調(diào)

*/

functionsend(data,?responseCallback)?{

_doSend({?data:data?},?responseCallback)

}

/*

*注冊(cè)消息處理器

*/

functionregisterHandler(handlerName,?handler)?{

messageHandlers[handlerName]?=?handler

}

/*

*調(diào)用處理器并設(shè)置回調(diào)

*/

functioncallHandler(handlerName,?data,?responseCallback)?{

_doSend({?data:data,?handlerName:handlerName?},?responseCallback)

}

涉及到的兩個(gè)內(nèi)部方法:

[javascript]view plaincopy

print?

??/*

*內(nèi)部方法:消息的發(fā)送

*/

function_doSend(message,?responseCallback)?{

//如果定義了回調(diào)

if(responseCallback)?{

//為回調(diào)對(duì)象產(chǎn)生唯一標(biāo)識(shí)

varcallbackId?='js_cb_'+(uniqueId++)

//并存儲(chǔ)到一個(gè)集合對(duì)象里

responseCallbacks[callbackId]?=?responseCallback

//新增一個(gè)key-value對(duì)-?'callbackId':callbackId

message['callbackId']?=?callbackId

}

sendMessageQueue.push(JSON.stringify(message))

messagingIframe.src?=?CUSTOM_PROTOCOL_SCHEME?+'://'+?QUEUE_HAS_MESSAGE

}

??/*

*內(nèi)部方法:處理來(lái)自objc的消息

*/

function_dispatchMessageFromObjC(messageJSON)?{

setTimeout(function_timeoutDispatchMessageFromObjC()?{

varmessage?=?JSON.parse(messageJSON)

varmessageHandler

if(message.responseId)?{

//取出回調(diào)函數(shù)對(duì)象并執(zhí)行

varresponseCallback?=?responseCallbacks[message.responseId]

responseCallback(message.error,?message.responseData)

deleteresponseCallbacks[message.responseId]

}else{

varresponse

if(message.callbackId)?{

varcallbackResponseId?=?message.callbackId

response?=?{

respondWith:function(responseData)?{

_doSend({?responseId:callbackResponseId,?responseData:responseData?})

},

respondWithError:function(error)?{

_doSend({?responseId:callbackResponseId,?error:error?})

}

}

}

varhandler?=?WebViewJavascriptBridge._messageHandler

//如果消息中已包含消息處理器,則使用該處理器际邻;否則使用默認(rèn)處理器

if(message.handlerName)?{

handler?=?messageHandlers[message.handlerName]

}

try{

handler(message.data,?response)

}catch(exception)?{

console.log("WebViewJavascriptBridge:?WARNING:?javascript?handler?threw.",?message,?exception)

}

}

})

}

還有兩個(gè)js方法是供native端直接調(diào)用的方法(它們本身也是為native端服務(wù)的):

[javascript]view plaincopy

print?

??/*

*獲得隊(duì)列芯丧,將隊(duì)列中的每個(gè)元素用分隔符分隔之后連成一個(gè)字符串【native端調(diào)用】

*/

function_fetchQueue()?{

varmessageQueueString?=?sendMessageQueue.join(MESSAGE_SEPARATOR)

sendMessageQueue?=?[]

returnmessageQueueString

}

??/*

*處理來(lái)自O(shè)bjC的消息【native端調(diào)用】

*/

function_handleMessageFromObjC(messageJSON)?{

//如果接收隊(duì)列對(duì)象存在則入隊(duì)該消息,否則直接處理

if(receiveMessageQueue)?{

receiveMessageQueue.push(messageJSON)

}else{

_dispatchMessageFromObjC(messageJSON)

}

}

最后還有一段代碼就是世曾,定義一個(gè)事件并觸發(fā)缨恒,同時(shí)設(shè)置設(shè)置上面定義的WebViewJavascriptBridge對(duì)象為事件的一個(gè)屬性:

[javascript]view plaincopy

print?

??vardoc?=?document

_createQueueReadyIframe(doc)

//創(chuàng)建并實(shí)例化一個(gè)事件對(duì)象

varreadyEvent?=?doc.createEvent('Events')

readyEvent.initEvent('WebViewJavascriptBridgeReady')

readyEvent.bridge?=?WebViewJavascriptBridge

//觸發(fā)事件

doc.dispatchEvent(readyEvent)

Native端實(shí)現(xiàn)

其實(shí)大致跟上面的類似,只是因?yàn)檎Z(yǔ)法不同(所以我上面才說(shuō)兩端是對(duì)等的):

WebViewJavascriptBridge.h/.m

它其實(shí)可以看作UIWebView的Controller轮听,實(shí)現(xiàn)了UIWebViewDelegate協(xié)議:

[cpp]view plaincopy

print?

@interface?WebViewJavascriptBridge?:?NSObject?

+?(id)bridgeForWebView:(UIWebView*)webView?handler:(WVJBHandler)handler;

+?(id)bridgeForWebView:(UIWebView*)webView?webViewDelegate:(id?)webViewDelegate?handler:(WVJBHandler)handler;

+?(void)enableLogging;

-?(void)send:(id)message;

-?(void)send:(id)message?responseCallback:(WVJBResponseCallback)responseCallback;

-?(void)registerHandler:(NSString*)handlerName?handler:(WVJBHandler)handler;

-?(void)callHandler:(NSString*)handlerName;

-?(void)callHandler:(NSString*)handlerName?data:(id)data;

-?(void)callHandler:(NSString*)handlerName?data:(id)data?responseCallback:(WVJBResponseCallback)responseCallback;

@end

方法的實(shí)現(xiàn)其實(shí)是跟前面類似的骗露,這里我們只看一下UIWebView的一個(gè)協(xié)議方法

shouldStartLoadWithRequest:

[cpp]view plaincopy

print?

-?(BOOL)webView:(UIWebView?*)webView?shouldStartLoadWithRequest:(NSURLRequest?*)request?navigationType:(UIWebViewNavigationType)navigationType?{

if(webView?!=?_webView)?{returnYES;?}

NSURL?*url?=?[request?URL];

if([[url?scheme]?isEqualToString:CUSTOM_PROTOCOL_SCHEME])?{

//隊(duì)列中有數(shù)據(jù)

if([[url?host]?isEqualToString:QUEUE_HAS_MESSAGE])?{

//刷出隊(duì)列中數(shù)據(jù)

[self?_flushMessageQueue];

}else{

NSLog(@"WebViewJavascriptBridge:?WARNING:?Received?unknown?WebViewJavascriptBridge?command?%@://%@",?CUSTOM_PROTOCOL_SCHEME,?[url?path]);

}

returnNO;

}elseif(self.webViewDelegate)?{

return[self.webViewDelegate?webView:webView?shouldStartLoadWithRequest:request?navigationType:navigationType];

}else{

returnYES;

}

}

使用示例

UI端

[javascript]view plaincopy

print?

??//給WebViewJavascriptBridgeReady事件注冊(cè)一個(gè)Listener

document.addEventListener('WebViewJavascriptBridgeReady',?onBridgeReady,false)

??//事件的響應(yīng)處理

functiononBridgeReady(event)?{

varbridge?=?event.bridge

varuniqueId?=?1

??//日志記錄

functionlog(message,?data)?{

varlog?=?document.getElementById('log')

varel?=?document.createElement('div')

el.className?='logLine'

el.innerHTML?=?uniqueId++?+'.?'+?message?+?(data??':?'+?JSON.stringify(data)?:'')

if(log.children.length)?{?log.insertBefore(el,?log.children[0])?}

else{?log.appendChild(el)?}

}

??//初始化操作,并定義默認(rèn)的消息處理邏輯

bridge.init(function(message)?{

log('JS?got?a?message',?message)

})

??//注冊(cè)一個(gè)名為testJavascriptHandler的處理器血巍,并定義用于響應(yīng)的處理邏輯

bridge.registerHandler('testJavascriptHandler',function(data,?response)?{

log('JS?handler?testJavascriptHandler?was?called',?data)

response.respondWith({'Javascript?Says':'Right?back?atcha!'})

})

??//創(chuàng)建一個(gè)發(fā)送消息給native端的按鈕

varbutton?=?document.getElementById('buttons').appendChild(document.createElement('button'))

button.innerHTML?='Send?message?to?ObjC'

button.ontouchstart?=function(e)?{

e.preventDefault()

??????//發(fā)送消息

bridge.send('Hello?from?JS?button')

}

document.body.appendChild(document.createElement('br'))

??//創(chuàng)建一個(gè)用于調(diào)用native端處理器的按鈕

varcallbackButton?=?document.getElementById('buttons').appendChild(document.createElement('button'))

callbackButton.innerHTML?='Fire?testObjcCallback'

callbackButton.ontouchstart?=function(e)?{

e.preventDefault()

log("Calling?handler?testObjcCallback")

//調(diào)用名為testObjcCallback的native端處理器萧锉,并傳遞參數(shù),同時(shí)設(shè)置回調(diào)處理邏輯

bridge.callHandler('testObjcCallback',?{'foo':'bar'},function(response)?{

log('Got?response?from?testObjcCallback',?response)

})

}

}

Native端

[cpp]view plaincopy

print?

//實(shí)例化一個(gè)webview并加入到window中去

UIWebView*?webView?=?[[UIWebView?alloc]?initWithFrame:self.window.bounds];

[self.window?addSubview:webView];

//啟用日志記錄

[WebViewJavascriptBridge?enableLogging];

//實(shí)例化WebViewJavascriptBridge并定義native端的默認(rèn)消息處理器

_bridge?=?[WebViewJavascriptBridge?bridgeForWebView:webView?handler:^(id?data,?WVJBResponse?*response)?{

NSLog(@"ObjC?received?message?from?JS:?%@",?data);

UIAlertView?*alert?=?[[UIAlertView?alloc]?initWithTitle:@"ObjC?got?message?from?Javascript:"message:data?delegate:nil?cancelButtonTitle:@"OK"otherButtonTitles:nil];

[alert?show];

}];

//注冊(cè)一個(gè)供UI端調(diào)用的名為testObjcCallback的處理器述寡,并定義用于響應(yīng)的處理邏輯

[_bridge?registerHandler:@"testObjcCallback"handler:^(id?data,?WVJBResponse?*response)?{

NSLog(@"testObjcCallback?called:?%@",?data);

[response?respondWith:@"Response?from?testObjcCallback"];

}];

//發(fā)送一條消息給UI端并定義回調(diào)處理邏輯

[_bridge?send:@"A?string?sent?from?ObjC?before?Webview?has?loaded."responseCallback:^(id?error,?id?responseData)?{

if(error)?{?NSLog(@"Uh?oh?-?I?got?an?error:?%@",?error);?}

NSLog(@"objc?got?response!?%@?%@",?error,?responseData);

}];

//調(diào)用一個(gè)在UI端定義的名為testJavascriptHandler的處理器柿隙,沒有定義回調(diào)

[_bridge?callHandler:@"testJavascriptHandler"data:[NSDictionary?dictionaryWithObject:@"before?ready"forKey:@"foo"]];

[self?renderButtons:webView];

[self?loadExamplePage:webView];

//單純發(fā)送一條消息給UI端

[_bridge?send:@"A?string?sent?from?ObjC?after?Webview?has?loaded."];

項(xiàng)目運(yùn)行截圖:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市辨赐,隨后出現(xiàn)的幾起案子优俘,更是在濱河造成了極大的恐慌,老刑警劉巖掀序,帶你破解...
    沈念sama閱讀 221,273評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件帆焕,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡不恭,警方通過(guò)查閱死者的電腦和手機(jī)叶雹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)换吧,“玉大人折晦,你說(shuō)我怎么就攤上這事≌赐撸” “怎么了满着?”我有些...
    開封第一講書人閱讀 167,709評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)贯莺。 經(jīng)常有香客問(wèn)我风喇,道長(zhǎng),這世上最難降的妖魔是什么缕探? 我笑而不...
    開封第一講書人閱讀 59,520評(píng)論 1 296
  • 正文 為了忘掉前任魂莫,我火速辦了婚禮,結(jié)果婚禮上爹耗,老公的妹妹穿的比我還像新娘耙考。我一直安慰自己谜喊,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,515評(píng)論 6 397
  • 文/花漫 我一把揭開白布倦始。 她就那樣靜靜地躺著斗遏,像睡著了一般。 火紅的嫁衣襯著肌膚如雪楣号。 梳的紋絲不亂的頭發(fā)上最易,一...
    開封第一講書人閱讀 52,158評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死节榜,一個(gè)胖子當(dāng)著我的面吹牛胡本,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,755評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了鄙早?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,660評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤椅亚,失蹤者是張志新(化名)和其女友劉穎限番,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體呀舔,經(jīng)...
    沈念sama閱讀 46,203評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡弥虐,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,287評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了媚赖。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片霜瘪。...
    茶點(diǎn)故事閱讀 40,427評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖惧磺,靈堂內(nèi)的尸體忽然破棺而出颖对,到底是詐尸還是另有隱情,我是刑警寧澤磨隘,帶...
    沈念sama閱讀 36,122評(píng)論 5 349
  • 正文 年R本政府宣布缤底,位于F島的核電站,受9級(jí)特大地震影響番捂,放射性物質(zhì)發(fā)生泄漏个唧。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,801評(píng)論 3 333
  • 文/蒙蒙 一白嘁、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧膘流,春花似錦絮缅、人聲如沸鲁沥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)画恰。三九已至,卻和暖如春吸奴,著一層夾襖步出監(jiān)牢的瞬間允扇,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工则奥, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留考润,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,808評(píng)論 3 376
  • 正文 我出身青樓读处,卻偏偏與公主長(zhǎng)得像糊治,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子罚舱,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,440評(píng)論 2 359

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

  • 前言:iOS 開發(fā)中井辜,h5 和原生實(shí)現(xiàn)通信有多種方式, JSBridge 就是最常用的一種管闷,各 JSBridge ...
    ShannonChenCHN閱讀 8,496評(píng)論 11 63
  • 本文使用了第三方框架:WebViewJavascriptBridge.h 一粥脚、obj-c調(diào)用javascript的...
    清風(fēng)沐沐閱讀 1,719評(píng)論 0 3
  • 如果某個(gè)東西很難用,我就不會(huì)經(jīng)常用它包个∷⒃剩——Steve Krug妻子 引用本書作者Steve Kru在書中提到的妻子...
    九O九閱讀 2,007評(píng)論 1 4
  • 感冒了, 就像想念了赃蛛, 默默著恃锉, 自己知道就好, 不必宣揚(yáng) 淺淺微笑
    微_風(fēng)起閱讀 133評(píng)論 2 0
  • 想南陽(yáng)呕臂,是因?yàn)槭亲婕仆校幌肽详?yáng),是因?yàn)閺奈慈ミ^(guò)歧蒋;想南陽(yáng)土砂,是因?yàn)閺囊黄恼轮袑?duì)南陽(yáng)的描述;于是勾起對(duì)南陽(yáng)的念想谜洽,以致于...
    辛平閱讀 592評(píng)論 6 6