我们在前文 《Cocoa 代码注释与文档生成》 中详细介绍了如何为 Swift & ObjC 的代码编写符合规范的注释,以及使用 Jazzy 来生成项目文档。 今天我们来尝试一下,如何一键生成多个私有库的文档,并将其部署到 Github page 或者 Gitlab page 上。
本文知识目录
背景
随着公司项目的迭代,一般都会沉淀出多个私有库。如果这些私有库可以够提供统一的文档查询和预览服务,那将有助于团队中的新成员快速了解业务。
作者所在的公司就维护者 20 多个的私有库,同时这些项目的代码注释完整度不一,注释的内容也参差不齐。如果我们可以通过这个在线文档,不仅可以提供快速的 API 查阅能力,也可以更好的监督和规范项目。
如何一键生成多依赖库的文档
我们先来简单分析一下要实现这个想法 💡需要做哪些事情。
- 现有的文档生成工具都是基于单个项目,而我们想要的是多依赖库的集合文档。那么就需要有一个索引页将各个依赖库串联起来,能够通过索引来访问它。
- 由于公司的项目是包含了 Swift & ObjC 混编的庞大项目,所维护的私有仓库不仅包含了纯 ObjC 和纯 Swift 实现的,还包括了 Swift & ObjC 混编代码的依赖库。所以需要支持这个三种场景。
- 生成的文档都是静态页面,需要将这些页面托管在静态资源服务上,关于这点 Github Page 和 Gitlab Page 就能解决。
- 毕竟项目是不断的迭代演进的,那如何在一定时机的情况下自动触发或者手动触发更新文档,也是十分重要的一件事情。
明确了我们要解决的问题,剩下的事情就简单了。
生成工具
就直接使用 shell
将上面的步骤串联起来,如果大家熟悉其他语言也可以,文档生成工具就是 Jazzy
+ SourceKitten
。Jazzy 之前介绍过了,一起看看 SourceKitten 吧:
An adorable little framework and command line tool for interacting with SourceKit.
Sourcekitten 是基于 Apple 的 SourceKit 封装的命令行工具,SourceKitten 链接并与 sourcekitd.framework 通信以解析 Swift AST,提取 Swift 或 ObjC 项目的注释文档,获取 Swift 文件的语法数据等等。
SourceKit is a framework for supporting IDE features like indexing, syntax-coloring, code-completion, etc. In general it provides the infrastructure that an IDE needs for excellent language support.
文档索引页的生成
为了整体的样式统一,我们的索引页采用与 Jazzy
所生成的文档相同的 CSS 样式。由于 Jazzy
支持切换生成文档的主题,这里我们使用默认主题。
当我们访问静态网站时,入口一般都指向一个名为 index.html
的页面。 Jazzy
生成的入口也是 index.html
。
我们要做的就是往 index.html
内添加含对应的标签,并将标签链接指向各个依赖库的文档地址就可以了。
索引页
下面是我们需要修改的代码,完整的 index.html 模版可访问 Jazzy-template。
1<div class="content-wrapper">
2 ...
3 <article class="main-content">
4 <section class="section">
5 <div class="section-content top-matter">
6 <h3 id='authors' class='heading'>业务库</h3>
7 </div>
8 </section>
9 <section class="section">
10 <div class="section-content">
11 <div class="task-group">
12 <ul class="item-container">
13 <li>token-business</li>
14 </ul>
15 </div>
16 </div>
17 </section>
18 <div class="section-content top-matter">
19 <h3 id='authors' class='heading'>基础库</h3>
20 </div>
21 <section class="section">
22 <div class="section-content">
23 <div class="task-group">
24 <ul class="item-container">
25 <li>token-base</li>
26 </ul>
27 </div>
28 </div>
29 </section>
30 </article>
31</div>
要修改的就是上面的 <li>token-*</lib>
元素,这里留的默认 token 是为了方便替换。
业务库
由于业务库逻辑一般会比较多,如果和基础库文档放一起,可能会导致生成文档的太大,Github Page 无法正常解析。因此,需要单独的文档仓库来存放文档。
基础库
基础库生成的文档会统一放到项目的 docs
目录下,同时 <li>token-base</li>
标签的地址最后会指向 docs/$lib_name/index.html
目录。
目前的结构是这样的:
文档结构
我们先来看一下以 Alamofire 项目生成的 docs
文档目录结构:
第一层包含了 Classes
、Enums
、Extensions
、Protocols
、Structs
等分类和对应的 index.html
索引文件。
第二层为具体到的每个 Class、Enum 或其他数据结构的 HTML 页面。如果该结构还存在嵌套的内部数据类型,会以递归的方式呈现。
整个 docs
的基础结构特别简单:
我们要做的就是复制上面的文件,以及修改的 index.html 就可以。
多依赖库的文档生成
对于 iOS 项目的依赖库管理标配为 CocoaPods (后面简称 Pod) ,它将所有的依赖库源码统一存放在项目的 /Pods
目录下。我们要做的就是遍历 /Pods
目录,逐一生成文档并将其输出到一个指定目录就可以了。
想法是美好的,现实是残酷的。在实际操作起来发现并没有那么简单。让我们开启踩坑之旅吧!
Swift 依赖库的文档生成
之前在 《Cocoa 代码注释与文档生成》 中介绍的 Swift 的文档生成都是基于该项目的 project
工程或者是 SwiftPM 配置来完成。好在 Pod 也为我们生成对应的 project
,我们仅需通过 --build-tool-arguments
来指定 project
和 target
就可以了。
从零开始,我们先新建一个 Demo.xcodeproj 并配置如下 Podfile:
1target 'Demo' do
2 pod 'SnapKit'
3 pod 'AFNetworking'
4end
调用 Jazzy 生成 Swift 库 SnapKit
的文档:
1$ bundle exec jazzy -o docs/SnapKit \
2 --build-tool-arguments -project,Pods/Pods.xcodeproj,-target,SnapKit
通过 -o
将结果输出到 docs/SnapKit
目录下,执行后输出结果如下:
1Running xcodebuild
2Parsing Constraint.swift (1/34)
3...
4Parsing UILayoutSupport+Extensions.swift (34/34)
5`ConstraintLayoutSupport` has no USR. ...
69% documentation coverage with 239 undocumented symbols
7included 264 public or open symbols
8skipped 81 private, fileprivate, or internal symbols (use `--min-acl` to specify a different minimum ACL)
9building site
10building search index
11jam out ♪♫ to your fresh new docs in `docs/SnapKit`
可以看到 Jazzy 会遍历项目下的每个 swift 文件,对于项目中未引用的代码也会有提示。最后会输出代码的注释覆盖率,SnapKit 的覆盖率为 9%,有 239 个未注释的符号或变量。
指定文档的范围
需要注意的是,Jazzy 可以通过 --min-acl
来控制输出文档的范围。
-
对于 Swift 项目,默认仅生成声明为
public
和open
的类、属性和方法等,如果想要输出私有变量的注释,还可以设置为internal
、fileprivate
或private
。 -
对于 ObjC 项目,Jazzy 仅会生成在
--umbrella-header
所指定的 header 文件中所引用的.h
文件。
ObjC 依赖库的文档生成
相比 Swift,Objc 的依赖库需要多处理 umbrella header
的问题。先看 AFNetworking 的文档生成命令:
1$ lib_name=AFNetworking
2lib_path=$(pwd)/Pods/$lib_name
3umbrella_header="$lib_path/$lib_name/$lib_name-umbrella.h"
4sdk_path=`xcrun --show-sdk-path --sdk iphonesimulator`
5
6bundle exec jazzy -o docs/$lib_name \
7--objc \
8--sdk iphoneos \
9--build-tool-arguments \
10--objc,$umbrella_header,--,-x,objective-c,-isysroot,$sdk_path,-I,$lib_path
第一个是需要指定 --objc
,因为 Jazzy 默认解析 Swift 项目。
再来看 --build-tool-arguments
后跟的几个参数:
- –objc <umbrella-header PATH>:这里的
--objc
是通知 SourceKitten 我要解析的是 Objc 的头文件,后面紧跟的为依赖库的 umbrella header - –:作为分割符,表示之后的参数会转发到
xcodebuild
或swift build
- -x objective:通知
xcodebuild
或swift build
我要编译 ObjC 啦 - -isysroot:指定所编译的 sdk,这里我们使用模拟器的 sdk
- -I $lib_path:指定 include 的搜索路径
获取 umbrella header
在 ObjC 中引用代码是需要通过 #import
来完成的,而对于 ObjC 的 framework 而言,我们可以通过引入 umbrella header
来引入该 framework 暴露出来的全部 public header 文件。因此,可以理解为 umbrella header
是 ObjC framework 的 master header。具体可以看:讨论。
这一点需要感谢 Pod,它为我们的依赖库统一生成了 A-umberlla.h
文件,存放在 Target Support Files/A/A-umberlla.h
。
在此之前很多依赖库的 umbrella header
并不是很规范。经常会有一些文件是 public 状态,却未添加到 umbrella header
中,导致无法直接通过 umbrella header
来完成引用。包括很多公司维护的私有库也会经常忘记更新 umbrella header
的情况,好在 Pod 帮我们自动生成了。
复制 umbrella header
细心的同学从 AFNetworking 的文档生成命令中能发现,AFNetworking-umbrella.h
的位置是在源码的文件夹下。如果直接指定为 Target Support Files
下的 umbrella header 文件是无法生成文档的。我们需要把它复制到源代码在同层目录下。
那么问题来了:如何正确的获取源码所在目录。
首先想到的是和通过 .podspec
文件就能准确拿到 Source 目录。不过比较难实现,我们只能拿到的是 Local Podspecs
下的 .podspec
文件,否则需要在 pod install
时才能获取到。但是这么做需要修改 Podfile 也比较麻烦。
选择简单粗暴的方式,直接列出可能出现的 Source 路径:
1# /A/Classes/...
2# /A/src/a/...
3# /A/A/Classe/...
4# /A/A/Classes/...
5# /A/A/Source/..
6# /A/A/Sources/..
7# /A/Source/A/...
8# /A/Sources/A/...
9# /A/Source/...
10# /A/A/..
11# /A/...
12# libextobjc/extobjc
有用 Classes
、 Source
、Sources
、src
等等,情况五花八门,逐一匹配就可以了。
这么做是可以覆盖大部分的情况,但是仍然发现部分私有库生成的文档缺失甚至是空的。最终发现的问题是:clang 没有递归处理多级目录的文件,这里应该是参数没有正确设置,查看了 Clang 手册 感觉就是 -I
参数,不过也没有生效,有了解的同学求指点。
咋办,先暴力解决:
1find $lib_path -type f ! -regex '*.\(h\|m\|swift\)' \
2 ! -name '*.json' \
3 ! -name '*.pdf' \
4 -exec mv -i {} $lib_path \;
将子目录下文件全部移到 framework 源码目录下,再通过 Jazzy 来生成文档,算是暂时解决问题了。
然而 AFNetworking 的文档依旧不是完整的,不过属于另外一种情况。目录如下,大家可以 🤔 一下:
Swift & ObjC 混编依赖库的文档生成
对 Swift & ObjC 混编的依赖库本身是不提倡的,虽然在实际开发过程中无法避免。
为了测试混编库的文档生成,这里新建一个 Pod 库:Mixin,添加了 MixinSwift 和 MixinObjC 两个类:
MixinSwift
1/// Test Swfit Class import Objective-C's property
2public class MixinSwift: NSObject {
3
4 /// say hello from Swift
5 @objc public static let sayHi: String = "Hi, I'm from Swift"
6
7 /// call Objective-C say Hi
8 @objc public class func callObjC() {
9 print("hello from MixinObjc: \(MixinObjC.sayHi)")
10 }
11}
MixinObjC
1#import "MixinObjC.h"
2#import <Mixin/Mixin-Swift.h>
3
4@implementation MixinObjC
5
6 + (NSString *)sayHi
7{
8 return @"Hi, I'm from Objective-C";
9}
10
11+ (void)callSwift
12{
13 NSLog(@"hello from MixinSwift: %@", MixinSwift.sayHi);
14}
15
16@end
由于 Jazzy 无法直接生成混编项目的文档,这里需要通过 SourceKitten
分别将 Swift 和 ObjC 的代码注释转成 json 的中间格式,才能生存完整的文档。生成命令如下:
1lib_name=Mixin
2output="public/docs/$lib_name"
3swift_doc="$output/$lib_name-swift-doc.json"
4objc_doc="$output/$lib_name-objc-doc.json"
5
6lib_path=$(pwd)/Pods/$lib_name/$lib_name/Classes
7umbrella_header="$lib_path/$lib_name-umbrella.h"
8sdk_path=`xcrun --show-sdk-path --sdk iphonesimulator`
9
10sourcekitten doc --objc $umbrella_header \
11 -- -x objective-c -isysroot $sdk_path \
12 -I $lib_path \
13 -fmodules > $objc_doc
14
15sourcekitten doc -- -project Pods/Pods.xcodeproj -target Mixin > $swift_doc
16
17jazzy -o $output --sourcekitten-sourcefile $swift_doc,$objc_doc
文档如下:
依赖库类型判断
由于不同类型的依赖库,其生成文档的脚本有所不同,我们还需要判断每个依赖库类型,是纯 ObjC、纯 Swift 还是混编类型。解决方式就是对 Source 目录下的文件类型进行 count 以判断依赖库类型:
1swift_count=`find $lib_path -maxdepth 6 -type f -name '*.swift' | wc -l`
2objc_count=`find $lib_path -maxdepth 6 -type f -name '*.m' | wc -l`
3
4# file state, 0: only objc, 1: only swift, 2: swift & objc
5lib_state=0
6if [[ $swift_count -ge 1 && $objc_count -ge 1 ]]; then
7 lib_state=2
8elif [[ $swift_count -eq 0 && $objc_count -ge 1 ]]; then
9 lib_state=0
10elif [[ $swift_count -ge 1 && $objc_count -eq 0 ]]; then
11 lib_state=1
12fi
静态文档的部署
我们使用是 Github Page 来进行文档部署,特别简单仅需在 repo 的设置页指定文档类型就可以了。剩下的就是提交代码,Git 会自动触发编译。
更多介绍请查看 Github Page 说明。
最后,完整 Demo 的托管地址为:Cocoa-Documentation-Example。
Git page 文档地址:https://looseyi.github.io/Cocoa-Documentation-Example,这个地址是 Github 自动生成的。效果如下:
One More Thing …
尽管我们当前的方案可以正确的生成文档,但是其实还可以更进一步。
当前的文档生成是基于 project
的方式,而我们完全可以针对每一个文件生成一份 json 数据,最后在把它们全部粘一起。命令的话 SourceKitten 都准备好了:
Swift 文件解析
1$ sourcekitten doc --single-file $input_file -- -j4 $input_file >> $temp_outout
ObjC .h 文件解析
1$ sourcekitten doc --objc \
2 --single-file $input_file \
3 -- -x objective-c \
4 -isysroot $sdk_path \
5 -I $lib_path -fmodules >> $temp_outout
通过这种方式,既不不需要配置 project
判断依赖库类型,也省去了查找找 umbrella header 的麻烦。
完整脚本传送门:docs_deploy.sh
总结
- 多依赖库的文档生成还是比较简单的,感觉最难的还是读懂 Jazzy + SourceKitten 的文档和参数的配置。
- 思路是充分利用了 CocoaPods 为我们搭好的环境,在其之上就可以轻松生成文档,主题可定制哦。
- 倒腾过个人博客的同学,对于 Github Page 和文档的部署应该很熟悉,免费的 Github 资源还是要充分利用的。
知识点问题梳理
这里罗列了四个问题用来考察你是否已经掌握了这篇文章,如果没有建议你加入收藏再次阅读:
Jazzy
对 API 的控制范围有几种选择?- 对于文中所采用的判断依赖库语言类型的方法是什么,还有更好的方式吗?
- ObjC 的
umbrella header
是从哪里获取的? - 扩展:
SourceKitten
所生成的 JSON 结构包括哪些字段?