RN 的新通信模型 JSI

本文所有代码如果没有特别标注的话,默认用的都是 v0.76.0 的 RN 代码

JSI 是 React Native 新架构中的 JS ↔ Native 通信模型

JSI 解决了过去 Bridge 架构中 JSON 序列化开销强制异步带来的一系列问题,为后续的 Turbo module、Fabric、RuntimeScheduler 系统的实现奠定了基础

本文将从其的设计理念,逐步深入到源码细节,跟读者一同探究 JSI 背后的原理

设计理念

JSI 全名 JavaScript Interface,直译为 JS 接口,它既不是系统,也不是通信协议,而是一个位于 C++ 层,面向 JS 的抽象接口

JSI 的设计理念用一句话总结的话就是:JSI 是一层 JS runtime 抽象,使 JS 与 C++ 可以直接共享对象与函数调用,从而实现无 Bridge 的高性能通信,并支持多 JS 引擎。

这句话引出了 JSI 的三个设计目标:

  1. 去 Bridge
  2. JS 与 C++ 能够直接互相操作
  3. JSI 与引擎无关,且可以替换引擎

JSI 是怎么实现这三个目标的呢?如果我们将 RN 中 JS 与 Native 的通信进行抽象,其中主要有 4 名角色,他们的关系如下:

js 复制代码
Native <---通信---> C++ ---编写---> JS 引擎 ---创建---> JS runtime

其中 Native 与 C++ 的通信在本专栏的 RN 通信机制已经聊完了,这里就不赘述

我们重点看看 C++ -> JS 引擎 -> JS runtime 这条线:

  1. JS 引擎会提供 C++ embedding API,使我们能够在 C++ 中访问和操作 JS runtime 中的对象
  2. JS 引擎创建了 JS runtime,且对 JS 内部的变量、对象有完全的控制权
  3. 如果我们让 JS 引擎暴露 JS 对象的句柄,提供对象访问、函数调用、生命周期管理等接口,就可以实现 C++ 与 JS 在一个 Runtime 中共享对象的操作

了解了 JS 与 C++ 共享对象的原理后,我们知道如果要实现这一套通信机制,需要在 C++ 层面针对特定引擎提供的 embedding API 实现一个胶水层来管理,但我们也可以更进一步:提供一套接入标准(Interface),让不同的引擎来适配这套标准

如此一来,我们就可以达成第三个设计目标:引擎无关

这也是 JSI 名字的由来

JSI 的三个层级

接下来我们来聊聊 JSI 实现过程中的三个层级:

  1. 抽象层:JSI 中最接近引擎的层级,负责定义一套统一的接口让引擎接入,也可以当成 JSI 的 "能力清单"
  2. 服务层:负责在应用初始化期间协调需要 "安装" 在 runtime 中的能力
  3. 应用层:负责往 Runtime 中绑定具体的能力(bindings)

下面我们分别详细介绍三个层级具体做了什么

抽象层:JSI 的"能力清单"

如果把 JSI 比喻成一个工具的话,抽象层就是这个工具的 "使用说明书"

在抽象层中,最重要的入口文件就是 packages/react-native/ReactCommon/jsi/jsi/jsi.h

这个文件定义了两件事:

  1. JS 引擎需要实现的一组接口
  2. JS 值模型的 C++ 包装

文件所有类的分类如下:

js 复制代码
// in jsi.h

// 1. 运行时与执行上下文 (Runtime & Context)
// 负责管理 JS 引擎实例及整体生命周期
class Runtime;          // 核心引擎接口,所有 JS 操作都必须通过 Runtime 实例执行
class PreparedJavaScript; // 已编译/预处理的 JS 代码块,用于提高重复执行效率

// 2. JS 基础类型包装 (Base Value Types)
// JS 数据在 C++ 层的通用表示
class Value;            // 顶层包装类,可表示 null, undefined, boolean, number, symbol, string, object
class Pointer;          // 堆中 JS 对象的引用基类(所有受 GC 管理的对象基类)

// 3. 引用类型 (Reference Types)
// 继承自 Pointer,对应 JS 中的非原始类型
class PropNameID : public Pointer;       // 属性名标识符,用于高效的属性访问
class Symbol : public Pointer;           // JS Symbol 类型
class String : public Pointer;           // JS String 类型
class BigInt : public Pointer;						// JS BigInt 类型
class Object : public Pointer;           // JS Object 类型
class Array : public Object;            // JS Array 类型
class ArrayBuffer : public Object;      // JS ArrayBuffer 类型
class Function : public Object;         // JS Function 类型,支持从 C++ 调用 JS 函数

// 4. Buffer 类
// 用于处理原始字节序列
class Buffer;												// 只读,用于传递脚本源码、静态资源
class StringBuffer : public Buffer;	// 只读,通常用于将 C++ 字符串转换为 Buffer
class MutableBuffer;								// 可写原始字节内存

// 5. 宿主扩展接口 (Host Interaction)
// 用于在 C++ 中实现 JS 可访问的对象或函数
class HostObject;       // 接口类:继承此类可在 C++ 中自定义 JS 对象的属性拦截逻辑 (get/set)
class NativeState;			// 挂在某个 JS Object 上的 Native 属性
HostFunctionType; 			// 类型别名:定义 C++ 函数如何被 JS 调用 (std::function 包装)

// 6. 异常与错误处理 (Error Handling)
// 处理 JS 与 C++ 边界处的异常
class JSIException;				// JSI 异常的基类
class JSError : public JSIException;		// 表示 JS 运行时的异常(包含堆栈信息,能被 JS 的 try...catch 捕获)
class JSINativeException : public JSIException;		// JSI 宿主环境(C++ 侧)发生非 JS 逻辑导致的错误时抛出(不一定有 JS 堆栈)

// 7. 辅助与生命周期管理 (Utilities & RAII)
// 确保 C++ 与 JS 交互过程中的内存与逻辑安全
class Scope;            // RAII 风格的作用域管理,批量释放局部引用
class WeakObject : public Pointer;       // 对 JS 对象的弱引用,不阻止 GC 回收
class Instrumentation;      // 提供运行时性能指标和内存使用情况的接口

如果一个 JS 引擎想要嵌入 RN ,它至少需要提供与这份 "能力清单" 兼容的 Runtime 实现,具体的引擎侧实现可以参考 hermes 代码库API/hermes/hermes.cpp 文件(引擎侧的内容超过了本专栏的范围,这里就不展开了)

服务层:JSI 的管家 ReactInstance

如果把 JSI 比喻成工具的话,服务层就是该工具的管理员

一旦引擎实现了所有 JSI 要求的能力并且成功在 RN 中创建了 Runtime 实例后,下一步就是根据这个 Runtime 搭建出可以由 RN 调度且可以安全调用的执行环境

负责搭建执行环境的类叫做 ReactInstance,它就像 RN 中负责协调 Runtime 的管家,它的主要方法有:

  • 构造函数 ReactInstance::ReactInstance,负责创建 4 个关键角色:

    • RuntimeExecutor:负责在 Runtime 中运行一些任务;确保所有 JS 调用都在 JS thread 中执行;负责把 runtime 接进异常处理

    • runtimeExecutorThatWaitsForInspectorSetup:负责把 runtime 接进 inspector

    • RuntimeScheduler:负责管理 Runtime 的事件循环,这个后面会有文章专门讲解

    • BufferedRuntimeExecutor:带有优先级调度的 RuntimeExecutor

  • ReactInstance::initializeRuntime:负责初始化 Runtime;调度 JSI 应用层 binding 所需的功能

  • ReactInstance::callFunctionOnModule:Native 侧调用 JS 方法的入口

  • ReactInstance::loadScript:加载 JS 代码,并执行积压的 JS 调用

下面是详细的代码解释:

ReactInstance::ReactInstance

cpp 复制代码
// in packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp

ReactInstance::ReactInstance(
  	// 接收的参数,主要看这个 runtime 就好,runtime 是被创建好了之后传入的,ReactInstance 并不负责创建 runtime
    std::unique_ptr<JSRuntime> runtime,
    std::shared_ptr<MessageQueueThread> jsMessageQueueThread,
    std::shared_ptr<TimerManager> timerManager,
    JsErrorHandler::OnJsError onJsError,
    jsinspector_modern::HostTarget* parentInspectorTarget)
    : runtime_(std::move(runtime)),
      jsMessageQueueThread_(jsMessageQueueThread),
      timerManager_(std::move(timerManager)),
      jsErrorHandler_(std::make_shared<JsErrorHandler>(std::move(onJsError))),
      parentInspectorTarget_(parentInspectorTarget) {
        // 这里的 runtimeExecutor 是一个 C++ 的 Lambda 表达式
        // 它的主要目标有 2:
        // 1. 确保交给他执行的 callback 都在 JS 线程中执行
        // 2. 确保 JS 线程正常运行且能捕获运行期间发生的错误
  RuntimeExecutor runtimeExecutor = [weakRuntime = std::weak_ptr(runtime_),
                                     weakTimerManager =
                                         std::weak_ptr(timerManager_),
                                     weakJsThread =
                                         std::weak_ptr(jsMessageQueueThread_),
                                     jsErrorHandler =
                                         jsErrorHandler_](auto callback) {
    // 如果 Runtime 没了,直接返回
    if (weakRuntime.expired()) {
      return;
    }

    // 如果当前有一个致命的 JS error,禁止执行其他代码
    if (!jsErrorHandler->isRuntimeReady() &&
        jsErrorHandler->hasHandledFatalError()) {
      LOG(INFO)
          << "RuntimeExecutor: Detected fatal error. Dropping work on non-js thread."
          << std::endl;
      return;
    }

    // 确保 JS 线程还存在
    if (auto jsThread = weakJsThread.lock()) {
      // 将另一个 Lambda 投入到 JS 线程执行
      jsThread->runOnQueue([jsErrorHandler,
                            weakRuntime,
                            weakTimerManager,
                            callback = std::move(callback)]() {
        // 由于这个 Lambda 在进入线程被执行前需要在 queue 中等待
        // 所以我们在执行之前需要再确认一下这个 Runtime 是否还在运行
        auto runtime = weakRuntime.lock();
        if (!runtime) {
          return;
        }

        // 这里的 runtime 其实是 C++ 的一层 wrapper,实际上执行 js 代码的是 jsiRuntime
        jsi::Runtime& jsiRuntime = runtime->getRuntime();
        SystraceSection s("ReactInstance::_runtimeExecutor[Callback]");
        try {
          // 真正执行 callback 的代码
          callback(jsiRuntime);

          // 在默认的 0.76.0 + hermes 下,这个 flag 始终为 true,代表由引擎来处理微任务的调度
          // 如果是从其他版本升级上来的话,需要用到这段代码,这本质就是把之前用 setImmediate 模拟微任务那一套包装成 timerManager,然后在这里清空一下模拟的 "微任务队列"
          if (!ReactNativeFeatureFlags::enableMicrotasks()) {
            if (auto timerManager = weakTimerManager.lock()) {
              timerManager->callReactNativeMicrotasks(jsiRuntime);
            }
          }
        } catch (jsi::JSError& originalError) {
          // 如果执行过程中有错,这里需要兜住
          jsErrorHandler->handleFatalError(jsiRuntime, originalError);
        }
      });
    }
  };

  // 如果需要接入 inspector 才进入这段逻辑
  if (parentInspectorTarget_) {
    auto executor = parentInspectorTarget_->executorFromThis();

    // 上面 runtimeExecutor 的装饰器,主要作用就是在下面 executor 方法执行完之前不要执行传进来的 callback
    auto runtimeExecutorThatWaitsForInspectorSetup =
        std::make_shared<BufferedRuntimeExecutor>(runtimeExecutor);

    // 核心逻辑
    executor([this, runtimeExecutor, runtimeExecutorThatWaitsForInspectorSetup](
                 jsinspector_modern::HostTarget& hostTarget) {
      // 把当前的 ReactInstance 绑定到 hostTarget
      // hostTarget 负责管理调试会话,把当前 ReactInstance 跟 Runtime 暴露给基于 CDP(Chrome DevTools Protocol)的调试工具
      inspectorTarget_ = &hostTarget.registerInstance(*this);
      // 绑定当前 Runtime
      runtimeInspectorTarget_ = &inspectorTarget_->registerRuntime(
          runtime_->getRuntimeTargetDelegate(), runtimeExecutor);
      // 把积压的 callback 一次 flush 了
      runtimeExecutorThatWaitsForInspectorSetup->flush();
    });

    // 用当前的 runtimeExecutorThatWaitsForInspectorSetup 替代上面的 runtimeExecutor
    // 主要目的就是为了等上面的 executor 执行完,一旦执行完,其他行为与之前的 runtimeExecutor 没有区别
    runtimeExecutor =
        [runtimeExecutorThatWaitsForInspectorSetup](
            std::function<void(jsi::Runtime & runtime)>&& callback) {
          runtimeExecutorThatWaitsForInspectorSetup->execute(
              std::move(callback));
        };
  }

	// 构造 RuntimeScheduler
  runtimeScheduler_ = std::make_shared<RuntimeScheduler>(
      runtimeExecutor,
      RuntimeSchedulerClock::now,
      [jsErrorHandler = jsErrorHandler_](
          jsi::Runtime& runtime, jsi::JSError& error) {
        jsErrorHandler->handleFatalError(runtime, error);
      });
	// 用来监控一些性能指标
  runtimeScheduler_->setPerformanceEntryReporter(
      PerformanceEntryReporter::getInstance().get());

	// 构造 BufferedRuntimeExecutor
	// 本质上也是 runtimeExecutor 的装饰器,区别在于它用上了 runtimeScheduler 提供的能力
	// 所以他可以进行优先级调度
  bufferedRuntimeExecutor_ = std::make_shared<BufferedRuntimeExecutor>(
      [runtimeScheduler = runtimeScheduler_.get()](
          std::function<void(jsi::Runtime & runtime)>&& callback) {
        runtimeScheduler->scheduleWork(std::move(callback));
      });
}

ReactInstance::initializeRuntime

cpp 复制代码
// in packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp

void ReactInstance::initializeRuntime(
    JSRuntimeFlags options,
    BindingsInstallFunc bindingsInstallFunc) noexcept {
  // 借助 runtimeScheduler_ 来执行这个 lambda
  runtimeScheduler_->scheduleWork([this, options, bindingsInstallFunc](
                                      jsi::Runtime& runtime) {
    SystraceSection s("ReactInstance::initializeRuntime");

    // 在 runtime 的 global 上绑定一个 native 的高精度时间能力
    bindNativePerformanceNow(runtime);

    // 在 runtime 的 global 上绑定 RuntimeScheduler 需要的能力
    RuntimeSchedulerBinding::createAndInstallIfNeeded(
        runtime, runtimeScheduler_);

    // 给当前 runtime 注册 profiler 并绑定当前线程,用于性能分析
    runtime_->unstable_initializeOnJsThread();

    // 把一些 Native 的 flag 挂到 global 上
    defineReactInstanceFlags(runtime, options);

		// 把异常处理相关方法挂到 global
    defineReadOnlyGlobal(
        runtime,
        "RN$handleException",
        jsi::Function::createFromHostFunction(
            runtime,
            jsi::PropNameID::forAscii(runtime, "handleException"),
            2,
            [jsErrorHandler = jsErrorHandler_](
                jsi::Runtime& runtime,
                const jsi::Value& /*unused*/,
                const jsi::Value* args,
                size_t count) {
              // 省略部分代码
            }));

    // 用来让 JS 侧注册可以被 Native 调用的 module
    // 这样 Native 就可以调用 JS 的 module 了
    defineReadOnlyGlobal(
        runtime,
        "RN$registerCallableModule",
        jsi::Function::createFromHostFunction(
            runtime,
            jsi::PropNameID::forAscii(runtime, "registerCallableModule"),
            2,
            [this](
                jsi::Runtime& runtime,
                const jsi::Value& /*unused*/,
                const jsi::Value* args,
                size_t count) {
              // 省略部分代码
              
              // 这里有个细节,callableModules_ 中的 value(第二个参数)并不是模块方法本身
							// 它代表的是一个 return 模块方法的方法
              // 这个做法可以实现模块的懒加载
              callableModules_.emplace(
                  std::move(name),
                  args[1].getObject(runtime).getFunction(runtime));
              return jsi::Value::undefined();
            }));

    // 把 setTimeout、clearTimeout、setInterval、
		// clearInterval、requestAnimationFrame、cancelAnimationFrame
    // 这些方法挂到 global 上,让 JS 可以调用,这些方法实现都在
    // packages/react-native/ReactCommon/react/runtime/TimerManager.cpp
    timerManager_->attachGlobals(runtime);

    // 最后绑定平台自己的 bindings
    bindingsInstallFunc(runtime);
  });
}

ReactInstance::callFunctionOnModule

cpp 复制代码
// in packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp

void ReactInstance::callFunctionOnModule(
    const std::string& moduleName,
    const std::string& methodName,
    folly::dynamic&& args) {
  // 用 bufferedRuntimeExecutor_ 来调度
  bufferedRuntimeExecutor_->execute([this,
                                     moduleName = moduleName,
                                     methodName = methodName,
                                     args = std::move(args)](
                                        jsi::Runtime& runtime) {
    SystraceSection s(
        "ReactInstance::callFunctionOnModule",
        "moduleName",
        moduleName,
        "methodName",
        methodName);
    auto it = callableModules_.find(moduleName);
    // 处理找不到 moduleName 的情况
    if (it == callableModules_.end()) {
      std::ostringstream knownModules;
      int i = 0;
      for (it = callableModules_.begin(); it != callableModules_.end();
           it++, i++) {
        const char* space = (i > 0 ? ", " : " ");
        knownModules << space << it->first;
      }
      throw jsi::JSError(
          runtime,
          "Failed to call into JavaScript module method " + moduleName + "." +
              methodName +
              "(). Module has not been registered as callable. Registered callable JavaScript modules (n = " +
              std::to_string(callableModules_.size()) +
              "):" + knownModules.str() +
              ". Did you forget to call `registerCallableModule`?");
    }

    // 如果当前模块没有被初始化过(第一次调用),需要加载该模块
    if (std::holds_alternative<jsi::Function>(it->second)) {
      auto module =
          std::get<jsi::Function>(it->second).call(runtime).asObject(runtime);
      it->second = std::move(module);
    }

    // 取得调用模块名字
    auto& module = std::get<jsi::Object>(it->second);
    // 取得调用模块方法
    auto method = module.getPropertyAsFunction(runtime, methodName.c_str());

    // 构造参数
    std::vector<jsi::Value> jsArgs;
    for (auto& arg : args) {
      jsArgs.push_back(jsi::valueFromDynamic(runtime, arg));
    }
    // 调用!
    method.callWithThis(
        runtime, module, (const jsi::Value*)jsArgs.data(), jsArgs.size());
  });
}

ReactInstance::loadScript

cpp 复制代码
// in packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp

void ReactInstance::loadScript(
    std::unique_ptr<const JSBigString> script,
    const std::string& sourceURL) {
  auto buffer = std::make_shared<BigStringBuffer>(std::move(script));
  std::string scriptName = simpleBasename(sourceURL);
	// 用 runtimeScheduler_ 来调度
  runtimeScheduler_->scheduleWork(
      [this,
       scriptName,
       sourceURL,
       buffer = std::move(buffer),
       // 这里用 weak_ptr 是不希望这一个 lambda 延长 weakBufferedRuntimeExecuter 的生命周期
       // 不然可能出现 runtime 没了,但是 Executer 还在的尴尬情况
       weakBufferedRuntimeExecuter = std::weak_ptr<BufferedRuntimeExecutor>(
           bufferedRuntimeExecutor_)](jsi::Runtime& runtime) {
        SystraceSection s("ReactInstance::loadScript");
        // 省略部分代码

        // 核心代码!执行 JS bundle
        runtime.evaluateJavaScript(buffer, sourceURL);

        // 处理异常情况
        if (!jsErrorHandler_->hasHandledFatalError()) {
          jsErrorHandler_->setRuntimeReady();
        }

        // 省略部分代码
        
        // 判断 runtime 是否还在
        // 如果是则调度执行 JS bundle 期间积压的任务
        if (auto strongBufferedRuntimeExecuter =
                weakBufferedRuntimeExecuter.lock()) {
          strongBufferedRuntimeExecuter->flush();
        }
      });
}

应用层:JSI 的消费者

如果把 JSI 比喻为工具的话,应用层就是该工具的使用者

JSI 的抽象层定义跨引擎统一的 JavaScript 运行时接口;服务层基于这些接口为 React Native 提供调度、模块和 UI 等运行时能力;应用层则通过 binding 建立 JS Runtime 与 Host(Native) Runtime 之间能力暴露机制,是的双方都可以互相调用彼此的能力

上面这句话可能有点抽象,我们用一个 binding 例子来说明一下

还记得我们上一小节讨论的 ReactInstance::initializeRuntime 的方法吗?在第 15 行,它调用了 RuntimeSchedulerBinding::createAndInstallIfNeeded 方法

这个方法就是一个 binding,让我们来看看它 bind 了什么:

cpp 复制代码
// in packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeSchedulerBinding.cpp

std::shared_ptr<RuntimeSchedulerBinding>
RuntimeSchedulerBinding::createAndInstallIfNeeded(
    jsi::Runtime& runtime,
    const std::shared_ptr<RuntimeScheduler>& runtimeScheduler) {
  auto runtimeSchedulerModuleName = "nativeRuntimeScheduler";

  // 试图从 runtime 的 global 中拿到 nativeRuntimeScheduler 对象
  auto runtimeSchedulerValue =
      runtime.global().getProperty(runtime, runtimeSchedulerModuleName);
  // 如果不成功,就自己创建一个
  if (runtimeSchedulerValue.isUndefined()) {
    // 创建了一个 RuntimeSchedulerBinding 类的实例
    auto runtimeSchedulerBinding =
        std::make_shared<RuntimeSchedulerBinding>(runtimeScheduler);
    // 通过 JSI 的 createFromHostObject 方法把 runtimeSchedulerBinding 包装成一个 JS 可以调用的对象
    auto object =
        jsi::Object::createFromHostObject(runtime, runtimeSchedulerBinding);
    // 把 JS 可以调用的对象挂到 global 的 nativeRuntimeScheduler 对象下
    runtime.global().setProperty(
        runtime, runtimeSchedulerModuleName, std::move(object));

    return runtimeSchedulerBinding;
  }
	// 省略部分代码
}

可以看到,这个 RuntimeSchedulerBinding 本质上就是把与自己同名的类实例化后 "绑定" 到 JS Runtime 的 global 对象上

正常来说 C++ 的对象并不能直接挂到 JS 的对象上,但是通过 jsi::Object::createFromHostObject 方法的封装,object 对象得以顺利把自己 "绑定" 到 JS Runtime 的 global 对象上

jsi::Object::createFromHostObject 就是 JSI 定义的其中一个能力,它由需要接入的引擎自己实现,而 binding 是基于这一底层能力,做了上层的具体能力封装(在 RuntimeSchedulerBinding 例子中是任务调度能力的封装)

RuntimeSchedulerBinding 这个例子里,它对 JS 暴露了自己类里的方法;与之相对的,JS 也可以通过类似方法向 Native 暴露自己的能力,所以我才说 binding 是 JS Runtime 与 Host(Native) Runtime 之间能力暴露机制

至于 RuntimeSchedulerBinding 具体暴露了哪些能力,本专栏的下一篇关于 RuntimeScheduler 的文章会有详细说明,本文就不展开了

总结

JSI 通过分层的方式构建了 React Native 新架构中的 JavaScript 运行时体系:抽象层定义了跨引擎统一的运行时能力接口 ,服务层在这些能力之上构建并管理 React Native 的调度、模块与渲染等核心运行机制 ,而应用层则通过各类 bindings 使用这些能力来实现具体的 UI 和业务逻辑

三者共同构成了一条从底层运行时能力到上层应用功能的完整链路,使 JS 与 Native 能够在同一运行时环境中高效协作

相关推荐
AD_wjk1 小时前
Android13系统集成方案
前端
somebody1 小时前
零经验学 react 的第6天 - 循环渲染和条件渲染
前端
青晚舟2 小时前
AI 时代前端还要学 Docker & K8s 吗?我用一次真实部署经历说清楚
前端·github
墨鱼笔记2 小时前
不使用微前端:如何实现主应用和子模块动态管理与通信实现
前端
兆子龙2 小时前
前端工程师转型 AI Agent 工程师:后端能力补全指南
前端·javascript
长安11082 小时前
web后端----HTTP协议与浏览器F12
前端·网络协议·http
前端大波2 小时前
Web Vitals 与前端性能监控实战
前端·javascript
AlienZHOU3 小时前
从零开始,跟着写一个产品级 Coding Agent
前端