在 Kickstarter-iOS 源码中学到的(一) - 工程相关

其实在 Kickstarter 刚开源他们客户端源码的时候,我就从 GitHub 的 trends 里看到了。作为一个实用主义者,悄悄点了个 star 就走掉了。

后来在 ObjC.io 上的这个 talk 里,我看到了 Kickstarter 的工程师 Brandon Williams 介绍了他们在客户端中大量使用 ViewModel 的经历,而且在视频里用 ViewModel 重构了一段代码。当时我对 MVVM 比较迷,就抱着试试看的心态 clone 了一下他们的客户端源码,看过了之后我只想说,为什么我没有早点看它的代码!

依赖管理

第一眼看到他们的代码,是使用的 git submodule 来管理依赖关系的。其实依赖的库并不是很多,所以这么管理也没有什么痛点。虽然比 Cocoapods 管理起来麻烦了一点,但是能对于依赖的东西有完整的把控能力,也算是一个优点吧。不过用 submodule 需要团队里的成员对 submodule 都有一定的了解,否则可能就会一团糟了。

关于 git submodules, Cocoapods 和 Carthage 管理依赖关系的优劣这篇文章里说得很清楚了,我就不再赘述了,适合自己的才是最好的。

Makefile

看到 README 中的 Getting Started 中讲到,clone repo 之后,只要执行 make bootstrap 就可以安装工具和相关依赖了。

不得不说这个思路很好,用 make 来管理和运行与项目相关的一些操作,如果加了一项与工程相关的操作比如 test、 build,直接在 make 里加一条对应的就好,清晰又直观。第一次拿到项目的人一眼就知道了所有的命令 不用像我司的客户端一样,脚本飞得到处都是,想运行啥都得找半天

build: dependencies
$(XCODEBUILD) $(BUILD_FLAGS) $(XCPRETTY)

test-all:
PLATFORM=iOS "$(MAKE)" test
PLATFORM=iOS TARGET=Library "$(MAKE)" test

test: bootstrap
$(XCODEBUILD) test $(BUILD_FLAGS) $(XCPRETTY)

clean:
$(XCODEBUILD) clean $(BUILD_FLAGS) $(XCPRETTY)

dependencies: submodules configs secrets opentok

bootstrap: hooks dependencies
brew update || brew update
brew unlink swiftlint || true
brew install swiftlint
brew link --overwrite swiftlint

submodules:
git submodule sync --recursive || true
git submodule update --init --recursive || true

几个方便的 Swift 脚本

其实客户端的颜色和本地化字符串管理一向是开发中的痛点。而在他们的客户端中,我看到了两个 swift 文件,就是专门解决这个问题的。

他们色表是一个 Colors.json 文件,放在一个 Design 的目录里,Design 里面还有他们全部设计的 Sketch 文件,不得不说很良心。有一个 colors 的 Swift 脚本,专门把色表 parse 成一个 Colors.swift 文件,调用者只需要 UIColor.ksr_green_400 就可以取到颜色了。这种色表管理颜色的方式应该值得每一个客户端的开发者学习。设计师负责管理色表,而脚本负责生成代码。和这样相比,直接在代码里写颜色的确不方便太多。

extension UIColor {
/// 0x07565F
public static var ksr_forest_600: UIColor {
return .hex(0x07565F)
}

...
}

再就是 localizedString 了,做过国际化的同学们一定都了解,iOS 的 localizedString 太难用了,难用到无法理解。而项目里的 strings.swift 就完美地解决了这件事情。它会 parse 工程中的 Localizable.strings 文件,生成对应的 Strings.swift,里面就有所有的本地化字符串方法,而且还有对应的提示。再也不怕本地化了!其实知乎也做过类似的东西,只不过脚本不是用 Swift 写的

public enum Strings {
/**
"About %{reward_amount}"

- **es**: "Aprox. %{reward_amount}"
- **de**: "Ungefähr %{reward_amount}"
- **fr**: "Environ %{reward_amount}"
- **en**: "About %{reward_amount}"
*/
public static func About_reward_amount(reward_amount: String) -> String {
return localizedString(
key: "About_reward_amount",
defaultValue: "About %{reward_amount}",
count: nil,
substitutions: ["reward_amount": reward_amount]
)
}
...
}

Pull request 流程

在 push 代码之前,会有一个 git hook 来执行 swiftlint,来保证代码符合规范并且避免一些低级的错误。

有代码就有测试是必须的 这一点可以秒杀大多数做客户端的公司了吧

在提每个 pull request 的时候,作者都需要说明做了什么,并且为什么这么做,这个 PR 改了哪里,截图大概长什么样子等等,可以参考一下这个

而每个 reviewer 都会提出很多意见。

丰富的测试

这里的测试不光指几乎完整覆盖的 Unit Test,还有几乎完整的 UI Test。

项目中使用 ios-snapshot-test-case 来做 UI Test,它会将项目里大部分界面配合 mock 数据生成截图保存在 Screenshots 文件夹里。 每次提交 PR 之前,都要跑这些测试,一旦图片发生变化,PR 的 diff 里就会显示这些变化,reviewer 一眼就可以看得出你的代码到底改了哪里,真是非常方便。

规范的 commit message

几乎每一条 commit 都会带上一个 task 的 id,而且同一个 task 的多个 commit 在 merge 的时候会 squash,非常整洁。如果不知道怎么写 commit message 可以参考我的这一篇博文

总结

这篇文章只是抛砖引玉,我可能只看到了这个客户端源码工程方面的冰山一角,还希望大家在有经历的情况下也去看看他们客户端的代码。其实工作经验的积累完全不需要换公司来解决。如果你有心,GitHub 上有大量的开源客户端源码,拿过来好好看一看,吸收一下,就好像吸收了一家公司的工作经验,多爽。

在 Kickstarter 的开源代码之外,我还了解到,他们 native 组的工程师同时维护 Andriod 和 iOS 客户端的代码!这也是一般的公司都比不了的吧…… 随着对他们公司了解地越来越多,我也被 Brandon 圈粉,他不但推进了 ios-oss 的开源,让我看到这么优秀的客户端源码,而且他的很多分享也令我印象深刻。只可惜最近他从 Kickstarter 离职了,不过期待他给我们带来更多精彩!

下篇关于 Kickstart iOS 的分析应该是在层次结构和代码方面 希望自己不要太监