从 has.echo 到异步 API 注册表:一次 ASCF API 回调不触发的排查复盘

从 has.echo 到异步 API 注册表:一次 ASCF API 回调不触发的排查复盘

这篇文章记录一次新增 has.echo API 的真实排查过程。为了避免涉及内部实现细节,文中的目录和配置名称做了适当泛化,重点放在调用链路、排查方法和框架设计思路上。

最近我在学习 ASCF Runtime 的 API 调用链路。为了不一上来就陷入复杂业务,我选了一个最小 API:has.echo

这里放一张流程图片

目标很简单:业务页面传一个 message,底层模块收到后返回结果,页面在 success 里更新文本。

期望用法是这样:

js 复制代码
has.echo({
  message: '我要去郑州',
  success(res) {
    console.log(res)
  },
  fail(err) {
    console.log(err)
  }
})

但真正做下来,我发现新增一个框架 API 并不是"写一个函数"这么简单。它至少涉及:

text 复制代码
业务页面
  ↓
全局 has 对象
  ↓
逻辑层 API 暴露
  ↓
requireAPI
  ↓
底层 moduleMap
  ↓
ArkTS 能力模块
  ↓
异步 API 注册表
  ↓
success / fail / complete 回调

这次最有价值的地方,不是最后跑通了 has.echo,而是完整经历了一个错误思路被推翻、再找到正确注册闭环的过程。


一、最开始的问题:has.echo 不存在

一开始页面调用:

js 复制代码
has.echo({
  message: '你好',
  success(res) {
    console.log(res)
  }
})

直接报:

text 复制代码
TypeError: has.echo is not a function

这个错误说明两件事:

text 复制代码
has 全局对象存在
但 echo 没有挂载到 has 上

因为 has.showToast 是可以正常调用的,所以问题不在页面事件,也不在全局对象,而在 API 暴露层。

后来确认,新增 ui/echo.js 之后,还必须把它加入最终导出的接口列表:

js 复制代码
import { echo } from './ui/echo'

hasInterfaceList.echo = echo

或者对象写法:

js 复制代码
const hasInterfaceList = {
  showToast,
  request,
  echo
}

这里第一个经验是:

只新增文件、只 import 不够,必须进入最终导出的 has API 列表。

解决后,has.echo 可以被业务页面识别。


二、先证明页面到 ui/echo.js 是通的

在继续打到底层之前,我先把 ui/echo.js 写成纯 JS 版本:

js 复制代码
export function echo(params = {}) {
  const result = {
    message: '先证明 ui/echo.js 已经执行',
    from: 'ui/echo.js',
    input: params.message
  }

  if (typeof params.success === 'function') {
    params.success(result)
  }

  if (typeof params.complete === 'function') {
    params.complete(result)
  }

  return result
}

页面成功更新文案。

这一步证明:

text 复制代码
Page
  ↓
has.echo
  ↓
hasInterfaceList.echo
  ↓
ui/echo.js
  ↓
params.success
  ↓
页面 setData

这一段链路是通的。

也就是说:页面事件、has.echo 挂载、params.success、页面 setData 都没有问题。


三、再证明 ui/echo.js 能打到底层模块

接下来把 ui/echo.js 改成真实底层调用:

js 复制代码
const demo = requireAPI('system.demo')

export function echo(params = {}) {
  return demo.echo(params)
}

底层模块大概是这样:

ts 复制代码
export interface EchoOption {
  message: string
}

export interface EchoResult {
  message: string
  from: string
  time: number
}

export class DemoModule {
  @JsMethod({
    alias: 'echo',
    callback: true,
    params: [
      {
        name: 'message',
        type: 'string',
        required: true
      }
    ]
  })
  public echo(option: EchoOption): Promise<EchoResult> {
    return new Promise<EchoResult>((resolve) => {
      resolve({
        message: option.message,
        from: 'DemoModule',
        time: new Date().getTime()
      })
    })
  }
}

同时注册底层模块:

ts 复制代码
moduleMap.set('system.demo', () => new DemoModule())

为了确认底层方法真的执行,我在 DemoModule.echo 里临时加了一个 Toast。

结果 Toast 成功弹出。

这说明:

text 复制代码
Page
  ↓
has.echo
  ↓
ui/echo.js
  ↓
requireAPI('system.demo')
  ↓
DemoModule.echo

主调用链路已经打到底层了。

但是新的问题来了:

text 复制代码
底层 Toast 能弹
Promise 也 resolve 了
但页面 success 没触发
文本没有变化

四、第一个错误思路:是不是 resolve 返回格式不对?

最开始我怀疑是返回格式不对。

比如我返回的是:

ts 复制代码
resolve({
  message: option.message,
  from: 'DemoModule',
  time: new Date().getTime()
})

但框架可能要求:

ts 复制代码
resolve({
  errMsg: 'echo:ok',
  data: {
    message: option.message
  }
})

于是我去看已有 API,发现很多成功场景只是:

ts 复制代码
resolve()

不传数据。

所以我也改成:

ts 复制代码
return new Promise<void>((resolve) => {
  resolve()
})

结果仍然是:

text 复制代码
Toast 能弹
success 不触发

这说明问题不是返回数据格式。


五、第二个错误思路:是不是 return 影响异步 API?

我又怀疑 ui/echo.js 里的 return

原来是:

js 复制代码
export function echo(params = {}) {
  return demo.echo(params)
}

我尝试改成:

js 复制代码
export function echo(params = {}) {
  demo.echo(params)
}

结果还是一样:

text 复制代码
Toast 能弹
success 不触发

所以 return 写不写不是关键。


六、关键日志:success 是函数,但 requireAPI 返回 undefined

这时我在 ui/echo.js 加了日志:

js 复制代码
export function echo(params = {}) {
  console.log('[has.echo] start')
  console.log('[has.echo] message =', params.message)
  console.log('[has.echo] success type =', typeof params.success)

  const demo = requireAPI('system.demo')
  const ret = demo.echo(params)

  console.log('[has.echo] ret =', ret)
  console.log('[has.echo] ret.then type =', ret && typeof ret.then)

  return ret
}

日志结果:

text 复制代码
[has.echo] start
[has.echo] message = 我要去郑州
[has.echo] success type = function
[has.echo] ret = undefined
[has.echo] ret.then type = undefined

这个日志直接改变了排查方向。

它说明:

text 复制代码
params.success 没有丢
底层 DemoModule.echo 能执行
但 requireAPI 调用没有返回 JS Promise

所以这类写法不成立:

js 复制代码
demo.echo(params).then(res => {
  params.success(res)
})

因为 demo.echo(params) 的返回值就是 undefined

换句话说,底层 ArkTS 方法虽然写了 Promise,但它并不会以普通 JS Promise 的形式返回到 ui/echo.js

问题已经不是"怎么 then",而是"框架到底在哪里把 native resolve 转成 success"。


七、临时手动 success 可以成功,但这不是最终答案

为了确认业务侧回调本身没问题,我临时写了一个手动回调版本:

js 复制代码
export function echo(params = {}) {
  try {
    const demo = requireAPI('system.demo')

    demo.echo({
      message: params.message
    })

    const res = {
      errMsg: 'echo:ok',
      message: params.message,
      from: 'ui/echo.js'
    }

    if (typeof params.success === 'function') {
      params.success(res)
    }

    if (typeof params.complete === 'function') {
      params.complete(res)
    }
  } catch (err) {
    const error = {
      errMsg: 'echo:fail',
      error: String(err)
    }

    if (typeof params.fail === 'function') {
      params.fail(error)
    }

    if (typeof params.complete === 'function') {
      params.complete(error)
    }
  }
}

页面可以变化。

但这只能证明:

text 复制代码
ui/echo.js 可以主动调用 params.success

不能证明:

text 复制代码
DemoModule.echo 的 resolve 回到了页面 success

所以这只是临时兜底,不是框架标准 API 的最终链路。


八、正确方向:对比已有异步 API

继续改 resolvereturn 已经没意义了。

正确方向是:找一个已有的、能触发 success 的异步 API,看它除了接口暴露、moduleMap 注册之外,还做了什么。

我优先看了类似 getSystemInfo 这种 API,因为它通常需要返回数据,更适合作为对照。

最后发现框架里还有一个异步 API 注册表,类似:

js 复制代码
const asyncApis = {
  'system.prompt': ['showToast'],
  'system.device': ['getSystemInfo']
}

这个表的作用可以理解为:

text 复制代码
声明哪些 module.method 是异步 API
这些 API 的 resolve/reject 需要被框架映射到 success/fail/complete

而我的 system.demo.echo 没有注册进去。

于是补上:

js 复制代码
const asyncApis = {
  'system.prompt': ['showToast'],
  'system.device': ['getSystemInfo'],

  // 新增
  'system.demo': ['echo']
}

重新构建后,页面 success 终于触发了。


九、为什么 success 触发后 res 是 undefined?

刚跑通时,页面确实触发了 success,但拿到的 resundefined

这个结果是合理的。

因为底层当时写的是:

ts 复制代码
return new Promise<void>((resolve) => {
  resolve()
})

既然 resolve() 没有传数据,那么页面 success(res) 中的 res 就是 undefined

这一步真正证明的是:

text 复制代码
DemoModule.echo resolve()
  ↓
框架异步 API 包装层
  ↓
业务侧 success

已经通了。

如果要返回数据,再改成:

ts 复制代码
return new Promise<EchoResult>((resolve) => {
  resolve({
    errMsg: 'echo:ok',
    message: option.message,
    from: 'DemoModule',
    time: new Date().getTime()
  })
})

十、最终完整链路

跑通之后,这个 API 的完整链路应该是:

text 复制代码
业务页面 Page
  ↓
has.echo({ message, success, fail, complete })
  ↓
hasInterfaceList.echo
  ↓
ui/echo.js
  ↓
requireAPI('system.demo').echo(params)
  ↓
异步 API 注册表命中 system.demo.echo
  ↓
框架识别为异步 callback API
  ↓
moduleMap.get('system.demo')
  ↓
new DemoModule()
  ↓
@JsMethod(alias: 'echo', callback: true)
  ↓
DemoModule.echo(option)
  ↓
Promise resolve / reject
  ↓
框架异步回调派发
  ↓
params.success / params.fail / params.complete
  ↓
页面 setData

这才是一个真正完整的 API 闭环。


十一、这件事背后的框架设计思想

这次排查让我意识到,框架里的 API 不是一个函数,而是一套协议。

1. hasInterfaceList:解决入口暴露

它决定业务侧能不能写:

js 复制代码
has.echo(...)

如果这里没配,就会出现:

text 复制代码
has.echo is not a function

2. ui/echo.js:解决逻辑层包装

它决定业务侧 API 如何映射到底层模块:

js 复制代码
requireAPI('system.demo').echo(params)

3. moduleMap:解决模块查找

它决定:

text 复制代码
system.demo
  ↓
DemoModule

如果这里只通,底层可以执行,但不代表回调能回来。

4. @JsMethod:解决底层方法暴露

它声明底层方法名、别名、参数规则、callback 类型等。

5. asyncApis:解决异步语义

这是这次真正漏掉的地方。

它告诉框架:

text 复制代码
这个 API 是异步 API
它的 resolve/reject 需要映射到 success/fail/complete

所以新增标准异步 API,不是只写方法,而是要完成:

text 复制代码
入口暴露
逻辑层包装
模块注册
方法暴露
异步语义注册

这几层缺一层,都会出现不同的问题。


十二、一开始的思路 vs 最终正确思路

一开始我的理解是:

text 复制代码
写 ui/echo.js
写 DemoModule.echo
moduleMap 注册
Promise resolve
页面 success 应该自动触发

这个思路的问题是:默认把底层 Promise resolve 和业务侧 success 画上了等号。

但实际排查后发现:

text 复制代码
DemoModule 能执行,只说明调用链通了
resolve() 能执行,不代表 success 会触发
requireAPI 返回 undefined,说明 JS 层拿不到普通 Promise
success 是函数,只说明回调传进来了,不代表框架会调用它

最终正确思路是:

text 复制代码
新增标准异步 API
= has 暴露
+ ui 包装
+ moduleMap 注册
+ @JsMethod 暴露
+ async API 注册表

补齐最后的异步 API 注册后,success 才真正触发。


十三、以后排查类似问题的顺序

这次之后,我会按这个顺序排查新增 API:

text 复制代码
1. has.xxx 是否存在?
   不存在:查接口导出 / hasInterfaceList

2. ui/xxx.js 是否执行?
   不执行:查 import、export、构建产物

3. requireAPI('module.key') 是否能找到模块?
   找不到:查 module key 和 moduleMap

4. 底层 @JsMethod 是否执行?
   不执行:查 alias、params、模块注册

5. success / fail / complete 是否触发?
   不触发:查 asyncApis / 异步 API 注册表 / callback 注册

6. success(res) 数据是否正确?
   不正确:查 resolve 返回结构和序列化规则

这个顺序的好处是:每一步都有明确现象,不会在源码里盲猜。


总结

这次 has.echo 跑通后,最核心的结论是:

text 复制代码
success 不触发的根因:
不是 return 问题,
不是 resolve() 问题,
不是 params.success 丢失,
而是没有把 system.demo.echo 注册进异步 API 表。

补齐之后,链路变成:

text 复制代码
DemoModule.echo resolve()
  ↓
框架异步回调派发
  ↓
业务侧 success
  ↓
页面 setData

这次排查其实很像一次小型框架设计训练:

API 不是一个函数,而是一组约定。入口、分发、模块、方法、异步语义、回调派发,全部对齐,才算一个真正可用的框架能力。

这也是我这次调试最大的收获。


建议标签

text 复制代码
HarmonyOS、ArkTS、JSBridge、框架设计、问题排查、API 设计
相关推荐
林瞅瞅2 小时前
Nuxt3 项目部署 Nginx 防盗链后特定 JS 文件 403 问题修复方案
前端
kyriewen3 小时前
别再每次都 Google 了:我整理了前端日常最常踩的 10 个 Git 坑,附速查表
前端·javascript·git
一颗奇趣蛋3 小时前
Web 视频开发完全指南:从入门到精通
前端
非洲农业不发达3 小时前
windows终端体验大升级,让你拥有macos级别的美化
前端·后端
妙码生花3 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(十七):登录接口完善,登录页接口整合,解决跨域
前端·后端·ai编程
唐诗3 小时前
改 3 行配置,我的 Tauri dev 冷启动从 100 秒干到 4 秒
前端·客户端
SmartBoyW4 小时前
深入ECMAScript规范:彻底搞懂JS隐式类型转换与底层ToPrimitive机制
前端·javascript
牧艺4 小时前
Cursor Rules / Skills 分层设计:让 Agent 像「团队新同事」
前端·人工智能·cursor
光影少年4 小时前
react navite 跨端核心原理
前端·react native·react.js