纳斯达克上市公司 iOS App组件化开发实践

分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友 微信微信
查看查看371 回复回复1 收藏收藏 分享淘帖 转播转播 分享分享 微信
查看: 371|回复: 1
收起左侧

纳斯达克上市公司 iOS App组件化开发实践

[复制链接]
cocoaswift 发表于 2016-10-11 20:32:50 | 显示全部楼层 |阅读模式
快来登录
获取优质的苹果资讯内容
收藏热门的iOS等技术干货
拷贝下载Swift Demo源代码
订阅梳理好了的知识点专辑

纳斯达克上市公司 iOS App组件化开发实践 1

纳斯达克上市公司 iOS App组件化开发实践 - 敏捷大拇指 - 纳斯达克上市公司 iOS App组件化开发实践 1





1、前因

其实我们这个7人iOS开发团队并不适合组件化开发。原因是因为性价比低,需要花很多时间和精力去做这件事,但收益不一定能超过付出的成本。但是因为有2~3个星期的空档期,并不是很忙;另外是可以用在一个全新的App上,所以决定想尝试下组件化开发,如果能解决组件化开发当中的一些问题,并且有比较好的解决方案,那就继续下去,否则就放弃。




2、背景

脱离实际情况去谈方案的选型是不合理的。

所以先简单介绍下背景:我们是一家纳斯达克交易所上市的科技企业。我们公司还有好几款App,由不同的几个团队去维护,我们是其中之一。我们这个团队是一个7人的iOS开发小团队。作者本人是小组长。

之前的App已经使用了模块化(CocoaPods)开发,并且已经使用了二进制化方案(详见敏捷大拇指上这个帖子《纳斯达克上市公司 CocoaPods组件平滑二进制化解决方案》)。App已经在使用自动化集成。

虽然要开发一个新App,但是很多业务和之前的App是一样的或者相似的。

在这里我想把整个过程记录下来,方便以后回顾。

我们的思路和解决方案不一定是对的或者是最好的。所以希望大家看了这篇博客之后,能给我们提供很多建议和别的解决方案,让我们可以优化使得这个组件化开发的方案能变得更加好。




3、技术栈

  • gitlab
  • gitlab-runner
  • CocoaPods
  • CocoaPods-Packager
  • fir
  • 二进制化
  • fastlane
  • deploymate
  • oclint
  • Kiwi





4、成果

使用组件化开发App之后:

  • 代码提交更规范,质量提高。体现在测试人员反馈的bug明显减少。
  • 编译加快。在都是源码的情况下:原App需要150s左右整个编译完毕,然后开发人员才可以开始调试。而现在组件化之后,某个业务组件只需要10s~20s左右。在依赖二进制化组件的情况下,业务组件编译速度一般低于10s。
  • 分工更为明确,从而提升开发效率。
  • 灵活,耦合低。
  • 结合MVVM。非常细致的单元测试,提高代码质量,保证App稳定性。体现在测试人员反馈的bug明显减少。
  • 回滚更方便。我们经常会发生业务或者UI变回之前版本的情况,以前我们都是checkout出之前的代码。而现在组件化了之后,我们只需要使用旧版本的业务组件Pod库,或者在旧版本的基础上再发一个Pod库。
  • 新人更容易上手。


对于我来说:

  • 更加容易地把控代码质量。
  • 更加容易地知道小组成员做了些什么。
  • 更加容易地分配工作。
  • 更加容易地安排新成员。





5、解耦

我们的想法是这样的,就算最后做不成组件化开发,把这些应该重用的代码抽出来做成Pod库也没有什么影响。所以优先做了这一步。

哪些东西需要抽成Pod库?

我们之前的App已经使用了模块化(CocoaPods化)开发。我们已经把会在App之间重用的Util、Category、网络层和本地存储等等这些东西抽成了Pod库。还有些一些和业务相关的,比如YTXChart,YTXChartSocket;这些也是在各个App之间重用的。

所以得出一个很简单的结论:要在App之间共享的代码就应该抽成Pod库,把它们作为一个个组件。

我们去仔细查看了原App代码,发现很多东西都需要重用而我们却没有把它们组件化。

为什么没有把这些代码组件化?

因为当时没想好怎么解耦,举个例子。

有一个类叫做YTXAnalytics。是依赖UMengAnalytics来做统计的。 它的耦合是在于一个方法。这个方法是用来收集信息的。它依赖了User,还依赖了currentServerId这个东西。

纳斯达克上市公司 iOS App组件化开发实践 2

纳斯达克上市公司 iOS App组件化开发实践 - 敏捷大拇指 - 纳斯达克上市公司 iOS App组件化开发实践 2


解决方案是,搞了一个block,把获取这些信息的责任丢出来。

纳斯达克上市公司 iOS App组件化开发实践 3

纳斯达克上市公司 iOS App组件化开发实践 - 敏捷大拇指 - 纳斯达克上市公司 iOS App组件化开发实践 3


我们的耦合大多数都是这种。解决方案都是弄了一个block,把获取信息的职责丢出来到外面。

我们解耦的方式就是以下几种:

  • 把它依赖的代码先做成一个Pod库,然后转而依赖Pod库。有点像是“依赖下沉”。
  • 使用category的方式把依赖改成组合的方式。
  • 使用一个block或delegate(协议)把这部分职责丢出去。
  • 直接copy代码。copy代码这个事情看起来很不优雅,但是它的好处就是快。对于一些不重要的工具方法,也可以直接copy到内部来用。





6、初始化

AppDelegate充斥着各种初始化。 比如我们自己的代码。已经只是截取了部分!

纳斯达克上市公司 iOS App组件化开发实践 4

纳斯达克上市公司 iOS App组件化开发实践 - 敏捷大拇指 - 纳斯达克上市公司 iOS App组件化开发实践 4


比如第三方:

纳斯达克上市公司 iOS App组件化开发实践 5

纳斯达克上市公司 iOS App组件化开发实践 - 敏捷大拇指 - 纳斯达克上市公司 iOS App组件化开发实践 5


首先这些初始化的东西是会被各个业务组件都用到的。

那我组件化开发的时候,每一个业务组件如何保证我使用这些东西的时候已经初始化过了呢?难道每一个业务组件都初始化一遍?有参数怎么办,能不能使用单例?

但问题是第三方库基本都需要注册一个AppKey,我每一个业务组件里都写一份?那样肯定不好,那我配置在主App里的info.plist里面,每一个业务组件都初始化一下好了,也不会有什么副作用。但这样感觉不优雅,而且有很多重复代码。万一某个AppKey或重要参数改了,那每一个业务组件岂不是都得改了。这样肯定不行。另外一点,那我的业务组件必须依赖主App的内容了。无论是在主App里调试还是把主App的info.plist的相关内容拷贝过来使用。

更关键的是有一些第三方的库需要在 application: didFinishLaunchingWithOptions: 时初始化。

[Objective-C] 纯文本查看 复制代码
//初始化环信,shareSDK, 友盟, Talking Data等
[self setupThirdParty:application didFinishLaunchingWithOptions:launchOptions];


有没有更好的办法呢?

首先我写了一个YTXModule。它利用runtime,不需要在AppDelegate中添加任何代码,就可以捕获App生命周期。

在某个想获得App生命周期的类中的.m中这样使用:

纳斯达克上市公司 iOS App组件化开发实践 6

纳斯达克上市公司 iOS App组件化开发实践 - 敏捷大拇指 - 纳斯达克上市公司 iOS App组件化开发实践 6





7、分层

因为在解决初始化问题的时候,要先设计好层级结构,所以这里说一下分层。

上个图:

纳斯达克上市公司 iOS App组件化开发实践 7

纳斯达克上市公司 iOS App组件化开发实践 - 敏捷大拇指 - 纳斯达克上市公司 iOS App组件化开发实践 7


我们自己定了几个原则。

  • 业务组件之间不能有依赖关系。
  • 按照图示不能跨层依赖。
  • 所谓弱业务组件就是包含着少部分业务,并且可以在这个App内的各个业务组件之间重用的代码。
  • 要依赖YTXModule的组件一定要以Module结尾,而且它一定是个业务组件或是弱业务组件。
  • 弱业务组件以App代号开头(比如PB),以Module结尾。例:PBBasicProviderModule。
  • 业务组件以App代号开头(比如PB)BusinessModule结尾。例:PBHomePageBusinessModule。


业务组件之间不能有依赖关系,这是公认的的原则。否则就失去了组件化开发的核心价值。

弱业务组件之间也不应当有依赖关系。如果有依赖关系说明你的功能划分不准确。




8、初始化设计

我们约定好了层级结构,明确了职责之后。我们就可以跳回初始化的设计了。

创建一个PBBasicProviderModule弱业务组件。

  • 它通过依赖YTXModule来捕捉App生命周期。
  • 它来负责初始化自己的和第三方的东西。
  • 所有业务组件都可以依赖这个弱业务组件。
  • 它来保证所有东西一定是是初始化完毕的。
  • 它来统一管理。
  • 它来暴露一些类和功能给业务组件使用。


反正就是业务组件中依赖PBBasicProviderModule,它保证它里面的所有东西都是好用的。

因为有了PBBasicProviderModule,所以才让我更明确了弱业务组件这个概念。

因为我们懒,如果把PBBasicProvider定义为业务组件,那它和其他业务组件之间的通信就必须通过Bus、Notification或协议等等。

但它又肯定是业务,因为那些AppKey肯定是和这个App有关系的,也就是App的相关配置和参数也可以说是业务;我需要初始化设置那些Block依赖User信息、CurrentServerId等等都是业务。

那只好搞个弱业务出来,因为我不能打破这个原则:业务组件之间不能互相依赖。

再进一步分清弱业务组件和业务组件。

业务组件里面基本都有:storyboard、nib、图片等等。弱业务组件里面一般没有。这不是绝对的,但一般情况是这样。

业务组件一般都是App上某一具体业务。比如首页、我、直播、行情详情、XX交易大盘、YY交易大盘、XX交易中盘、资讯、发现等等。而弱业务组件是给这些业务组件提供功能的,自己不直接表现在App上展示。

我们还可以创建一些弱业务组件给业务组件提供功能。当然了,不能够滥用,需要准确划分职责。

最后,代码大概是这样的:

纳斯达克上市公司 iOS App组件化开发实践 8

纳斯达克上市公司 iOS App组件化开发实践 - 敏捷大拇指 - 纳斯达克上市公司 iOS App组件化开发实践 8





9、设想

这个PBBasicProviderModule简直就是个大杂烩啊,把很多以前写在AppDelegate里的东西都丢在里面了。毫无优雅可言。

的确是这样的,感觉没有更好的办法了。

既然已经这样了。我们可不可以大胆地设想一下:每个开发者开发自己负责的业务组件的时候不需要关心主App。

因为我知道美团的组件化开发必须依赖主App的AppDelegate的一大堆设置和初始化。所以干脆他们就直接在主App中集成调试,他们通过二进制化和去Pod依赖化的方式让主App的构建非常快。

所以我们是不是可以继续污染这个PBBasicProviderModule。不需要在主App项目里的AppDelegate写任何初始化代码?基本或者尽量不在主App里写任何代码?改依赖主App变为依赖这个弱业务组件?

按照这个思路我们搬空了AppDelegate里的所有代码。比如一些初始化App样式的东西、初始化RootViewController等等这些都可以搬到一个新的弱业务组件里。

而业务组件其实根本不需关心这个弱业务组件,开发人员只需要在业务组件中的Example App中的AppDelegate中初始化自己业务组件的RootViewController就好了。

其他的事情交给这个新的弱业务组件就好了。而主App和Example App只要在Podfile中依赖它就好了。

所以最后的设想就是:开发者不会去改主App项目,也不需要知道主App项目。对于开发者来说,主App和业务组件之间是隔绝的。

有一个更大的好处,我只要更换这个弱业务组件,这个业务组件就能马上适配一个新App。这也是某种意义上的解耦。




10、Debug/Release

谁说不用在主App里的AppDelegate写任何代码的,打脸。。。

我们在对二进制Pod库跑测试的发现,源码能过,二进制(.a)不能过。百思不得其解,然后仔细查看代码,发现是这个宏的锅:

[Objective-C] 纯文本查看 复制代码
#ifdef DEBUG
#endif


DEBUG在编译阶段就已经决定了,但二进制化的时候已经编译完成了,无法修改。 而我们的代码中充满着#ifdef DEBUG。那怎么办,这是二进制化的锅。但是我们的二进制化已经形成了标准,大家都自觉会这么做,怎么解决这个问题呢。

解决方案是:

创建了一个PBEnvironmentProvider。大家都去依赖它。

然后原来判断宏的代码改成这样:

[Objective-C] 纯文本查看 复制代码
if([PBEnvironmentProvider testing])
{
    //...
}


在主App的AppDelegate中这样:

[Objective-C] 纯文本查看 复制代码
#if DEBUG && TESTING
//PBEnvironmentProvider提供的宏
CONFIG_ENVIRONMENT_TESTING
#endif


原理是:如果AppDelegate有某个方法(CONFIG_ENVIRONMENT_TESTING宏会提供这个方法),[PBEnvironmentProvider testing]得到的结果就是YES。

为什么要写在主App里呢?其实也可以丢在PBBasicProviderModule里面,提供一个方法啊。

因为主App的AppDelegate.m是源码,未经编译。另外注意TESTING这个宏。我们可以在xcode设置里加一个macro参数TESTING,并且修改为0的情况下,能够生成一个实际是DEBUG的App但里面内容却是线上的内容。

这个需求是来自于我们经常需要紧急通过Xcode直接build一个app到手机上以解决或确认线上的问题。

虽然打脸了,但是也还好,以后也不用改了。再说这个是特殊需求。除了这个之外,主App没有其他代码了。




11、业务组件间通信

我们解决了初始化和解耦的问题。接下来只要解决组件间通信的问题就好了。

然后我找了几个第三方库,选用了MGJRouter。本来直接依赖它就好了。

后来觉得都使用Block的方式会导致这样的代码,全部堆在了一个方法里:

纳斯达克上市公司 iOS App组件化开发实践 9

纳斯达克上市公司 iOS App组件化开发实践 - 敏捷大拇指 - 纳斯达克上市公司 iOS App组件化开发实践 9


这样感觉很不爽。那我干脆就把MGJRouter代码复制了下来,把Block改成了@selector。并且把它直接加入了YTXModule里面。并且使用了宏,让结果看起来优雅些。代码看起来是这样的:

纳斯达克上市公司 iOS App组件化开发实践 10

纳斯达克上市公司 iOS App组件化开发实践 - 敏捷大拇指 - 纳斯达克上市公司 iOS App组件化开发实践 10


调用的时候看起来是这样的:

纳斯达克上市公司 iOS App组件化开发实践 11

纳斯达克上市公司 iOS App组件化开发实践 - 敏捷大拇指 - 纳斯达克上市公司 iOS App组件化开发实践 11


通信问题解决了。其实页面跳转问题也解决了。



11.1、页面跳转

页面跳转解决方案与业务组件之间通信问题是一样的。

但是需要注意的是,你一个业务组件内部的页面跳转也请使用URL+Router的方式跳转,而不要自己直接pushViewController。

这样的好处是:如果将来某些内部跳转页面需要给其他业务组件调用,你就不需要再注册个URL了。因为本来就有。



11.2、是否去Model化

去Model化主要体现在业务组件间通信,要不要传一个Model过去(传过去的Dictionary中的某个键是Model)。

如果去Model化,这个业务组件的开发者如何确定Dictionary里面有哪些内容分别是什么类型呢?那需要有个地方传播这些信息,比如写在头文件,wiki等等。

如果不去Model化的话,就需要把这个Model做成Pod库。两个业务组件都去依赖它。

最后决定不去Model。因为实际上有一些Model就是在各个业务组件之间公用的(比如User),所以肯定就会有Model做成Pod库。我们可以把它做成重Model,Model里可以带网络请求和本地存储的方法。唯一不能避免的问题是,两个业务组件的开发者都有可能去改这个Model的Pod库。



11.3、信息的披露

跳转的页面需要传哪些参数? 业务组件之间传递数据时候本质的载体是什么?

不同业务开发者如何知晓这些信息。

使用去Model化和不使用去Model化,我们都有各自的方案。

去Model化,则披露头文件,在头文件里面写详细的注释。

如果不去Model化,则就看Model就可以了。如有特殊情况,那也是文档写在头文件内。

总结的话:信息披露的方式就是把注释文档写在头文件内。



11.4、组件的生命周期

业务组件的生命周期和App一样。它本身就是个类,只暴露类方法,不存在需要实例,所以其实不存在生命周期这个概念。而它可以使用类方法创建很多ViewController,ViewController的生命周期由App管理。哪怕这些ViewController之间需要通信,你也可以使用Bus/YTXModule/协议等等方式来做,而不应该让业务组件这个类来负责他们之间的通信;也不应该自己持有ViewController;这样增加了耦合。

弱业务组件的生命周期由创建它的对象来管理。按需创建和ARC自动释放。

基础功能组件和第三方的生命周期由创建它的对象来管理。按需创建和ARC自动释放。



11.5、版本规范

我们自己定的规则。

所有Pod库都只依赖到minor

[Swift] 纯文本查看 复制代码
"~> 2.3"


主App中精确依赖到patch

[Swift] 纯文本查看 复制代码
"2.3.1"


主App中的业务组件版本号的Main.Minor要和主App版本保持一致。

参考:Semantic Versioning RubyGems Versioning Policies



11.6、二进制化

二进制化我认为是必须的,能够加快开发速度。

而我使用的这个二进制方案有个坑就是在gitlab-runner上在二进制和源码切换时,经常需要pod cache clean --all,test/lint/publish才能成功。而每次pod cache clean --all之后CocoaPods会去重新下载相关的pod库,增加了时间和不必要的开销。

我们现在通过podspec中增加preserve_paths和执行download_zip.sh解决了cache的问题。原理是让pod cache既有源码又有二进制.a。具体可以看ytx-pod-template项目中的Name.podspec和download_zip.sh。

二进制化还得注意宏的问题。小心使用宏,尤其是#ifdef。避免源码和二进制代码运行的结果不一样。



11.7、集成调试

集成调试很简单。每一个业务组件在自己的Example App中调试。

这个业务组件的podspec只要写清楚自己依赖的库有哪些。剩下的其他业务组件应该写在Example App的Podfile里面。

依赖的Pod库都是二进制的。如有问题可以装源码(IS_SOURCE=1 pod install)来调试。

开发人员其实只需要关心自己的业务组件,这个业务组件是自洽的。



11.8、公共库谁来维护的问题

这个问题在我们这种小Team不存在。没有仔细地去想过。但是只要做好代码准入(Test/Lint/Code Review)和权限管理就应该不会存在大的问题。



11.9、单元测试

单元测试我们用的是Kiwi。 结合MVVM模式,对每一个业务组件的ViewModel都进行单元测试。每次push代码,gitlab-runner都会自动跑测试。一旦开发人员发现测试挂了就能够及时找到问题。也可以很容易的追溯哪次提交把测试跑挂了。

这也是我们团队的强制要求。没有测试,测试写的不好,测试挂了,直接拒绝merge request。

纳斯达克上市公司 iOS App组件化开发实践 12

纳斯达克上市公司 iOS App组件化开发实践 - 敏捷大拇指 - 纳斯达克上市公司 iOS App组件化开发实践 12




11.10、lint

对每一个组件进行lint再发布,保证了正确性。这也是一步强制要求。

lint的时候能够发现很多问题。通常情况下不允许warning出现的。如果不能避免(比如第三方)请用--allow-warnings。

[Bash shell] 纯文本查看 复制代码
pod lib lint --sources=$SOURCES --verbose --fail-fast --use-libraries




11.11、统一的网络服务和本地存储方式

这个就很简单。把这两个部分抽象成几个Pod库供所有业务组件使用就好了。 我们这边分别是三个Pod库:

  • YTXRequest
  • YTXRestfulModel
  • NSUserDefault+YTX


其他一些内容:

  • ignore了主App中的Podfile.lock尽量避免冲突。
  • 主App Archive的时候要使用源码,而不是二进制。
  • 后期可以使用oclint和deploymate检查代码。
  • 使用fastlane match去维护开发证书。
  • 一些需要从plist或者json读取配置的Pod库模块,要注意读出来的内容最好要加一个namespace。namespace可以是这个业务组件的名字。





12、业务组件读取资源文件的区别

纳斯达克上市公司 iOS App组件化开发实践 13

纳斯达克上市公司 iOS App组件化开发实践 - 敏捷大拇指 - 纳斯达克上市公司 iOS App组件化开发实践 13





13、持续集成

原来的App就是持续集成的。想当然的,我们希望新的组件化开发的App也能够持续集成。

Podfile应该是这样的:这里面出现的全是私有Pod库。

纳斯达克上市公司 iOS App组件化开发实践 14

纳斯达克上市公司 iOS App组件化开发实践 - 敏捷大拇指 - 纳斯达克上市公司 iOS App组件化开发实践 14


如果Pod依赖的东西特别特别多,比如100多个。另外又必须依赖主App做集成调试。 你也可以用这种方案:把你所有的Pod库的依赖都展开写到主App的Podfile中。而发布Pod库时podspec中不带任何的依赖的。这样就避免了pod install的时候解析依赖特别耗时的问题。

各个脚本都在这个ytx-pod-template:https://github.com/mdsb100/ytx-pod-template

先从.gitlab-ci.yml看起。

持续集成的整个流程是:


第一步:

使用template创建Pod。像这样:

[Bash shell] 纯文本查看 复制代码
pod lib create <Pod库名称> --template-url="http://gitlab.baidao.com/pods/ytx-pod-template"



第二步:

创建dev分支。用来开发。


第三步:

每次push dev的时候会触发runner自动跑Stage: Init Lint(中的test)

纳斯达克上市公司 iOS App组件化开发实践 15

纳斯达克上市公司 iOS App组件化开发实践 - 敏捷大拇指 - 纳斯达克上市公司 iOS App组件化开发实践 15



第四步:

1.准备发布Pod库。修改podspec的版本号,打上相应tag。

2.使用merge_request.sh向master提交一个merge request。

纳斯达克上市公司 iOS App组件化开发实践 16

纳斯达克上市公司 iOS App组件化开发实践 - 敏捷大拇指 - 纳斯达克上市公司 iOS App组件化开发实践 16



第五步:


1.其他有权限开发者code review之后,接受merge request。

2.master合并这个merge request

3.master触发runner自动跑Stage: Init Package Lint ReleasePod UpdateApp


第六步:

如果第五步正确。主App的dev分支会收到一个merge request,里面的内容是修改Podfile。 图中内容出现了AFNetworking等是因为这个时候在做测试。

纳斯达克上市公司 iOS App组件化开发实践 17

纳斯达克上市公司 iOS App组件化开发实践 - 敏捷大拇指 - 纳斯达克上市公司 iOS App组件化开发实践 17



第七步:

主App触发runner,会构建一个ipa自动上传到fir。

Init

  • 初始化一些环境。
  • 打印一些信息。


Package

  • 二进制化打包成.a


Lint

  • Pod lib lint。二进制和源码都lint。
  • 测试。
  • 以后考虑加入oclint和deploymate。


ReleasePod

  • 把相关文件zip后,传到静态服务器库。以提供二进制化下载包。
  • pod repo push。发布该Pod库。


ReleasePod的时候不允许Pod库出现警告。

UpdateApp

  • 下载App代码
  • 修改Podfile文件。如果匹配到pod库文件名则修改,否则添加。
  • 生成一个merge request到主App的dev分支。


关于gitlab runner。

stage这个功能非常的厉害。强烈推荐。

每一个stage可以跑在不同的runner上。每一个stage失败了可以单独retry。而某一个stage里面的任务可以并行执行:(test和lint就是并行的)。

纳斯达克上市公司 iOS App组件化开发实践 18

纳斯达克上市公司 iOS App组件化开发实践 - 敏捷大拇指 - 纳斯达克上市公司 iOS App组件化开发实践 18





欢迎订阅全球最大的Swift开发者社区敏捷大拇指Swifthumb.com)上的淘帖“iOS App 组件化架构”。




相关内容

滴滴/淘宝/微信/蘑菇街/casatwy等 iOS App的组件化架构漫谈

滴滴 iOS App的组件化实践与优化

纳斯达克上市公司 iOS App组件化开发实践

纳斯达克上市公司 CocoaPods组件平滑二进制化解决方案

iOS CocoaPods组件平滑二进制化解决方案及详细教程1

iOS CocoaPods组件平滑二进制化解决方案及详细教程2之subspecs篇




作者:曹俊

都看到这里了,就把这篇资料推荐给您的好朋友吧,让他们也感受一下。

回帖是一种美德,也是对楼主发帖的尊重和支持。您的赞赏是我前进的方向。

*声明:敏捷大拇指是全球最大的Swift开发者社区、苹果粉丝家园、智能移动门户,所载内容仅限于传递更多最新信息,并不意味赞同其观点或证实其描述;内容仅供参考,并非绝对正确的建议。本站不对上述信息的真实性、合法性、完整性做出保证;转载请注明来源并加上本站链接,敏捷大拇指将保留所有法律权益。如有疑问或建议,邮件至marketing@swifthumb.com

*联系:微信公众平台:“swifthumb” / 腾讯微博:@swifthumb / 新浪微博:@swifthumb / 官方QQ一群:343549891(满) / 官方QQ二群:245285613 ,需要报上用户名才会被同意进群,请先注册敏捷大拇指

评分

参与人数 1金钱 +10 贡献 +10 专家分 +10 收起 理由
Anewczs + 10 + 10 + 10 32个赞!专家给力!

查看全部评分

本帖被以下淘专辑推荐:

女汉子 发表于 2016-10-15 23:48:14 | 显示全部楼层
这么好的帖子都没人回复一下?
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

淘帖专辑
我要发帖

分享扩散

都看到这里了,就把这资料推荐给您的好朋友吧,让他们也感受一下。
您的每一位朋友访问此永久链接后,您都将获得相应的金钱积分奖励
热门推荐

合作伙伴

Swift小苹果

  • 北京治世天下科技有限公司
  • ©2014-2016 敏捷大拇指
  • 京ICP备14029482号
  • Powered by Discuz! X3.1 Licensed
  • swifthumb Wechat Code
  •   
快速回复 返回顶部 返回列表