JSBridge(Android和IOS平臺(tái))的設(shè)計(jì)和實(shí)現(xiàn)

前言

對(duì)于商務(wù)類(lèi)的app鳖昌,隨著app注冊(cè)使用人數(shù)遞增备畦,app的運(yùn)營(yíng)者們就會(huì)逐漸考慮在應(yīng)用中開(kāi)展一些推廣活動(dòng)。大多數(shù)活動(dòng)具備時(shí)效性強(qiáng)许昨、運(yùn)營(yíng)時(shí)間短的特征懂盐,一般產(chǎn)品們和運(yùn)營(yíng)者們都是通過(guò)wap頁(yè)面快速投放到產(chǎn)品的活動(dòng)模塊。Wap頁(yè)面可以聲文并茂地介紹活動(dòng)糕档,但活動(dòng)的最終目標(biāo)是通過(guò)獲取特權(quán)莉恼、跳轉(zhuǎn)進(jìn)入本地功能模塊拌喉,最后達(dá)成交易。如何建立wap頁(yè)面和本地Native頁(yè)面的深度交互俐银,這就需要用到本文介紹的JSBridge尿背。

此外一些平臺(tái)類(lèi)的產(chǎn)品,如大家每天都在使用的微信捶惜、支付寶田藐、手機(jī)qq等,無(wú)一例外都在使用集成JSBridge的webContainer完成眾多業(yè)務(wù)組件功能吱七,大大減少了客戶端Native開(kāi)發(fā)的工作量汽久,不僅節(jié)約了大量人力開(kāi)發(fā)成本,還能避開(kāi)產(chǎn)品上線更新的版本審核周期限制(特別是IOS平臺(tái))踊餐。當(dāng)然這些超級(jí)APP有強(qiáng)大的技術(shù)力量支撐景醇,通過(guò)JSBridge有計(jì)劃的進(jìn)行API規(guī)范接口,不斷向前端Wap開(kāi)發(fā)人員開(kāi)放吝岭,并在版本上向下兼容三痰。但對(duì)于我們剛起步運(yùn)營(yíng)的中小級(jí)app來(lái)說(shuō)暫時(shí)還沒(méi)有必要如此大張旗鼓,相反前面提到的wap活動(dòng)推廣則是我們的主要需求窜管。

為了滿足這個(gè)需求散劫,本文通過(guò)提煉JSBridge的核心部分改造成JSService方式供各個(gè)不同的產(chǎn)品零修改方式使用。各個(gè)不同的產(chǎn)品只需要按照插件的方式提供Native擴(kuò)展接口微峰,并在各自封裝的webContainer中調(diào)用JSService對(duì)Wap調(diào)用進(jìn)行攔截處理舷丹。

具體產(chǎn)品應(yīng)用

目前該框架同時(shí)覆蓋了Android和IOS平臺(tái),在我司的幾個(gè)電商類(lèi)產(chǎn)品中都得到了很好的使用蜓肆,并趨于穩(wěn)定颜凯。
本文的Demo工程運(yùn)行效果如下:

jsapidemo_ios.png

jsapidemo_android.jpg

關(guān)于JSAPI的接口封裝

JSAPI的封裝包括核心JS和對(duì)外開(kāi)放接口JS兩個(gè)部分。 核心JS部分通過(guò)攔截某Q的wap請(qǐng)求頁(yè)面獲取仗扬,獲取的JS進(jìn)行編碼混淆處理症概,已經(jīng)通過(guò)調(diào)試進(jìn)行了注釋?zhuān)渲饕^(guò)程就是對(duì)參數(shù)和回調(diào)進(jìn)行封裝,并構(gòu)建一個(gè)url鏈接通過(guò)創(chuàng)建一個(gè)隱藏的iframe進(jìn)行發(fā)送早芭。核心JS代碼閱讀

對(duì)參數(shù)和回調(diào)進(jìn)行封裝部分的代碼如下:

//invoke
    //mapp.invoke("device", "getDeviceInfo", e);
    //@param e 類(lèi) 必須
    //@param n 類(lèi)方法 必須
    //@param i 同步回調(diào)的js方法
    //@param s 
    function k(e, n, i, s) {
        if (!e || !n) return null;
        var o, u;
        i = r.call(arguments, 2), //相當(dāng)于調(diào)用Array.prototype.slice(arguments) == arguments.slice(2),獲取argument數(shù)組2以后的元素
        
        //令s等于回調(diào)函數(shù)
        s = i.length && i[i.length - 1],
        s && typeof s == "function" ? i.pop() : typeof s == "undefined" ? i.pop() : s = null,
        
        //u為當(dāng)前存儲(chǔ)回調(diào)函數(shù)的index彼城;
        u = b(s);
        
        //如果當(dāng)前版本支持Bridge
        if (C(e, n)) {
            //將傳進(jìn)來(lái)的所有參數(shù)生成一個(gè)url字符串;
            o = "ldjsbridge:" + "/" + "/" + encodeURIComponent(e) + "/" + encodeURIComponent(n),
            i.forEach(function(e, t) {
                typeof e == "object" && (e = JSON.stringify(e)),
                t === 0 ? o += "?p=": o += "&p" + t + "=",
                o += encodeURIComponent(String(e))
            }),
            (o += "#" + u); //帶上存儲(chǔ)回調(diào)的數(shù)組index;
            
           
            //執(zhí)行生成的url, 有些函數(shù)是同步執(zhí)行完畢退个,直接調(diào)用回調(diào)函數(shù)募壕;而有些函數(shù)的調(diào)用要通過(guò)異步調(diào)用執(zhí)行,需要通過(guò)
            //全局調(diào)用去完成语盈;
            var f = N(o);
            if (t.iOS) {
                f = f ? f.result: null;
                if (!s) return f; //如果無(wú)回調(diào)函數(shù)舱馅,直接返回結(jié)果;
            }
        }else {
            console.log("mappapi: the version don't support mapp." + e + "." + n);
        }
    }

創(chuàng)建iframe發(fā)送JSBridge調(diào)用請(qǐng)求:

    //創(chuàng)建一個(gè)iframe刀荒,執(zhí)行src代嗤,供攔截
    function N(n, r) {
        console.log("logOpenURL:>>" + n);
        var i = document.createElement("iframe");
        i.style.cssText = "display:none;width:0px;height:0px;";
        var s = function() {
            //通過(guò)全局執(zhí)行函數(shù)執(zhí)行回調(diào)函數(shù)棘钞;監(jiān)聽(tīng)iframe是否加載完畢
            E(r, {
                r: -201,
                result: "error"
            })
        };
        
        //ios平臺(tái),令iframe的src為url干毅,onload函數(shù)為全局回調(diào)函數(shù)
        //并將iframe插入到body或者h(yuǎn)tml的子節(jié)點(diǎn)中宜猜;
        t.iOS && (i.onload = s, i.src = n);
        var o = document.body || document.documentElement; 
        o.appendChild(i),
        t.android && (i.onload = s, i.src = n);
        
        //
        var u = t.__RETURN_VALUE;
        //當(dāng)iframe執(zhí)行完成之后,最后執(zhí)行settimeout 0語(yǔ)句
        return t.__RETURN_VALUE = e,
        setTimeout(function() {
            i.parentNode.removeChild(i)
        },
        0),
        u
    }

對(duì)外開(kāi)放接口的封裝:(使用者只需要對(duì)該部分進(jìn)行接口擴(kuò)展即可)

mapp.build("mapp.device.getDeviceInfo", {
    iOS: function(e) {
        return mapp.invoke("device", "getDeviceInfo", e);
    },
    android: function(e) {
        var t = e;
        e = function(e) {
            try {
                e = JSON.parse(e)
            } catch(n) {}
            t && t(e)
        },
        mapp.invoke("device", "getDeviceInfo", e)
    },
    support: {
        iOS: "1.0",
        android: "1.0"
    }
}),


核心JS代碼調(diào)用說(shuō)明


mapp.version: mappAPI自身版本號(hào)

mapp.iOS: 如果在ios app中硝逢,值為true

mapp.android: 如果在android app中姨拥,值為true

mapp.support: 檢查當(dāng)前app環(huán)境是否支持該接口,支持返回true

    mapp.support("mqq.device.getClientInfo")

mapp.callback: 用于生成回調(diào)名字趴捅,跟著invoke參數(shù)傳給客戶端垫毙,供客戶端回調(diào)

    var callbackName = mapp.callback(function(type, index){
        console.log("type: " + type + ", index: " + index);
    });

mapp.invoke 方法:

mapp核心方法,用于調(diào)用客戶端接口拱绑。

        @param {String} namespace 命名空間
        @param {String} method 接口名字
        @param {Object/String} params 可選,API調(diào)用的參數(shù)
        @param {Function} callback 可選丽蝎,API調(diào)用的回調(diào)

* 調(diào)用普通的無(wú)參數(shù)接口:

        mapp.invoke("ns", "method");
        
* 調(diào)用有異步回調(diào)函數(shù)的接口:

        mapp.invoke("ns", "method", function(data){
            console.log(data);
        });
        
        或
        
        mapp.invoke("ns", "method", {
            "params" : params   //參數(shù)通過(guò)json封裝
            "callback" : mapp.callback(handler), //生成回調(diào)名字
        });


* 如果有多個(gè)參數(shù)調(diào)用:

        mapp.invoke("ns", "method", param1, param2 /*,...*/,callback);
        
        

JSService的具體實(shí)現(xiàn)-插件運(yùn)行機(jī)制

JSService部分是基于Phonegap的Cordova引擎的基礎(chǔ)上簡(jiǎn)化而來(lái)猎拨,其基本原理參照Cordova的引擎原理如圖所示:

JSBridgeIOS_1.png

一般app中都有自己定制的Webcontainer,為了更好的跟已有項(xiàng)目相融合屠阻,在Cordova的基礎(chǔ)上我們進(jìn)行了簡(jiǎn)化红省,通過(guò)JSAPIService服務(wù)的方式進(jìn)行插件擴(kuò)展開(kāi)發(fā)如圖所示:

JSBridgeIOS_2.png

本JSBridge是基于Phonegap的Cordova引擎的基礎(chǔ)上簡(jiǎn)化而來(lái), Android平臺(tái)Webview和JS的交互方式共有三種:

  1. ExposedJsApi:js直接調(diào)用java對(duì)象的方法;(同步)
  2. 重載chromeClient的prompt 截獲方案国觉;(異步)
  3. url截獲+webview.loadUrl回調(diào)的方案吧恃;(異步)

為了和IOS保持一致的JSAPI,只能選用第三套方案麻诀;

基于JSService的插件開(kāi)發(fā)痕寓、配置和使用

IOS平臺(tái)

git地址:https://github.com/Lede-Inc/LDJSBridge_IOS.git

在Native部分,定義一個(gè)模塊插件對(duì)應(yīng)于創(chuàng)建一個(gè)插件類(lèi), 模塊中的每個(gè)插件接口對(duì)應(yīng)插件類(lèi)中某個(gè)方法蝇闭。

集成LDJSBridge_IOS框架之后呻率,只需要繼承框架中的插件基類(lèi)LDJSPlugin,如下所示:

  • 插件接口定義
    #import "LDJSPlugin.h"
    @interface LDPDevice : LDJSPlugin
    {}
    
    //@func 獲取設(shè)備信息
    - (void)getDeviceInfo:(LDJSInvokedUrlCommand*)command;
    
    @end

  • 自定義插件接口實(shí)現(xiàn)
@implementation LDPDevice

/**
 *@func 獲取設(shè)備信息
 */
- (void)getDeviceInfo:(LDJSInvokedUrlCommand*)command{
    //讀取設(shè)備信息
    NSMutableDictionary* deviceProperties = [NSMutableDictionary dictionaryWithCapacity:4];
    
    UIDevice* device = [UIDevice currentDevice];
    [deviceProperties setObject:[device systemName] forKey:@"systemName"];
    [deviceProperties setObject:[device systemVersion] forKey:@"systemVersion"];
    [deviceProperties setObject:[device model] forKey:@"model"];
    [deviceProperties setObject:[device modelVersion] forKey:@"modelVersion"];
    [deviceProperties setObject:[self uniqueAppInstanceIdentifier] forKey:@"identifier"];
    
    LDJSPluginResult* pluginResult = [LDJSPluginResult resultWithStatus:LDJSCommandStatus_OK messageAsDictionary:[NSDictionary dictionaryWithDictionary:deviceProperties]];
    
    [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}

@end
  • 在plugin.json文件中對(duì)plugin插件的統(tǒng)一配置
{
    "update": "",
    "module": "mapp",
    "plugins": [
        {
            "pluginname": "device",
            "pluginclass": "LDPDevice",
            "exports": [
                {
                    "showmethod": "getDeviceInfo",
                    "realmethod": "getDeviceInfo"
                }
            ]
        }
    ]
}
  • 在webContainer中對(duì)JSService初始化, 當(dāng)初始化完成之后呻引,向前端頁(yè)面發(fā)送一個(gè)ReadyEvent礼仗,前端即可開(kāi)始調(diào)用JSAPI接口;
//注冊(cè)插件Service
    if(_bridgeService == nil){
        _bridgeService = [[LDJSService alloc] initBridgeServiceWithConfig:@"PluginConfig.json"];
    }
    [_bridgeService connect:_webview Controller:self];


/**
 Called when the webview finishes loading.  This stops the activity view.
 */
- (void)webViewDidFinishLoad:(UIWebView*)theWebView{
    NSLog(@"Finished load of: %@", theWebView.request.URL);
    //當(dāng)webview finish load之后逻悠,發(fā)event事件通知前端JSBridgeService已經(jīng)就緒
    //監(jiān)聽(tīng)事件由各個(gè)產(chǎn)品自行決定
    [_bridgeService readyWithEvent:@"LDJSBridgeServiceReady"];
}

Android平臺(tái)

git地址:https://github.com/Lede-Inc/LDJSBridge_Android.git

  • 插件接口定義
    public class LDPDevice extends LDJSPlugin {
        public static final String TAG = "Device";

        /**
         * Constructor.
         */
        public LDPDevice() {
        }
    }
    
  • LDJSPlugin 屬性方法說(shuō)明
    /**
    * Plugins must extend this class and override one of the execute methods.
    */
    public class LDJSPlugin {
        public String id;
        
        //在插件初始化的時(shí)候元践,會(huì)初始化當(dāng)前插件所屬的webview和controller
        //供插件方法接口 返回處理結(jié)果
        public WebView webView; 
        public LDJSActivityInterface activityInterface;
        
        //所有自定義插件需要重載此方法
        public boolean execute(String action, LDJSParams args, LDJSCallbackContext callbackContext) throws JSONException {
            return false;
        }
        
    }   
    
  • 自定義插件接口實(shí)現(xiàn)
@Override
    public boolean execute(String action, LDJSParams args, LDJSCallbackContext callbackContext) throws JSONException {
        if (action.equals("getDeviceInfo")) {
            JSONObject r = new JSONObject();
            r.put("uuid", LDPDevice.uuid);
            r.put("version", this.getOSVersion());
            r.put("platform", this.getPlatform());
            r.put("model", this.getModel());
            callbackContext.success(r);
        }
        else {
            return false;
        }
        return true;
    }

  • 在封裝的webContainer中注冊(cè)服務(wù)并調(diào)用:
  /**
     * 初始化Activity,打開(kāi)網(wǎng)頁(yè),注冊(cè)插件服務(wù)
     */
    public void initActivity() {
        //創(chuàng)建webview和顯示view
        createGapView();
        createViews();

        //注冊(cè)插件服務(wù)
        if(jsBridgeService == null){
            jsBridgeService = new LDJSService(_webview, this, "PluginConfig.json");
        }

        //加載請(qǐng)求
        if(this.url != null && !this.url.equalsIgnoreCase("")){
            _webview.loadUrl(this.url);
        }
    }
    
    
    
 /**
     * 初始化webview童谒,如果需要調(diào)用JSAPI单旁,必須為Webview注冊(cè)WebViewClient和WebChromeClient
     */
    @SuppressLint("SetJavaScriptEnabled")
    public void createGapView(){
        if(_webview == null){
            _webview = new WebView(LDPBaseWebViewActivity.this, null);
            //設(shè)置允許webview和javascript交互
            _webview.getSettings().setJavaScriptEnabled(true);
            _webview.getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE);

            //綁定webviewclient
            _webviewClient = new WebViewClient(){
                public void onPageStarted(WebView view, String url, Bitmap favicon){
                    super.onPageStarted(view, url, favicon);
                    isWebviewStarted = true;
                }

                public void onPageFinished(WebView view, String url) {
                    super.onPageFinished(view, url);
                        //發(fā)送事件通知前端
                    if(isWebviewStarted){
                        //在page加載之后,加載核心JS惠啄,前端頁(yè)面可以在document.ready函數(shù)中直接調(diào)用了慎恒;
                        jsBridgeService.onWebPageFinished();
                            jsBridgeService.readyWithEventName("LDJSBridgeServiceReady");
                    }
                    isWebviewStarted = false;
                }

                  @Override
                  public boolean shouldOverrideUrlLoading(WebView view, String url) {
                        if(url.startsWith("about:")){
                            return true;
                        }
                        if(url.startsWith(LDJSService.LDJSBridgeScheme)){
                            //處理JSBridge特定的Scheme
                            jsBridgeService.handleURLFromWebview(url);
                            return true;
                        }

                        return false;
                  }
            };

            _webview.setWebViewClient(_webviewClient);
            //綁定chromeClient
            _webviewChromeClient = new WebChromeClient(){
                @Override
                public boolean onJsAlert(WebView view, String url, String message,
                        JsResult result) {
                    return super.onJsAlert(view, url, message, result);
                }
            };
            _webview.setWebChromeClient(_webviewChromeClient);
        }
    }


結(jié)束

第一次寫(xiě)博客任内,寫(xiě)得糙和不好的地方望見(jiàn)諒,本人將會(huì)不斷改善和提高自身能力融柬;所以本博客主要提供大概的解決方案死嗦,望能夠和有需要的人士交流溝通具體實(shí)現(xià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)店門(mén)蚤认,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)糕伐,“玉大人砰琢,你說(shuō)我怎么就攤上這事×记疲” “怎么了陪汽?”我有些...
    開(kāi)封第一講書(shū)人閱讀 167,709評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵莺褒,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我遵岩,道長(zhǎng),這世上最難降的妖魔是什么舍哄? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,520評(píng)論 1 296
  • 正文 為了忘掉前任誊锭,我火速辦了婚禮,結(jié)果婚禮上蟆沫,老公的妹妹穿的比我還像新娘。我一直安慰自己饭庞,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,515評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布绸狐。 她就那樣靜靜地躺著累盗,像睡著了一般。 火紅的嫁衣襯著肌膚如雪符相。 梳的紋絲不亂的頭發(fā)上蠢琳,一...
    開(kāi)封第一講書(shū)人閱讀 52,158評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音挪凑,去河邊找鬼逛艰。 笑死,一個(gè)胖子當(dāng)著我的面吹牛散怖,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播咬最,決...
    沈念sama閱讀 40,755評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼欠动,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了翅雏?” 一聲冷哼從身側(cè)響起人芽,我...
    開(kāi)封第一講書(shū)人閱讀 39,660評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎橄抹,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(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
  • 文/蒙蒙 一根欧、第九天 我趴在偏房一處隱蔽的房頂上張望端蛆。 院中可真熱鬧,春花似錦嫌拣、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,272評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至酝润,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間构回,已是汗流浹背疏咐。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,393評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(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)容

  • 蘇格拉底認(rèn)為要把握真理溅蛉,不能輕信出現(xiàn)于頭腦中的偶然的意見(jiàn)他宛。我們頭腦里充滿混亂、模糊和空洞的思想镜撩。我們有許多從未加考...
    黑八先生閱讀 369評(píng)論 0 1
  • 七月底閨蜜們自助郴州行队塘,三天的行程開(kāi)心快樂(lè)宜鸯,感受很多遮怜。這是我們首次出游,一直想以文字記錄下來(lái)即碗,這次的21天無(wú)戒寫(xiě)作...
    隨性自在的杜姐閱讀 3,608評(píng)論 12 4
  • 本來(lái)這篇文章不應(yīng)該寫(xiě)的陌凳,因?yàn)樽鳛橐粋€(gè)主業(yè)為技術(shù)的IT從業(yè)人員,重新開(kāi)博快一個(gè)月了還沒(méi)產(chǎn)出一篇技術(shù)文章實(shí)屬不該(這...
    當(dāng)紅辣椒炒肉閱讀 362評(píng)論 0 1
  • 張愛(ài)玲曾經(jīng)說(shuō)過(guò),愛(ài)一個(gè)人有些時(shí)候我們會(huì)卑微到塵埃里裸准。 我們以為愛(ài)情可以填滿人生的遺憾。然而盐肃,制造更多遺憾的权悟,卻偏偏...
    南燭姑娘閱讀 1,431評(píng)論 0 2