本节摘要:用 http 模块发起第一个真实请求,建立成功/失败双分支的正确处理姿势;把 JSON 解析为带校验的类型实例;封装带超时与统一错误分流的请求模块,为云同步功能打底。
发请求前有一道必过的门:在模块的 module.json5 里声明网络权限,否则真机上请求直接失败。
{ "module": { "requestPermissions": [ { "name": "ohos.permission.INTERNET" } ] } }
这条配置是 system_grant 类型(安装即授予,见下一节分类),没有弹窗、没有运行时申请,但不写就是不通——模拟器上偶尔"侥幸"通过更是让真机调试变成玄学,所以把它当成发请求的第零步。
最小请求代码:
import { http } from '@kit.NetworkKit'; async function fetchRaw(url: string): Promise<string> { const req = http.createHttp(); try { const response = await req.request(url, { method: http.RequestMethod.GET, connectTimeout: 10000, readTimeout: 10000, header: { 'Content-Type': 'application/json' } }); if (response.responseCode === 200) { return response.result as string; } throw new Error(`HTTP ${response.responseCode}`); } finally { req.destroy(); } }
五个细节撑起这段代码的健壮性:connectTimeout 与 readTimeout 必设,默认策略下弱网会挂很久;responseCode 先判再取体,4xx/5xx 不是异常而是"另一种结果",要给用户能懂的话;finally 里 destroy,请求对象不复用不销毁就是资源泄漏;result 的类型按 header 协商(这里断言为字符串);请求对象一次一建,不复用旧对象发第二次请求。
拿到 JSON 字符串后,一步到位的写法是 JSON.parse 再直接用——问题出在"直接用":解析结果本质是 any 形态的裸对象,字段名拼错编译器不吭声,服务端少给一个字段就是运行时 undefined。稳妥做法是"解析加构造",让类型系统接手:
class SyncItem { id: number = 0 title: string = '' serverUpdatedAt: number = 0 static fromJson(raw: string): SyncItem { const obj: Record<string, Object> = JSON.parse(raw) as Record<string, Object>; const item = new SyncItem(); item.id = (obj['id'] as number) ?? 0; item.title = (obj['title'] as string) ?? ''; item.serverUpdatedAt = (obj['serverUpdatedAt'] as number) ?? 0; return item; } }
逐字段断言加缺省值的写法略显笨重,但它把"服务端的不可信输出"隔离在了一个函数里——界面层拿到的永远是完整的类型实例。数组场景同理,先断言数组再逐项 fromJson。ArkTS 禁用 any 的约束在这里恰好是助力:解析层被迫显式,脏数据在边界处就被缺省值或抛错拦下。
散装的 fetchRaw 不配进业务代码。按第 5 章数据模块的分层思路,收拢一个统一入口,把"鉴权头、错误分流、日志"集中:
enum ApiError { Network = 'NETWORK', Server = 'SERVER', Parse = 'PARSE' } class ApiClient { private static token: string = ''; static setToken(t: string): void { ApiClient.token = t; } static async getJson<T>(path: string, parse: (raw: string) => T): Promise<T> { const req = http.createHttp(); try { const resp = await req.request(`https://api.example.com${path}`, { method: http.RequestMethod.GET, connectTimeout: 10000, readTimeout: 10000, header: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${ApiClient.token}` } }); if (resp.responseCode >= 500) { throw new Error(ApiError.Server); } if (resp.responseCode !== 200) { throw new Error(`HTTP ${resp.responseCode}`); } try { return parse(resp.result as string); } catch { throw new Error(ApiError.Parse); } } finally { req.destroy(); } } }
设计要点:泛型加解析回调,调用方指定"我要什么类型、怎么解",模块只管通道;错误分成网络/服务端/解析三类,界面按类给提示(断网提示检查网络、服务端错误提示稍后再试、解析错误是开发问题直接打日志);令牌收在模块内静态字段,页面代码不接触裸 header。调用侧的样子:
const items = await ApiClient.getJson('/sync/items', (raw) => { const arr = JSON.parse(raw) as Record<string, Object>[]; return arr.map(o => SyncItem.fromJson(JSON.stringify(o))); });
背景:给备忘录加"拉取云端示例数据"。操作:找一个可达的测试接口(或本地起一个返回固定 JSON 的服务,模拟器访问宿主机地址即可),用 ApiClient 拉取并解析成 SyncItem 数组,灌进第 5 章的列表。结果与解读按三档展开:顺利时列表出现云端数据,整条链路(权限、请求、解析、状态回流)闭环;把设备断网再点,约十秒后收到 Network 类错误——超时设置的价值此刻可见;把解析回调里的字段名故意拼错,收到 Parse 类错误且日志里能看到原始串——边界隔离让问题定位在一处。变式:加一个 POST 同步接口(method 换 POST、extraData 传 JSON 字符串),把本地新增的备忘推上去;再给 ApiClient 加一个简单的重试(仅对 Server 类错误重试一次),体会"哪些错值得重试"的判断——断网重试无意义,服务端瞬时抖动重一次常常就过。
⚠️ 常见坑:在 UI 回调里直接 await 长请求却不给任何中间状态,用户以为点击无效而连点,请求翻倍。发请求前把按钮置灰或给加载态,是移动端的基本礼貌,也是防止重复提交的最后防线。
💡 关键直觉:请求模块的本质是"把不可信的外部世界翻译成类型安全的内部世界"。头部的鉴权、错误的三分类、解析的显式构造,都是在加固这条翻译边界。
本节要点回顾:
请求能发、数据能收,但"权限"两个字在本章开头的配置里只露了一角。下一节正面讲平台的权限体系与网络与数据的安全纪律。