ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

React-Native系列 2、RN与native交互与数据传递:TaoToken 统一 Key 通道下的调试链路

React-Native系列 2、RN与native交互与数据传递:TaoToken 统一 Key 通道下的调试链路 1. RN 与 Native 双向通信到底难在哪从通讯录回传看数据链路React-Native 与原生交互这件事说穿了就是两套运行时之间搭桥。JS 侧跑在 Hermes 或 JSC 引擎里原生侧跑在 Android 的 Java/Kotlin 或 iOS 的 Objective-C/Swift 里两边内存不共享、线程也不同步。你写NativeModules.MyModule.doSomething()的时候RN 框架其实是在背后把调用序列化成一个跨语言的消息丢到原生侧的队列里执行执行完再想办法把结果送回来。这个「送回来」的路径就是很多人第一次写 RN 原生模块时最容易卡住的地方。我见过太多同学在ReactMethod里直接return一个字符串然后发现 JS 侧拿到的是undefined。原因很简单原生方法被调用时是异步的RN 不允许ReactMethod有返回值所有结果必须通过 Callback、Promise 或者事件EventEmitter三种通道之一回传。这三种通道各有适用场景选错了不是不能跑而是代码会变得别扭。这篇要做的 Demo 是一个很典型的场景RN 页面点一下按钮调起原生侧的系统通讯录选择界面用户选完联系人后原生把电话号码回传给 RN 展示。这个链路里包含了「RN 调 Native」和「Native 回传 RN」两个方向正好把 NativeModules 和 DeviceEventEmitter 都用上。更关键的是我会把原生侧最终要发起的网络请求端点统一改到 TaoToken 的 API 通道上用一个 Key 走完全部调试链路这样你在验证数据回传的同时也顺手把模型调用通道跑通了。适合谁看如果你已经能跑起一个 RN 工程知道index.android.js在哪但对ReactPackage、ReactContextBaseJavaModule这些类名还有点发怵那这篇就是给你写的。我会把每个文件放哪、每个方法为什么这么写讲清楚代码可以直接复制。整个流程跑通后你会得到一条 RN → Native → TaoToken API → Native → RN 的完整数据回传链路后面再接入更复杂的原生能力扫码、定位、蓝牙时套路是一样的。先说清楚一个概念避免后面混淆。RN 和 Native 之间的「数据传递」不是共享内存而是序列化传输。能传的类型有限基本类型、字符串、数组、Map复杂对象要自己转成 JSON 字符串。所以你在原生侧拿到通讯录的Cursor不能直接丢给 JS必须先提取成String再 emit 出去。这个约束贯穿全文记住它。2. TaoToken 前置准备统一 Key 通道与 Native 侧请求端点改造在动手写原生模块之前先把 TaoToken 这条通道准备好。为什么要在这里做因为原生侧发起网络请求和 JS 侧不一样JS 侧你可以用fetch直接打原生侧你得自己写 HTTP 客户端端点、鉴权头、模型 ID 都得手动配。如果每个原生模块都散着写端点后面维护会疯掉。统一到一个 Key 通道改一处就全生效。TaoToken 在这里扮演的角色是一个兼容 OpenAI 接口规范的模型调用入口。你拿到一个 API Key就可以用它去请求对话、补全等能力Base URL 指向https://taotoken.net/api。注意这个地址不带任何查询参数是纯粹的 API 根路径。官网入口在https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end注册和查看文档都从这里进。具体要准备三样东西我把它叫做「三件套」后面配置里会反复出现配置项值说明Base URLhttps://taotoken.net/api所有请求的前缀不要漏掉/apiAPI Key你在控制台生成的 Key形如sk-开头的一串字符Model ID例如gpt-4o-mini等以控制台实际可用的模型为准生成 Key 的入口在控制台的 API Keys 页面路径是https://taotoken.net/console/api-keys。进去之后新建一个 Key复制出来存好因为它只完整显示一次。如果你只是想先在浏览器里验证模型通不通可以用模型对话页面https://taotoken.net/model-chat发一条消息试试确认 Key 有效再往代码里塞。这里有个我踩过的坑要提醒你原生侧请求和 JS 侧请求的鉴权头格式必须一致都是Authorization: Bearer 你的Key。有些同学在 JS 侧写对了到原生侧手一抖写成Authorization: Key结果收到 401然后回头怀疑 Key 有问题。其实 Key 没问题是头写错了。后面排障章节我会专门讲这个。还有一个容易忽略的点Android 从 9.0 开始默认禁止明文 HTTP 请求。TaoToken 的 API 是 HTTPS所以不受影响但如果你本地调试时用了什么 HTTP 代理工具记得在AndroidManifest.xml里配usesCleartextTraffic否则请求会被系统直接掐断报错信息还特别隐晦。这个坑我在第五节会展开。准备阶段最后一步确认你的 RN 工程能正常跑起来。npx react-native run-android能出界面adb logcat能看到 JS 的 console 输出这就够了。原生模块的代码要重新编译才生效改完 Java 文件记得重新 run 一次热更新不会带上原生改动。3. 可复制配置NativeModule 封装与 Key 配置片段这一节是全文的核心代码量最大。我按文件拆开讲每个文件告诉你放哪、为什么这么写。工程包名我假设是com.rnandroid01你换成自己的包名即可。3.1 MyNativeModule原生模块主体这个类继承ReactContextBaseJavaModule是 RN 能识别到的原生模块。getName()返回的名字就是 JS 侧NativeModules.XXX里的 XXX必须一致。package com.rnandroid01; import android.content.Intent; import android.os.Bundle; import android.provider.ContactsContract; import android.widget.Toast; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.modules.core.DeviceEventManagerModule; public class MyNativeModule extends ReactContextBaseJavaModule { private final ReactApplicationContext mContext; public MyNativeModule(ReactApplicationContext reactContext) { super(reactContext); mContext reactContext; } Override public String getName() { return MyNativeModule; } ReactMethod public void rnCallNative(String msg) { Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show(); Intent intent new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI); Bundle bundle new Bundle(); mContext.startActivityForResult(intent, 200, bundle); } public void sendMsgToRn(String msg) { mContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) .emit(AndroidToRNMessage, msg); } }注意rnCallNative没有返回值这是硬性规定。它做两件事弹个 Toast 告诉你调用成功然后拉起系统通讯录。startActivityForResult的第二个参数 200 是请求码后面在 Activity 的onActivityResult里要用同一个值来匹配。sendMsgToRn是给 Activity 回调时调用的它通过RCTDeviceEventEmitter发一个名为AndroidToRNMessage的事件JS 侧监听这个名字就能收到数据。3.2 MyReactPackage把模块注册进去RN 不会自动发现你的模块得通过 Package 注册。package com.rnandroid01; import com.facebook.react.ReactPackage; import com.facebook.react.bridge.NativeModule; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.uimanager.ViewManager; import java.util.ArrayList; import java.util.Collections; import java.util.List; public class MyReactPackage implements ReactPackage { public MyNativeModule myNativeModule; Override public ListNativeModule createNativeModules(ReactApplicationContext reactContext) { ListNativeModule modules new ArrayList(); myNativeModule new MyNativeModule(reactContext); modules.add(myNativeModule); return modules; } Override public ListViewManager createViewManagers(ReactApplicationContext reactContext) { return Collections.emptyList(); } }这里把myNativeModule存成 public 字段是为了让MainActivity在收到通讯录结果时能拿到这个实例调用它的sendMsgToRn。3.3 MainApplication挂载 Packagepackage com.rnandroid01; import android.app.Application; import com.facebook.react.ReactApplication; import com.facebook.react.ReactNativeHost; import com.facebook.react.ReactPackage; import com.facebook.react.shell.MainReactPackage; import java.util.Arrays; import java.util.List; public class MainApplication extends Application implements ReactApplication { private static final MyReactPackage myReactPackage new MyReactPackage(); public static MyReactPackage getMyReactPackage() { return myReactPackage; } private final ReactNativeHost mReactNativeHost new ReactNativeHost(this) { Override protected boolean getUseDeveloperSupport() { return BuildConfig.DEBUG; } Override protected ListReactPackage getPackages() { return Arrays.ReactPackageasList( new MainReactPackage(), myReactPackage ); } }; Override public ReactNativeHost getReactNativeHost() { return mReactNativeHost; } }用 static 持有 Package 实例是为了让MainActivity能通过MainApplication.getMyReactPackage()拿到模块。3.4 MainActivity接收通讯录结果并回传package com.rnandroid01; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.provider.ContactsContract; import com.facebook.react.ReactActivity; public class MainActivity extends ReactActivity { Override protected String getMainComponentName() { return RNAndroid01; } Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode ! 200 || resultCode ! RESULT_OK || data null) return; Uri contactData data.getData(); Cursor cursor managedQuery(contactData, null, null, null, null); if (cursor ! null cursor.moveToFirst()) { String num getContactPhone(cursor); MainApplication.getMyReactPackage().myNativeModule.sendMsgToRn(num); } } private String getContactPhone(Cursor cursor) { int phoneColumn cursor.getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER); int phoneNum cursor.getInt(phoneColumn); String result ; if (phoneNum 0) { int idColumn cursor.getColumnIndex(ContactsContract.Contacts._ID); String contactId cursor.getString(idColumn); Cursor phone getContentResolver().query( ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, ContactsContract.CommonDataKinds.Phone.CONTACT_ID contactId, null, null); if (phone ! null phone.moveToFirst()) { int index phone.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER); result phone.getString(index); phone.close(); } } return result; } }3.5 Key 配置片段原生侧请求端点现在把 TaoToken 的配置落到原生侧。我建议单独建一个常量类别把 Key 硬编码散在各处。package com.rnandroid01; public class TaoTokenConfig { public static final String BASE_URL https://taotoken.net/api; public static final String API_KEY sk-你的Key粘贴在这里; public static final String MODEL_ID gpt-4o-mini; }如果你用的是 iOS等价配置放在一个.plist或常量头文件里字段名保持一致。这样 JS 侧和原生侧引用的是同一套语义排查问题时不会因为命名不一致而绕弯。3.6 JS 侧监听事件与发起调用import React, { Component } from react; import { AppRegistry, StyleSheet, Text, NativeModules, DeviceEventEmitter, View, } from react-native; class RNAndroid01 extends Component { componentDidMount() { this.listener DeviceEventEmitter.addListener( AndroidToRNMessage, this.handleAndroidMessage, ); } componentWillUnmount() { if (this.listener) this.listener.remove(); } handleAndroidMessage (msg) { console.log(收到原生回传数据:, msg); }; callAndroid () { NativeModules.MyNativeModule.rnCallNative(rn调用原生模块-成功); }; render() { return ( View style{styles.container} Text style{styles.welcome} onPress{this.callAndroid} 点我调起通讯录 /Text /View ); } } const styles StyleSheet.create({ container: { flex: 1, justifyContent: center, alignItems: center }, welcome: { fontSize: 20, textAlign: center, margin: 10 }, }); AppRegistry.registerComponent(RNAndroid01, () RNAndroid01);注意componentWillUnmount里用this.listener.remove()而不是DeviceEventEmitter.remove后者在新版本 RN 里已经废弃用前者更稳。这个细节很多老教程没更新照着抄会报 warning。4. 验证请求与成功结果跑通 RN→Native→API 回传代码写完了现在验证。整个过程分三步先确认 RN 能调到原生再确认原生能回传数据最后确认原生侧能打通 TaoToken 的 API。第一步启动工程。npx react-native run-android等界面出来。点一下「点我调起通讯录」你应该看到 Toast 弹出紧接着系统通讯录界面被拉起。如果 Toast 没弹说明NativeModules.MyNativeModule是 undefined八成是 Package 没注册成功回去检查MainApplication里的getPackages。第二步在通讯录里选一个联系人。选完后界面回到 RNadb logcat里应该能看到收到原生回传数据: 138xxxx。这一步验证的是DeviceEventEmitter通道。如果 logcat 里没有先确认componentDidMount里的监听名字和原生 emit 的名字完全一致大小写都不能差。第三步验证 TaoToken 通道。我在原生侧加一个方法用 OkHttp 发一条请求到 TaoToken把返回的模型回复也 emit 给 RN。这样整条链路就闭环了。ReactMethod public void callTaoToken(String prompt) { new Thread(() - { try { okhttp3.OkHttpClient client new okhttp3.OkHttpClient(); String json {\model\:\ TaoTokenConfig.MODEL_ID \, \messages\:[{\role\:\user\,\content\:\ prompt \}]}; okhttp3.RequestBody body okhttp3.RequestBody.create( json, okhttp3.MediaType.parse(application/json)); okhttp3.Request request new okhttp3.Request.Builder() .url(TaoTokenConfig.BASE_URL /v1/chat/completions) .addHeader(Authorization, Bearer TaoTokenConfig.API_KEY) .addHeader(Content-Type, application/json) .post(body) .build(); okhttp3.Response response client.newCall(request).execute(); String respBody response.body() ! null ? response.body().string() : ; sendMsgToRn(API返回: respBody); } catch (Exception e) { sendMsgToRn(请求异常: e.getMessage()); } }).start(); }JS 侧加一个按钮调它callTaoToken () { NativeModules.MyNativeModule.callTaoToken(用一句话介绍 React Native); };跑起来后logcat 里应该能看到一段 JSON里面choices[0].message.content就是模型的回复。看到这个说明 RN → Native → TaoToken API → Native → RN 整条链路通了。这里有个实测经验网络请求必须放在子线程Android 主线程不允许做网络 IO否则直接抛NetworkOnMainThreadException。上面代码用new Thread包了一层别省。如果你只想在浏览器里先确认模型通不通可以打开模型对话页面https://taotoken.net/model-chat发一条消息看有没有回复。这一步和代码验证是独立的能帮你快速区分「是 Key 的问题」还是「是代码的问题」。5. 本篇常见错排查401、local proxy failed 与 reading choices排障这块我按真实报错来每个都给你定位思路。401 Unauthorized。这是最常见的。先看Authorization头是不是Bearer开头注意 Bearer 后面有一个空格。我见过写成Bearer sk-xxx少空格、bearer sk-xxx小写、Authorization: sk-xxx漏了 Bearer的各种版本。其次确认 Key 没有多余空格从控制台复制时容易带上换行。最后确认 Key 没过期或被删。三件套里的 Base URL、Key、Model ID 任何一个写错都可能触发 401 或 404建议对照https://taotoken.net/console/api-keys里的实际值逐个核对。local proxy failed / connection refused。这个报错通常出现在你本地配了什么代理工具但代理没起来或者端口不对。原生侧的 OkHttp 默认会读系统代理设置如果你之前为了调试配过代理现在代理关了请求就会打到空气上。解决办法是在 OkHttp 里显式禁用代理new OkHttpClient.Builder().proxy(Proxy.NO_PROXY).build()。注意这里说的是本地调试工具的代理配置问题不是让你去搞什么网络穿透纯粹是清理本地环境。Cannot read property choices of undefined。这个报错在 JS 侧解析响应时出现说明返回的 JSON 结构和你预期的不一样。可能是请求根本没成功返回的是错误对象也可能是你解析的层级不对。正确路径是data.choices[0].message.content。建议先把原始响应console.log出来看结构别急着取字段。如果返回体里是{error: {...}}那就是请求本身失败了回到 401 那条去查。DeviceEventEmitter 收不到消息。检查三处emit 的事件名和 addListener 的名字是否完全一致componentDidMount是否真的执行了打个 log 确认原生侧sendMsgToRn是否真的被调用在方法里打 log。还有一个隐蔽的坑如果你在componentWillMount里注册监听某些 RN 版本下组件还没挂载完监听会丢改用componentDidMount。startActivityForResult 不回调。确认onActivityResult里的 requestCode 和startActivityForResult传的 200 一致。另外如果MainActivity继承的不是ReactActivity而是别的onActivityResult可能被父类拦截记得调super.onActivityResult。OAuth 相关报错。如果你在原生侧接的是需要 OAuth 流程的服务报错里出现invalid_grant或redirect_uri_mismatch那是 OAuth 配置问题和本篇的 Key 通道不是一回事。本篇用的是静态 Key 鉴权不涉及 OAuth 跳转。如果你确实需要 OAuth那是另一套流程别混在一起调。排障时有个通用技巧把请求的完整 URL、完整请求头、完整响应体都打出来。90% 的问题看一眼原始报文就清楚了。别只看异常信息异常信息往往只告诉你「失败了」不告诉你「为什么失败」。6. 继续深入从 Callback/Promise 到 Coding Plan 的调试链路这篇把 NativeModules 和 DeviceEventEmitter 跑通了但 RN 与原生交互还有另外两条通道没展开Callback 和 Promise。它们适合「一次调用一次返回」的场景比如原生算个哈希值返回给 JS用 Promise 会比事件更顺手。事件适合「原生主动推、可能推多次」的场景比如传感器数据流。选哪个取决于你的数据是「请求-响应」模式还是「推送」模式。如果你后面要接的原生能力越来越多建议把 TaoToken 的配置抽成一个统一的网络层所有原生模块共用同一个 OkHttp 实例和同一套鉴权逻辑。这样换 Key、换模型 ID 只改一处。长期做编码和 Agent 类项目的话可以了解一下 Coding Plan它面向的是持续性的编码辅助场景和单次 API 调用是互补的。入口在https://taotoken.net/coding-plan。接入文档在https://taotoken.net/doc里面有各语言的请求示例原生侧用 OkHttp 或 Retrofit 都能对上。API Keys 管理在https://taotoken.net/console/api-keys。这三个地址建议存书签调试时来回切。最后留一个实操建议把sendMsgToRn这个方法名改成带业务前缀的比如sendContactToRn因为一个原生模块里往往有多个回传通道名字太泛后面会分不清哪个事件对应哪个业务。这个习惯在模块变多之后能省很多事。
返回列表