场景说明:本文
ManifestHarbor的MH-1421、v17、8 个依赖项、ETag"ohpm-v17"、304 响应与 HiLog 均为受控演示数据。示意图不是 DevEco Studio、真机或真实服务器的测试结果;代码依据公开 Network Kit API 设计,尚未在实际 HarmonyOS SDK 中编译和发起网络请求。
为工具型应用添加一份三方库版本清单,看上去只需要一次 GET。真正令人头疼的反而是第二次请求:服务器返回304 Not Modified,界面拿到的正文通常是空的;产品层却把空正文解释成“当前没有库”。于是已经保存的八条依赖项忽然消失,用户刷新一次反而看不到原来的数据。
这次不从网络性能优化出发,而从“什么内容可以成为新的事实”来设计一个小型清单查看器。ManifestHarbor在应用层维护最近一次通过校验的快照:版本v17、依赖8 项、ETag"ohpm-v17"。一次演示性的条件请求返回304,最终状态是“缓存有效 · 8项”,不是“下载成功 0 项”,更不代表我们实际访问了在线仓库。
一、先决定哪些响应有权改写清单
1. 304 是复用指令,不是另一份清单
HTTP 的 ETag 由服务端定义,客户端可在有本地快照时发送If-None-Match。当服务端判定目标资源未改变,返回 304,应用应继续展示本地已验证的内容。若本地没有对应快照,304 不能凭空生成那八个条目,也不能把空正文保存成新版本。这里要区分“请求成功完成”与“获得新内容”:前者是传输状态,后者需要经过应用自己的数据校验。
我们给本次业务流分四种出口:304 且快照存在,则复用;200 且正文通过结构校验,则替换;200 但正文不合格,则保留旧快照并展示错误;请求超时或断网,则展示“离线快照”并标注无法判断新鲜程度。空白屏和“成功 0 项”都不是合理兜底。还应防止旧请求后到,用代际编号阻止上一轮覆盖用户刚刷出的新版本。
2. 快照不等于缓存命中率
设计文档里的缓存只是一份内存演示对象,不直接宣称已经接入 Preferences、RDB 或磁盘持久化。实际产品如果希望重启后仍可离线展示,必须将正文和对应的 ETag 一起持久化,并保证成对更新;只保存 ETag 而丢失正文,会让下次 304 陷入没有数据可用的状态。数据落盘不是本篇 Network Kit 示例的已实现能力。
二、先给 200 响应加一道人为的完整性门槛
网络返回 JSON,并不等于格式可以信任。清单会影响升级提示、构建兼容建议等信息,不能容许一个只传了半个数组的结果直接替换旧版。我们先写一个纯业务解析器,限定正整数修订号、非空数组、唯一包名及可接受的版本形式。它的正则表达式只是本项目示例规则,不是 ohpm 官方完整包名规范。
interfaceLibRow{name:string;version:string;}interfaceManifestDoc{revision:number;packages:LibRow[];}interfaceManifestSnapshot{doc:ManifestDoc;etag:string;}functionparseManifest(body:string):ManifestDoc|null{try{constvalue=JSON.parse(body)asManifestDoc;if(!Number.isInteger(value.revision)||value.revision<=0)returnnull;if(!Array.isArray(value.packages)||value.packages.length===0)returnnull;constseen=newSet<string>();for(constitemofvalue.packages){if(!item||typeofitem.name!=='string'||typeofitem.version!=='string')returnnull;if(!/^[a-z0-9@/_-]{2,64}$/.test(item.name)||!/^\d+\.\d+\.\d+$/.test(item.version)||seen.has(item.name))returnnull;seen.add(item.name);}returnvalue;}catch(_){returnnull;}}这段校验能阻断空数据、重复包名和显然错误的版本串,却不会证明版本在仓库中真实存在,也没有验证签名。上架工具若用它形成自动发布决策,还需要绑定可信 HTTPS 域名、签名策略、离线清单有效期及依赖来源策略。本文只做清单展示,不做依赖安装或软件安全判定。
三、Network Kit 的一次请求就创建一次句柄
接下来是唯一需要真实系统接口的部分。官方 HTTP 指南明确http.createHttp()创建请求对象、request()发起请求、destroy()在使用结束时清理,并要求声明ohos.permission.INTERNET。示例在一个固定的 HTTPS 地址上演示条件头;那个地址是配置占位符,开发者必须替换为自己的真实服务,不能直接拿去请求。
下面的fetchManifest故意不更新任何 UI,它只传回 304、200 或错误。收到 304 时直接复用已通过校验的快照;200 必须解析为字符串并读取响应头中的 ETag。若服务端没给出可存储的 ETag,就保留旧数据并进入错误态,避免客户端在下次刷新时发送不对应正文的旧标签。
import{http}from'@kit.NetworkKit';interfaceFetchResult{state:'UNCHANGED'|'UPDATED'|'ERROR';snapshot:ManifestSnapshot|null;detail:string;}asyncfunctionfetchManifest(url:string,cached:ManifestSnapshot|null):Promise<FetchResult>{if(!url.startsWith('https://'))return{state:'ERROR',snapshot:cached,detail:'HTTPS required'};constrequest=http.createHttp();try{constheaders:Record<string,string>={'Accept':'application/json'};if(cached)headers['If-None-Match']=cached.etag;constresponse=awaitrequest.request(url,{method:http.RequestMethod.GET,header:headers,connectTimeout:5000,readTimeout:8000});if(response.responseCode===304){returncached?{state:'UNCHANGED',snapshot:cached,detail:'304 cache reused'}:{state:'ERROR',snapshot:null,detail:'304 without cache'};}if(response.responseCode!==200||typeofresponse.result!=='string'){return{state:'ERROR',snapshot:cached,detail:`HTTP${response.responseCode}`};}constparsed=parseManifest(response.result);constmeta=response.headerasRecord<string,string>;consttag=meta['ETag']??meta['etag']??'';if(!parsed||!tag)return{state:'ERROR',snapshot:cached,detail:'invalid response'};return{state:'UPDATED',snapshot:{doc:parsed,etag:tag},detail:'200 validated'};}catch(_){return{state:'ERROR',snapshot:cached,detail:'network unavailable'};}finally{request.destroy();}}response.header的字段名是否被代理重写需要实际环境验证,因此这里展示最常见的两种大小写,不宣称覆盖所有网关。为了避免随意跳转到非可信主机,正式项目应把 URL 固定在配置白名单内;仅用startsWith('https://')只能校验协议前缀,不能抵御恶意但合法的 HTTPS 地址。令牌、登录 Cookie 和签名信息也不应输出到 HiLog。
四、迟到的 200,不能覆盖更新的 304
在应用页面里,用户可以连续刷新。第一轮请求慢,第二轮请求先返回,随后第一轮又传来一份旧的 v16 内容;如果按完成时间直接赋值,最终页面会倒退。Network Kit 不替业务理解“哪次点击是最新意图”。为此我们用一个requestEpoch,将结果提交绑定到本次点击;页面离开时令牌加一,让迟到的回调即使抵达也不能继续改界面。
代码里的mock304是配图所用的受控输入,目的只是表现分支,不是伪装发起了网络请求。实际接入时应把useMock改为false并配置自有服务;自动化测试也应分别检查 304 无缓存、200 格式损坏和超时等分支。
@Entry@Componentstruct ManifestPage{@Stateprivatestatus:string='缓存有效 · 8项';@Stateprivatesource:string='304复用(演示)';@Stateprivaterevision:string='v17';privateepoch:number=0;privatealive:boolean=true;privatecached:ManifestSnapshot={doc:{revision:17,packages:[{name:'@demo/core',version:'1.2.0'},{name:'@demo/ui',version:'2.0.1'},{name:'@demo/log',version:'1.4.0'},{name:'@demo/net',version:'3.1.0'},{name:'@demo/store',version:'2.3.0'},{name:'@demo/text',version:'1.0.3'},{name:'@demo/image',version:'2.1.1'},{name:'@demo/check',version:'1.6.2'}]},etag:'"ohpm-v17"'};privateasyncrefresh(useMock:boolean=true):Promise<void>{constticket=++this.epoch;constresult:FetchResult=useMock?{state:'UNCHANGED',snapshot:this.cached,detail:'304 cache reused (mock)'}:awaitfetchManifest('https://example.invalid/manifest.json',this.cached);if(!this.alive||ticket!==this.epoch)return;if(result.snapshot)this.cached=result.snapshot;this.revision=`v${this.cached.doc.revision}`;this.status=result.state==='ERROR'?`离线快照 ·${this.cached.doc.packages.length}项`:`缓存有效 ·${this.cached.doc.packages.length}项`;this.source=result.state==='UNCHANGED'?'304复用(演示)':result.detail;}aboutToAppear():void{this.alive=true;}aboutToDisappear():void{this.alive=false;this.epoch++;}build(){Column({space:18}){Text('ManifestHarbor · MH-1421').fontSize(22)Text(`${this.revision}·${this.status}`).fontSize(18)Text(`ETag:${this.cached.etag}`)Text(this.source)Button('条件请求演示').onClick(()=>{this.refresh(true);})Text('示意数据,未连接三方库仓库').fontSize(12)}.padding(24)}}这里保持cached为内存对象,是为了把视图逻辑聚焦在“提交资格”上。epoch在组件退出时会失效,但它不负责中断正在进行的 TCP 传输;网络对象在fetchManifest的finally中销毁,页面也可以再设计主动取消通道。正式接入数据落盘要先写入新的快照和配套 ETag,再一次性切换业务指针,否则失败时容易出现“新标签配旧正文”的破坏性组合。
五、把“复用”放在可见位置,避免误导用户
ManifestHarbor的演示屏幕固定使用10:24,状态栏显示 5G、Wi-Fi、信号、电量86%。页头显示任务MH-1421,当前修订v17、依赖8项、缓存标记"ohpm-v17";右侧小标签写“304复用(演示)”,主体状态为“缓存有效 · 8项”,下方有八条虚拟三方库。HiLog 中的示意记录统一为MH-1421 response=304 cache=v17 packages=8。
不能把这条状态写成“最新已同步”。304 只代表服务器对某个条件请求作出未修改响应,取决于服务器缓存语义和中间代理行为;断网时的回退更不表示清单已经经过远程重验证。如果想给用户信心,界面可以另外展示“上次验证时间”,但本篇故意不填真实时间,避免演示数据看起来像线上追踪记录。
六、交付前至少检查六个失败出口
第一种是没有快照却收到 304,必须显示错误而非空列表;第二种是服务端返回 200 但 JSON 结构不完整,继续展示 v17 并提示校验失败;第三种是同名依赖重复,不能默默取后一个;第四种是两个刷新请求乱序完成,只允许最新代际提交;第五种是页面退出后网络才返回,不触发已销毁页面状态变更;第六种是超时或关闭网络,仍能阅读已有的八条演示数据,但应明确标记“离线”。这些是建议的验收清单,不是本文已经在设备上执行的测试结果。
还有两个上线前不可省略的条件。其一是网络权限,工程需按应用使用情况在配置文件中声明ohos.permission.INTERNET,而不是靠代码导入自动获取。其二是可信来源,网络层必须限制域名并按应用安全策略处理证书与重定向;不要为了“离线可用”静默接受证书错误,也不要把服务端返回的 ETag 当成签名或来源认证。
回到最开始的问题:304 的空正文不是一份空清单。这个很小的区分决定了缓存机制是保护用户工作进度,还是在网络重验证时主动清除已有信息。把 HTTP 响应、结构校验、快照提交和视图提示分成四层,三方库清单才不会被一次看似成功的刷新抹掉。
官方接口参考:HarmonyOS HTTP 数据请求指南、Network Kit HTTP 接口。本文不提供实际网络测速、在线依赖校验或应用市场审核结论。