ReactiveCocoa 和 MVVM 入门

ReactiveCocoa 简单介绍

  • 观察值

你别动,你一动我就知道。

@weakify(self);
[RACObserve(self, value) subscribeNext:^(NSString* x) {
    @strongify(self);
    NSLog(@"你动了");
}];
  • 单边

你唱歌,我就跳舞

RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        [subscriber sendNext:@"唱歌"];
        [subscriber sendCompleted];
        return nil;
    }];
    RAC(self, value) = [signalA map:^id(NSString* value) {
        if ([value isEqualToString:@"唱歌"]) {
            return @"跳舞";
        }
        return @"";
    }];
  • 双边

你向西,他就向东,他向左,你就向右。

RACChannelTerminal *channelA = RACChannelTo(self, valueA);
    RACChannelTerminal *channelB = RACChannelTo(self, valueB);
    [[channelA map:^id(NSString *value) {
        if ([value isEqualToString:@"西"]) {
            return @"东";
        }
        return value;
    }] subscribe:channelB];
    [[channelB map:^id(NSString *value) {
        if ([value isEqualToString:@"左"]) {
            return @"右";
        }
        return value;
    }] subscribe:channelA];
    [[RACObserve(self, valueA) filter:^BOOL(id value) {
        return value ? YES : NO;
    }] subscribeNext:^(NSString* x) {
        NSLog(@"你向%@", x);
    }];
    [[RACObserve(self, valueB) filter:^BOOL(id value) {
        return value ? YES : NO;
    }] subscribeNext:^(NSString* x) {
        NSLog(@"他向%@", x);
    }];
    self.valueA = @"西";
    self.valueB = @"左";
  • 代理

你是程序员,你帮我写个app吧

@protocol Programmer <NSObject>
- (void)makeAnApp;
@end
RACSignal *ProgrammerSignal =  
[self rac_signalForSelector:@selector(makeAnApp)
               fromProtocol:@protocol(Programmer)];
[ProgrammerSignal subscribeNext:^(RACTuple* x) {
    NSLog(@"花了一个月,app写好了");
}];
[self makeAnApp];

RACSignal *ProgrammerSignal =  
[self rac_signalForSelector:@selector(makeAnApp)
               fromProtocol:@protocol(Programmer)];
[ProgrammerSignal subscribeNext:^(RACTuple* x) {
    NSLog(@"花了一个月,app写好了");
}];
[self makeAnApp];
  • 广播

知道你的频道,我就能听到你了。

[[[NSNotificationCenter defaultCenter] rac_addObserverForName:@"代码之道频道" object:nil] subscribeNext:^(NSNotification* x) {
        NSLog(@"技巧:%@", x.userInfo[@"技巧"]);
    }];
    [[NSNotificationCenter defaultCenter] postNotificationName:@"代码之道频道" object:nil userInfo:@{@"技巧":@"用心写"}];
  • 节流

不好意思,这里一秒钟只能通过一个人

[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        [subscriber sendNext:@"旅客A"];
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [subscriber sendNext:@"旅客B"];
        });
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [subscriber sendNext:@"旅客C"];
            [subscriber sendNext:@"旅客D"];
            [subscriber sendNext:@"旅客E"];
        });
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [subscriber sendNext:@"旅客F"];
        });
        return nil;
    }] throttle:1] subscribeNext:^(id x) {
        NSLog(@"%@通过了",x);
    }];

// Test[2618:83764] 旅客A  
//Test[2618:83764] 旅客B  
// Test[2618:83764] 旅客E  
// Test[2618:83764] 旅客F
  • 连接

生活是一个故事接一个故事

RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        [subscriber sendNext:@"我恋爱啦"];
        [subscriber sendCompleted];
        return nil;
    }];
    RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        [subscriber sendNext:@"我结婚啦"];
        [subscriber sendCompleted];
        return nil;
    }];
    [[signalA concat:signalB] subscribeNext:^(id x) {
        NSLog(@"%@",x);
    }];
  • 合并

污水都应该流入污水处理厂被处理

RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        [subscriber sendNext:@"纸厂污水"];
        return nil;
    }];
    RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        [subscriber sendNext:@"电镀厂污水"];
        return nil;
    }];
    [[RACSignal merge:@[signalA, signalB]] subscribeNext:^(id x) {
        NSLog(@"处理%@",x);
    }];
  • 映射

我可以点石成金。

RACSignal *signal = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        [subscriber sendNext:@"石"];
        return nil;
    }] map:^id(NSString* value) {
        if ([value isEqualToString:@"石"]) {
            return @"金";
        }
        return value;
    }];
    [signal subscribeNext:^(id x) {
        NSLog(@"%@", x);
    }];
  • 过滤

未满十八岁,禁止进入。

[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        [subscriber sendNext:@(15)];
        [subscriber sendNext:@(17)];
        [subscriber sendNext:@(21)];
        [subscriber sendNext:@(14)];
        [subscriber sendNext:@(30)];
        return nil;
    }] filter:^BOOL(NSNumber* value) {
        return value.integerValue >= 18;
    }] subscribeNext:^(id x) {
        NSLog(@"%@", x);
    }];
    }];
  • 扁平

打蛋液,煎鸡蛋,上盘。

[[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        NSLog(@"打蛋液");
        [subscriber sendNext:@"蛋液"];
        [subscriber sendCompleted];
        return nil;
    }] flattenMap:^RACStream *(NSString* value) {
        return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
            NSLog(@"把%@倒进锅里面煎",value);
            [subscriber sendNext:@"煎蛋"];
            [subscriber sendCompleted];
            return nil;
        }];
    }] flattenMap:^RACStream *(NSString* value) {
        return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
            NSLog(@"把%@装到盘里", value);
            [subscriber sendNext:@"上菜"];
            [subscriber sendCompleted];
            return nil;
        }];
    }] subscribeNext:^(id x) {
        NSLog(@"%@", x);
    }];
  • 重放

一次制作,多次观看。

RACSignal *replaySignal = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        NSLog(@"大导演拍了一部电影《我的男票是程序员》");
        [subscriber sendNext:@"《我的男票是程序员》"];
        return nil;
    }] replay];
    [replaySignal subscribeNext:^(id x) {
        NSLog(@"小明看了%@", x);
    }];
    [replaySignal subscribeNext:^(id x) {
        NSLog(@"小红也看了%@", x);
    }];


//Test[1854:54712] 大导演拍了一部电影《我的男票是程序员》  
//Test[1854:54712] 小明看了《我的男票是程序员》  
//Test[1854:54712] 小红也看了《我的男票是程序员》
  • 重试

成功之前可能需要数百次失败

__block int failedCount = 0;
    [[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        if (failedCount < 100) {
            failedCount++;
            NSLog(@"我失败了");
            [subscriber sendError:nil];
        }else{
            NSLog(@"经历了数百次失败后");
            [subscriber sendNext:nil];
        }
        return nil;
    }] retry] subscribeNext:^(id x) {
        NSLog(@"终于成功了");
    }];

//Test[2411:77080] 我失败了  
// Test[2411:77080] 我失败了  
//.....
// Test[2411:77080] 经历了数百次失败后  
// Test[2411:77080] 终于成功了

以上


下面是 MVVM 与 ReactiveCocoa 项目实例演示
摘录翻译自ReactiveCocoa and MVVM, an Introduction


定义MVVM


  • Model - model 在 MVVM 中没有真正的变化. 取决于你的偏好, 你的 model 可能会或可能不会封装一些额外的业务逻辑工作. 我更倾向于把它当做一个容纳表现数据-模型对象信息的结构体, 并在一个单独的管理类中维护的创建/管理模型的统一逻辑.

  • View - view 包含实际 UI 本身(不论是 UIView 代码, storyboard 和 xib), 任何视图特定的逻辑, 和对用户输入的反馈. 在 iOS 中这不仅需要 UIView 代码和那些文件, 还包括很多需UIViewController 处理的工作.

  • View-Model - 这个术语本身会带来困惑, 因为它混搭了两个我们已知的术语, 但却是完全不同的东东. 它不是传统数据-模型结构中模型的意思(又来了, 只是我喜欢这个例子). 它的职责之一就是作为一个表现视图显示自身所需数据的静态模型;但它也有收集, 解释和转换那些数据的责任. 这留给了 view (controller) 一个更加清晰明确的任务: 呈现由 view-model 提供的数据.

View-Model 和 View Controller, 在一起,但独立


  • 有一个让用户输入他们姓名的 UITextField , 和一个写着 “Go” 的 UIButton
  • 有显示被查看的当前用户头像和姓名的 UIImageViewUILabel 各一个
  • 下面放着一个显示最新回复推文的UITableView
  • 允许无限滚动

View-Model 实例

我们的 view-model 头文件应该长这样:

//MYTwitterLookupViewModel.h
@interface MYTwitterLookupViewModel: NSObject
 
@property (nonatomic, assign, readonly, getter=isUsernameValid) BOOL usernameValid;
@property (nonatomic, strong, readonly) NSString *userFullName;
@property (nonatomic, strong, readonly) UIImage *userAvatarImage;
@property (nonatomic, strong, readonly) NSArray *tweets;
@property (nonatomic, assign, readonly) BOOL allTweetsLoaded;
 
@property (nonatomic, strong, readwrite) NSString *username;
 
- (void) getTweetsForCurrentUsername;
- (void) loadMoreTweets;

相当直截了当的填充. 注意到这些壮丽的 readonly 属性了么?这个 view-model 暴漏了视图控制器所必需的最小量信息, 视图控制器实际上并不在乎 view-model 是如何获得这些信息的. 现在我们两者都不在乎. 仅仅假定你习惯于标准的网络服务请求, 校验, 数据操作和存储.

视图控制器从 view-model 获取的数据将用来:

  • usernameValid 的值发生变化时触发 “Go” 按钮的 enabled 属性
  • usernameValid 等于 NO 时调整按钮的 alpha 值为0. 5(等于 YES 时设为1. 0)
  • 更新 UILabletext 属性为字符串 userFullName 的值
  • 更新 UIImageViewimage 属性为 userAvatarImage 的值
  • tweets 数组中的对象设置表格视图中的 cell (后面会提到)
  • 当滑到表格视图底部时如果 allTweetsLoadedNO, 提供一个 显示 “loading” 的 cell

视图控制器将对 view-model 起如下作用:

  • 每当 UITextField 中的文本发生变化, 更新 view-model 上仅有的 readwrite 属性 username
  • 当 “Go” 按钮被按下时调用 view-mode的 getTweetsForCurrentUsername 方法
  • 当到达表格中的 “loading” cell 时调用 view-model 上的 loadMoreTweets 方法

视图控制器不做的事儿:

  • 发起网络服务调用
  • 管理 tweets 数组
  • 判定 username 内容是否有效
  • 将用户的姓和名格式化为全名
  • 下载用户头像并转成 UIImage(如果你习惯在 UIImageView 上使用类别从网络加载图片, 你可以暴漏 URL 而不是图片. 这样就给 view-model 与 UIKit 之间一个更清晰的划分, 但我视 UIImage 为数据而非数据的确切显示. 这些东西不是固定死的. )

进入 ReactiveCocoa



这看起来可能像是为我们应用流程文档中的一张老旧的计算机科学图解. 通过陈述式的编程, 我们使用了更高层次的抽象, 来让我们实际编程更靠近我们在脑海中设计流程的方式. 我们让电脑为我们做更多工作. 实际的代码更加像这幅图了.

RACSignal

RACSignal (信号)就 RAC 来说是构造单元. 它代表我们最终将要收到的信息. 当你能将未来某时刻收到的消息具体表示出来时, 你可以开始预先(陈述性)运用逻辑并构建你的信息流,而不是必须等到事件发生(命令式).

信号会为了控制通过应用的信息流而获得所有这些异步方法(委托, 回调 block, 通知, KVO, target/action 事件观察, 等)并将它们统一到一个接口下.这只是直观理解. 不仅是这些, 因为信息会流过你的应用, 它还提供给你轻松转换/分解/合并/过滤信息的能力.

用 ReactiveCocoa 将 view-model 与视图控制器连接起来.

// View Controller

- (void) viewDidLoad {
    [super viewDidLoad];
 
    RAC(self.viewModel,  username) = [myTextfield rac_textSignal];
 
    RACSignal *usernameIsValidSignal = RACObserve(self.viewModel,  usernameValid);
 
    RAC(self.goButton,  alpha) = [usernameIsValidSignal
        map:  ^(NSNumber *valid) {
            return valid. boolValue ? @1 :  @0. 5;
        }];
 
    RAC(self.goButton,  enabled) = usernameIsValidSignal;
 
    RAC(self.avatarImageView,  image) = RACObserve(self.viewModel,  userAvatarImage);
    
    RAC(self.userNameLabel,  text) = RACObserve(self.viewModel,  userFullName);
 
    @weakify(self);
    [[[RACSignal merge: @[RACObserve(self.viewModel,  tweets), 
                        RACObserve(self.viewModel,  allTweetsLoaded)]]
        bufferWithTime: 0 onScheduler: [RACScheduler mainThreadScheduler]]
        subscribeNext: ^(id value) {
            @strongify(self);
            [self.tableView reloadData];
        }];
    
    [[self.goButton rac_signalForControlEvents: UIControlEventTouchUpInside]
        subscribeNext:  ^(id value) {
            @strongify(self);
            [self.viewModel getTweetsForCurrentUsername];
        }];
}
 
-(UITableViewCell*)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath {
    // if table section is the tweets section
    if (indexPath. section == 0) {
        MYTwitterUserCell *cell =
        [self.tableView dequeueReusableCellWithIdentifier: @"MYTwitterUserCell" forIndexPath: indexPath];
        
        // grab the cell view model from the vc view model and assign it
        cell.viewModel = self.viewModel. tweets[indexPath. row];
        return cell;
    } else {
        // else if the section is our loading cell
        MYLoadingCell *cell =
        [self.tableView dequeueReusableCellWithIdentifier: @"MYLoadingCell" forIndexPath: indexPath];
        [self.viewModel loadMoreTweets];
        return cell;
    }
}
 
 
//
// MYTwitterUserCell
//
 
// this could also be in cell init
- (void) awakeFromNib {
    [super awakeFromNib];
    
    RAC(self.avatarImageView,  image) = RACObserve(self,  viewModel. tweetAuthorAvatarImage);
    RAC(self.userNameLabel,  text) = RACObserve(self,  viewModel. tweetAuthorFullName);
    RAC(self.tweetTextLabel,  text) = RACObserve(self,  viewModel. tweetContent);
}
RAC(self.viewModel,  username) = [myTextfield rac_textSignal];

在这我们用 RAC 库中的方法从 UITextField 拉取一个信号. 这行代码将 view-model 上的可读写属性 username 绑定到文本框上的用户输入的任何更新.


RACSignal *usernameIsValidSignal = RACObserve(self.viewModel,  usernameValid);

RAC(self.goButton,  alpha) = [usernameIsValidSignal
    map:  ^(NSNumber *valid) {
        return valid. boolValue ? @1 :  @0. 5;
    }];

RAC(self.goButton,  enabled) = usernameIsValidSignal;

在这我们用 RACObserve 方法在 view-model 的 usernameValid 属性上创建了一个信号 usernameIsValidSignal. 无论何时属性发生变化, 它将会沿着管道发送一个新的 @YES 或 @NO. 我们拿到那个值并将其绑定到 goButton 的两个属性上. 首先我们将 alpha 分别对应 YES 或 NO 更新到1或0. 5(记着在这必须返回 NSNumber). 然后我们直接将信号绑定到enabled 属性, 因为 YES 和 NO 在这无需转换就能完美地运作.


RAC(self.avatarImageView,  image) = RACObserve(self.viewModel,  userAvatarImage);
RAC(self.userNameLabel,  text) = RACObserve(self.viewModel,  userFullName);

下面我们为表头的图像视图和用户标签创建绑定, 再次在 view-model 上对应的属性上用 RACObserve 宏创建信号

@weakify(self);
[[[RACSignal merge: @[RACObserve(self.viewModel,  tweets), 
                     RACObserve(self.viewModel,  allTweetsLoaded)]]
    bufferWithTime: 0 onScheduler: [RACScheduler mainThreadScheduler]]
    subscribeNext: ^(id value) {
        @strongify(self);
        [self.tableView reloadData];
    }];

这货看上去有点诡异, 所以我们在这上多花点时间. 我们想在 view-model 上 tweets 数组或 allTweetsLoaded 属性发生变化时更新表格视图. (在这个例子中, 我们要用一个简单的方法来重新加载整张表. )所以我们将这两个属性被观察后创建的两个信号合并成一个更大的信号, 当两个属性中有一个发生变化, 这个信号就会发送值. (你一贯认为信号的值是同类型的, 不会像这个信号有一样混杂的值. 这很可能在 Swift 版本的 RAC 中强制要求, 但在这我们不关心发出的真实值, 我们只是用它来触发表格式图的重新加载. )

那么这儿看起来最吓人的部分可能是信号链中的bufferWithTime: onScheduler: 方法. 需要它来围绕 UIKit 中的一个问题进行变通. tweetsallTweetsLoaded 这两个属性我们都需要追踪, 万一 tweets 变化和 allTweetsLoaded 为否(不管怎样我们都得重新加载表格). 有时两个属性都将在同一准确的时间发生变化, 意味着合并后的大信号中的两个信号都会发送一个值, 那么 reloadData 方法将会在同一个运行循环中被调用两次. UIKit 不喜欢这样. bufferWithTime: 在给明的时间内抓取所有下一个到来的值, 当给定的时间过后将所有值合在一起发给订阅者. 通过传入0作为时间, bufferWithTime: 将会抓取那个合并信号在特定的运行循环中发出的全部值, 并将他们一起发送出去. (NSTimer 以同样的方式工作, 这不是巧合, 因为 bufferWithTime: 是用 NSTimer 构建的. )暂时不用担心 scheduler, 试把它想做指明这些值必须在主线程上被发送. 现在我们确保 reloadData 每次运行循环只被调用一次.

注意我在这用 @weakify/@strongify宏切换 strong 和 weak. 这在创建所有这些 block 时非常重要. 在 RAC 的 block 中使用 selfself 将会被捕获为强引用并得到保留环, 除非你尤其意识到要破除保留环

[[self.goButton rac_signalForControlEvents: UIControlEventTouchUpInside]
    subscribeNext:  ^(id value) {
        @strongify(self);
        [self.viewModel getTweetsForCurrentUsername];
    }];

我们已经搞定了 cellForRowAtIndexPath 的第一部分, 那么我在这将只说下 loading cell:

MYLoadingCell *cell =
    [self.tableView dequeueReusableCellWithIdentifier: @"MYLoadingCell" forIndexPath: indexPath];
[self.viewModel loadMoreTweets];
return cell;

这是另一块我们以后将利用到 RACCommand 的地方, 但目前我们只是调用 view-model 的 loadMoreTweets 方法. 我们将只是信任如果 cell 显示或隐藏多次的话 view-model 会避免多次内部调用.

- (void) awakeFromNib {
    [super awakeFromNib];

    RAC(self.avatarImageView,  image) = RACObserve(self,  viewModel. tweetAuthorAvatarImage);
    RAC(self.userNameLabel,  text) = RACObserve(self,  viewModel. tweetAuthorFullName);
    RAC(self.tweetTextLabel,  text) = RACObserve(self,  viewModel. tweetContent);
}

这段现在应该非常直接了, 除此之外我想指出一点. 我们正在将图片和文字绑定到 UI 上对应的属性, 但注意 viewModel 出现在 RACObserve 宏中逗号右边. 这些 cell 终将被重用, 新的 view-models 将会被赋值. 如果我们不将 viewModel 放在逗号右边, 那就会监听 viewModel 属性的变化然后每次都要重新设置绑定;如果放在逗号右边, RACObserve 将会为我们负责这些事儿. 因此我们只需要设定一次绑定并让 Reactive Cocoa 做剩余的部分. 这是在绑定表格 cell 时为了性能需要记住的好东西. 我在实践中即使是有很多表格 cell 依然没有出过问题.


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

推荐阅读更多精彩内容