ArkTS 流式响应与UI布局
- 背景说明
接到任务,要做一款很牛逼的应用,具体有多牛逼,HarmonyOS Plus AI。有点类似于 ChatGPT,能进行问答交互,接入专门的大模型接口,快速生成相关领域知识。

- 流式响应
- 核心功能用户和应用交互,需要实现类似打字机输出方式的流式返回,而不是等待很久,一次性显示回复信息。这就要用到了 SSE 类型的网络请求。
- SSE(Server-Sent Events)是一种基于 HTTP 协议的协议,允许服务器向客户端推送事件。这意味着客户端不再需要不断地向服务器请求数据,服务器可以主动将数据推送给客户端,并且只支持服务器到客户端的单向通信。
- 工作原理: 客户端通过普通的 HTTP 请求连接到服务器,并通过特定的 HTTP 头信息告知服务器它希望保持连接以便接收实时数据。服务器接收到这个请求后,保持连接不断开,并周期性地向客户端发送消息。
请求响应示例:
- 前后端约定传输数据格式,根据每条数据的特定标识符获取数据,如:event=message,根据特定的标识符判断是否为结尾数据,如:event=message_end。
- 前端并根据对应后端返回响应数据的格式,解析 json 字符串,提取回复的文本进行拼接。
- ArkTS 流式响应
- 从@kit.NetworkKit中导入 http 命名空间。
TypeScript import { http } from '@kit.NetworkKit';
|
- 调用createHttp()方法,创建一个HttpRequest对象。
TypeScript let httpRequest = http.createHttp();
|
- 调用on()方法,可以根据业务需要订阅HTTP响应头事件、HTTP流式响应数据接收事件、HTTP流式响应数据接收进度事件和HTTP流式响应数据接收完毕事件。
TypeScript // 用于订阅HTTP流式响应数据接收进度事件 httpClient.on('dataReceiveProgress', (data: Data) => { console.log(TAG, "dataReceiveProgress receiveSize:" + data.receiveSize + ", totalSize:" + data.totalSize); }); // 用于订阅HTTP响应头事件 httpRequest.on('headersReceive', (header: Object) => { console.info('header: ' + JSON.stringify(header)); }); // 用于订阅HTTP流式响应数据接收事件 httpRequest.on('dataReceive', (data: ArrayBuffer) => { }); // 用于订阅HTTP流式响应数据接收完毕事件 httpRequest.on('dataEnd', () => { console.info('No more data in response, data receive end'); });
|
- 调用该对象的requestInStream()方法,传入http请求的url地址和可选参数,发起网络请求。
TypeScript let httpRequestOptions: http.HttpRequestOptions = { method: http.RequestMethod.POST, header: { "Content-Type": 'application/json', "Authorization": Bearer ${AppStorage.get<string>('docToken')} }, extraData: JSON.stringify({ query: this.prompt, inputs: new Map(), response_mode: "streaming", user:'mythcsj' }) } httpRequest.requestInStream(url, httpRequestOptions).then((data: number) => { // …… });
|
TypeScript httpRequest.on('dataReceive', (data: ArrayBuffer) => { const decoder = util.TextDecoder.create( "utf-8" ); const str = decoder.decodeWithStream(new Uint8Array(data)); let resultArr: Array<string> = str.split('\n'); let answer = '' for (const result of resultArr) { if (result == '' || result.indexOf('event: ping') >= 0) { break; } if (result.indexOf('*') >= 0) { break; } const messageMeta = JSON.parse(result.split('data:')[1]) as MessageMeta if (messageMeta.event == 'message') { answer += messageMeta.answer AppStorage.setOrCreate<string>('currentTaskId', messageMeta.task_id); } else if (messageMeta.event == 'message_end') { AppStorage.setOrCreate<string>('currentMessageId', messageMeta.message_id); AppStorage.setOrCreate<string>('currentConversationId', messageMeta.conversation_id); const retriever_resources = messageMeta.metadata.retriever_resources for (const retrieverResource of retriever_resources) { answer += "\n\n相关文献:【" + retrieverResource.dataset_name + "】 (" + retrieverResource.content + ")" } } } console.log( resultArr:${resultArr} ) return answer; });
|
TypeScript // 取消订阅HTTP响应头事件 httpRequest.off('headersReceive'); // 取消订阅HTTP流式响应数据接收事件 httpRequest.off('dataReceive'); // 取消订阅HTTP流式响应数据接收进度事件 httpRequest.off('dataReceiveProgress'); // 取消订阅HTTP流式响应数据接收完毕事件 httpRequest.off('dataEnd');
|
- 当该请求使用完毕时,调用destroy()方法主动销毁。
HTML // 请求使用完毕,调用destroy方法主动销毁 httpRequest.destroy();
|
- ArkUI 流式响应
TypeScript export interface Chat { groupId: string type: ChatType, content: string } …… @State arrayList: Chat[] = []
|
- 在响应完成之后才有会状态响应码,可以用 Promise 封装返回状态,用于页面逻辑处理
TypeScript getHttpStreamResult(callback: (result: string) => void) { return new Promise<number>((resolve, reject) => { this.httpClient.on('dataReceive', (data: ArrayBuffer) => { …… callback(resultStr); }); let httpRequestOptions: http.HttpRequestOptions = { method: http.RequestMethod.POST, header: { "Content-Type": 'application/json', "Authorization": `Bearer ${AppStorage.get<string>('docToken')}` }, extraData: JSON.stringify({ query: this.prompt, inputs: new Map(), response_mode: "streaming", user:'mythcsj' }) } const httpStatus = await this.httpClient.requestInStream(url,requestOption); }) }
|
onClick 事件调用流式响应接口
TypeScript // 缓存提问框文本 const query = this.query if (query == '') { promptAction.showToast({ message: "请输入问题" }) return } // 清空提问框文本 this.query = '' this.send = true this.currentGroupId = UUID(); // 记录用户提问 this.arrayList.push({ groupId: this.currentGroupId, type: ChatType.USER, content: query }) // 拼接接口返回数据 this.answer = '' const doc = new DocHttp(query) const httpStatus = await doc.getHttpStreamResult((result: string) => { this.answer += result }) // 请求完毕后,记录 AI 对话记录 if (httpStatus == 200) { this.arrayList.push({ groupId: this.currentGroupId, type: ChatType.BOT, content: this.answer }) this.currentGroupId = '' this.send = false }
|
自定义对话组件
TypeScript @Component export struct BotChat { @Prop answer: string = '' build() { ListItem() { Row({ space: 10 }) { Image($r('app.media.logo')) .width(80) .height(80) .borderRadius(40) .objectFit(ImageFit.Contain) Text(this.answer).chatStyle() }.alignItems(VerticalAlign.Top) }.width('100%') } } @Component export struct UserChat { @Prop query: string = '' build() { ListItem() { Row({ space: 10 }) { Text(this.query).chatStyle() Image($r('app.media.user')).chatPhotoStyle() }.alignItems(VerticalAlign.Top) }.width('100%') } } @Extend(Image) function chatPhotoStyle() { .width(80) .height(80) .borderRadius(40) .borderWidth(1) .borderColor(Color.Red) .fillColor(Color.Red) .objectFit(ImageFit.Contain) } @Extend(Text) function chatStyle() { .borderRadius(10) .backgroundColor('#fbebfc') .fontSize(20) .width('70%') .padding(20) }
|
Index.ts
TypeScript @State @Watch('onAnswer') answer: string = '' private scroller: Scroller = new Scroller() // 设置监听变量,流式响应时,滚动条始终在底部 onAnswer() { this.scroller.scrollEdge(Edge.Bottom) } …… // 加载显示对话数据 List({ space: 10, scroller: this.scroller }) { ForEach(this.arrayList, (item: Chat) => { //用户提问 if (item.type == ChatType.USER) { UserChat({ query: item.content }) } //当前 GroupId 等于当前遍历对象,说明是接口响应中,展示响应是文本,否则展示存储好的对话记录 if (this.currentGroupId == item.groupId) { BotChat({ answer: this.answer }) } else if (item.type == ChatType.BOT) { BotChat({ answer: item.content }) } }) }
|
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
评论(0)