我们在前文 《Cocoa 代码注释与文档生成》 中详细介绍了如何为 Swift & ObjC 的代码编写符合规范的注释,以及使用 Jazzy 来生成项目文档。 今天我们来尝试一下,如何一键生成多个私有库的文档,并将其部署到 Github page 或者 Gitlab page 上。
随着公司项目的迭代,一般都会沉淀出多个私有库。如果这些私有库可以够提供统一的文档查询和预览服务,那将有助于团队中的新成员快速了解业务。
作者所在的公司就维护者 20 多个的私有库,同时这些项目的代码注释完整度不一,注释的内容也参差不齐。如果我们可以通过这个在线文档,不仅可以提供快速的 API 查阅能力,也可以更好的监督和规范项目。
我们先来简单分析一下要实现这个想法 💡需要做哪些事情。
明确了我们要解决的问题,剩下的事情就简单了。
就直接使用 shell + Jazzy
+ SourceKitten
将上面步骤串联起来就可以了。Jazzy 之前介绍过了,一起看看 SourceKitten
:
SourceKitten
An adorable little framework and command line tool for interacting with SourceKit.
Sourcekitten 是基于 Apple 的 SourceKit 封装的命令行工具,SourceKitten 链接并与 sourcekitd.framework 通信以解析 Swift AST 树,最终提取 Swift 或 ObjC 文件的类结构和方法等。
SourceKit
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。
<div class="content-wrapper"> ... <article class="main-content"> <section class="section"> <div class="section-content top-matter"> <h3 id='authors' class='heading'>业务库</h3> </div> </section> <section class="section"> <div class="section-content"> <div class="task-group"> <ul class="item-container"> <li>token-business</li> </ul> </div> </div> </section> <div class="section-content top-matter"> <h3 id='authors' class='heading'>基础库</h3> </div> <section class="section"> <div class="section-content"> <div class="task-group"> <ul class="item-container"> <li>token-base</li> </ul> </div> </div> </section> </article> </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
目录,逐一生成文档并将其输出到一个指定目录就可以了。
想法是美好的,现实是残酷的。在实际操作起来发现并没有那么简单。让我们开启踩坑之旅吧!
之前在 《Cocoa 代码注释与文档生成》 中介绍的 Swift 的文档生成都是基于该项目的 project
工程或者是 SwiftPM 配置来完成。好在 Pod 也为我们生成对应的 project
,我们仅需通过 --build-tool-arguments
来指定 project
和 target
就可以了。
从零开始,我们先新建一个 Demo.xcodeproj 并配置如下 Podfile:
target 'Demo' do pod 'SnapKit' pod 'AFNetworking' end 复制代码
调用 Jazzy 生成 Swift 库 SnapKit
的文档:
$ bundle exec jazzy -o docs/SnapKit \ --build-tool-arguments -project,Pods/Pods.xcodeproj,-target,SnapKit 复制代码
通过 -o
将结果输出到 docs/SnapKit
目录下,执行后输出结果如下:
Running xcodebuild Parsing Constraint.swift (1/34) ... Parsing UILayoutSupport+Extensions.swift (34/34) `ConstraintLayoutSupport` has no USR. ... 9% documentation coverage with 239 undocumented symbols included 264 public or open symbols skipped 81 private, fileprivate, or internal symbols (use `--min-acl` to specify a different minimum ACL) building site building search index jam 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
文件。
相比 Swift,Objc 的依赖库需要多处理 umbrella header
的问题。先看 AFNetworking 的文档生成命令:
$ lib_name=AFNetworking lib_path=$(pwd)/Pods/$lib_name umbrella_header="$lib_path/$lib_name/$lib_name-umbrella.h" sdk_path=`xcrun --show-sdk-path --sdk iphonesimulator` bundle exec jazzy -o docs/$lib_name \ --objc \ --sdk iphoneos \ --build-tool-arguments \ --objc,$umbrella_header,--,-x,objective-c,-isysroot,$sdk_path,-I,$lib_path 复制代码
第一个是需要指定 --objc
,因为 Jazzy 默认解析 Swift 项目。
再来看 --build-tool-arguments
后跟的几个参数:
--objc
是通知 SourceKitten 我要解析的是 Objc 的头文件,后面紧跟的为依赖库的 umbrella headerxcodebuild
或 swift build
xcodebuild
或 swift build
我要编译 ObjC 啦在 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 帮我们自动生成了。
细心的同学从 AFNetworking 的文档生成命令中能发现,AFNetworking-umbrella.h
的位置是在源码的文件夹下。如果直接指定为 Target Support Files
下的 umbrella header 文件是无法生成文档的。我们需要把它复制到源代码在同层目录下。
那么问题来了:如何正确的获取源码所在目录。
首先想到的是和通过 .podspec
文件就能准确拿到 Source 目录。不过比较难实现,我们只能拿到的是 Local Podspecs
下的 .podspec
文件,否则需要在 pod install
时才能获取到。但是这么做需要修改 Podfile 也比较麻烦。
选择简单粗暴的方式,直接列出可能出现的 Source 路径:
# /A/Classes/... # /A/src/a/... # /A/A/Classe/... # /A/A/Classes/... # /A/A/Source/.. # /A/A/Sources/.. # /A/Source/A/... # /A/Sources/A/... # /A/Source/... # /A/A/.. # /A/... # libextobjc/extobjc 复制代码
有用 Classes
、 Source
、Sources
、src
等等,情况五花八门,逐一匹配就可以了。
这么做是可以覆盖大部分的情况,但是仍然发现部分私有库生成的文档缺失甚至是空的。最终发现的问题是:clang 没有递归处理多级目录的文件,这里应该是参数没有正确设置,查看了 Clang 手册 感觉就是 -I
参数,不过也没有生效,有了解的同学求指点。
咋办,先暴力解决:
find $lib_path -type f ! -regex '*.\(h\|m\|swift\)' \ ! -name '*.json' \ ! -name '*.pdf' \ -exec mv -i {} $lib_path \; 复制代码
将子目录下文件全部移到 framework 源码目录下,再通过 Jazzy 来生成文档,算是暂时解决问题了。
然而 AFNetworking 的文档依旧不是完整的,不过属于另外一种情况。目录如下,大家可以 🤔 一下:
对 Swift & ObjC 混编的依赖库本身是不提倡的,虽然在实际开发过程中无法避免。
为了测试混编库的文档生成,这里新建一个 Pod 库:Mixin,添加了 MixinSwift 和 MixinObjC 两个类:
/// Test Swfit Class import Objective-C's property public class MixinSwift: NSObject { /// say hello from Swift @objc public static let sayHi: String = "Hi, I'm from Swift" /// call Objective-C say Hi @objc public class func callObjC() { print("hello from MixinObjc: \(MixinObjC.sayHi)") } } 复制代码
#import "MixinObjC.h" #import <Mixin/Mixin-Swift.h> @implementation MixinObjC + (NSString *)sayHi { return @"Hi, I'm from Objective-C"; } + (void)callSwift { NSLog(@"hello from MixinSwift: %@", MixinSwift.sayHi); } @end 复制代码
由于 Jazzy 无法直接生成混编项目的文档,这里需要通过 SourceKitten
分别将 Swift 和 ObjC 的代码注释转成 json 的中间格式,才能生存完整的文档。生成命令如下:
lib_name=Mixin output="public/docs/$lib_name" swift_doc="$output/$lib_name-swift-doc.json" objc_doc="$output/$lib_name-objc-doc.json" lib_path=$(pwd)/Pods/$lib_name/$lib_name/Classes umbrella_header="$lib_path/$lib_name-umbrella.h" sdk_path=`xcrun --show-sdk-path --sdk iphonesimulator` sourcekitten doc --objc $umbrella_header \ -- -x objective-c -isysroot $sdk_path \ -I $lib_path \ -fmodules > $objc_doc sourcekitten doc -- -project Pods/Pods.xcodeproj -target Mixin > $swift_doc jazzy -o $output --sourcekitten-sourcefile $swift_doc,$objc_doc 复制代码
文档如下:
由于不同类型的依赖库,其生成文档的脚本有所不同,我们还需要判断每个依赖库类型,是纯 ObjC、纯 Swift 还是混编类型。解决方式就是对 Source 目录下的文件类型进行 count 以判断依赖库类型:
swift_count=`find $lib_path -maxdepth 6 -type f -name '*.swift' | wc -l` objc_count=`find $lib_path -maxdepth 6 -type f -name '*.m' | wc -l` # file state, 0: only objc, 1: only swift, 2: swift & objc lib_state=0 if [[ $swift_count -ge 1 && $objc_count -ge 1 ]]; then lib_state=2 elif [[ $swift_count -eq 0 && $objc_count -ge 1 ]]; then lib_state=0 elif [[ $swift_count -ge 1 && $objc_count -eq 0 ]]; then lib_state=1 fi 复制代码
我们使用是 Github Page 来进行文档部署,特别简单仅需在 repo 的设置页指定文档类型就可以了。剩下的就是提交代码,Git 会自动触发编译。
更多介绍请查看 Github Page 说明。
最后,完整 Demo 的托管地址为:Cocoa-Documentation-Example。
Git page 文档地址:https://looseyi.github.io/Cocoa-Documentation-Example,这个地址是 Github 自动生成的。效果如下:
尽管我们当前的方案可以正确的生成文档,但是其实还可以更进一步。
当前的文档生成是基于 project
的方式,而我们完全可以针对每一个文件生成一份 json 数据,最后在把它们全部粘一起。命令的话 SourceKitten 都准备好了:
Swift 文件解析
$ sourcekitten doc --single-file $input_file -- -j4 $input_file >> $temp_outout 复制代码
ObjC .h 文件解析
$ sourcekitten doc --objc \ --single-file $input_file \ -- -x objective-c \ -isysroot $sdk_path \ -I $lib_path -fmodules >> $temp_outout 复制代码
通过这种方式,既不不需要配置 project
判断依赖库类型,也省去了查找找 umbrella header 的麻烦。
完整脚本传送门:docs_deploy.sh
这里罗列了四个问题用来考察你是否已经掌握了这篇文章,如果没有建议你加入**收藏 **再次阅读:
Jazzy
对 API 的控制范围有几种选择?umbrella header
是从哪里获取的?SourceKitten
所生成的 JSON 结构包括哪些字段?