一个订单查询页,点击“查看物流”调云函数拉取轨迹。本地用 localUrl 连本地云函数调试,响应很快,页面正常渲染。打包上真机,连远程云函数,部分用户反馈页面一直转圈,转了几十秒之后要么白屏,要么弹一个语焉不详的网络错误。日志里 cloudFunction.call 的 catch 确实进了,err.code 是 -1,message 是“网络超时”。你去看云函数控制台的日志,函数本身执行成功,耗时不到一秒,返回的数据也是对的。

问题不在云函数有没有执行,在于客户端的超时窗口和云函数的执行窗口不是同一个时钟。云函数侧的执行、网络往返、AGC网关的排队、以及客户端侧的等待,每一段都有各自的耗时。cloudFunction.call 默认等待 70 秒,但很多业务场景下用户在 3 秒内没看到结果就会认为“卡死了”。你如果用默认值去等一个本来该在 1 秒内返回的请求,等到的“超时”大概率不是服务端慢,而是客户端给了自己一个过长的耐心,然后在用户已经放弃之后才把失败交回来。更隐蔽的情况是,云函数侧确实执行成功了,但因为客户端提前放弃或者网络中断,响应没有回到端侧,用户看到的是失败,服务端已经写入的数据却真实存在。

端侧调用:三行 import,但配置项一个都不能省

云函数的端侧调用入口很干净。import { cloudFunction } from '@kit.CloudFoundationKit',然后 cloudFunction.call({ name, version, timeout, data })。name 是云函数的函数名,version 默认 '$latest',timeout 单位是毫秒,默认 70 * 1000,data 是请求体,没有参数就不传。

typescript

import { cloudFunction } from '@kit.CloudFoundationKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

interface OrderTrackResult {
  orderId: string;
  status: string;
  updatedAt: number;
}

async function fetchOrderTrack(orderId: string): Promise<OrderTrackResult> {
  try {
    const res: cloudFunction.FunctionResult = await cloudFunction.call({
      name: 'fetch-order-track',        // 云侧函数名,必须一致
      version: '$latest',
      timeout: 8 * 1000,                // 显式收窄,不要用默认的 70 秒
      data: { orderId }                 // 参数放在 data 里,键名和云侧对齐
    });
    // res.result 是云函数 callback 或 return 的值
    return res.result as OrderTrackResult;
  } catch (err) {
    const e = err as BusinessError;
    // -1 网络超时;401 鉴权失败;500 服务端异常
    hilog.error(0x0000, 'CloudFn', `code=${e.code}, msg=${e.message}`);
    throw e;                            // 交给业务层决定降级还是重试
  }
}

timeout 这一行是第一个需要显式决策的配置。默认 70 秒对于“用户主动点击、期望秒级反馈”的交互来说太长,长到用户早就离开了页面,你的 catch 才姗姗来迟。把它收窄到业务可接受的范围,比如 8 秒,让失败更早地暴露出来,比让用户对着转圈猜要诚实。

name 必须和云侧部署的函数名逐字符一致。函数名对不上时,客户端拿到的是一个失败,但这个失败发生在“找到函数”之前,和函数内部的业务逻辑无关。version 的 $latest 在调试期方便,但发布后建议锁定一个具体版本,避免“云端发布了新版本,端侧还没走审核”造成的版本错位。

端侧还需要 ohos.permission.INTERNET 权限,在 module.json5 的 requestPermissions 里声明。少了它,call 会在网络层直接失败,错误码不会直白指向权限。

云侧函数:callback 是必须的,return 不是

云函数的代码结构比端侧多一个约束:入口方法必须显式调用 callback 把结果交回去。export async function myHandler(event, context, callback),event 是端侧传来的 data,context 提供 logger,callback 是返回结果的唯一通道。

typescript

// CloudProgram/cloudfunctions/fetch-order-track/fetch-order-track.ts
export async function myHandler(event: any, context: any, callback: any) {
  const logger = context.logger;
  try {
    const orderId = event.orderId as string;
    if (!orderId) {
      callback({ code: 400, message: 'orderId required' });
      return;
    }

    // 模拟查询逻辑,真实场景替换为数据库或内部服务调用
    const track: OrderTrackResult = {
      orderId,
      status: 'in_transit',
      updatedAt: Date.now()
    };

    callback({ code: 0, data: track });   // 结果必须通过 callback 交回
  } catch (err) {
    logger.error(`fetch-order-track failed: ${JSON.stringify(err)}`);
    callback({ code: 500, message: 'internal error' });
  }
}

callback 那一行不是可选的收尾。如果函数执行完毕但没有调用 callback,端侧的 cloudFunction.call 会一直等到超时,然后以超时的形式失败。一个忘记调用 callback 的分支,在本地调试时可能因为快速返回而被忽略,上真机之后就会变成“页面一直转圈、云函数日志显示执行成功”的经典现场。每一条路径——正常、参数校验失败、异常——都必须走到 callback。

event 里的字段名要和端侧 data 的键名对齐。端侧传 { orderId },云侧读 event.orderId,大小写不一致或者拼写偏差,云侧拿到的是 undefined,然后以“参数缺失”的形式失败。把端侧和云侧的请求/响应类型抽到一个共享的 .d.ts 里,两端 import 同一份定义,比靠人工对齐可靠。

依赖和本地调试:oh-package 管端侧,npm 管云侧,两条账不要混

端侧的依赖走 oh-package.json5,云侧的依赖走 package.json 或 npm install。@kit.CloudFoundationKit 是端侧 SDK 的一部分,不需要在云函数目录里额外安装。云函数里如果引用了第三方库,在 CloudProgram/cloudfunctions/<function-name>/package.json 里加,然后在函数目录下执行 npm install。

本地调试通过 localUrl 指向本地启动的云函数地址,比如 http://localhost:18090,同时用 hdc rport tcp:18090 tcp:18090 把设备端口映射到主机端口。localUrl 只在本地调试时传,正式调用时去掉这个字段。一个容易犯的错是把 localUrl 留在了发布代码里,端侧会尝试连一个不存在的本地地址,然后以超时失败。

超时不是只有客户端一个旋钮,云侧配置和幂等要一起看

端侧 timeout 控制的是“客户端愿意等多久”,云侧 function-config.json 里的 timeout 控制的是“平台允许函数跑多久”。两者是独立的。客户端把 timeout 设成 5 秒,云函数侧的平台超时是 30 秒,函数在第 10 秒才结束,客户端会在第 5 秒放弃,然后以超时失败——但云函数可能还在继续执行,并且最终成功写入了数据。

对于写操作,这种“客户端超时、服务端成功”的窗口是数据不一致的来源。用户看到“失败”,重试一次,服务端可能已经执行了两遍。云函数侧需要用业务幂等键去重:端侧生成一个 requestId 放在 data 里,云函数按 requestId 判重,重复的请求直接返回上一次的结果,而不是再写一次。重试策略只对查询类操作或者带幂等键的写操作做有限次退避,不要对无幂等的写操作盲目重放。

云函数内部也要避免同步阻塞。Thread.sleep、长时间的 while 循环、同步文件 IO,都会把函数执行时间拉到平台超时,端侧等到的就是失败。耗时的外部调用一律异步化,函数总执行时间尽量控制在数秒以内,超出时拆成任务队列或异步回调模式。

排查顺序:先看是哪一段超时,再看 callback 走没走,最后看幂等

下次云函数“页面一直转圈”或者“日志说成功但客户端报错”的时候,按这个顺序走。

第一步,看云函数控制台的执行日志。如果函数正常执行并返回,但端侧超时,问题在客户端 timeout 太短或者网络往返太长。如果函数日志显示执行时间接近平台超时,问题在云函数内部的阻塞或外部调用。

第二步,在云函数的每一条返回路径上打 logger.info,确认 callback 有没有被调用。端侧超时但云函数日志显示“执行完成”,第一嫌疑人就是某条分支漏了 callback。

第三步,如果是写操作并且出现了重复,检查端侧 data 里有没有带幂等键,云函数有没有按幂等键判重。重试逻辑只对查询和带幂等键的写操作启用。

第四步,检查 name 和 version 是否对齐。$latest 在发布后要锁定,端侧和云侧的请求/响应字段名要一致。localUrl 不要出现在发布代码里。

云函数是一条跨进程、跨网络的调用链,端侧的 timeout 和云侧的 callback 是两个必须显式对齐的边界。默认 70 秒的超时不适合大多数用户交互,callback 漏一条分支就会让端侧等到天荒地老,写操作没有幂等键就会在超时重试中重复执行。下一次页面转圈而云函数日志干净的时候,先别怀疑网络——打开云函数入口看一眼,那条路径上的 callback,到底有没有被交出去。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐