iOSApp组件化详解(从0到1实现一个完整的组件化项目)

何为组件化

一种能够解决代码耦合的技术。项目经过组件化的拆分,不仅可以解决代码耦合的问题,还可以增强代码的复用性,工程的易管理性,减少编译时间等

1.组件化分层架构图

App组件化架构分层.png

2.架构分层详解

1.Lib层

基础模块跟业务无关,只定义接口和基本配置,子类可以重写,便于扩展

  • LibBase
    • LibBaseController
    • LibBaseNavController
    • ...
  • LibCommon基础公共组件
  • LibFlexBox移动端FlexBox布局

Widget层

  • 由Lib延伸而来,便于组件的扩展和复用

2.Mediator层

  • 采用Bifrost框架,创建调度组件并定义交互协议,处理业务模块之间的数据传递及逻辑交互处理
  • Module层必须依赖该库

3.Module层

业务层,跟业务相关的一些组件,业务组件之间互不依赖,且依赖于ModuleCommon层

ModuleCommon

跟业务相关的公共组件部分,例如

  • CommonBaseController
  • CommonLodingController
  • CommonListController
  • 数据模型Models
  • Tools工具类
  • Macro常见宏等
  • QMUI常见配置/换肤配置
  • 第三方分享/登录/支付配置

3.组件化要点罗列

  • 多Target分模块开发,代码解耦
  • 单独编译项目
  • 组件之间传值,通过调度组件Biforst
  • 组件间访问公共图片资源,解决命名冲突
  • 文件夹分层:子组件subspec
  • pod 引入依赖方式
    • 引入本地依赖
    • 引入远程依赖
    • 引入指定分支
  • 组件命名方式
  • App路由管理
  • podspec使用及格式校验
  • 组件间依赖管理
  • coapods私有化仓库搭建

**

3.1多Target分模块开发,代码解耦

workspace 依赖多个功能文件开发

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
def commonPods()
    #基础宏定义,类别
    pod 'HKMacros', :git => 'https://gitee.com/Steven_Hu/HKMacros.git'
    # 组件化基类-引用本地依赖
    pod 'HKBaseModule', :path => './PrivateRepo/HKBaseModule/HKBaseModule.podspec'
    #pod 'HKBaseModule', :git => 'https://gitee.com/Steven_Hu/HKBaseModule.git'
end

def mediatorPods()
    #pod 'Bifrost', :path => '../'
end

  # Comment the next line if you don't want to use dynamic frameworks
  #use_frameworks!
  #workspace文件名
  workspace 'HKiOSTools.xcworkspace'
  #主工程路径
  project 'HKiOSTools/HKiOSTools.xcodeproj'

target 'HKiOSTools' do
    project 'HKiOSTools/HKiOSTools.xcodeproj'
    commonPods()
  
    target "HKBaseModule_Example" do
        project 'PrivateRepo/HKBaseModule/Example/HKBaseModule.xcodeproj'
        commonPods()
    end

end

**

3.2单独编译项目

如何区分你编译的是主工程项目还是子工程项目

  • 最简单的方式,通过项目名称:ProjectName

[[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString *)kCFBundleExecutableKey]

  • 判断项目名称是否和子项目名称一致即可

3.3组件之间传值Bifrost

image.png

注册路由(ViewController)

+ (void)load {
    [Bifrost bindURL:kRouteHomePage
           toHandler:^id _Nullable(NSDictionary * _Nullable parameters) {
        return [HKHomeViewController new];
    }];
}

获取路由(ViewController)

UIViewController * vc = [Bifrost handleURL:kRouteHomePage];
[self.navigationController pushViewController:vc animated:YES];

页面传值

  • 以type为例
//1.当前页面声明一个type属性
/// 验证类型
@property (nonatomic, assign) HKVerifyType  type ;
// 2.bindURL
[Bifrost bindURL:kRouteBindPhoneNumPage toHandler:^id _Nullable(NSDictionary * _Nullable parameters) {
  HKBindPhoneNumViewController *vc = [[self alloc] init];
  vc.type = [parameters[kRouteBindPhoneNumParamType] integerValue];
  return vc;
}];
// 3.传值type
NSString *routeURL = BFStr(@"%@?%@=%@", kRouteBindPhoneNumPage, kRouteBindPhoneNumParamType, @(HKVerifyTypeForgetPassword));
UIViewController * vc = [Bifrost handleURL:routeURL]
[self.navigationController pushViewController:vc animated:YES];

页面回调处理

//1.当前页面声明一个type属性和Block
/// 验证类型
@property (nonatomic, assign) HKVerifyType  type ;
/// 完成回调
@property (nonatomic, strong) BifrostRouteCompletion complete ;
// 2.bindURL
[Bifrost bindURL:kRouteBindPhoneNumPage toHandler:^id _Nullable(NSDictionary * _Nullable parameters) {
    HKBindPhoneNumViewController *vc = [[self alloc] init];
    vc.type = [parameters[kRouteBindPhoneNumParamType] integerValue];
    vc.complete = parameters[kBifrostRouteCompletion];
    return vc;
}];
// 3.点击事件处理
/// 获取验证
- (void)getVerifyCodeEvent
{
    [self.view endEditing:YES];
    BFComplete(@{kBifrostRouteCompletion:self.complete}, @(self.type));
}
// 4.传值type
UIViewController *vc = [Bifrost handleURL:kRouteBindPhoneNumPage complexParams:@{kRouteBindPhoneNumParamType:@(HKVerifyTypeForgetPassword)} completion:^(id  _Nullable result) {
    HKVerifyType type = (YNVerifyType)result;
    [HKKeyWindow hk_showWithText:HKStr(@"你点击的类型是:%@",type)];
}];
[self.navigationController pushViewController:vc animated:YES];

3.4组件间访问公共图片资源

主工程有一个a.png的图片,而pod库里面也有一个a.png的图片,此时就产生命名冲突了

思路:不同的组件都有自己独立的bundle,组件内部资源提供自身的Bundle来获取,以避免资源重复

  • 本地资源如图片等存放位置(Assets文件夹,否则无法访问)
image.png
  • 指定Bundle名称
# resource_bundles
s.resource_bundles = {
   'HKLibCommon' => ['HKLibCommon/**/*.{xib,jpg,gif,png,xcassets}']
  }
  • 解决命名冲突:resource_bundles
  • resource_bundles会自动生成bundle把资源文件打包进去

加载本地资源方法

工具类抽取
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@interface ModuleBundle : NSObject

/*
 * 根据bundle的名称获取bundle
 */
+ (NSBundle *)bundleWithName:(NSString *)bundleName;

//获取bundle 每次只要重写这个方法就可以在指定的bundle中获取对应资源
+ (NSBundle *)bundle;

//根据xib文件名称获取xib文件
+ (__kindof UIView *)viewWithXibFileName:(NSString *)fileName;

//根据图片名称获取图片
+ (UIImage *)imageNamed:(NSString *)imageName;

//根据sb文件名称获取对应sb文件
+ (UIStoryboard *)storyboardWithName:(NSString *)storyboardName;

//获取nib文件
+ (UINib *)nibWithName:(NSString *)nibName;

@end
#import "ModuleBundle.h"

@implementation ModuleBundle

+ (NSBundle *)bundleWithName:(NSString *)bundleName {
    if(bundleName.length == 0) {
        return nil;
    }
    NSString *path = [[NSBundle mainBundle] pathForResource:bundleName ofType:@"bundle"];
    NSAssert([NSBundle bundleWithPath:path], @"not found bundle");
    return  [NSBundle bundleWithPath:path];
}

+ (NSBundle *)bundle {
//    NSAssert([NSBundle mainBundle], @"not found bundle");
    return [NSBundle mainBundle];
}

+ (UIView *)viewWithXibFileName:(NSString *)fileName {
    NSAssert([self viewWithXibFileName:fileName inBundle:[self.class bundle]], @"not found view");
    return [self viewWithXibFileName:fileName inBundle:[self.class bundle]];
}

+ (UIImage *)imageNamed:(NSString *)imageName {
    NSAssert([self imageNamed:imageName inBundle:[self.class bundle]], @"not found image");
    return [self imageNamed:imageName inBundle:[self.class bundle]];
}

+ (UIStoryboard *)storyboardWithName:(NSString *)storyboardName {
    NSAssert([self storyboardWithName:storyboardName inBundle:[self.class bundle]], @"not found storyboard");
    return [self storyboardWithName:storyboardName inBundle:[self.class bundle]];
}

+ (UINib *)nibWithName:(NSString *)nibName {
    NSAssert([self nibWithNibName:nibName inBundle:[self.class bundle]], @"not found nib");
    return [self nibWithNibName:nibName inBundle:[self.class bundle]];
}

#pragma mark - private
+ (UIImage *)imageNamed:(NSString *)imageName inBundle:(NSBundle *)bundle {
    if(imageName.length == 0 || !bundle) {
        return nil;
    }
    return [UIImage imageNamed:imageName inBundle:bundle compatibleWithTraitCollection:nil];
}

+ (UIImage *)imageNamed:(NSString *)imageName bundleName:(NSString *)bundleName {
    return [self imageNamed:imageName inBundle:[self bundleWithName:bundleName]];
}

+ (UIView *)viewWithXibFileName:(NSString *)fileName inBundle:(NSBundle *)bundle {
    if(fileName.length == 0 || !bundle) {
        return nil;
    }
    //如果没有国际化,则直接去相应内容下的文件
    UIView *xibView = [[bundle loadNibNamed:fileName owner:nil options:nil] lastObject];
    if(!xibView) {
        //文件国际化之后,所有的bundle的文件资源都在base的目录下
        xibView = [[[NSBundle bundleWithPath:[bundle pathForResource:@"Base" ofType:@"lproj"]] loadNibNamed:fileName owner:nil options:nil] lastObject];
    }
    return xibView;
}

+ (UIView *)viewWithXibFileName:(NSString *)fileName bundleName:(NSString *)bundleName {
    return [self viewWithXibFileName:fileName inBundle:[self bundleWithName:bundleName]];
}

+ (UIStoryboard *)storyboardWithName:(NSString *)storyboardName inBundle:(NSBundle *)bundle {
    if(storyboardName.length == 0 || !bundle) {
        return nil;
    }
    return [UIStoryboard storyboardWithName:storyboardName bundle:bundle];
}

+ (UIStoryboard *)storyboardWithName:(NSString *)storyboardName bundleName:(NSString *)bundleName {
    return [self storyboardWithName:storyboardName inBundle:[self bundleWithName:bundleName]];
}

+ (UINib *)nibWithNibName:(NSString *)nibName inBundle:(NSBundle *)bundle {
    if(nibName.length == 0 || !bundle ) {
        return nil;
    }
    return [UINib nibWithNibName:nibName bundle:bundle];
}

@end
Bundle获取

创建一个类CommonBundle继承自ModuleBundle,并实现以下方法

#import "CommonBundle.h"
@implementation CommonBundle
+ (NSBundle *)bundle{
    //TODO:注意Bundle名字需跟模块名称一致,否则会找不到path,直接Crash
    return [self.class bundleWithName:@"HKLibCommon"];
}
@end
使用

替换系统加载图片的方式:[UIImage imageNamed:@"navigationbar_background"]

[CommonBundle imageNamed:@"navigationbar_background"]
多个包
spec.resource_bundles = {
    'MapBox' => ['MapView/Map/Resources/*.png'],
    'OtherResources' => ['MapView/Map/OtherResources/*.png']
  }

3.5文件夹分层子组件subspec

我们在编写podspec文件时,sourcefiles只是告诉pods你需要哪些文件是这个项目中需要的,而没有包括文件的层级结构,那么就需要我们来实现这个层级结构:subspec


image.png

比如这里面的每一个文件夹,就是一个子pod,这样的好处是条理清晰,而且我们可以只用你需要的功能,在编写podfile时 就可以这样写
pod 'HKModuleModels/User' 只使用其中的一个功能。

主podspec

主pod可以是一个头文件,也可以具有一定的功能,我写的组件sourcefiles只是一个import子组件的头文件, sourcebundle是项目中需要的一些图片


image.png

编写subspec

  • 让pods支持子subspec其实很简单,只要搞清楚三件事
  1. 文件夹结构 subspec sourcefiles的路径
  2. subspec 所依赖的系统库
  3. subspec 所依赖的第三方,和其它subspec的路径
image.png

3.6pod 引入依赖方式

引入本地依赖

pod 'ManageLocalCode', :path => '../ManageLocalCode'
pod 'BioAuthAPI', :path => '../BioAuthAPI'
pod 'HKTool', :path => '../'

远程私有库依赖:

# 基础宏定义byStevenHu
pod 'HKMacros', :git => 'https://gitee.com/Steven_Hu/HKMacros.git'

指定分支

# 支付代码测试
  iap_pay:
    git:
      url:  https://gitee.com/Steven_Hu/iap_pay.git
      ref: master

常规依赖

pod "AFNetworking",  "~>0.2.0"

3.7 组件命名方式

组件性质 建议名称 示例
基础组件拆分 项目前缀Lib组件名称 HKLibBase:基类模块
业务组件拆分 项目前缀Module组件名称 HKModuleHome:首页模块
调度组件拆分 项目前缀Mediator组件名称 HKMediatorDriver:司机端调度组件
Wdiget组件拆分 项目前缀Widget组件名称 HKWidgetAddressPicker:地址选择器组件
公共组件 项目前缀_组件名称_Common HKLibCommon,HKModuleCommon

3.8 App端路由管理

格式

app内链

举例yunquedriver://home/homepage/detail?name=steven&age=18
appscheme://moduleName/pageName/secondPageName?key1=value1&key2=value2
其中value中有中文等特殊字符时需要进行urlEncode。

app外链外链

http://xxx
https://xxx

格式说明

  • appscheme分解
    • 项目名称
      • yunque
    • 业务侧
      • driver
      • user
  • moduleName
    • 业务模块名称
  • pageName
    • 页面名称
  • secondPage
    • 二级页面

scheme

云雀司机: yunquedriver
云雀用户:yunqueuser

模块名

lib:工具类,跟业务无关的

页面

image.png

3.9 podspec使用及格式校验

注意事项

  • podspec版本号和git仓库代码的tag版本必须一致,否则找不到
    • 比如:pod lib 默认生成的是0.1.0,我们根据业务需要改成0.0.1
  • 索引仓库和组件仓库关联
    • cd到组件仓库目录下
    • pod repo push #{本地关联到远程仓库的名字} #{第三方私有库的podspec文件} --verbose --allow-warnings 示例: pod repo push YNSpecs YNLibDemo.podspec --verbose --allow-warnings
  • 项目引入
# 远程私有仓库
source 'https://gitlab.com/xxxx/xxSpecs'

# 引入使用
pod 'xxLibNetwork'

3.10 组件间依赖管理

通过 git submodule add https://gitee.com/Steven_Hu/hk-module-components.git 添加submodule
会在git上添加一个.gitmodules 文件,可以查看各种依赖

[submodule "PrivateRepo/hk-lib-base"]
    path = PrivateRepo/hk-lib-base
    url = https://gitee.com/Steven_Hu/hk-lib-base.git
[submodule "PrivateRepo/hk-lib-common"]
    path = PrivateRepo/hk-lib-common
    url = https://gitee.com/Steven_Hu/hk-lib-common.git
[submodule "PrivateRepo/hk-lib-network"]
    path = PrivateRepo/hk-lib-network
    url = https://gitee.com/Steven_Hu/hk-lib-network.git
[submodule "PrivateRepo/hk-mediator"]
    path = PrivateRepo/hk-mediator
    url = https://gitee.com/Steven_Hu/hk-mediator.git
[submodule "PrivateRepo/hk-lib-keyboard"]
    path = PrivateRepo/hk-lib-keyboard
    url = https://gitee.com/Steven_Hu/hk-lib-keyboard.git
[submodule "PrivateRepo/hk-lib-avoid-app-crash"]
    path = PrivateRepo/hk-lib-avoid-app-crash
    url = https://gitee.com/Steven_Hu/hk-lib-avoid-app-crash.git
[submodule "PrivateRepo/hk-lib-load-picture"]
    path = PrivateRepo/hk-lib-load-picture
    url = https://gitee.com/Steven_Hu/hk-lib-load-picture.git
[submodule "PrivateRepo/hkmodule-models"]
    path = PrivateRepo/hkmodule-models
    url = https://gitee.com/Steven_Hu/hkmodule-models.git
[submodule "PrivateRepo/hk-module-uikit"]
    path = PrivateRepo/hk-module-uikit
    url = https://gitee.com/Steven_Hu/hk-module-uikit.git
[submodule "PrivateRepo/hk-module-components"]
    path = PrivateRepo/hk-module-components
    url = https://gitee.com/Steven_Hu/hk-module-components.git
[submodule "PrivateRepo/hk-module-main"]
    path = PrivateRepo/hk-module-main
    url = https://gitee.com/Steven_Hu/hk-module-main.git

最终结果如下图所示,点击跳转新的submodule仓库地址


image.png

本地Clone方式

git clone https://gitee.com/Steven_Hu/hk-iostools.git
git submodule init && git submodule update

#下面这一句的效果和上面三条命令的效果是一样的,多加了个参数  `--recursive`
git clone https://gitee.com/Steven_Hu/hk-iostools.git --recursive

常见问题

error: Server does not allow request for unadvertised object b22e0a5a2a8dce1d0454bdead70353bf45f33f81

Fetched in submodule path 'PrivateRepo/hk-module-main', but it did not contain b22e0a5a2a8dce1d0454bdead70353bf45f33f81. Direct fetching of that commit failed.

原因分析:

未拉取到该submodule的最新代码

解决

git submodule foreach git checkout master
git submodule foreach git submodule update

3.11 coapods私有化仓库搭建

准备工作

  • 安装cocoapods
  • 准备gitlab/gitee/getlab(下文统一称为gitlab)等代码仓库账号

创建组建索引Spec仓库(用于存放组件库的索引)

pod repo add `specFileName(给spec仓库在本地的命名)` `spec(仓库的地址)`
实例:
pod repo add XXSpecs https://gitee.com/Steven_Hu/objective-c-specs

~/.cocoapods/repos目录中可以看到创建的文件夹

image.png

  • 创建组件库
    • pod lib create #{库的名称}
    • 实例 pod lib create XXLibDemo
  • 修改podspec文件
  • 将组件push到gitlab
  • 将索引仓库和组件仓库关联
  • 项目引入
# 远程私有仓库
source 'https://gitlab.com/xxxx/xxSpecs'

# 引入使用
pod 'xxLibNetwork'

4.常见问题补充

4.1target has transitive dependencies that include statically linked binaries:

解决办法

s.static_framework = true

4.2CocoaPods did not set the base configuration of your project because your project already...

使用CocoaPods安装三方库后有两个警告,如下所示:


image.png

解决办法:
将第三方库 的 PROJECT → Info → Configurations 下Debug和Release下的.debug和.release选项替换为None,如下图所示:


image.png

然后在pod install即可

4.3清除 CocoaPods 本地缓存

特殊情况下,由于网络或者别的原因,通过CocoaPods下载的文件可能会有问题。
这时候您可以删除CocoaPods的缓存(~/Library/Caches/CocoaPods/Pods/Release目录),再次导入即可。

4.4pod spec lint编译时报error: include of non-modular header inside framework module

解决办法

pod lib lint --allow-warnings --use-libraries --verbose

有警告⚠️可以使用-allow-warnings忽略。

4.5推送到本地LocalRepo

  • 使用了第三方库
pod repo push driver_spec XXLibBase.podspec --allow-warnings --verbose --use-libraries
  • 未使用第三方库
pod repo push driver_spec XXLibBase.podspec --allow-warnings --verbose

4.6pod lib lint 可选参数

pod lib lint SPEC_NAME.podspec
可选参数:
--verbose : 显示详细信息
--allow-warnings: 是否允许警告,用到第三方框架时,用这个参数可以屏蔽讲稿
--fail-fast: 在出现第一个错误时就停止
--use-libraries:如果用到的第三方库需要使用库文件的话,会用到这个参数
--sources:如果一个库的podspec包含除了cocoapods仓库以外的其他库的引用,则需要改参数指明,用逗号分隔。
--subspec=Name:用来校验某个子模块的情况。

注意:如果库用到了第三方的话要带上 --use-libraries,否则会报错,上传不上去。

4.7--sources 使用

1.私有pod的验证

使用pod spec lint去验证私有库能否通过验证时应该,应该要添加--sources选项,不然会出现找不到repo的错误。

pod spec lint --sources='私有仓库repo地址,https://github.com/CocoaPods/Specs'

2.私有库引用私有库的问题

在私有库引用了私有库的情况下,在验证和推送私有库的情况下都要加上所有的资源地址,不然pod会默认从官方repo查询。

pod spec lint --sources='私有仓库repo地址,https://github.com/CocoaPods/Specs'
pod repo push 本地repo名 podspec名 --sources='私有仓库repo地址,https://github.com/CocoaPods/Specs'

4.组件化案例

1.效果图

组件化案例.gif

2.代码地址

https://gitee.com/Steven_Hu/hk-iostools

搬砖不易,转载请注明出处,谢谢!

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

推荐阅读更多精彩内容