基于RxSwift的MVVMR架构(一)思想、基本原理及初步实现

摘要

本系列文章将详细分析iOS的MVVMR架构模式,并基于Swift的响应式、函数式编程框架RxSwift提供相应的实现。
系列共分为两个部分:

  1. MVVMR架构的思想、基本原理及其初步实现
  2. 架构中某些细节部分的实现封装以及实战

这篇文章讲述的是第一部分的内容,我会先把架构的各个组成粗略地罗列出,然后再对它们进行详细的分析,最后结合代码进行实现。若文章中存在模糊或不合理的地方,还请各位包涵,也欢迎大家向我进行反馈。

前言

寒假期间,我和小组的伙伴们针对团队之前的一款还未上线的项目进行大重构,开发语言从Objective-C转到了Swift,作为组长,我负责了整个项目架构的搭建。在旧项目中,我是基于ReactiveCocoa搭起了MVVM这种架构模式。鉴于此架构模式优点颇多,并且在前面也已实践过,所以在新项目里我也依旧沿用了MVVM,不过用于事件以及数据绑定的框架就改用了RxSwift。关于RxSwift,我在之前也写过一篇文章: RxSwift进阶与实战 ,这篇文章的实战部分也提供了一个简单的MVVM架构模式实现。

分析下现在移动端开发比较热门的几款架构模式:MVC, MVP, MVVM, VIPER,除去最经典的模式MVC外,其余的模式究其根本,其实都是从MVC衍变而来,并且都是针对其中的Controller层进行分层再细化,而本文所针对的MVVM架构模式,原理上它为了减轻MVC架构中Controller层的业务负担,将Controller再细分为两个部分,一部分跟原本的View层合并在一起形成新的View层,另一部分就变成了ViewModel层。(这里所说的Controller并不能指代iOS中的UIViewController)对于我来说MVPMVVM极其相似,如果偏要让我说出它们的不同,我会主观地认为MVPPresentation做的事情只是数据与事件的解析转换等业务逻辑,而MVVMViewModel中除了这些业务逻辑外,里面还可以存有某些状态变量,像在RAC中,专门有一个宏用于状态变量与信号的绑定:RAC(viewModel, userToken) = userTokenSignal,而RxSwift我个人认为是倾向于流的转换,尽量避免出现状态变量。所以重构项目中有时候我会想: "我TM是在写MVVM还是MVP?!",算了,不要在意这些细节...

在上面我一直都在说MVVM模式,但是文章的标题呈现的却是MVVMRR其实是我“自作主张”增添的Router(路由器),这个设计在下面会说到。

接下来我就本此项目的架构,向大家进行详细的分析。

MVVMR基本蓝图

首先给大家看两张示意图:


基本构成
事件、数据流转换

这两张图分别表示了MVVMR架构中的基本构成要素和流转换示意。
在图从,我们可以看到整个架构的组成要素分别是:View(视图)ViewModel(视图模型)Input(输入)Output(输出)Router(路由器)UnitCase(单位实例), 其中,ViewInput同组成View层,ViewModelOutput组成ViewModel层,UnitCase为一个简单的数据结构,用于保存ViewViewModel的关系,用于后期绑定器对它们相互间进行绑定,这个在后面会详细说到。

接下来有两方面需要提及:

架构目的

前面说到,传统的MVC架构中,ViewController由于要处理过多的业务逻辑以及对View层的显示逻辑,会变得越来越复杂,最后将会成为一个重量级角色,在开发中容易乱了手脚,并且严重缺乏可维护性。MVVM架构致力于减轻ViewController层的负担,将一部分属于纯业务逻辑处理放到了ViewModel中,而对于View的显示逻辑,如UIView布局渲染、动画等就一并归于View层。

View : 视图的布局、渲染、动画、UIViewController的转场

ViewModel : 纯业务逻辑处理

Model : 提供数据,如网络请求数据,本地数据库、UserDefaults

有一点需要注意的是,因为MVVM比起MVC来说在层级的数量上有所增加,所以我们需要再从原来的基础上多维护了某些东西,这很容易造成架构中耦合度的上升,为了降低耦合,我在架构中引入了Input以及Output的概念,后面有详细的分析。

架构思想

整套架构围绕着的一个思想是: 事件与数据基于流的抽象
我们把事件(如用户的触发事件)以及数据(网络、本地数据)抽象成在一条在管道中流动的,每一次的业务处理,都像是一个接入了这条管道中的流处理器,将流入的流转换加工,并输出处理过后的流。因为事件或数据可能会涉及多个不同的业务处理,所以在管道中也可以接入多个流处理器,让事件和数据在管道中流动的时候发生连锁反应。

流思想

在本架构中,RxSwift框架就是这条包裹着事件与数据流的管道。

各模块详解

接下来我就MVVMR架构中各重要模块的概念,结合iOS的实际开发来详细说明。

View

View层做的东西都只是跟视图有关系,布局、渲染、动画等等,并不会接触与业务相关的内容。它汇总视图的触发事件或数据,构建出Input传入ViewModel,并接受ViewModel传过来的Output,刷新视图显示。
架构中,我把UIViewController也归入了View层中,因为个人觉得ViewController与视图有着非常密切的关联,若要强制性分离职责,应该将ViewController里面的所有业务逻辑抽离出来,让UIViewController其只充当View的一部分。
View中还持有路由器Router,用于视图的跳转。(接下来会说到)

ViewModel

说简单点,ViewModel做的事情就只有一件: 转换,说复杂点,ViewModel需要将View传入的Input中所有的事件数据流进行转换处理,最终将完成处理后的流放入Output中传递给View,所有的业务逻辑都是在这里进行实现,其中涉及到数据的请求(网络、本地)需要向Model请求获取。

Model

模型层,数据提供者。提供网络请求数据,本地数据库、UserDefaults缓存数据,支持对数据进行解析处理。不过在实际项目中,Model层并没有很明显地表示出来,我是将网络请求、JSON数据解析和本地缓存封装在一起构建出一个较为强大的“流转换器”,其也算是Model层的一部分。相关网络请求的封装我会在下一篇中谈到。

Input & Output

InputOutput其实是一个容器,里面装载着各种事件数据流,在ViewViewModel通信中起到传递的作用。
看到这里,可能有人会认为,ViewViewModel之间的相互通信较为简单,只需通过方法去调用即可,没必要又另外再构建多一个InputOutput。其实,在框架设计中,我构建了这两个东西,主要目的就是实现View层与ViewModel层的完全解耦。架构在工作的时候,View以及ViewModel各自维护着自己的运作,且它们之间不存在过多的耦合,即两层之间互不关注,也互不知道对方的实际情况,它们间的通信只依赖于InputOutput。这样,在开发以及后期的维护中,我们在对其中一层进行修改或重构时,另外一层可完全不需要改动。
在实际项目中,我使用的是Swift的struct(结构体)去实现InputOutput,并将每个事件数据流作为结构体中的属性来持有。这样做的好处是每个事件数据流都能清晰明了地列举在代码中,通过观察Input的属性,我们能知道View能够产出多少种视图触发的事件数据流,通过观察Output的属性,我们也得知View最终要接收哪些更新视图的事件数据流。

这里总结下设计InputOutput的目的:

  • 实现ViewViewModel层之间的解耦
  • 能够清晰罗列出各种事件数据流

Binder & UnitCase

首先来说下Binder(绑定器),它要做的事情就是将View以及ViewModel进行绑定。这里就抛出了一个问题: 我们为什么需要绑定?
我们知道,iOS应用是以页面为单元的,一个页面就是一套MVCMVVM工作的结果。而普通的应用本身是拥有非常多的页面,若我们使用的架构模式是MVVM,这就需要创建同等数量的若干套MVVM,而MVVM的构建需要将各层各模块联系绑定在一起。若每次我们在需要跳转到一个新页面时才去对架构进行绑定,这就增大了代码的复杂度以及冗余度,所以,我们需要一套机制,在我们需要呈现一个新页面时,自动帮我们将架构各层各模块进行绑定。在MVVMR架构中,我使用的是Binder来实现这种机制。
但是,在绑定时,我们必须要将ViewViewModel一一对应起来,不可能说将一个页面的View跟其他页面的ViewModel进行绑定。所以,为了明确这种一一对应关系,我引入了UnitCase,它是一个存有ViewViewController对应关系的容器。通过UnitCaseBinder就能正确绑定ViewViewModel层。

Router

路由器就是为了实现各页面之间的跳转。为什么架构中不直接使用iOS API提供的pushViewControllerpresentViewController等方法呢?这里考虑到两个问题:

  • 页面需要创建后才能够进行跳转,而在创建页面的时候需要进行绑定。如果将页面绑定跟跳转封装在一起将带来较大的便利。
  • 页面间需要传递事件和数据,通常的做法是使用方法传递(正向传递)或者代理模式(反向传递),这样做耦合度较大。需要一种实现页面间传递数据且耦合度较低的机制。

路由器就是解决以上问题的优雅方案,它与Binder(绑定器)密切结合,并提供页面间数据传递的接口。所以,这篇文章所讲述的架构模式名为MVVMR(传统MVVM+Router)

架构实现

下面就是"Show My Code"的时间,以下我会贴出初步实现MVVMR架构的相关代码,对于一些更为细节的实现封装,我会在下一篇文章中谈到。

协议部分

Swift作为一门倾于“面向协议”编程范式的语言,编写的时候当然要更好地去发挥其协议的作用。

Input & Output Protocol

/// Input Output
protocol ViewToViewModelInput {
    init(view: MVVMView)
}
protocol ViewModelToViewOutput {
    init(viewModel: MVVMViewModel)
}

上面就是InputOutput的协议定义,从名字上可以很清晰地看出它们的作用,一个是从View传递到ViewModel,而另一个则是反过来传递。它们的构造都需要自身的发出者。

Provider Protocol

/// Provider
protocol ViewToViewModelInputProvider {
    var inputType: ViewToViewModelInput.Type? { get }
    func provideInput() -> ViewToViewModelInput?
}

extension ViewToViewModelInputProvider where Self: MVVMView {
    func provideInput() -> ViewToViewModelInput? {
        return self.inputType?.init(view: self)
    }
}

protocol ViewModelToViewOutputProvider {
    var outputType: ViewModelToViewOutput.Type? { get }
    func provideOutput() -> ViewModelToViewOutput?
}

extension ViewModelToViewOutputProvider where Self: MVVMViewModel {
    func provideOutput() -> ViewModelToViewOutput? {
        return self.outputType?.init(viewModel: self)
    }
}

Provider(提供者)是针对InputOutput的构建而设计的,意为InputOutput的提供者。每个提供者里具有一个元类类型的属性以及一个提供方法,而在分类中,提供方法已经帮我们去实现了。所以在实现提供者协议的时候,我们只需提供相应的InputOutput类型即可。

View 和 ViewModel 实现

View以及ViewModel的实现,我是拟好了两个抽象类:MVVMViewMVVMViewModel

// MARK: - View & ViewModel
class MVVMView: UIViewController, ViewToViewModelInputProvider {
    private let viewModelType: MVVMViewModel.Type?
    private(set) var router: Router!
    var viewModel: MVVMViewModel?
    var inputType: ViewToViewModelInput.Type? { return nil }
    var receive: Driver<Any>?
    
    required init(_ viewModelType: MVVMViewModel.Type?) {
        self.viewModelType = viewModelType
        super.init(nibName: nil, bundle: nil)
        self.router = Router(from: self)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        if let viewModelType = self.viewModelType, let input = self.provideInput() {
            self.viewModel = viewModelType.init(input: input)
            if let output = self.viewModel!.provideOutput() {
                rxDrive(viewModelOutput: output)
            }
        }
    }
    
    func rxDrive(viewModelOutput: ViewModelToViewOutput) { crash("抽象方法,在此进行绑定,此方法必须重写!") }
    func provideCallBack() -> Driver<Any>? { return nil }
    
    let disposeBag = DisposeBag()
}

class MVVMViewModel: NSObject, ViewModelToViewOutputProvider {
    let input: ViewToViewModelInput
    var outputType: ViewModelToViewOutput.Type? { return nil }
    required init(input: ViewToViewModelInput) {
        self.input = input
    }
}

我们先看这两个抽象类的继承以及协议关系:MVVMView继承的是UIViewController,所以这时候UIViewController作为View层的一员,只负责视图的显示相关,并不会接触到业务逻辑的处理。MVVMViewModel则简单地继承NSObject。这两个抽象类都是实现了Provider协议,担任了InputOutput的提供职责。在提供者协议实现元类型的属性时,我返回的是nil,若继承抽象类的子类没有重写此属性,则告诉框架自己并没有提供了InputOutput,对此框架就取消对它们的绑定。
我们可以看到,MVVMViewMVVMViewModel进行了强引用,我们只需持有MVVMView实例,就能依旧维持MVVMViewModel的存在;并且,MVVMViewModelMVVMView中的访问修饰为private,因此所有继承MVVMView都抽象类都无法访问此属性,做到了两者的高度解耦。
这里在详细看回MVVMView,其具有router(路由器)属性,通过路由器,我们可以进行页面的跳转;另外有几个抽象的属性和方法:

  • receive : 用于页面的通信,与Router(路由器密切相关),当此页面收到上一个页面所传递进来的事件数据时,receive就会赋入这些信息。在设计上,receive的赋值紧接在MVVMView的初始化之后,所以,我们不可以在重写的初始化方法中获取receive的值。
  • rxDrive(viewModelOutput:) : 这个方法就是用于将从ViewModel层出来的事件数据流驱动整个页面的显示,这方法中,我们可以通过传进来的参数Ouput驱动视图的刷新显示、进行页面的跳转。这个方法也是ViewModel层向View层传递信息的唯一出口。
  • provideCallBack() : 此方法是用于页面跳转中的反向数据传递,即将数据从本页面传到上一个页面,因为我们使用的是响应式的RxSwift框架,所以数据的反向传递就不需要使用到闭包或代理模式。在后面的Router实现中会说到它的机制。
  • disposeBag : 用于RxSwift资源的回收。

MVVMViewviewDidLoad()方法中,我们进行MVVMViewMVVMViewModel的关联,如果MVVMViewMVVMViewModel没有提供InputOutput,则表明此时ViewViewModel层没有通信,所以也就不会调用rxDrive方法了。

UnitCase

// MARK: - Unit
struct MVVMUnit {
    let viewType: MVVMView.Type
    let viewModelType: MVVMViewModel.Type
}

extension MVVMUnit: ExpressibleByArrayLiteral {
    typealias Element = AnyClass
    init(arrayLiteral elements: Element...) {
        guard elements.count == 2 else { crash("单元初始化参数长度错误") }
        guard let viewType = elements[0] as? MVVMView.Type else { crash("单元初始化参数类型错误") }
        guard let viewModelType = elements[1] as? MVVMViewModel.Type else { crash("单元初始化参数类型错误") }
        self.viewType = viewType
        self.viewModelType = viewModelType
    }
}

struct MVVMUnitCase: RawRepresentable {
    typealias RawValue = MVVMUnit
    
    let rawValue: MVVMUnit
    
    init(rawValue: RawValue) {
        self.rawValue = rawValue
    }
}

上面的代码使用到了一个函数crash(_ message:),为断言函数,这里就不需要给出具体实现了。
我们先来看MVVMUnit,它具有两个元类型的属性,分别代表一个页面中的MVVMViewMVVMViewModel类型,通过这种关系,绑定器就能正确地按照一一对应关系绑定MVVMViewMVVMViewModelMVVMUnit还实现了ExpressibleByArrayLiteral,我们可以直接简便地通过数组字面量来初始化MVVMUnit
MVVMUnitCase则是对MVVMUnit的再一次封装,其实现了RawRepresentable协议,这样我们就能像使用枚举一样通过点.语法来创建它。
使用的话我这里举个例子,加入现在我们的项目中需要用到两个页面,一个是主页面"main",一个是登录页面"login",它们都有对应的MVVMViewMVVMViewModelMainMVVMView、MainMVVMViewModelLoginMVVMView、LoginMVVMViewModel,我们则需要在MVVMUnitCase中进行添加:

extension MVVMUnitCase {
    static let main = MVVMUnitCase(rawValue: [MainMVVMView.self, MainMVVMViewModel.self])
    static let simpleInfo = MVVMUnitCase(rawValue: [LoginMVVMView.self, LoginMVVMViewModel.self])
}

Binder

//  Binder
struct MVVMBinder {
    /// 根据标识符获取视图,会在背后做视图与视图模型的绑定
    ///
    /// - Parameter identifier: 标识符
    /// - Returns: 返回已经绑定好了的视图
    static func obtainBindedView(_ unitCase: MVVMUnitCase) -> MVVMView {
        let unit = unitCase.rawValue
        let viewType = unit.viewType
        let viewModelType = unit.viewModelType
        let view = viewType.init(viewModelType)
        return view
    }
}

绑定器做的事情是对MVVMViewMVVMViewModel的绑定,它具有一个静态方法obtainBindedView(_ unitCase:)在这个方法中我们需要传入一个MVVMUnitCase的实例,然后绑定器会帮我们创建MVVMViewMVVMViewModel实例并进行绑定,最后将返回MVVMView的实例,我们拿到这个实例就能进行页面的跳转。

Router

// MARK: - Router
enum RouterType {
    case push(MVVMUnitCase)
    case present(MVVMUnitCase)
    case root(MVVMUnitCase)
    case back
}

struct Router {
    let from: MVVMView
    init(from: MVVMView) {
        self.from = from
    }
    func route(_ type: RouterType, send: Driver<Any>? = nil) -> Driver<Any>? {
        switch type {
        case let .push(unitCase):
            let view = MVVMBinder.obtainBindedView(unitCase)
            view.receive = send
            from.navigationController?.pushViewController(view, animated: true)
            return view.provideCallBack()
        case let .present(unitCase):
            let view = MVVMBinder.obtainBindedView(unitCase)
            view.receive = send
            from.present(view, animated: true, completion: nil)
            return view.provideCallBack()
        case let .root(unitCase):
            let view = MVVMBinder.obtainBindedView(unitCase)
            view.receive = send
            UIApplication.shared.keyWindow?.rootViewController = view
            return view.provideCallBack()
        case .back:
            if from.presentationController != nil {
                from.dismiss(animated: true, completion: nil)
            } else {
                _ = from.navigationController?.popViewController(animated: true)
            }
            return nil
        }
    }
}

extension MVVMView {
    func route(_ type: RouterType, send: Driver<Any>? = nil) -> Driver<Any>? {
        return self.router.route(type, send: send)
    }
}

常见的页面跳转为导航控制器的PushPop,模态的PresentDismissUIWindowrootViewController(根视图控制器切换),我们把这些跳转作为一个枚举设计了RouterType,其中back则代表PopDismiss,在使用RouterType时,我们将目标的MVVMUnitCase作为枚举的关联值传入。
对于路由器Router,我们需要使用一个MVVMView来初始化它,代表跳转是从这个MVVMView开始的。调用路由器中的route(_ type: , send:)方法就能进行页面的跳转,其中,参数send就是要传递到下一个页面的事件数据,而方法的返回值则为下一个页面反向传递过来的事件数据,通过MVVMView的抽象方法provideCallBack()
我也为MVVMView创建了一个扩展,在扩展中我们可以直接调用自身路由器的路由方法。
这里贴出个路由器的使用例子:

_ = view.route(.push(.login), send: Driver.just(userToken))

因为MVVMUnitCase实现了RawRepresentable协议,所以我们可以直接通过点语法来取得登录的Unit: .login

架构使用

到此,整套架构的实现就基本完成了,下面我们来结合RxSwift来构建一个使用此套架构的Demo:

View 层

class DemoMVVMView: MVVMView {
    override func viewDidLoad() {
        super.viewDidLoad()
        self.view.addSubview(self.mButton)
    }
    
    fileprivate lazy var mButton: UIButton = {
        $0.frame = self.view.bounds
        $0.setTitle("点击", for: .normal)
        return $0
    }(UIButton())
    
    override var inputType: ViewToViewModelInput.Type? { return DemoInput.self }
    
    override func rxDrive(viewModelOutput: ViewModelToViewOutput) {
        let output = viewModelOutput as! DemoOutput
        output.color.drive(self.rx.updateButtonBackgroundColor).addDisposableTo(self.disposeBag)
        output.title.drive(self.rx.updateButtonTitle).addDisposableTo(self.disposeBag)
    }
}

// MARK: - Reactive
extension Reactive where Base: DemoMVVMView {
    var updateButtonTitle: AnyObserver<String> {
        return UIBindingObserver<Base, String>(UIElement: base) { view, newTitle in
            view.mButton.setTitle(newTitle, for: .normal)
        }.asObserver()
    }
    
    var updateButtonBackgroundColor: AnyObserver<UIColor> {
        return UIBindingObserver<Base, String>(UIElement: base) { view, newColor in
            view.mButton.backgroundColor = newColor
            }.asObserver()
    }
}

// MARK: - Input
struct DemoInput: ViewToViewModelInput {
    let refresh: Driver<()>
    init(view: MVVMView) {
        let view = view as! DemoMVVMView
        self.refresh = view.mButton.rx.tap.asDriver()
    }
}

可以看到,我们在DemoMVVMView中添加了一个按钮,按钮的点击事件作为从View层传入ViewModel层的事件数据流,所以在DemoInput中我们定义了按钮点击的刷新流。
在属于DemoMVVMViewReactive分类中,里面的观察者代表DemoMVVMView接收到ViewModel层传来的事件数据流时进行的驱动操作,为更新按钮的标题和切换按钮的背景色。
我们重写inputType属性以及rxDrive方法,在inputType属性中返回DemoInput类型,在rxDrive方法中将ViewModel传过来的Output驱动视图的刷新。

ViewModel 层

// MARK: - ViewModel
class DemoMVVMViewModel: MVVMViewModel {
    override var outputType: ViewModelToViewOutput.Type? { DemoOutput.self }
}

struct DemoOutput: ViewModelToViewOutput {
    let color: Driver<UIColor>
    let title: Driver<String>
    init(viewModel: MVVMViewModel) {
        let viewModel = viewModel as! DemoMVVMViewModel
        let input = viewModel.input as! DemoInput
        self.color = input.refresh.map { _ in UIColor.orange }
        self.title = input.refresh.map { _ in "数据已刷新" }
    }
}

ViewModel层相对较为简单,因为现在还未涉及网络请求操作或数据库操作,也没有进行某些业务逻辑的判断处理,所以DemoMVVMViewModel中只有一个重写的OutputType属性。在实际开发中,MVVMViewModel中会持有某些临时的状态变量,或网络、本地数据库框架实例。
对于流的转换,发生在Output的初始化方法中,可能有人会疑惑: “流的转换不是发生在MVVMViewModel中的吗,为什么会在Output的初始化方法中?” 在前面我说到,流的转换是在ViewModel层发生的,而ViewModel层是包含OutputMVVMViewModel的,并不是说MVVMViewModel名字的关系所以MVVMViewModel就代表整个ViewModel层。在此架构中,MVVMViewModel主要是用于持有某些状态变量、某些服务模块和提供Output类型的。

MVVMUnitCase

不要忘记在UnitCase中对刚构建好的MVVM进行配置:

extension MVVMUnitCase {
    static let demo = MVVMUnitCase(rawValue: [DemoMVVMView.self, DemoMVVMViewModel.self])
}

在项目开发中,我们就可以在任意MVVMView中进行向"Demo"页面的跳转了:

_ = self.route(.push(.demo))

上面展示的架构使用Demo较为简单,在下一篇文章中,我会结合封装完成后的网络请求框架,对MVVMR架构进行一此较为大型的实战。

总结

这篇文章详细说明了我在项目中搭建出来的MVVMR架构其思想、基本原理以及初步实现,为基于RxSwift的MVVMR架构系列文章的第一篇。若大家发现文章中的内容存在问题或者有更好的建议,欢迎向我反馈!
在下一篇文章中,我会对架构中的某些细节进行实现与封装,如基于Moya + Argo的网络框架封装,并在后面使用架构进行实战使用。

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

推荐阅读更多精彩内容