在 HarmonyOS 上做"看 Git 仓库"的 App:HTTP 请求、懒加载树、平板分栏四个工程点
先说结论:一个"看 Git 仓库"的 App,技术底座其实只有四件事——HTTP 请求、解压、文件树、Markdown 渲染。听起来都不难,但每一步踩进去,都能遇到点鸿蒙特有的"惊喜"。
① 访问 Git 平台 API:连 PATCH 都没有的 http 模块
MarkBook 支持 GitHub / Gitee / GitLab / 云效多个平台。公开仓库的浏览基本不需要登录,走的就是各平台的 REST API。鸿蒙这边用 @ohos.net.http:
import { http } from '@kit.NetworkKit';
async function getJson(url: string): Promise<Record<string, Object>> {
const req = http.createHttp();
try {
const res = await req.request(url, {
method: http.RequestMethod.GET,
header: { 'Accept': 'application/json', 'User-Agent': 'MarkBook' },
readTimeout: 30000
});
const text = res.result as string;
return JSON.parse(text) as Record<string, Object>;
} finally {
req.destroy(); // 复用实例必须手动销毁,否则连接泄漏
}
}
坑有两个。第一个是平台差异:GitHub、Gitee、GitLab、云效的接口形状各不相同(树接口、提交接口、文件 URL 规则都不一样),得在 Service 层做一层归一化适配器,上层只管"给我 owner / repo / 路径"。第二个是 ArkTS 严格模式:res.result 不能当 any 直接用,要 as string 单次转型;接口必须显式定义,不能写未类型化的对象字面量。
顺带一提 API 24 的一个限制:http.RequestMethod 里没有 PATCH,GitLab 某些接口要用 PATCH 时,只能拿 PUT 替代或换接口。
② 自研 DEFLATE 解压:最硬核的一段
Git 平台的响应默认 gzip 压缩,尤其是仓库文件内容,体积能差一个数量级。按说解压是现成的事——但鸿蒙 util 模块在这个 API 级别没有 Inflater 类,DEFLATE 解压得自己实现。
DEFLATE 是什么?一句话:LZ77 长度-距离匹配 + Huffman 编码。压缩流里分三种块:不压缩块、固定 Huffman 块、动态 Huffman 块。动态块要先解出"码长序列"再重建 Huffman 表。位流是 LSB-first(低字节在前),这是最容易写错的地方。
核心骨架长这样(完整实现约两百行,这里是关键思路):
// 逐位读取:DEFLATE 位流从最低位开始
class BitReader {
private pos: number = 0;
private bit: number = 0;
constructor(private buf: Uint8Array) {}
readBits(n: number): number {
let val = 0;
for (let i = 0; i < n; i++) {
const b = this.buf[this.pos];
val |= ((b >> this.bit) & 1) << i;
if (++this.bit === 8) { this.bit = 0; this.pos++; }
}
return val;
}
}
// 动态 Huffman 块:先读码长 → 重建字面/长度、距离两张 Huffman 表
// 然后主循环:读符号 → 256 结束 / 字面量直出 / 长度+距离 → 从滑动窗口回拷
主循环就一句话:读到 0~255 直接输出;读到 256 结束当前块;读到 257~285,再跟一个距离,从"滑动窗口"里回拷一段历史字节拼出来。gzip 格式额外带一个 1f 8b 头和一个 CRC32 尾巴,zlib 又是另一套头(78 开头 + ADLER32)。Git 平台给的是 gzip,所以 1f 8b 头、CRC32 校验都要自己处理。
这段写完,最有成就感的一刻是:一个几十 MB 的仓库文件列表,手机上从"转圈"变成"秒开"。那一刻你才真正理解什么叫"把原理吃透了"。
③ 文件树:展开哪层,才加载哪层
仓库目录是棵树。最朴素的写法是调一次"递归树"接口,把整棵目录拉下来——小仓库没问题,大仓库直接卡死。
MarkBook 的做法是懒加载:列表展开一个目录,才去请求它的子项,并且用 V2 状态管理装饰器管住每一层节点的加载状态:
@ObservedV2
class RepoNode {
@Trace path: string = '';
@Trace type: string = 'blob'; // 'tree' | 'blob'
@Trace children: RepoNode[] = []; // 已加载的子节点
@Trace loaded: boolean = false; // 是否已请求过
get isDir(): boolean { return this.type === 'tree'; }
}
用户点开 src/,才去请求 src/ 的子项;点开 docs/,才加载 docs/。树的每一层互不打扰,滚动也顺。
④ Markdown 渲染 + 代码块复制
README、文档在手机上直接渲染成排版好的页面。这里有两个移动端特有的讲究:一是代码块要能一键复制(长按弹出复制菜单,ArkUI 里用自定义上下文菜单实现);二是长文档别一上来就全量解析,先渲染首屏、滚动再补充,避免大 Markdown 卡 UI。
⑤ 平板分栏:一套布局,两种形态
最新版给平板做了专门适配:平板横屏时 Navigation 走 Split 分栏模式——左栏仓库/文件列表,右栏文件详情,像桌面 IDE 一样对照着看;手机上退回底部导航的 Stack 模式。ArkUI 的 Navigation + NavPathStack 天然支持这两种模式切换:
Navigation(this.pathStack) {
// 左栏列表 + 右栏详情
}
.mode(isWide() ? NavigationMode.Split : NavigationMode.Stack)
判断宽屏用的是 display.getDefaultDisplaySync(),宽度够就分栏、不够就堆叠,一套代码两种形态;折叠屏合上、展开时还要监听尺寸变化动态切换。
ArkTS 严格模式:编译期就把你教做人
写 MarkBook 是第一次全量吃 ArkTS 严格模式,几个"名场面":
catch (e: Error)不能写类型注解,得去掉,函数体里再手动判断;- 不许解构,
const { a } = obj直接编译失败,改成obj.a; - 不许
any、不许未类型化对象字面量,逼着你把数据结构都定义成interface; Column组件没有.maxWidth(),只有.constraintSize({ maxWidth: ... })。
一开始很烦,写完发现:这些约束其实在逼你写出更可维护的代码——接口先定义清楚,数据形状不会写着写着就飘了。
做得好的 & 还能优化的
做得好的:免登录看公开仓库、路径设计合理;自研解压让大仓库也能秒开;树的懒加载 + 分栏适配,手机上轻快、平板上像 IDE;收藏离线看,没网也能查。
还能优化的:超大仓库的树渲染性能还可以再压;Markdown 的渲染覆盖度(表格、流程图等)还有提升空间;跨平台 API 的归一化适配器目前是"够用",离"优雅"还有距离。
最后
很多人问"鸿蒙开发难不难"。我的答案在 MarkBook 里:难的不是语法,是"系统思维"——util 里没有 Inflater,那就自己写一个;http 没有 PATCH,那就换条路走。每解决一个这样的问题,你就比"会用框架"的开发者多懂一层。
下一期,我的第二个鸿蒙 App「速印 QuickMark」也上架了——给照片批量加水印,像素级合成引擎 + AGC 端云一体化,继续拆实战。
📥 MarkBook:华为应用市场搜索「MarkBook」即可下载(免费、免登录)。
关注「APP开发实战派」——这个号既分享我的鸿蒙 App,也拆鸿蒙开发实战。下期拆「速印 QuickMark」。
转载请注明来源:
原文链接:http://hanhan.pro/harmonyos-markbook-read-github-code-self-built-deflate-unzip
作者:Reno