Skip to content

Commit 6c26a02

Browse files
committed
update: docs
1 parent 164ed3b commit 6c26a02

File tree

4 files changed

+331
-4
lines changed

4 files changed

+331
-4
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
node_modules
22

33
.vitepress/cache
4-
.vitepress/dist
4+
.vitepress/dist
5+
6+
.idea

dev/fudan-kit/api-entity-store.md

Lines changed: 200 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,200 @@
1-
# API、缓存与对象 <Badge type="warning" text="已完成,尚未搬运" />
1+
# API、缓存与实体类
2+
3+
当我们看 Fudan Kit 包的代码结构的时候,目录结构是比较清晰的,每个目录代表一项服务。如校车对应 `Bus`,教务对应 `Course` 等。每个服务都有一个
4+
5+
展开这些目录时,会发现有很多代码文件,被命名为 `*API, *Entity, *Store`,如 `BusAPI.swift`, `BusEntity.swift`, `BusStore.swift`。这三个文件分别代表了 API、实体类和缓存。
6+
7+
它们的关系很直接:API 层负责封装校园网站上提供的各类信息接口,Store 层负责缓存这些信息并向上层业务提供,Entity 负责建模业务中涉及到的各项实体。
8+
9+
## API 层和解析数据常用的工具箱
10+
11+
API 层的核心任务是从校园网站上获取数据,并将其解析为我们需要的格式。这些 API 方法被放置在一个 `enum` 中作为命名空间。以下例说明:
12+
13+
```swift
14+
enum ReservationAPI {
15+
static func getPlaygrounds() async throws -> [Playground]
16+
static func getReservations(playground: Playground, date: Date) async throws -> [Reservation]
17+
}
18+
```
19+
20+
调用时,调用 `ReservationAPI.getPlaygrounds()` 即可获取所有可预约的场馆。
21+
22+
API 层需要解析复杂的 HTML 或 JSON 数据,并将其转换为 Entity 中结构明确的 `struct` 并呈现给用户。
23+
24+
:::info
25+
这部分功能的开发没有固定的模式,需要具体查看对应页面的结构。这个过程中需要频繁用到浏览器开发者工具或者抓包软件,和比较丰富的经验。毕竟我们没有学校官方提供的数据接口。
26+
:::
27+
28+
### 网络请求
29+
30+
:::tip
31+
如果没有 Swift 网络标准库的基础,可以先阅读 [网络基本 API](/dev/danxi-kit/api) 一节。
32+
:::
33+
34+
我们为校园服务设置了专门的 `URLSession`
35+
36+
```swift
37+
extension URLSession {
38+
static let campusSession = URLSession(configuration: .default)
39+
}
40+
```
41+
42+
并封装了两个常用的构造 `URLRequest` 的方法:
43+
44+
```swift
45+
func constructRequest(_ url: URL, payload: Data? = nil, method: String? = nil) -> URLRequest
46+
func constructFormRequest(_ url: URL, method: String = "POST", form: [String: String]) -> URLRequest
47+
```
48+
49+
这些方法会设置请求的 `User-Agent` 字段,后者还会自动将字典转换为 URL-encoded form,用起来比较方便。
50+
51+
### HTML 解析
52+
53+
对于有写爬虫经验的同学,Python 中的 `BeautifulSoup` 应该不陌生。在 Swift 中,我们使用 `SwiftSoup` 来解析 HTML。这里建议读一读它们的官方 [入门文档](https://scinfu.github.io/SwiftSoup/),还是比较简短的。
54+
55+
下面我们来看一个使用 SwiftSoup 解析 HTML 的例子:
56+
57+
```swift
58+
let html = """
59+
<html><body>
60+
<p class='message'>SwiftSoup is powerful!</p>
61+
<p class='message'>Parsing HTML in Swift</p>
62+
</body></html>
63+
"""
64+
65+
let document: Document = try SwiftSoup.parse(html)
66+
let messages: Elements = try document.select("p.message")
67+
for message in messages {
68+
// message: Element
69+
let text: String = try message.text()
70+
}
71+
```
72+
73+
这里我们为了让大家理解,明确标注了各个变量的类型。可以看到 SwiftSoup 中的核心概念包括 `Document``Elements``Element`。我们可以用 CSS 选择器等各种方式检索元素。实际应用中,CSS 选择器通常已经足够好用了。
74+
75+
为了减少重复代码,我们封装了几个常用方法,可以应付多数情况:
76+
77+
```swift
78+
func existHTMLElement(_ data: Data, selector: String) -> Bool
79+
func decodeHTMLDocument(_ data: Data) throws -> Document
80+
func decodeHTMLElement(_ data: Data, selector: String) throws -> Element
81+
func decodeHTMLElementList(_ data: Data, selector: String) throws -> Elements
82+
```
83+
84+
它们的名字足够直接,因此就不进一步解释了。
85+
86+
### JSON 解析
87+
88+
Swift 内置的 `Codable` 协议可以帮助我们解析 JSON 数据,但是它对类型要求严格,不太适用于复杂的 JSON 结构。因此我们使用 `SwiftyJSON` 来解析 JSON 数据。
89+
90+
`SwiftyJSON` 的使用方法也很简单,我们可以直接将 `Data` 转换为 `JSON` 对象,然后使用下标访问:
91+
92+
```swift
93+
let json = try JSON(data: data)
94+
json["message"].string
95+
json["code"].int
96+
json["subtype"].rawData()
97+
// ...
98+
```
99+
100+
学校提供的大部分 JSON API 有以下格式:
101+
102+
```json
103+
{
104+
"e": 0,
105+
"m": "error message",
106+
"d": {
107+
// actual data
108+
}
109+
}
110+
```
111+
112+
其中 `e` 代表错误码(0即无错误),`m` 代表错误信息,`d` 代表数据。我们封装了以下函数来处理。如果检测到错误会直接抛出。
113+
114+
```swift
115+
func unwrapJSON(_ data: Data) throws -> JSON
116+
```
117+
118+
## Store
119+
120+
Store 层负责将收到的数据缓存起来,避免重复请求。部分 Store 层的数据是存储在磁盘上的,其他则只在内存中存储。
121+
122+
Store 层通常有以下方法(具体名字可能有出入):
123+
- `getCachedData`:获取缓存的数据
124+
- `getRefreshedData`:获取最新的数据,并刷新缓存
125+
- `clearCache`:清除缓存(在用户退出登录时调用)
126+
127+
:::warning
128+
如果存在 Store 层,UI 层不应该绕过 Store 层直接调用 API 层的数据。
129+
:::
130+
131+
## Entity
132+
133+
Entity 结尾的文件会提供该服务中所有涉及到的实体。通常建议先阅读这个文件,它可以帮助你理清这个服务模块的业务逻辑和核心概念。
134+
135+
以场馆预约 `ReservationEntity` 为例:
136+
137+
```swift
138+
struct Playground: Identifiable, Codable, Hashable {
139+
let id: String
140+
let name: String
141+
let campus: String
142+
let category: String
143+
}
144+
145+
struct Reservation: Identifiable, Codable {
146+
let id: UUID
147+
let name: String
148+
let begin, end: Date
149+
let reserved, total: Int
150+
}
151+
```
152+
153+
其中 `Playground` 代表一个可预约的场馆,而 `Reservation` 代表一个可预约的时间段。
154+
155+
:::info
156+
有时服务器返回的信息结构很乱,为了方便处理,我们会在 API 层定义一些辅助结构体,用于解析服务器返回的数据,并将其转换为 Entity 层的实体。
157+
158+
当服务器返回的数据结构需要进一步处理时,如将 `String` 转换为 `Date`、将部分字段重新命名等,我们会在 API 层进行处理,而不是在 Entity 层。我们会将此类结构用 Response 作为命名后缀。参见以下例子:
159+
160+
```swift
161+
// In BusAPI:
162+
struct ScheduleResponse: Codable {
163+
let id: String
164+
let start: String
165+
let end: String
166+
let stime: String
167+
let etime: String
168+
let arrow: String
169+
let holiday: String
170+
}
171+
172+
// In BusEntity
173+
struct Schedule: Identifiable, Codable {
174+
let id: Int
175+
let time: Date
176+
let start, end: String
177+
let holiday: Bool
178+
let bidirectional: Bool
179+
}
180+
```
181+
182+
还有一个场景,是需要将多个服务器返回的数据结构合并为一个。我们同样在 API 层进行处理,以 Builder 作为命名后缀。参见以下例子:
183+
184+
```swift
185+
struct CourseBuilder {
186+
let name, code, teacher, location: String
187+
let weekday: Int
188+
var start, end: Int
189+
var onWeeks: [Int] = []
190+
191+
func build() -> Course {
192+
Course(id: UUID(), name: name, code: code, teacher: teacher, location: location, weekday: weekday - 1, start: start, end: end, onWeeks: onWeeks)
193+
}
194+
}
195+
```
196+
197+
这个例子中,如果有同一个课程跨越几个时间段,如高等数学课在周四第2和第3节课都要上课,那么服务器会返回2节课,我们要将它合并为同一节课,就可以调用 `courseBuilder.build()`
198+
199+
200+
:::

dev/fudan-kit/uis.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88

99
### 单点登录(SSO)的概念
1010

11-
> 这里为了易读性,对 SSO 的机制做了简化,仅在概念上做了介绍,会有一些不那么精确甚至错误的表述。读者如果希望了解完整的 SSO 机制,可以去阅读其他资料。
11+
:::info
12+
这里为了易读性,对 SSO 的机制做了简化,仅在概念上做了介绍,会有一些不那么精确甚至错误的表述。读者如果希望了解完整的 SSO 机制,可以去阅读其他资料。
13+
:::
1214

1315
我们自己做一个小网站时,一般是自己负责认证服务,即用户登录的部分。我们会直接把用户名、(加密后的)密码和其他业务数据一起存到数据库中。对于一个个人网站,乃至一个几人合作开发的小网站,这当然没有问题。
1416

@@ -115,3 +117,16 @@ func authenticateWithResponse(_ request: URLRequest, manualLoginURL: URL? = nil)
115117
在外部服务请求以上 API 时,会进行并发保护。由于 Swift 没有内置的和异步系统兼容的读写锁,我们使用 `Queue` 来做这个。它可能看起来比较复杂,但是还是容易理解的,只需要记住我们刚才提到过的设计目标就可以。
116118

117119
在以上的封装下,业务系统基本不需要考虑 UIS 登录问题,只需要调用 API 然后等着数据返回解析数据就可以了。
120+
121+
:::details AsyncQueue
122+
123+
Swift 的 actor 本质上不支持读写锁。actor 通过完全避免对保护状态的并行读写来保证线程安全,但读写锁允许写者并行地访问保护状态。
124+
125+
因此我们使用 AsyncQueue 这一外部库,它能新建一个任务队列,我们可以向这个任务队列中添加任务,其中
126+
127+
- `AsyncQueue(attributes: [.concurrent])` 创建一个可以并行执行的任务队列
128+
- `addOperation` 添加一个可以并行执行的任务
129+
- `addBarrierOperation` 添加一个会占据队列的任务,该任务执行时,其他任何任务不能执行
130+
131+
可以看到,这个库的设计目标和我们的需求,即读写锁,是一致的。
132+
:::

ops/github.md

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,112 @@
1-
# GitHub 工作流 <Badge type="warning" text="TODO" />
1+
# GitHub 工作流
2+
3+
:::info
4+
读者应熟悉 Git 的基本操作,包括提交、分支管理、rebase、push 等。
5+
:::
6+
7+
GitHub 是一个代码托管网站,你可以将你的项目代码托管在上面。但是如果你只是把它当成一个「代码云盘」在用,那么你就没有值回票价(~~尽管 GitHub 也不收钱~~)。如果善于利用 GitHub 提供的各项功能,就可以把整个工作流放到 GitHub 上。
8+
9+
GitHub 有很多功能,比如 Issue、PR 等等,这些功能可以帮助你更好地管理你的项目。我们会简单介绍一下 Issue 和 PR,以及它们在我们项目工作流中的使用。
10+
11+
:::details
12+
GitHub 有一些很实用的功能,但是我们这个项目通常用不到,但旦夕的其他项目时常用到。它们包括:
13+
14+
- Actions。这是 GitHub 提供的 CI 服务。我们项目的 CI 服务是在 Xcode Cloud 上的。
15+
- Releases。这是 GitHub 提供的版本发布功能。我们项目的版本发布是直接在 App Store 上的。
16+
- Milestones。这是 GitHub 提供的里程碑功能,用来记录下个版本发布前还有哪些工作需要完成。~~我们没有项目经理,通常采用意识流开发,发版前很少有什么一定要完成的工作,也没有工作规划,所以不需要里程碑。~~
17+
:::
18+
19+
## Issue 管理
20+
21+
Issue,英文原意是「问题」,在 GitHub 上,它可以用来记录项目中的问题、任务、需求等等。以下是几个 Issue 的例子:
22+
23+
- 首页校车时刻表显示的时间错误
24+
- 添加校园卡余额查询功能
25+
- 优化树洞首页加载速度
26+
27+
可以看到,Issue 可以充当一个「备忘录」,记录项目中接下来要做的工作。
28+
29+
:::tip
30+
我们推荐所有想到的新功能、遇到的 bug 在经过内部讨论后都应该记录到 Issue 中,以免自己忘记。
31+
:::
32+
33+
Issue 创建以后自带一个「评论区」板块,任何人都可以在这里发言。
34+
35+
:::tip
36+
**我们推荐将开发过程中遇到的重要问题或阶段性成果记录到 Issue 中,以免丢失。** 此类内容包括但不限于:
37+
38+
- 修复 bug 中找到的重要线索
39+
- 重构代码的思路
40+
- 找到的有用的第三方库
41+
:::
42+
43+
由于 Issue 里的回复周期较长,不推荐在 Issue 里进行实时讨论。如果有问题需要讨论,可以在 Issue 里提出,然后在群里进行展开讨论,并将讨论结果记录在 Issue 里。
44+
45+
~~GitHub 别人一回复就给我发邮件的毛病真的该改改了,我的收件箱都要塞满了。~~
46+
47+
Issue 可以 Close,Close 代表这个 Issue 已经完成了。Close as not planned 代表这个 Issue 不会被做了,可以直接 Close。Close as completed 代表这个 Issue 已经完成了,我们更推荐用链接 PR(见后文)的方式来 Close 此类 Issue。
48+
49+
### Issue 类别和标签
50+
51+
GitHub 里可以给 Issue 添加类别和标签,用于分类。
52+
53+
类别包括:
54+
55+
- Feature:新功能。
56+
- Bug:~~what can I say?~~
57+
- Task:说实话我也不知道这是什么,我们目前把这个当垃圾桶。
58+
59+
标签有很多,挑一些常用的出来说:
60+
61+
- `good first issue`:这个 Issue 比较简单适合新手。
62+
- `help wanted`:这个 Issue 比较复杂,没人知道怎么做,需要帮助。
63+
- `waiting for upstream`:这个 Issue 等待上游项目的更新(上游项目包括后端和依赖库)。
64+
- `internal`:这个 Issue 是内部提出的,和用户没关系,例如为树洞管理员提供的功能等。
65+
66+
### Assign
67+
68+
Issue 可以指派给某个人,这个人就是 Issue 的「负责人」。负责人负责跟进 Issue 的进展,以及在 Issue 里回复问题。
69+
70+
:::warning 重要
71+
Issue 指派是记录工作分配的核心方式。**为了避免无人负责和重复工作**,需要注意以下几点:
72+
- Issue 的负责人应该积极开发手头的 Issue,不要让 Issue 一直处于「待定」状态。
73+
- Issue 的负责人应该在不能完成 Issue 的时候及时告知,并在 GitHub 里 unassign,以便其他人接手。
74+
- 在 Issue 列表里挑选工作时尽量挑选自己负责的或无人负责的 Issue。对于有 Assignee 的 Issue,**务必与其协商后再接手**,避免重复工作。
75+
- 当自己被指定开发,或自己希望开发一个功能时,**一定要在 GitHub 里 assign 自己,避免别人重复工作**
76+
:::
77+
78+
## PR 流程
79+
80+
PR,全称 Pull Request,是 GitHub 上的一个功能,用于提交代码。当你完成了一些工作,想要提交这些工作的时候,就用 PR。PR 提交后,会有人审核你的工作是否符合项目标准并决定是否接受它。如果接受它,那么它就会被纳入项目中,成为项目的一部分,称为 Merge。
81+
82+
:::warning 重要
83+
我们的项目不能直接在 `main` 分支上提交。当你在开发一个功能时,你应该新建一个分支,在这个分支上开发,开发完成后提交 PR。
84+
:::
85+
86+
### 链接 Issue
87+
88+
当你的 PR 和一个 Issue 直接相关时,你应该把它们链接起来。链接的方式是在 PR 的描述里写 `close #123`,参见 [GitHub 文档](https://docs.github.com/zh/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue)
89+
90+
当 PR 和 Issue 链接起来后,当 PR 被 Merge 时,Issue 会被自动 Close。这样就会避免已经被完成的 Issue 仍然挂在 Issue 列表里。
91+
92+
:::warning 重要
93+
如果 PR 有对应的 Issue 存在,**一定要将 PR 和 Issue 关联起来。** 这样可以更好地追踪工作进度,避免遗漏。
94+
:::
95+
96+
### Review
97+
98+
Review 是 PR 的重要环节。在 Review 中,你的代码会被其他人检查,他们会提出问题、建议,或者直接通过你的 PR。你可以在提交 PR 时点击右侧的 Reviewer 面板请求项目负责人来 Review 你的代码。
99+
100+
Review 时,其他人会在你的代码上直接提出评论,你可以在这些评论中~~和他们吵架~~回复,或按照评论修改你的代码。当修改完成后,将相关的评论标记为 Resolved,然后重新请求 Review。
101+
102+
如果 Review 最终没有问题,它会被 Approve,这时你就可以点击 Merge 来合并你的代码了。
103+
104+
## 一个标准的工作流程
105+
106+
1. 去 Issue 列表里选择一个 Issue,或者新开一个 Issue
107+
2. 在 Issue 里 Assign 自己
108+
3. 新建一个分支,开始写代码
109+
4. 写好代码后,push 到 GitHub
110+
5. 提交 PR 并请求别人 Review
111+
6. 根据别人的 Review 修改代码,再次 push
112+
7. Review 通过后,Merge PR

0 commit comments

Comments
 (0)