使用Rx實現(xiàn)MVVM架構(gòu)

簡介

最近接手了一個N年前的老項目,由于實在是過于陳舊,于是提出想要重構(gòu)項目該項目的想法炉峰,沒想到和項目經(jīng)理竟然達成了共識当悔。在重構(gòu)初始階段項目框架搭建時傅瞻,考慮到將來所承載的業(yè)務(wù)的可變性比較大,如果單純的從翻新項目的角度出發(fā)盲憎,可能會在日后的迭代過程中埋下隱藏的炸彈嗅骄,所以決定使用MVVM架構(gòu)進行新項目的研發(fā)。項目基礎(chǔ)組件已經(jīng)完成開發(fā)饼疙,目前溺森,正在進行登錄業(yè)務(wù)模塊的功能開發(fā),通過使用RxMVVM的實現(xiàn)不管是從可測試性還是可擴展性而言窑眯,都變得十分的優(yōu)雅了屏积。甚至對于業(yè)務(wù)的剝離的效果十分明顯。

接下來磅甩,我把到目前為止的所有實現(xiàn)分為以下3個核心部分:
???(1). MVVM架構(gòu)思想炊林;
???(2). 函數(shù)響應式編程;
???(3). 登錄的實踐卷要;

MVVM架構(gòu)思想

分析下現(xiàn)在熱門的幾款架構(gòu)模式中:MVP渣聚、MVCS却妨、VIPER以及MVVM究其根本饵逐,其實都是沖MVC衍變優(yōu)化而來。當然彪标,對于經(jīng)典的MVC我們用一句話概括:盡管這種設(shè)計足夠優(yōu)秀倍权,但是在使用過程中很難確定究竟應該在哪個模塊做什么事情,以至于出現(xiàn)了Controller變臃腫和胖Model的窘境。

回過頭來看MVVM薄声,現(xiàn)在可以說但凡是個程序猿当船,沒有不知道它的。不管你在項目中如何使用它默辨,都逃離不掉3大模塊:
???(1). 數(shù)據(jù)層德频,也就是Model
???(2). 視圖模型層缩幸,也就是ViewModel壹置;
???(3). 視圖層,也就是View或者ViewController表谊;
???PS:有些會把它分為4塊钞护,我更傾向于ViewController包含于視圖層的觀點。

正是由于在ViewModel之間多加了一層ViewModel爆办,讓原來無處安放的網(wǎng)絡(luò)請求和大量的復雜業(yè)務(wù)有了明確的去處难咕,解決了原來MVC測試性差和日益臃腫的Controller的問題。同樣將業(yè)務(wù)抽離到ViewModel中后距辆,也降低了代碼的耦合度余佃,提高了復用性。因為對于同樣的業(yè)務(wù)跨算,如果采用在Controller中實現(xiàn)的話爆土,當不同界面的Controller要使用到相同的業(yè)務(wù)時,就沒辦法服用之前的代碼漂彤,而需要復制粘貼了雾消。但是有了ViewModel后,同樣的業(yè)務(wù)挫望,只需在對應的Controller中執(zhí)行同樣的ViewModel就行了。

MVVM的精髓我認為是在于如何處理ViewModel綁定的問題狂窑。按照以前的慣例媳板,我們或許會在某一個cell中聲明一個model屬性,然后在實現(xiàn)文件中自定義它的setter方法泉哈,最后拿到數(shù)據(jù)進行展示蛉幸。除非你的model沒有做過翻譯直接是一個dictionary,或者通過id類型使用kvc取值丛晦,否則這種方法會導致viewmodel形成跨層的訪問奕纫。從大大降低了代碼的可閱讀性,對于后期維護十分不便烫沙。

為了解決這樣的跨層訪問匹层,通常ViewModel中可以通過kvo來實現(xiàn)modelview的映射。但是當view接收到用戶操作后需要進行model修改時锌蓄,常用的就是Target-Actiondelegate升筏,甚至于Notification撑柔、block也可以,這些方法并非不可取您访,也足夠優(yōu)秀铅忿,但是卻比較繁雜。而雙向綁定的的出現(xiàn)就完美且優(yōu)雅的改變了這種方式灵汪,這也是為什么一提到MVVM我們就能連想到RxRAC的原因了檀训。

同樣,正是由于雙向綁定的出現(xiàn)享言,Controller完成的職責就很明確了峻凫,即將ViewModelView進行綁定。所以業(yè)內(nèi)流傳的這張圖片就很好的體現(xiàn)了這一變化:

MVVM.gif

經(jīng)過這樣的衍變之后担锤,不難看出MVVM的關(guān)系基本就是:View <-> C <-> ViewModel <-> Model蔚晨。最終,促成了業(yè)務(wù)和UI的分離肛循,規(guī)范了ViewController強耦合的性質(zhì)铭腕。邏輯上而言,只有Controller知道需要呈現(xiàn)的View多糠,也只有它知道需要使用的業(yè)務(wù)ViewModel累舷,兩個層級之間通過Controller形成橋梁進行通信。當然這里有個問題是夹孔,當用戶操作通過View想要改變Model的屬性時被盈,業(yè)務(wù)線會變得比較長,而這塊將會在后面列表展示時體現(xiàn)出來搭伤,本次實踐中暫不做討論只怎。

盡管MVVM如此優(yōu)秀,但是任然需要注意很多細節(jié)怜俐,否則就會重蹈MVCController的覆轍身堡,我總結(jié)了下,大概有以下幾點需要注意的:
???(1). Controller中盡量不要做業(yè)務(wù)相關(guān)的事情拍鲤,它的主要職責是管理subview和進行ViewViewModel的綁定贴谎;
???(2). 業(yè)務(wù)邏輯盡量讓ViewModel來處理。同時應該避免ViewModel過于龐大而導致和MVC相同的問題季稳,所以ViewModel之間是可以存在依賴的擅这;
???(3). subview是否能夠直接引用ViewModel?既然UIViewControllerUIView都是View層的組件景鼠,所以是可以引用的仲翎。但是反過來卻不行,因為一旦ViewModel包含了View,就會導致View產(chǎn)生耦合谭确,代碼復用性和可測試性都會降低帘营;
???(4). ViewModel可以引用Model。但是反過來卻不行逐哈,原因同上芬迄;

函數(shù)響應式編程

通常我們在開發(fā)中會建立很多網(wǎng)狀關(guān)系,代碼如下:

int a = 1;
int b = a + 1;
print(b);  // b = 2
a = 5;
print(b);  // b = 2

這樣的代碼沒有任何問題昂秃,但是我們想要的卻是變量a和變量b形成對應關(guān)系禀梳,當a改變時應該觸發(fā)b的更新操作。在沒有響應式的前提下肠骆,需要花精力去維護一套這樣的“關(guān)系”算途,無疑會增加開發(fā)成本。而響應式編程就是想通過某些操作來幫我們構(gòu)建這種關(guān)系:

int a = 1;
int b <- a + 1; // <- 是響應式中定義的一種操作
print(b); // b = 2
a = 5;
print(b); // b = a + 1 = 6

所以響應式編程的核心就是通過定義好的操作來建立這種關(guān)系蚀腿,而不是通過賦值等嘴瓤。響應式編程就是一種通過異步和數(shù)據(jù)流來建立事務(wù)關(guān)系的編程模型。它是一種基于事件的莉钙,專注于數(shù)據(jù)流和變化傳遞的編程范式廓脆。

而函數(shù)響應式編程就是通過函數(shù)來創(chuàng)建這種“關(guān)系”,并在適當?shù)牡胤巾憫@種“關(guān)系”磁玉,從而得到正確的結(jié)果停忿。

在眾多函數(shù)響應式的框架中ReactiveX應該算是代表了。它是一套完整的跨平臺的解決方案蚊伞,而RxSwift則是它對iOS/macOS平臺支持的三方庫席赂。在RxSwift中最核心的包括:
???(1). Observable - 產(chǎn)生事件;
???(2). Observer - 響應事件时迫;
???(3). Operator - 創(chuàng)建變化組合事件颅停;
???(4). Disposable - 管理綁定(訂閱)的生命周期;
???(5). Schedulers - 線程隊列調(diào)配掠拳;
這里假設(shè)你對RxSwift有一定的了解便监,理應知道這5大核心的左右和如何使用,如若不清楚的碳想,可以通過連接自行查看中文教程。

我們有說到Rx改變了常見的響應方式毁靶,包括Target-Action胧奔、DelegateNotification预吆、Block等等龙填。從代碼層面是如何體現(xiàn)的呢:

// MARK: Target-Action 使用 RxSwift 實現(xiàn)的對比
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
func buttonTapped() {
    print("button Tapped")
}
// Rx
button.rx.tap
    .subscribe(onNext: {
        print("button Tapped")
    })
    .disposed(by: disposeBag)

// MARK: Delegate 使用 RxSwift 實現(xiàn)的對比
class ViewController: UIViewController {
    ...
    override func viewDidLoad() {
        super.viewDidLoad()
        scrollView.delegate = self
    }
}
extension ViewController: UIScrollViewDelegate {
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        print("contentOffset: \(scrollView.contentOffset)")
    }
}
// Rx
class ViewController: UIViewController {
    ...
    override func viewDidLoad() {
        super.viewDidLoad()

        scrollView.rx.contentOffset
            .subscribe(onNext: { contentOffset in
                print("contentOffset: \(contentOffset)")
            })
            .disposed(by: disposeBag)
    }
}

// MARK: 閉包或 Block 使用 RxSwift 實現(xiàn)的對比
URLSession.shared.dataTask(with: URLRequest(url: url)) {
    (data, response, error) in
    guard error == nil else {
        print("Data Task Error: \(error!)")
        return
    }

    guard let data = data else {
        print("Data Task Error: unknown")
        return
    }

    print("Data Task Success with count: \(data.count)")
}.resume()
// Rx
URLSession.shared.rx.data(request: URLRequest(url: url))
    .subscribe(onNext: { data in
        print("Data Task Success with count: \(data.count)")
    }, onError: { error in
        print("Data Task Error: \(error)")
    })
    .disposed(by: disposeBag)

// MARK: Notification 使用 RxSwift 實現(xiàn)的對比
var ntfObserver: NSObjectProtocol!

override func viewDidLoad() {
    super.viewDidLoad()

    ntfObserver = NotificationCenter.default.addObserver(
          forName: .UIApplicationWillEnterForeground,
          object: nil, queue: nil) { (notification) in
        print("Application Will Enter Foreground")
    }
}

deinit {
    NotificationCenter.default.removeObserver(ntfObserver)
}
// Rx
override func viewDidLoad() {
    super.viewDidLoad()

    NotificationCenter.default.rx
        .notification(.UIApplicationWillEnterForeground)
        .subscribe(onNext: { (notification) in
            print("Application Will Enter Foreground")
        })
        .disposed(by: disposeBag)
}

// MARK: 多任務(wù)依賴使用 RxSwift 實現(xiàn)的對比
API.token(username: "beeth0ven", password: "987654321",
    success: { token in
        API.userInfo(token: token,
            success: { userInfo in
                print("獲取用戶信息成功: \(userInfo)")
            },
            failure: { error in
                print("獲取用戶信息失敗: \(error)")
        })
    },
    failure: { error in
        print("獲取用戶信息失敗: \(error)")
})
// Rx
API.token(username: "beeth0ven", password: "987654321")
    .flatMapLatest(API.userInfo)
    .subscribe(onNext: { userInfo in
        print("獲取用戶信息成功: \(userInfo)")
    }, onError: { error in
        print("獲取用戶信息失敗: \(error)")
    })
    .disposed(by: disposeBag)

// MARK: 等待多個并發(fā)任務(wù)完成后處理結(jié)果使用 RxSwift 實現(xiàn)
Observable.zip(
      API.teacher(teacherId: teacherId),
      API.teacherComments(teacherId: teacherId)
    ).subscribe(onNext: { (teacher, comments) in
        print("獲取老師信息成功: \(teacher)")
        print("獲取老師評論成功: \(comments.count) 條")
    }, onError: { error in
        print("獲取老師信息或評論失敗: \(error)")
    })
    .disposed(by: disposeBag)

代碼從來都不會說謊。RxSwift從維護性、易讀性甚至代碼量上都遠優(yōu)于未使用時岩遗,對比十分明顯扇商。特別是對異步操作時簡化了代碼邏輯,統(tǒng)一了代碼風格宿礁,而我們只需要專注的做業(yè)務(wù)相關(guān)的研發(fā)就可以了案铺。同時,這也是為什么我們要使用它的原因梆靖。

那么Rx是不是就是完美的呢控汉?我認為至少存在以下問題:
???(1). 學習Rx的難度相對于老方法要難得多,學習成本大返吻;
???(2). 不便于調(diào)試姑子。原來出現(xiàn)問題會在固定的地方發(fā)現(xiàn),而現(xiàn)在經(jīng)過 Rx傳遞后可能被轉(zhuǎn)移测僵;
想要了解并學習更多關(guān)于RxSwift的知識可以在這里獲得 - RxSwift中文教程街佑。

登錄的實踐

和普通登錄頁面一樣,賬號和密碼通過對應的正則判斷后控制了登錄按鈕的交互狀態(tài)捍靠。點擊登錄后登錄按鈕關(guān)閉交互狀態(tài)并禁止頁面上的其他控件產(chǎn)生交互沐旨,如下圖所示:


Rx實現(xiàn)MVVM的登錄頁面.png

MVVM設(shè)計模式中,ViewModel的實現(xiàn)至關(guān)重要剂公。良好的ViewModel的設(shè)計將直接影響程序的維護性和可測試性等希俩。在這篇文章中對ViewModel的實現(xiàn)有十分重要的指導作用,我也是根據(jù)它的原理來設(shè)計和實現(xiàn)的 - 怎么搞定ViewModel纲辽。

不難看出颜武,在本例中Inputs是:
???(1). 手機號輸入事件;
???(2). 密碼輸入事件拖吼;
???(3). 登錄按鈕點擊事件鳞上;
Outputs是:
???(1). 登錄狀態(tài),控制登錄按鈕的UI呈現(xiàn)吊档。在正常狀態(tài)下顏色亮一些篙议,不可點擊時顏色淺,登錄執(zhí)行中存在菊花怠硼;
???(2). 賬號和密碼檢測鬼贱,項目中沒有用到,將來可能會在UI上體現(xiàn)出來香璃;
???(3). 登錄中这难,用戶不能和登錄頁面進行交互;
???(4). 登錄結(jié)果葡秒;
從而姻乓,得出InputsOutputs應該長這樣:

inputs: (
    account: Driver<String>,
    password: Driver<String>,
    loginTaps: Signal<()>
)
struct Outputs {
    // 賬號和密碼的驗證
    let validatedAccount: Driver<SignValidationResult>
    let validatedPassword: Driver<SignValidationResult>
    let signupState: Driver<ActivityButton.State> // 登錄狀態(tài)
    let signedIn: Driver<Bool>  // 登錄結(jié)果
    let signingIn: Driver<Bool> // 登錄中
}

這時候嵌溢,我們可以通過服務(wù)的方式提供具體的驗證實現(xiàn)和網(wǎng)絡(luò)接口:

enum SignValidationResult {
    case ok(message: String)
    case empty
    case failed(message: String)
}
extension SignValidationResult {
    var isValid: Bool {
        switch self {
        case .ok:
            return true
        default:
            return false
        }
    }
}

// 提供登錄賬號和登錄密碼正則判斷的驗證服務(wù)
protocol SignValidationService {
    func validateAccount(_ account: String) -> SignValidationResult
    func validatePassword(_ password: String) -> SignValidationResult
}

// 提供登錄接口的服務(wù)
protocol MxsSignAPI {
    func signup(_ account: String, password: String) -> Observable<Bool>
}

在驗證服務(wù)中,由于項目本身使用的是本地正則來判斷是否合法蹋岩,是能夠立即得到結(jié)果而并非異步等待赖草,所以直接返回的驗證結(jié)果的枚舉。當這里的驗證是由服務(wù)端執(zhí)行的剪个,那么在和服務(wù)端交互時秧骑,驗證返回的應該是一個事件Observable <SignValidationResult>,這樣才能統(tǒng)一異步序列禁偎,否則就會出現(xiàn)因為異步開發(fā)導致的一些不太好處理的問題腿堤。當然,之所以使用協(xié)議來定義這些服務(wù)是想要增加擴展性和穩(wěn)定性如暖,當默認實現(xiàn)不滿足需求時笆檀,可以通過新的實現(xiàn)來擴展功能,而無需對已存在的接口進行修改盒至,只要滿足條件酗洒,那么修改的就只有協(xié)議的實現(xiàn)部分,其他任然保持原樣(PS:協(xié)議的默認實現(xiàn)我就不給出來了枷遂,留給你們想象的空間>_<) 樱衷。這樣我們可以得出一個小技巧:異步操作時使用Observable,否則服務(wù)方直接返回結(jié)果酒唉。

登錄是一個異步的網(wǎng)絡(luò)過程矩桂,當?shù)卿浾埱笳谟|發(fā)工程中時,輸入框和按鈕都是無法交互的痪伦。所以需要有一個東西能夠監(jiān)聽到請求服務(wù)的狀態(tài)變化侄榴,ActivityIndicator正好能夠做到這樣的事情。ActivityIndicator服從協(xié)議SharedSequenceConvertibleType网沾,直接調(diào)用asObservable()就可以得到_loading的狀態(tài)癞蚕。

_loading可以看成一個發(fā)射器,當_relay行為的值為0是發(fā)送一個false的值辉哥,當_relay行為值大于0時發(fā)送一個true的值桦山,表明當前還有Observable正在執(zhí)行。再通過incrementdecrement來處理Observable的數(shù)量:

private let _relay = BehaviorRelay(value: 0)
private let _loading: SharedSequence<SharingStrategy, Bool>

_loading = _relay.asDriver()
    .map { $0 > 0 }
    .distinctUntilChanged()
private func increment() {
    _lock.lock()
     _relay.accept(_relay.value + 1)
    _lock.unlock()
}

private func decrement() {
    _lock.lock()
    _relay.accept(_relay.value - 1)
    _lock.unlock()
}

為了能夠?qū)?code>ActivityIndicator和需要監(jiān)聽的Observable綁定醋旦,專門為Observable擴展了一個trackActivity()的方法恒水,傳入一個ActivityIndicator來跟蹤Observable的狀態(tài):

public func trackActivity(_ activityIndicator: ActivityIndicator) -> Observable<Element> {
    return activityIndicator.trackActivityOfObservable(self)
}

class ActivityIndicator {
    fileprivate func trackActivityOfObservable<Source: ObservableConvertibleType>(_ source: Source) -> Observable<Source.Element> {
        return Observable.using({ () -> ActivityToken<Source.Element> in
            self.increment()
            return ActivityToken(source: source.asObservable(), disposeAction: self.decrement)
        }) { t in
            return t.asObservable()
        }
    }
}

通過調(diào)用Observableusing方法將incrementdecrementObservable的生命周期綁定。當using調(diào)用resourceFactory時執(zhí)行increment饲齐,當dispose時執(zhí)行decrement寇窑,這樣就可以檢測有多少個Observable處于正在處理的狀態(tài)了。

至此箩张,ViewModel實現(xiàn)代碼如下:

class SignupViewModel {
    struct Outputs {
        let validatedAccount: Driver<SignValidationResult>
        let validatedPassword: Driver<SignValidationResult>
        let signupState: Driver<ActivityButton.State>
        let signedIn: Driver<Bool>
        let signingIn: Driver<Bool>
    }

    let outputs: Outputs

    init(
        inputs: (
            account: Driver<String>,
            password: Driver<String>,
            loginTaps: Signal<()>
        ),
        dependency: (
            API: MxsSignAPI,
            validationService: SignValidationService
        ) = (MxsSignDefaultAPI(), SignValidationDefaultService())
    ) {
        let API               = dependency.API
        let validationService = dependency.validationService

        let accountAndPassword = Driver.combineLatest(inputs.account, inputs.password) { ($0, $1) }
        let activity                          = ActivityIndicator()
        let signingIn                       = activity.asDriver()
        let validatedAccount        = inputs.account
                                     .map { account in
                                        return validationService.validateAccount(account)
                                    }
        let validatedPassword     = inputs.password
                                    .map { password in
                                        return validationService.validatePassword(password)
                                    }
        let signedIn            = inputs.loginTaps.withLatestFrom(accountAndPassword)
                                    .flatMapLatest {
                                        return API.signup($0.0, password: $0.1)
                                            .trackActivity(activity)
                                            .asDriver(onErrorJustReturn: false)
                                    }
        let signupState         = Driver.combineLatest(
                                        validatedAccount, validatedPassword, signingIn
                                    ) { username, password, signingIn -> ActivityButton.State in
                                        if username.isValid && password.isValid {
                                            if signingIn {
                                                return ActivityButton.State.activiting
                                            } else {
                                                return ActivityButton.State.default
                                            }
                                        } else {
                                            return ActivityButton.State.disable
                                        }
                                    }
                                    .distinctUntilChanged()
        outputs                 = Outputs(validatedAccount: validatedAccount,
                                          validatedPassword: validatedPassword,
                                          signupState: signupState,
                                          signedIn: signedIn,
                                          signingIn: signingIn)
    }
}

將賬號(account)和密碼(password)的輸入利用combineLatest組合起來得到賬號密碼的組合序列accountAndPassword甩骏,只要賬號和密碼中的任意一個改變都會觸發(fā)一次事件。將賬號和密碼的輸入先慷,利用map和提供的服務(wù)饮笛,轉(zhuǎn)化為校驗結(jié)果的序列validatedAccountvalidatedPassword。當發(fā)送網(wǎng)絡(luò)請求時论熙,利用ActivityIndicator監(jiān)聽請求狀態(tài)福青,再和校驗結(jié)果的信號組合,從而得到登錄按鈕當前所屬狀態(tài)脓诡。最后將得到的信號包裝為Outputs提供給Controller用于綁定或事件无午,完成了ViewModel的功能。

番外

在這次重構(gòu)中祝谚,一邊學習RxSwift一邊整理宪迟,真正體會到了MVVM的精髓。由于登錄業(yè)務(wù)還是比較簡單的交惯,還不能以偏概全的認為“就是這樣”次泽,在重構(gòu)過程中,后面會遇到真正的難點席爽,對于復雜頁面和復雜業(yè)務(wù)的實現(xiàn)意荤。到時候再以續(xù)集的博客形式記錄。
最后附上本次RxSwift官方的例子作為參考:GitHubSignup - 簡易仔細閱讀

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末只锻,一起剝皮案震驚了整個濱河市玖像,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌齐饮,老刑警劉巖捐寥,帶你破解...
    沈念sama閱讀 216,496評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異沈矿,居然都是意外死亡上真,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評論 3 392
  • 文/潘曉璐 我一進店門羹膳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來睡互,“玉大人,你說我怎么就攤上這事陵像【椭椋” “怎么了?”我有些...
    開封第一講書人閱讀 162,632評論 0 353
  • 文/不壞的土叔 我叫張陵醒颖,是天一觀的道長妻怎。 經(jīng)常有香客問我,道長泞歉,這世上最難降的妖魔是什么逼侦? 我笑而不...
    開封第一講書人閱讀 58,180評論 1 292
  • 正文 為了忘掉前任匿辩,我火速辦了婚禮,結(jié)果婚禮上榛丢,老公的妹妹穿的比我還像新娘铲球。我一直安慰自己,他們只是感情好晰赞,可當我...
    茶點故事閱讀 67,198評論 6 388
  • 文/花漫 我一把揭開白布稼病。 她就那樣靜靜地躺著,像睡著了一般掖鱼。 火紅的嫁衣襯著肌膚如雪然走。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,165評論 1 299
  • 那天戏挡,我揣著相機與錄音芍瑞,去河邊找鬼。 笑死增拥,一個胖子當著我的面吹牛啄巧,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播掌栅,決...
    沈念sama閱讀 40,052評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼秩仆,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了猾封?” 一聲冷哼從身側(cè)響起澄耍,我...
    開封第一講書人閱讀 38,910評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎晌缘,沒想到半個月后齐莲,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,324評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡磷箕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,542評論 2 332
  • 正文 我和宋清朗相戀三年选酗,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片岳枷。...
    茶點故事閱讀 39,711評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡芒填,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出空繁,到底是詐尸還是另有隱情殿衰,我是刑警寧澤,帶...
    沈念sama閱讀 35,424評論 5 343
  • 正文 年R本政府宣布盛泡,位于F島的核電站闷祥,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏傲诵。R本人自食惡果不足惜凯砍,卻給世界環(huán)境...
    茶點故事閱讀 41,017評論 3 326
  • 文/蒙蒙 一箱硕、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧果覆,春花似錦颅痊、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽菱属。三九已至钳榨,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間纽门,已是汗流浹背薛耻。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留赏陵,地道東北人饼齿。 一個月前我還...
    沈念sama閱讀 47,722評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像蝙搔,于是被迫代替她去往敵國和親缕溉。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,611評論 2 353

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