Canvas iOS 工具链:高效构建与协作自动化套件
Canvas iOS 是一套服务于 Instructure 教育科技生态系统的原生 iOS 应用解决方案。它不仅包含 Student、Teacher、Parent 三款核心 App,更提供了一套深度集成于 CI/CD 流程与日常开发的高度自动化工具链。该项目旨在通过智能化的脚本和标准化的流程,将开发人员从繁琐的构建配置、发布流程及代码维护中解放出来,让团队更专注于教育创新体验本身。
功能特性
-
智能化的 PR 构建触发
通过在 PR 描述中添加
builds:指令,可精准控制 CI 仅构建指定的应用(Student/Teacher/Parent),避免资源浪费。支持builds: All一键构建全家桶。 -
一键式应用发布与版本管理
提供完整的脚本化发布流程。单条命令即可完成版本号更新、Release Notes 自动生成、Git Tag 打标及远程 Bitrise 构建触发,实现从代码提交到应用商店的自动化衔接。
-
先进的 PR 构建反馈机制
针对每次 PR 构建,自动生成包含具体 commit 信息、构建时间及可扫码安装的 QR Code 评论。测试人员无需配置环境,扫码即可在真机验证功能。
-
跨平台组件开发支持(Horizon)
为 Canvas Career 体验提供了独立的 Horizon 项目,并内置 Web 文本高亮引擎。该引擎通过 XPath 与字符偏移量双重锚定算法,精准定位并高亮 WKWebView 中的任意文本片段。
-
严格的代码质量与许可合规
集成 SwiftLint 强制代码风格统一。同时,提供了自动化脚本统一管理所有代码文件的 AGPL 许可头,确保开源合规性。
-
一站式资源与配置管理
- 图标自动化:从 Instructure 设计系统自动拉取并生成 SVG/PDF 资源。
- 密钥混淆:构建时通过异或混淆算法将 License Key 等敏感信息编译进 Data Asset。
- 国际化同步:通过 S3 自动导入导出 XLIFF 文件,实现与翻译平台的无缝对接。
安装指南
Canvas iOS 工具链主要为开发者体验及 CI 环境设计,无需最终用户安装。若需在本地运行或调试此项目,请遵循以下步骤:
系统要求
- Xcode: 15.0+
- iOS Target: 15.0+
- 依赖管理: Homebrew, Ruby (3.0+), Node.js (18+), yarn
- 版本控制: Git
分步安装
-
克隆仓库
bashgit clone https://github.com/instructure/canvas-ios.git cd canvas-ios -
安装项目依赖 项目依赖 Swift Package Manager (SPM) 及少量 Node 包用于工具脚本。
bash# 安装 Node 依赖 (用于图标生成、国际化等) yarn install # 可选:安装用于 PDF 转换的 Python 工具 pip3 install cairosvg -
生成 Xcode 项目 项目使用 XcodeGen 生成
.xcodeproj,避免 Git 冲突。bashmake sync -
配置环境变量(用于发布/国际化) 如需执行发布或同步翻译功能,需设置对应的 Bitrise Token 或 AWS 凭证。
bashexport BITRISE_TOKEN="your_bitrise_personal_token" export AWS_ACCESS_KEY_ID="your_aws_key" export AWS_SECRET_ACCESS_KEY="your_aws_secret"
使用说明
场景一:控制 PR 构建范围
开发者在提交 PR 时,在描述中包含特定指令即可决定 CI 构建哪些 App,大幅节省排队时间。
bash
# 仅构建 Student 和 Teacher App
builds: Student, Teacher
# 构建所有 App
builds: All
对应代码:scripts/builds/set-require-builds.sh 解析 BITRISE_GIT_MESSAGE 并设置环境变量。
场景二:触发正式版本发布
Release Manager 执行以下命令,脚本将自动更新版本号、提交分支、打 Tag 并触发 Bitrise 的 App Store 构建流。
bash
# 语法: yarn release <AppName> <Version>
yarn release Student 7.18.0
对应代码:scripts/release/release.sh 通过 curl 调用 Bitrise API。
场景三:在 WKWebView 中高亮文本(Horizon 项目)
Horizon 模块通过 JavaScript 与原生交互,实现类似"笔记标注"的功能。
javascript
// 1. 获取用户当前选中的文本及位置信息
const selection = window.getCurrentTextSelection();
// 2. 设置高亮样式(如背景色、图标)
selection.backgroundColor = "yellow";
selection.borderColor = "red";
// 3. 应用高亮
window.applyHighlights([selection]);
对应代码:Horizon/Horizon/Resources/WebHighlighting.js 提供 applyHighlights 及 getCurrentTextSelection 接口。
场景四:处理应用的本地化字符串
导出最新的英文 XLIFF 源文件并推送至翻译平台 S3 桶。
bash
yarn export-translations
如需拉取翻译好的语言文件并导入 Xcode 工程:
bash
yarn import-translations
对应代码:scripts/translations/export.js 与 import.js。
核心代码
1. PR 构建智能触发引擎
该脚本通过解析 PR 描述中的 builds: 关键字,动态决定 CI Pipeline 需要构建的 App,有效管理 CI 资源。
bash
#!/usr/bin/env bash
# scripts/builds/set-require-builds.sh
# 解析 Bitrise 环境变量中的 Git 提交信息
BUILDS_LINE=$(echo "$BITRISE_GIT_MESSAGE" | grep -i "^builds:" || true)
# 若包含 "student"(不区分大小写),标记需要构建 Student
if [[ $BUILDS_LINE_LOWER == *"student"* ]]; then
envman add --key REQUIRE_STUDENT --value "true"
fi
# 若包含 "all",标记构建所有应用
if [[ $BUILDS_LINE_LOWER == *"all"* ]]; then
envman add --key REQUIRE_PARENT --value "true" &&
envman add --key REQUIRE_TEACHER --value "true" &&
envman add --key REQUIRE_STUDENT --value "true"
fi
2. 通用归档拆分策略
该脚本通过一次性构建包含三个 App 的通用归档文件,再利用 cp 和 PlistBuddy 快速拆分为独立的 .xcarchive,将传统模式下串行构建的 15 分钟压缩至 5 分钟。
bash
#!/bin/zsh
# scripts/archive-all.sh
# 构建包含所有 App 的通用归档
xcodebuild -workspace Canvas.xcworkspace -scheme All -archivePath build/archives/All.xcarchive archive
# 循环为每个 App 生成专属归档
apps=(Student Teacher Parent)
for app in $apps; do
# 拷贝主归档作为模板
cp -r $allArchive $appArchive
# 删除其他 App 的二进制文件
for otherApp in ${(@)apps:#$app}; do
rm -rf $appArchive/Products/Applications/$otherApp.app
done
# 修改归档 Info.plist,指向正确的 ApplicationPath
/usr/libexec/PlistBuddy $appArchive/Info.plist \
-c "Add :ApplicationProperties:ApplicationPath string Applications/$app.app"
done
3. 基于 XPath 与字符偏移量的精准文本锚定
为了解决 WebView 中 DOM 结构动态变化导致高亮丢失的问题,核心模块实现了双锚点策略:既存储 XPath 用于结构定位,又存储字符偏移量用于容灾恢复。
typescript
// WebHighlighting/src/util/RangeAnchor.ts
export class RangeAnchor {
// 将 DOM Range 转换为可序列化的 Selector
toSelector(): RangeSelector | null {
return {
// 存储起始容器的 XPath 路径
startContainer: xpathFromNode(textRange.start.element, this.root),
// 存储在该元素内部的字符偏移量
startOffset: textRange.start.offset,
endContainer: xpathFromNode(textRange.end.element, this.root),
endOffset: textRange.end.offset,
};
}
// 从持久化的 Selector 重建 Range
static fromSelector(root: Element, selector: RangeSelector): RangeAnchor | null {
// 通过 XPath 重新查找节点
const startContainer = nodeFromXPath(selector.startContainer, root);
const endContainer = nodeFromXPath(selector.endContainer, root);
// 重建文本位置并还原高亮
const startPos = TextPosition.fromCharOffset(startContainer, selector.startOffset);
const range = new TextRange(startPos, endPos).toRange();
return new RangeAnchor(root, range);
}
}
```FINISHED
YBgybjVjkKVzJKzJoZoHmrhRJT6DO4glvxXXE2sHYTw=