普通视图

发现新文章,点击刷新页面。
今天 — 2025年11月25日首页

React Native 通信机制详解 - 新架构

作者 Tamarous
2025年11月25日 10:39

React Native 通信机制详解 - 新架构

一、概述

本篇是 React Native 通信机制详解的第二篇文章,主要介绍 React Native 新架构下的通信机制。如果想要了解旧架构下的通信机制,可以参考上一篇文章。

二、示例

我们仍然从前文末展示的新架构下注册一个原生模块的示例开始。如果你有兴趣自己动手实践下整个过程,可以参考 React Native 的官方文档

1.定义模块规范(TypeScript)

首先,我们需要使用 TypeScript 定义模块的接口规范。通过继承 TurboModule 接口,我们可以声明模块的方法签名,包括参数类型和返回值类型。

// CalcModule.ts
import type { TurboModule } from 'react-native/Libraries/TurboModule/RCTExport';
import { TurboModuleRegistry } from 'react-native';

export interface Spec extends TurboModule {
  add(a: number, b: number): Promise<number>;
}

export default TurboModuleRegistry.get<Spec>('CalcModule');

最后将这个文件命名为 NativeCalcModule.ts。文件名中的 Native 前缀是一个约定的命名方式,表示这是一个原生模块,这样 Codegen 工具才能生成对应的原生代码。

2.使用 Codegen 生成接口代码

接下来,我们使用 Codegen 工具将 TypeScript 定义转换为 Objective-C 桥接代码以及 JSI 绑定代码。

.
├── CalcModule
│   ├── CalcModule-generated.mm
│   └── CalcModule.h
├── CalcModuleJSI-generated.cpp
├── CalcModuleJSI.h
├── RCTAppDependencyProvider.h
├── RCTAppDependencyProvider.mm
├── RCTModulesConformingToProtocolsProvider.h
├── RCTModulesConformingToProtocolsProvider.mm
├── RCTThirdPartyComponentsProvider.h
├── RCTThirdPartyComponentsProvider.mm
├── ReactAppDependencyProvider.podspec
└── ReactCodegen.podspec.json

1 directory, 12 files

在生成目录下,CalcModule.hCalcModule-generated.mm是 Objective-C 桥接代码,CalcModuleJSI.hCalcModuleJSI-generated.cpp是 JSI 绑定代码。

3.实现原生模块

最后,我们实现原生模块的具体功能。

@interface CalcModule : NSObject <NativeCalcSpec>
@end

@implementation CalcModule
RCT_EXPORT_MODULE()

- (void)add:(double)a b:(double)b resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
{
  NSNumber *result = @(a + b);
  resolve(result);
}
@end

三、模块注册机制

让我们结合上文中的示例代码生成的内容,来深入了解新架构下模块注册和调用的完整流程。

3.1 Codegen 生成代码

Objc 桥接层

Objective-C 这一层首先定义了一个 NativeCalcSpec 协议,它继承自 RCTBridgeModuleRCTTurboModule。开发者需要自己来实现这个协议,来提供 add 方法的实际实现。其次,定义了一个 NativeCalcSpecBase 类,它是 NativeCalcSpec 协议的基类,用于实现一些公共的方法。最后,定义了一个 NativeCalcSpecJSI 类,它是一个 Objective-C 桥接类,继承自 ObjCTurboModule,用于构建方法映射表并将 C++ 调用转发给 Objective-C 方法。

// CalcModule.h
#ifndef CalcModule_H
#define CalcModule_H

NS_ASSUME_NONNULL_BEGIN

@protocol NativeCalcSpec <RCTBridgeModule, RCTTurboModule>

- (void)add:(double)a
          b:(double)b
    resolve:(RCTPromiseResolveBlock)resolve
     reject:(RCTPromiseRejectBlock)reject;

@end

@interface NativeCalcSpecBase : NSObject {
@protected
facebook::react::EventEmitterCallback _eventEmitterCallback;
}
- (void)setEventEmitterCallback:(EventEmitterCallbackWrapper *)eventEmitterCallbackWrapper;

@end

namespace facebook::react {
  /**
   * ObjC++ class for module 'NativeCalc'
   */
  class JSI_EXPORT NativeCalcSpecJSI : public ObjCTurboModule {
  public:
    NativeCalcSpecJSI(const ObjCTurboModule::InitParams &params);
  };
} // namespace facebook::react

NS_ASSUME_NONNULL_END
#endif // CalcModule_H
// CalcModule-generated.mm
#import "CalcModule.h"

@implementation NativeCalcSpecBase

- (void)setEventEmitterCallback:(EventEmitterCallbackWrapper *)eventEmitterCallbackWrapper
{
  _eventEmitterCallback = std::move(eventEmitterCallbackWrapper->_eventEmitterCallback);
}
@end

namespace facebook::react {
  
  static facebook::jsi::Value __hostFunction_NativeCalcSpecJSI_add(facebook::jsi::Runtime& rt, TurboModule &turboModule, const facebook::jsi::Value* args, size_t count) {
    return static_cast<ObjCTurboModule&>(turboModule).invokeObjCMethod(rt, PromiseKind, "add", @selector(add:b:resolve:reject:), args, count);
  }

  NativeCalcSpecJSI::NativeCalcSpecJSI(const ObjCTurboModule::InitParams &params)
    : ObjCTurboModule(params) {
      
        methodMap_["add"] = MethodMetadata {2, __hostFunction_NativeCalcSpecJSI_add};
        
  }
} // namespace facebook::react
JSI 桥接层

JSI 的这一层中主要是定义了两个类: NativeCalcCxxSpecJSINativeCalcCxxSpec。前者是一个 C++ 抽象类,定义了模块方法的 C++ 接口。后者是一个 C++ 模板类,用于创建 Host Object,将 JavaScript 调用转发给原生方法,以及处理参数和返回值的类型转换。

// CalcModuleJSI.h
#include <ReactCommon/TurboModule.h>
#include <react/bridging/Bridging.h>

namespace facebook::react {
  class JSI_EXPORT NativeCalcCxxSpecJSI : public TurboModule {
  protected:
    NativeCalcCxxSpecJSI(std::shared_ptr<CallInvoker> jsInvoker);

  public:
    virtual jsi::Value add(jsi::Runtime &rt, double a, double b) = 0;
  };

  template <typename T>
  class JSI_EXPORT NativeCalcCxxSpec : public TurboModule {
  public:
    jsi::Value create(jsi::Runtime &rt, const jsi::PropNameID &propName) override {
      return delegate_.create(rt, propName);
    }

    std::vector<jsi::PropNameID> getPropertyNames(jsi::Runtime& runtime) override {
      return delegate_.getPropertyNames(runtime);
    }

    static constexpr std::string_view kModuleName = "CalcModule";

  protected:
    NativeCalcCxxSpec(std::shared_ptr<CallInvoker> jsInvoker)
      : TurboModule(std::string{NativeCalcCxxSpec::kModuleName}, jsInvoker),
        delegate_(reinterpret_cast<T*>(this), jsInvoker) {}


  private:
    class Delegate : public NativeCalcCxxSpecJSI {
    public:
      Delegate(T *instance, std::shared_ptr<CallInvoker> jsInvoker) :
        NativeCalcCxxSpecJSI(std::move(jsInvoker)), instance_(instance) {

      }

      jsi::Value add(jsi::Runtime &rt, double a, double b) override {
        static_assert(
            bridging::getParameterCount(&T::add) == 3,
            "Expected add(...) to have 3 parameters");

        return bridging::callFromJs<jsi::Value>(
            rt, &T::add, jsInvoker_, instance_, std::move(a), std::move(b));
      }

    private:
      friend class NativeCalcCxxSpec;
      T *instance_;
    };

    Delegate delegate_;
  };

} // namespace facebook::react
#include "CalcModuleJSI.h"

namespace facebook::react {

  static jsi::Value __hostFunction_NativeCalcCxxSpecJSI_add(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {
    return static_cast<NativeCalcCxxSpecJSI *>(&turboModule)->add(
      rt,
      count <= 0 ? throw jsi::JSError(rt, "Expected argument in position 0 to be passed") : args[0].asNumber(),
      count <= 1 ? throw jsi::JSError(rt, "Expected argument in position 1 to be passed") : args[1].asNumber()
    );
  }

  NativeCalcCxxSpecJSI::NativeCalcCxxSpecJSI(std::shared_ptr<CallInvoker> jsInvoker)
    : TurboModule("CalcModule", jsInvoker) {
    methodMap_["add"] = MethodMetadata {2, __hostFunction_NativeCalcCxxSpecJSI_add};
  }
} // namespace facebook::react

3.2 原生模块实现

开发者需要创建一个遵循 NativeCalcSpec 协议的类:

@interface CalcModule : NSObject <NativeCalcSpec>
@end

@implementation CalcModule
RCT_EXPORT_MODULE()

- (void)add:(double)a b:(double)b resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject
{
  NSNumber *result = @(a + b);
  resolve(result);
}
@end

并且用 RCT_EXPORT_MODULE() 宏来将这个类暴露给 React Native。RCT_EXPORT_MODULE()在编译时会展开成:

+ (NSString *)moduleName { return @"CalcModule"; }
+ (void)load { RCTRegisterTurboModule(self); }

这个过程同旧架构中的 RCT_EXPORT_MODULE() 宏类似,但也不完全相同。旧架构中的RCT_EXPORT_MODULE()宏会在编译时调用RCTRegisterModule(class)将模块注册到RCTModuleRegistry中,而新架构中的RCT_EXPORT_MODULE()宏则会在运行时调用RCTRegisterTurboModule(class)将模块注册到TurboModuleRegistry中。

3.3 RCTRegisterTurboModule 实现原理

RCTRegisterTurboModule 是新架构中的核心注册机制,它主要完成以下三个任务:

1.创建模块工厂

static std::unordered_map<std::string, TurboModuleFactory> turboModuleFactories;

void RCTRegisterTurboModule(Class moduleClass) {
  NSString *moduleName = [moduleClass moduleName];
  turboModuleFactories[moduleName.UTF8String] = [moduleClass](jsi::Runtime &runtime) {
    return std::make_shared<NativeCalcSpecJSI>(runtime, moduleClass);
  };
}

这里使用 C++ 模板机制创建了一个模块工厂函数,用于按需创建模块实例。工厂函数以模块名为键,存储在全局的 turboModuleFactories 映射表中。

2.维护模块元数据

class TurboModuleMetadata {
  std::string name;
  std::unordered_map<std::string, MethodMetadata> methods;
  std::shared_ptr<CallInvoker> jsInvoker;
};

void RCTRegisterTurboModule(Class moduleClass) {
  // ... 前面的代码 ...
  
  // 收集模块的方法信息
  auto metadata = std::make_shared<TurboModuleMetadata>();
  metadata->name = moduleName.UTF8String;
  metadata->jsInvoker = jsInvoker;
  
  // 通过运行时反射获取模块的方法列表
  unsigned int methodCount;
  Method *methods = class_copyMethodList(moduleClass, &methodCount);
  for (unsigned int i = 0; i < methodCount; i++) {
    Method method = methods[i];
    metadata->methods[sel_getName(method_getName(method))] = {
      .argumentCount = method_getNumberOfArguments(method) - 2,
      .returnType = method_getReturnType(method)
    };
  }
  free(methods);
  
  // 存储元数据
  turboModuleMetadata[moduleName.UTF8String] = metadata;
}

这部分代码负责维护模块的元数据,包括模块名、方法列表、参数类型等信息。这些信息在后续的方法调用中用于参数类型检查和转换。

3.JSI 绑定

void RCTRegisterTurboModule(Class moduleClass) {
  // ... 前面的代码 ...
  
  // 创建 JSI 绑定
  jsi::Runtime &runtime = ...; // 获取 JSI 运行时
  auto moduleObject = jsi::Object(runtime);
  
  // 为每个方法创建 JSI 函数
  for (const auto &method : metadata->methods) {
    moduleObject.setProperty(
      runtime,
      method.first.c_str(),
      jsi::Function::createFromHostFunction(
        runtime,
        jsi::PropNameID::forAscii(runtime, method.first),
        method.second.argumentCount,
        [metadata, method](jsi::Runtime &rt, const jsi::Value &thisVal, const jsi::Value *args, size_t count) {
          // 参数类型检查和转换
          // 调用原生方法
          // 返回结果转换
          return ...; // 返回 JSI 值
        }
      )
    );
  }
  
  // 将模块对象注册到 TurboModuleRegistry
  runtime.global().getProperty(runtime, "__turboModuleProxy")
    .asObject(runtime)
    .setProperty(runtime, moduleName.UTF8String, moduleObject);
}

这部分代码通过 JSI 机制将原生模块暴露给 JavaScript 环境。它为每个原生方法创建对应的 JSI 函数,使其能够直接访问原生代码,无需通过 Bridge 进行序列化和反序列化。

通过以上三个步骤,RCTRegisterTurboModule 完成了新架构中原生模块的注册过程。

3.4 小结

在新架构下,定义一个原生模块需要先在 JavaScript 层定义一个 Spec 文件,然后用 Codegen 生成 JSI、C++ 的胶水代码。下面总结一下这些不同层次代码的作用:

JavaScript 层

Spec 接口:TypeScript 定义的模块接口规范 TurboModuleRegistry:负责获取和管理 TurboModule 实例

C++ 层

NativeCalcCxxSpecJSI:C++ 抽象类,定义了模块方法的 C++ 接口 NativeCalcCxxSpec:C++ 模板类,用于创建 Host Object TurboModule:所有 TurboModule 的基类

ObjC++ 层

NativeCalcSpecJSI:ObjC++ 桥接类,继承自 ObjCTurboModule ObjCTurboModule:ObjC++ TurboModule 的基类

Objective-C 层

NativeCalcSpec:Objective-C 协议,定义了原生模块需要实现的方法 CalcModule:开发者实现的原生模块类

四、通信流程详解

让我们继续分析下 JavaScript 和原生模块实现通信的原理。

当 JavaScript 代码首次导入模块时:

import CalcModule from './NativeCalcModule';

实际上是调用了 TurboModuleRegistry.get<Spec>('CalcModule') 方法。这个方法的执行流程如下:

  1. 检查模块缓存:首先检查模块是否已经被加载过,如果已加载则直接返回缓存的实例。

  2. 获取原生模块:如果模块未加载,则通过 JSI 调用原生方法 __turboModuleProxy.getModule('CalcModule')

  3. 创建 Host Object:原生层会调用之前注册的模块工厂函数,创建一个 NativeCalcCxxSpec 实例作为 Host Object。

  4. 绑定方法:Host Object 会将所有在 TypeScript 中定义的方法绑定到 JavaScript 对象上。

  5. 缓存模块实例:将创建的模块实例缓存起来,以便后续使用。

  6. 返回 JavaScript 对象:最终返回一个 JavaScript 对象,该对象的方法实际上是与原生方法绑定的 JSI 函数。

当 JavaScript 调用 CalcModule.add(1, 2) 时:

  1. JavaScript 引擎执行调用:JavaScript 引擎识别到这是对一个对象方法的调用。

  2. JSI 拦截调用:由于 CalcModule 是一个 Host Object,其方法调用会被 JSI 拦截。

  3. 查找方法映射:JSI 根据方法名 add 在 Host Object 的 methodMap_ 中查找对应的处理函数,找到 __hostFunction_NativeCalcCxxSpecJSI_add

  4. 执行 Host 函数:调用 __hostFunction_NativeCalcCxxSpecJSI_add 函数,传入 JavaScript 运行时、模块实例和参数数组。

  5. 参数类型转换:Host 函数会将 JavaScript 参数转换为 C++ 类型,例如将 args[0]args[1] 转换为 double 类型。

return static_cast<NativeCalcCxxSpecJSI *>(&turboModule)->add(
  rt,
  count <= 0 ? throw jsi::JSError(rt, "Expected argument in position 0 to be passed") : args[0].asNumber(),
  count <= 1 ? throw jsi::JSError(rt, "Expected argument in position 1 to be passed") : args[1].asNumber()
);
  1. 调用 C++ 方法:转发调用到 NativeCalcCxxSpecJSI::add 方法。

  2. 桥接到 ObjC++:C++ 层通过 bridging::callFromJs 将调用转发到 ObjC++ 层的 NativeCalcSpecJSI

class Delegate : public NativeCalcCxxSpecJSI {
    public:
      jsi::Value add(jsi::Runtime &rt, double a, double b) override {
        static_assert(
            bridging::getParameterCount(&T::add) == 3,
            "Expected add(...) to have 3 parameters");

        return bridging::callFromJs<jsi::Value>(
            rt, &T::add, jsInvoker_, instance_, std::move(a), std::move(b));
      }
};
  1. ObjC++ 层处理NativeCalcSpecJSIinvokeObjCMethod 方法负责将 JSI 参数转换为 Objective-C 类型,并创建 Promise 对象。
- (jsi::Value)invokeObjCMethod:(jsi::Runtime &)runtime
                   methodKind:(MethodKind)methodKind
                   methodName:(const char *)methodName
                      method:(SEL)selector
                        args:(const jsi::Value *)args
                        count:(size_t)count {
  // 创建 Promise 对象
  if (methodKind == MethodKind::Promise) {
    return createPromise(runtime,
      ^(jsi::Runtime &rt, std::shared_ptr<Promise> promise) {
        // 创建 resolve 和 reject 回调
        RCTPromiseResolveBlock resolveBlock = ^(id result) {
          promise->resolve(rt, convertObjCValueToJSIValue(rt, result));
        };
        RCTPromiseRejectBlock rejectBlock = ^(NSString *code, NSString *message, NSError *error) {
          promise->reject(rt, [code UTF8String], [message UTF8String], error);
        };
        
        // 调用原生方法
        [instance selector:args[0].asNumber()
                        b:args[1].asNumber()
                  resolve:resolveBlock
                   reject:rejectBlock];
    });
  }
}
  1. 原生方法执行:原生方法执行完成后,通过 Promise 的 resolve 或 reject 回调返回结果。

  2. 结果返回

  • 如果执行成功,调用 resolve 回调,将 Objective-C 的结果值转换为 JSI 值。
  • 如果执行失败,调用 reject 回调,将错误信息传递给 JavaScript。
  • JSI 层将结果传递给 Promise 对象,最终返回给 JavaScript 层。

这种多层转发机制不仅确保了 JavaScript 和原生代码之间的类型安全和高效通信,还保持了代码的模块化和可维护性。完整的通信流程如下图所示:

sequenceDiagram
    participant JS as JavaScript
    participant TMR as TurboModuleRegistry
    participant HO as Host Object
    participant JSI as JSI Interface
    participant JSIC as JSI C++ Bindings
    participant OBJC as ObjC++ Bridge
    participant Native as Native Module

    %% 模块注册阶段
    Note over JS,Native: 模块注册阶段
    JS->>JS: 定义TypeScript接口规范(Spec)
    Native->>Native: 实现NativeCalcSpec协议
    
    %% Codegen生成代码
    Note over JSI,Native: Codegen生成代码
    JSI->>JSIC: 生成NativeCalcCxxSpecJSI抽象类
    JSIC->>JSIC: 生成NativeCalcCxxSpec模板类
    OBJC->>OBJC: 生成NativeCalcSpecJSI类
    
    %% 运行时注册
    Note over TMR,Native: 运行时注册
    Native->>OBJC: 调用RCTRegisterTurboModule注册模块
    OBJC->>OBJC: 创建NativeCalcSpecJSI实例
    OBJC->>OBJC: 构建methodMap_方法映射表
    OBJC->>JSI: 通过JSI注册模块到JavaScript运行时
    JSI->>TMR: 注册到TurboModuleRegistry全局对象
    
    %% JavaScript调用阶段
    Note over JS,Native: JavaScript调用阶段
    JS->>TMR: TurboModuleRegistry.get<Spec>('CalcModule')
    TMR->>JSI: 检查模块是否已注册
    JSI->>JSIC: 创建C++层Host Object
    JSIC->>HO: 通过NativeCalcCxxSpec创建JavaScript对象
    JSIC->>HO: 绑定方法到JavaScript对象
    HO-->>JS: 返回JavaScript对象

    %% 方法调用和结果返回阶段
    Note over JS,Native: 方法调用和结果返回阶段
    JS->>HO: CalcModule.add(a, b)
    HO->>JSIC: 调用__hostFunction_NativeCalcCxxSpecJSI_add
    JSIC->>JSIC: 参数类型检查和转换(args[0].asNumber())
    JSIC->>OBJC: 通过methodMap_查找对应的方法
    OBJC->>OBJC: 调用NativeCalcSpecJSI.invokeObjCMethod
    OBJC->>OBJC: 创建Promise(resolve/reject回调)
    OBJC->>Native: 调用add:b:resolve:reject:
    Native->>Native: 执行原生计算逻辑
    Native-->>OBJC: 调用resolve回调返回结果
    OBJC-->>JSIC: 将ObjC值转换为JSI值类型
    JSIC-->>HO: 将结果传递给Promise对象
    HO-->>JS: 通过Promise resolve返回结果

五、小结

通过上述详细分析,我们深入了解了 React Native 新架构下 JavaScript 和原生模块之间的通信机制。相比旧架构,新架构在通信方面有以下几个显著改进:

  1. 直接通信:通过 JSI(JavaScript Interface)实现 JavaScript 和原生代码的直接通信,无需通过 Bridge 进行序列化和反序列化,大幅提升性能。

  2. 类型安全:借助 Codegen 工具,从 TypeScript 接口定义自动生成原生代码,确保了 JavaScript 和原生代码之间的类型一致性,减少了运行时错误。

  3. 模块化设计:TurboModule 系统允许按需加载原生模块,减少了应用启动时间和内存占用。

  4. 多层架构:新架构采用了多层设计(JavaScript、JSI、C++、ObjC++、Objective-C),每一层都有明确的职责,使得代码更加模块化和可维护。

React Native 通信机制详解 - 旧架构

作者 Tamarous
2025年11月25日 10:37

React Native 通信机制详解 - 旧架构

一、概述

前文中,我们介绍了 React Native 的新旧两种架构,并在文章末尾介绍了如何在这两种架构中注册和实现一个原生模块。由于篇幅限制,我们并未对其底层原理进行深入介绍。接下来,我们将聚焦于此,深入探讨 React Native 新旧架构下原生模块和 JavaScript 调用的底层实现。

本篇是 React Native 通信机制详解系列的第一篇文章,主要介绍 React Native 旧架构下的通信机制。如果想要了解 React Native 新架构下的通信机制,请点击这里

二、示例

让我们回顾一下前文中注册原生模块的过程:

1.定义原生模块类

首先需要创建一个继承自 NSObject 并遵循 RCTBridgeModule 协议的类。

// CalcModule.h
#import <React/RCTBridgeModule.h>

@interface CalcModule : NSObject <RCTBridgeModule>
@end

2.实现模块方法

在实现文件中,我们使用 RCT_EXPORT_MODULE() 宏来导出模块,并通过 RCT_EXPORT_METHOD 宏来导出具体的方法。

// CalcModule.m
#import "CalcModule.h"

@implementation CalcModule

RCT_EXPORT_MODULE()

RCT_EXPORT_METHOD(add:(NSInteger)a
                  b:(NSInteger)b
                  callback:(RCTResponseSenderBlock)callback)
{
    NSInteger result = a + b;
    callback(@[@(result)]);
}

@end

3.导出模块到 JavaScript

最后,我们在 JavaScript 端使用这个原生模块。

// Calculator.js
import { NativeModules } from 'react-native';

const { CalcModule } = NativeModules;

export const add = (a, b) => {
  return new Promise((resolve) => {
    CalcModule.add(a, b, (result) => {
      resolve(result);
    });
  });
};

三、模块注册机制

在旧架构中,原生模块的注册和原生模块中方法的暴露分别是通过 RCT_EXPORT_MODULE()RCT_EXPORT_METHOD宏来实现的。将上述代码进行宏展开:

// RCT_EXPORT_MODULE() 宏展开
@implementation CalcModule

// 生成模块名映射函数
+ (NSString *)moduleName {
    return @"CalcModule";
}

// 注册模块到 Bridge
+ (void)load {
    RCTRegisterModule(self);
}

// RCT_EXPORT_METHOD 宏展开
- (void)__rct_export__add_a_b_callback {
    RCTMethodInfo methodInfo = {
        .objcName = "add:b:callback:",
        .jsName = "add",
        .isSync = NO
    };
    RCTRegisterMethodInfo(self, methodInfo);
}

// 原始方法实现
- (void)add:(NSInteger)a
          b:(NSInteger)b
  callback:(RCTResponseSenderBlock)callback
{
    NSInteger result = a + b;
    callback(@[@(result)]);
}

@end

RCT_EXPORT_MODULE() 宏会在类的 +load 方法中调用 RCTRegisterModule(class) 函数,将模块注册到 Bridge。RCT_EXPORT_METHOD 宏则会调用 RCTRegisterMethodInfo,将方法信息注册到 Bridge。

// RCTRegisterModule 实现
static NSMutableArray<Class> *RCTModuleClasses;
static dispatch_once_t onceToken;

void RCTRegisterModule(Class moduleClass)
{
    dispatch_once(&onceToken, ^{
        RCTModuleClasses = [NSMutableArray new];
    });

    // 确保类遵循 RCTBridgeModule 协议
    if ([moduleClass conformsToProtocol:@protocol(RCTBridgeModule)]) {
        // 添加到全局模块注册表
        [RCTModuleClasses addObject:moduleClass];
    }
}

这个方法的本质是将当前 module 添加到全局变量 RCTModuleClasses 中。

// RCTRegisterMethodInfo 实现
static NSMutableDictionary<NSString *, NSMutableSet<RCTMethodInfo *> *> *methodsMap;
static dispatch_once_t methodMapOnceToken;

void RCTRegisterMethodInfo(Class moduleClass, RCTMethodInfo methodInfo)
{
    dispatch_once(&methodMapOnceToken, ^{
        methodsMap = [NSMutableDictionary new];
    });

    // 获取或创建模块的方法集合
    NSString *moduleName = [moduleClass moduleName];
    NSMutableSet *methods = methodsMap[moduleName];
    if (!methods) {
        methods = [NSMutableSet new];
        methodsMap[moduleName] = methods;
    }

    // 添加方法信息到方法集合
    RCTMethodInfo *info = [[RCTMethodInfo alloc] init];
    info.objcName = methodInfo.objcName;
    info.jsName = methodInfo.jsName;
    info.isSync = methodInfo.isSync;
    [methods addObject:info];
}

这个方法的本质是将当前 module 对应的方法信息添加到全局变量 methodsMap 中。

当 RCTBridge 初始化时,会遍历 RCTModuleClasses 数组,为每个注册的模块类创建实例:

- (void)initModules
{
    // 遍历已注册的模块类
    for (Class moduleClass in RCTModuleClasses) {
        // 创建模块实例
        id<RCTBridgeModule> module = [[moduleClass alloc] init];
        
        // 将模块实例添加到 bridge 的模块表中
        NSString *moduleName = [moduleClass moduleName];
        _modulesByName[moduleName] = module;
    }
}

当 JavaScript 端调用 CalcModule.add 方法时,会先经过 MessageQueue:

// MessageQueue.js
class MessageQueue {
  callNativeMethod(moduleID, methodID, params, onFail, onSucc) {
    // 将调用信息放入队列
    this.enqueueNativeCall(moduleID, methodID, params, onFail, onSucc);
    // 触发队列刷新
    this.flushQueue();
  }

  flushQueue() {
    // 将队列中的调用序列化为 JSON
    const calls = JSON.stringify(this._queue);
    // 通过 Bridge 发送到原生层
    global.nativeFlushQueueImmediate(calls);
  }
}

而在 React Native 初始化时,会通过 JavaScriptCore 引擎为 JavaScript 环境注入一个全局函数 nativeFlushQueueImmediate。这个注入过程发生在 RCTBridge 初始化时:

// RCTBridge.m
- (void)setupJSExecutor
{
    // 获取 JSContext
    JSContext *context = [JSContext new];
    
    // 注入全局函数
    context[@"nativeFlushQueueImmediate"] = ^(JSValue *calls) {
        // 将 JSValue 转换为 NSString
        NSString *callsString = [calls toString];
        
        // 解析调用请求
        NSArray *requests = [NSJSONSerialization JSONObjectWithData:[callsString dataUsingEncoding:NSUTF8StringEncoding]
                                                         options:0
                                                           error:NULL];
        
        // 处理每个调用请求
        for (NSDictionary *request in requests) {
            NSNumber *moduleID = request[@"moduleID"];
            NSString *methodName = request[@"methodID"];
            NSArray *args = request[@"args"];
            
            // 转发到 Bridge 处理
            [self _handleRequestNumber:moduleID
                              method:methodName
                           arguments:args];
        }
    };
}

因此当 JavaScript 调用 global.nativeFlushQueueImmediate(calls) 时,实际上是调用了上述注入的函数。这个函数会将 JavaScript 传入的调用队列转换为原生数据结构,然后解析出每个调用请求的模块 ID、方法名和参数,最终将请求转发给 RCTBridge_handleRequestNumber:method:arguments: 方法处理

// RCTBridge.m
- (void)_handleRequestNumber:(NSNumber *)requestNumber
                    method:(NSString *)method
                   arguments:(NSArray *)arguments
{
    // 序列化参数
    NSData *messageData = [NSJSONSerialization dataWithJSONObject:arguments
                                                        options:0
                                                          error:NULL];
    // 发送到原生模块
    [self enqueueJSCall:method args:messageData];
}

最后,原生层通过 invokeMethod 方法找到并执行对应的原生实现:

- (void)invokeMethod:(NSString *)moduleName
           methodName:(NSString *)methodName
            arguments:(NSArray *)arguments
{
    // 获取模块的方法集合
    NSMutableSet<RCTMethodInfo *> *methods = methodsMap[moduleName];
    
    // 查找对应的方法信息
    RCTMethodInfo *methodInfo = nil;
    for (RCTMethodInfo *info in methods) {
        if ([info.jsName isEqualToString:methodName]) {
            methodInfo = info;
            break;
        }
    }
    
    if (methodInfo) {
        // 获取模块实例
        id<RCTBridgeModule> module = _modulesByName[moduleName];
        
        // 通过 performSelector 调用原生方法
        SEL selector = NSSelectorFromString(methodInfo.objcName);
        [module performSelector:selector withArguments:arguments];
    }
}

四、流程图

sequenceDiagram
    participant JS as JavaScript
    participant Bridge as Bridge
    participant Native as Native Module
    participant Runtime as Runtime System

    %% 模块注册阶段
    Note over Native: 定义原生模块类 CalcModule
    Native->>Runtime: RCT_EXPORT_MODULE() 宏展开
    activate Runtime
    Runtime->>Runtime: 生成 moduleName 方法
    Runtime->>Runtime: 生成 load 方法
    Runtime->>Bridge: RCTRegisterModule(CalcModule)
    Bridge->>Bridge: 将模块类添加到 RCTModuleClasses
    deactivate Runtime

    Native->>Runtime: RCT_EXPORT_METHOD 宏展开
    activate Runtime
    Runtime->>Runtime: 生成方法信息 RCTMethodInfo
    Runtime->>Bridge: RCTRegisterMethodInfo(CalcModule, methodInfo)
    Bridge->>Bridge: 将方法信息添加到 methodsMap
    deactivate Runtime

    %% Bridge 初始化阶段
    Note over Bridge: RCTBridge 初始化
    Bridge->>Bridge: initModules
    activate Bridge
    Bridge->>Native: 创建模块实例
    Native-->>Bridge: 返回模块实例
    Bridge->>Bridge: 保存实例到 _modulesByName
    deactivate Bridge

    Bridge->>JS: setupJSExecutor
    activate JS
    JS->>JS: 注入 nativeFlushQueueImmediate
    deactivate JS

    %% JavaScript 调用阶段
    Note over JS: 调用 CalcModule.add()
    JS->>JS: 创建 Promise
    JS->>Bridge: MessageQueue.callNativeMethod
    activate Bridge
    Bridge->>Bridge: enqueueNativeCall
    Bridge->>Bridge: flushQueue
    Bridge->>Bridge: nativeFlushQueueImmediate
    Bridge->>Bridge: 解析调用请求
    Bridge->>Native: invokeMethod
    deactivate Bridge

    activate Native
    Native->>Native: 执行 add 方法
    Native->>JS: 通过 callback 返回结果
    deactivate Native

    JS->>JS: Promise resolve

五、小结

本篇文章主要介绍了 React Native 旧架构下原生模块和 JavaScript 调用的底层实现。通过对模块注册机制和 JavaScript 调用流程的分析,我们深入了解了 React Native 旧架构下的通信机制。希望本文能帮助你更好地理解 React Native 的通信机制。

埃安UT super官宣全面开启交付

2025年11月25日 10:28
36氪获悉,据广汽埃安消息,京东联合宁德时代、广汽集团推出的埃安UT super宣布开启交付,首批车主交付仪式将于今日在广州车展广汽埃安展台举行。

今年前10个月全国电力市场交易电量同比增长7.9%

2025年11月25日 10:27
国家能源局今天(11月25日)发布数据显示,今年1月-10月,全国累计完成电力市场交易电量54920亿千瓦时,同比增长7.9%,占全社会用电量比重63.7%,同比提高1.5个百分点。其中,省内交易电量41659亿千瓦时,同比增长6.6%;跨省跨区交易电量13261亿千瓦时,同比增长12.5%。绿电交易电量2627亿千瓦时,同比增长39.4%。(央视新闻)

茅台价格逼近1600元,今日25年飞天散瓶批价报1615元/瓶

2025年11月25日 10:15
36氪获悉,今日酒价披露的批发参考价显示,11月25日,25年飞天茅台原箱较前一日下跌20元,报1630元/瓶;25年飞天茅台散瓶较前一日下跌25元,报1615元/瓶。24年飞天茅台原箱较前一日持平,报1700元/瓶;24年飞天茅台散瓶较前一日下跌15元,报1660元/瓶。

中国身份证注册美区 Apple Developer 个人账号完整教程

作者 HBLOG
2025年11月25日 10:10

一、注册前准备

必备条件

  1. 已有 Apple ID,并开启双重认证(2FA)。
  2. 年满 18 岁。
  3. 拥有中国身份证,姓名需与 Apple ID 一致(拼音形式)。
  4. 准备一个美国地址和电话号码(可用虚拟地址、Google Voice 等)。
  5. 有可以支付美元的信用卡或虚拟卡(用于支付年费 $99)。

二、虚拟信用卡推荐

如果没有国际信用卡,可以使用以下虚拟美卡服务:

名称

特点

适合场景

Wise 虚拟卡

支持美元账户,真实银行背景,审核简单

个人开发者首选

Revolut 卡

欧系金融牌照,美元支付稳定

海外支付频繁用户

StatesCard

专为 App Store、美区订阅设计

美区 Apple 年费支付

Payoneer 虚拟卡

有美元收款账户,可绑 Apple ID

有外贸或收款需求者

建议:提前充值 120 美元,避免支付时因汇率或手续费导致失败。

三、注册步骤

步骤 1:调整 Apple ID 设置

  1. 登录你的 Apple ID 账户。
  2. 国家或地区改为美国(United States)
  3. 填写美国地址(州名、城市、邮编)与美国手机号。
  4. 确认 Apple ID 的姓名与身份证拼音一致。
  5. 开启双重认证(2FA)。

步骤 2:进入开发者注册页

  1. 访问 Apple Developer 官网,点击 “加入 Apple Developer Program”。developer.apple.com/programs/en…
  2. 登录你的 Apple ID,选择 Individual(个人) 类型。
  3. 点击 “Start Your Enrollment” 开始注册。

步骤 3:填写个人信息

  • Entity Type(类型):Individual / Sole Proprietor

  • Full Name(姓名):与身份证拼音一致,例如

    LIU HAIHUA

  • Country / Region(国家/地区):United States

  • Address(地址):填写美国地址(可使用转运或虚拟办公地址)

  • Phone(电话):填写 +1 开头的美国号码

  • Email(邮箱):你的 Apple ID 邮箱

注意:填写信息必须与付款信息一致,否则可能被拒。

步骤 4:支付年费

  • 年费为 99 美元/年
  • 使用国际信用卡或虚拟卡支付。
  • 账单地址需与虚拟卡提供的美国账单地址一致。
  • 支付成功后,等待苹果审核。

步骤 5:身份验证与审核

  1. 支付后,Apple 可能会要求上传身份证。
    • 选择“Government ID”,上传中国身份证正反面照片。
    • 也可选择“Passport”,上传护照。
  2. 审核人员可能会通过邮件或电话联系确认身份。
  3. 一般 1~3 个工作日即可通过。
  4. 通过后,你会收到 “Welcome to the Apple Developer Program” 邮件。

四、注册后操作

  • 登录 App Store Connect 上传应用。
  • 可发布到除中国大陆以外的所有地区。
  • 支持 TestFlight、内购(In-App Purchase)等完整功能。

五、常见问题与注意事项

问题

说明

是否必须美国护照?

不需要,中国身份证或护照均可。

支付失败怎么办?

检查卡片是否支持美元支付、账单地址是否匹配。

虚拟卡会被拒吗?

大部分能通过,但某些卡需多尝试。

账号审核多久?

通常 1~3 个工作日,节假日顺延。

可以上架中国区吗?

不行,美区账号上架不含中国大陆区。

六、建议与总结

  • 若目标是全球上架(包括美国、日本、欧盟等市场),推荐使用美区个人账号
  • 若仅面向国内用户,可使用中国区账号(支持支付宝支付)。
  • 注册美区账号前,务必:
    • 统一姓名拼音;
    • 准备美元信用卡;
    • 使用美国账单地址;
    • 备份所有资料(身份证、付款截图、收据)。

市场监管总局:以计量促进民营经济发展壮大

2025年11月25日 10:02
11月24日,从市场监管总局获悉,近日,市场监管总局印发《计量促进民营经济发展壮大若干措施》,围绕加强政策扶持、征集企业需求、支持科研创新、加大服务力度、加强平台建设、助推节能降碳、支持计量“走出去”以及加强人才培养8个方面,提出系列政策措施,旨在进一步发挥计量对民营经济发展的基础支撑保障作用,助力民营经济高质量发展。《措施》提出发挥大型企业带动作用,向民营经济组织开放共享计量科研基础设施、测量仪器设备、计量实验室和计量数据等资源,为民营经济组织提供一揽子的计量支持。(新华社)

面试官说: "你怎么把 Radio 组件玩出花的,教教我! “

作者 孟祥_成都
2025年11月25日 09:59

前言

面试官问我 radio 组件知道怎么实现吗?我把我的组件库 radio 网页丢给它,我说我这个 radio 组件什么样式都支持,你看这是传统样式:

image.png

你看,这是自定义样式,理论上什么样式都可以:

image.png

image.png

然后我嘿嘿一笑说:"我的 radio 组件本质上不涉及任何样式,只负责岁月静好,使用者负责貌美如花!"

最后面试完毕,面试官很满意,并问这个怎么实现的,我就写一篇文章来说说吧!

这是上面组件的网站地址

更多组件库教程,欢迎在 github 上给个 star,加群交流哦!

本文章及更多案例已经在官网上了,大家也可以去看 本文链接

可自定义的核心

正常的 radio 组件主要是通过 <input> 标签属性 type="radio" 实现的,浏览器都会显示一个默认的样式

image.png

所以我们要自定义样式,就不能使用原生的 radio样式,那么问题来了,你是不是只要实现了 radio 内在的逻辑,其实也就是实现了 radio 组件,有人会问?内在的逻辑是什么呢?

  • 其一,选中态逻辑,就是多个元素,我们只要点击,就是选中态(checked)
  • 其二,传入 disabled 参数,那么这个元素就不能点击(或者 cursor(也就是鼠标的状态)设置为 not-allowed 也就是不可选中)
  • 其三,原生 radio 并不支持 readyonly 状态,但我们自定义组件应该实现,在表单中, readyonly 是一种很常见的业务需求。所以传入 readyonly 参数,那么这个元素就不能改变状态,只能看。
  • 最后最最重要的逻辑是多个 radio 元素组合时,你只能选中其中一个,即单选的逻辑。

所以我们只要实现了上述逻辑,那就是跟原生的单选就没有区别了。

但问题来了,能不能既保持 radio 组件的原本的逻辑,还能支持自定义呢?也就是用户如果还是希望使用原生 radio 我们支持,自定义也支持呢?这样的话,语义性完好的基础上还能自由拓展,简直完美!

答案是肯定的,我们的组件就是这样的。

在介绍核心逻辑之前,我们先说一个小技巧。

radio 组件基本结构

image.png

首先先介绍一一个小技巧,如上图,一般 radio 包含了一个圆圈表示是否选中,圆圈的右边是文字,正常来说,我们点击圆圈才能选中,可这些组件库如何实现的点击圆圈右边的文字也能选中圆圈呢?这就涉及到 <label> 标签了。

如上图是 MDN 中的方式:

  <div>
    <input type="radio" id="huey" name="drone" value="huey" checked />
    <label for="huey">Huey</label>
  </div>
  • 首先需要在 input 上使用 id 属性
  • 然后在 label 组件上使用 for属性,跟 id 的值一致即可实现点击 label 标签就能选中对应的 radio

但这种方式比较繁琐,我们的组件使用的另一种方式达到同样的效果,就是 labelinput 标签包裹就好:

<label>
 <input type="radio" value="huey" />
huey
</label>

好了,接下来我们梳理一下状态的切换逻辑,这样我们实现起来就有一个蓝图:

  • 首先点击 label ,也就是绑定在 label 上的 onClick 事件
  • 然后这个 onClick 事件会触发 input(radio) 上的 onClick 事件
  • input 上的 onClick 事件会触发自身的 onChange 事件,也就是选中的value 值在 onChange 的时候可以设置为点击的 input 的值,
    • 这里有些新同学可能不知道 input 这类表单元素,目的就是收集值,也就是可以传入 value 属性。

整体逻辑很清晰,也很简单,但其中的坑不少,我们把坑介绍完,基本上你就可以实现一个自己的 radio 组件了

核心逻辑梳理

如上所述,我们第一步是给 label 标签绑定 onClick 事件。

  const onLabelClick = function (e) {
    // 只读或禁用时,阻止点击产生任何行为
    if (disabled || readonly) {
      e.preventDefault();
      return;
    }
    rest?.onClick?.(e);
  };

需要注意

  • 当外界传入 disabled 或者 readonly 参数的时候,我们直接 return
  • 注意要使用 e.preventDefault(); 来组织默认事件,默认事件就是点击 label 触发 inputonClick 事件,从而阻止input 上值被选中

然后需要给 input 绑定 onClick 事件

onClick={(e) => {
   // 阻止 input 的点击事件冒泡,避免重复处理
   e.stopPropagation();
}}

这里需要注意的是

  • 调用 e.stopPropagation(); 防止用户在直接点击 input(radio) 的时候,事件冒泡到 label 标签,从而多次触发 labelonClick 事件。

最后,就是在 input 绑定 onChange 事件

 const [checked, setChecked] = useMergeValue(false, {
    value: propsChecked,
    defaultValue: mergeProps.defaultChecked,
  });
  
  const onChange = (e) => {
    e.persist();
    e.stopPropagation();

    // 禁用或只读都不改变状态,不触发外部 onChange
    if (disabled || readonly) return;

    if (context.group) {
      context?.onChangeValue?.(value, e);
    } else if (!('checked' in props) && !checked) {
      setChecked(true);
    }
    if (!checked) {
      propsOnChange?.(true, e);
    }
  };

其中细节很多,我们简单说一下:

  • e.persist();react 17 之前需要(现在都已经 19 版本,是可以删掉这行代码的),大概介绍下它的作用
// React 16 及之前版本的问题,在 setTimeout 中无法获得正常的事件对象的值
const handleClick = (e) => {
  console.log(e.type); // 正常访问
  
  setTimeout(() => {
    console.log(e.type); // null 或 undefined!
    console.log(e.target); // null!
  }, 1000);
};
  • e.stopPropagation(); 阻止事件冒泡
    • 如果 Radio 嵌套 Radio,点击里面的 Radio 就会让 onChnage 事件冒泡到外层去,这不是我们希望的,所以隔离一下
  • if (disabled || readonly) return; 检查是否是 disabled 或者 readonly 状态,这样的状态不能让它触发 onChange 事件
if (context.group) {
  context?.onChangeValue?.(value, e);
}

这个我们暂且不讲,是要配合 Radio.Group 组件使用,这个 context 是使用 useContext api 获取的 Radio.Group 透传的数据。也就是状态最终会被 Radio.Group 接管。

} else if (!('checked' in props) && !checked) {
  setChecked(true);
}
  • 作用:独立使用时的状态管理

  • !('checked' in props):检查是否是非受控组件,这是识别是否是受控还是非受控组件的关键。

    • 没有传入 checked prop → 组件自己管理状态

这里非常非常细节,有人可能疑惑了,为什么要这么做,而不是用 checked === undefined 来判断,因为不传的值默认不是 undefined 吗?我们来解释一下

大家需要注意,假设你这样传入 checked 参数

<Radio checked={undefiend} />
  • 'checked' in props 得到的是 true 因为你还是传了 undefined

只有这样

<Radio />
  • 'checked' in props 得到的是 false, 也就是什么也没传,代表是非受控组件、
} else if (!('checked' in props) && !checked) {
  setChecked(true);
}

然后 setChecked(true); 这个很关键,有的人说,!('checked' in props) 不是代表非受控组件吗,非受控组件是组件自己控制值,怎么还有直接 setCheck 控制,这不是受控组件的控制的方式吗?

这里需要解释两点

  • 首先,很多组件库,一般都会用受控的形式来模拟非受控,为什么呢?因为我们要确确实实拿到 Radio 组件的 checked(选中) 还是 非 checked 状态,如果都交给原生,我们获取很不方便

  • 其次 useMergeValue 是组件库很常用函数,它把受控和非受控组合起来,是个非常实用的函数,我们来介绍一下逻辑。相信你写组件库也一定会用到。

'use client';
import React, { useState, useEffect, useRef } from 'react';
import { isUndefined } from '../utils';
import { usePrevious } from './use-previous';

export function useMergeValue<T>(
  defaultStateValue: T,
  props?: {
    defaultValue?: T;
    value?: T;
  },
): [T, React.Dispatch<React.SetStateAction<T>>, T] {
  const { defaultValue, value } = props || {};
  const firstRenderRef = useRef(true);
  const prevPropsValue = usePrevious(props?.value);

  const [stateValue, setStateValue] = useState<T>(
    !isUndefined(value) ? value : !isUndefined(defaultValue) ? defaultValue : defaultStateValue,
  );

  // 受控转为非受控的时候,需要做转换处理
  useEffect(() => {
    if (firstRenderRef.current) {
      firstRenderRef.current = false;
      return;
    }
    if (value === undefined && prevPropsValue !== value) {
      setStateValue(value);
    }
  }, [value]);

  const mergedValue = isUndefined(value) ? stateValue : value;

  return [mergedValue, setStateValue, stateValue];
}

这里简单解释一下,其实就是你传了 value 我就认为你是受控组件,然后 value 就透传出去, defaultValue 或者组件库想默认给个默认值, 我会用这个值其初始化 stateValue 然后传出去。并且 setStateValue 方法能改变其值。

setStateValue 其实在传入 value 的情况下,也没什么用,因为改变不了 value 的值。

如何将状态传递给子组件

我们可以使用 context api,将最终的 checked, disabled, readonly 状态让子组件使用 useContext 来获取。

    <RadioContext.Provider value={{ checked, disabled, readonly }}>
      <label {...rest} onClick={onLabelClick} aria-disabled={!!disabled} aria-readonly={!!readonly} aria-checked={!!checked}>
        {/* 为什么没有 readonly 状态, 标准里本来也没有 */}
        <input type="radio">
        {children}
      </label>
    </RadioContext.Provider>

如何保持语义性

我们只需要将 input 组件依然接受之前我们的状态,就能保持原生 radio 组件的语义性,所以我们完善一下 input 组件,也就是把之前的状态传递过去即可:

    <RadioContext.Provider value={{ checked, disabled, readonly }}>
      <label {...rest} onClick={onLabelClick} aria-disabled={!!disabled} aria-readonly={!!readonly} aria-checked={!!checked}>
        {/* 为什么没有 readonly 状态, 标准里本来也没有 */}
        <input
          ref={inputRef}
          disabled={!!disabled}
          value={value}
          type="radio"
          checked={!!checked}
          onChange={onChange}
          onClick={(e) => {
            // 阻止 input 的点击事件冒泡,避免重复处理
            e.stopPropagation();
          }}
          aria-readonly={!!readonly}
        />
        {children}
      </label>
    </RadioContext.Provider>

Radio Group 逻辑

这里简单介绍一下如何使用 Radio Group 组件包裹上面我们完成的 Radio 组件。 核心逻辑为, 使用同样是 useContext api,我们命名为 RadioGroupContext 来把当前选中的 Radio 标签的 value 传递即可 :

    <div role="radiogroup" {...rest}>
      <RadioGroupContext.Provider
        value={{
          onChangeValue,
          type,
          value,
          disabled,
          readonly,
          group: true,
          name,
        }}
      >
        {children}
      </RadioGroupContext.Provider>
    </div>

小结

文章把主要的核心逻辑梳理了一下,并没有过多解释每行代码。如果你想讨论关于如何实现自己组件库的的内容,欢迎加群一起讨论,组件还在不断拓展中,最终会对标大厂组件库。

其实对于一个前端来说,组件库算是囊括所有日常常见的前端技术了,无论是学习还是面试,都是绝佳的项目。

年营收超3000万美元,前华为人掘金中东AI浪潮|Insight全球

2025年11月25日 09:57

作者丨欧雪

编辑丨袁斯来

编者按:当出海越来越成为一家中国公司核心战略时,如何征战全球市场就成为一个极其专业的话题。在全球化的演变中,已有不少中国品牌站立潮头。鉴于此,硬氪特推出「Insight全球」专栏,从品牌成长与变迁,探索中国品牌出海的前沿方向与时代契机,为出海玩家与行业提供思考与启发。

这是我们专栏第52期——以“深度本地化+中台化AI能力”为核心优势,Neuxnet成功卡位中东数字化浪潮。公司聚焦金融、基建、文旅三大核心行业,通过自主研发的EAIP企业级AI平台,为客户提供从智能体应用到业务流程重构的全栈解决方案。其AI智能体已在实际场景中替代50%以上人工操作,预计2025年营收突破3000万美元。

当国内AI“百模大战”激战正酣时,一家中国背景的科技企业已悄然在中东打开新局面。

很多中国创业者或多或少听说沙特的“2030愿景”和“智能迪拜”倡议。谈论起对新技术的热情,中东政府不输于任何大国。

曾经发生在中国的数字化基建浪潮,如今在中东重演。 

以沙特利雅得为例,当地银行正逐步采用AI系统接替传统的人工信用卡审核岗位。与此同时,保险公司也引入人工智能体,接管每日因礼拜仪式而暂停的车险勘查流程,以求提升运营效率。

这会是一场持续十年、价值4000亿美元的增长机会。

2015年,连续创业者张磊成立软件公司Neuxnet,最初做C端业务,2021年转向B端。凭借其企业级AI解决方案,Neuxnet深度参与沙特、卡塔尔、阿联酋等国的数字化进程,目前年营收近3000万美元。其中,AI业务已占公司营收50%以上,预计明年将提升至三分之二。

尽管中东市场广阔,但竞争格局复杂。这是一个全球企业充分竞争的市场。Neuxnet在摸索后,找到了自己的差异点。它们不做印度公司的低价替代,不做欧美企业的标准化产品输出,而是通过深度本地化和可配置的中台能力,为客户提供高价值的AI服务。

目前,Neuxnet已深度切入金融、基建、文旅三大核心领域,其EAIP企业级AI能力平台已成为增长最快的业务线,预计明年将占公司营收三分之二。

“中东真正缺乏的不是技术,而是既懂技术又懂场景的公司。”张磊告诉硬氪。

Neuxnet在中东的实践为中国科技企业出海提供了新的思路:在不同国家都在追逐AI时,提供深度本地化能力或许比参数本身更具竞争力。

 

卡位中东

能在中东扎根的企业,创始人大多有多年本地工作经历,人脉广泛。

Neuxnet公司总部虽然在新加坡,但中东背景深厚。

张磊早年曾在华为参与海外通信基础设施建设,后加入百度成为无线业务核心成员。

2010年创办点心移动后,于2012年张磊就开始专注国际化,先后搭建过2个工具矩阵体系,用户覆盖北美、南美、东南亚、中东北非等地区,2017年初步开始深入涉足中东中亚市场,完成多了千万日活的工具性产品。此外,2019年开始和阿联酋皇室企业合作,先后完成多个国家级重大项目,为后续在中东地区的业务大规模拓展打下了深厚的基础。

即便如此,Neuxnet也是经历波折,才最终将业务定准中东市场的B端客户。

刚开始,张磊如同很多创业者,切入显得更容易增长的C端市场。但很快,公司旗下ToC通信类产品曾接连遭遇中东、北非地区封禁及印度市场批量下架。

他意识到,全球市场环境已发生根本性变化。“从ToC转向ToB,本质上是应对系统性风险的必然选择。”张磊回忆。

他也梳理了几个大市场的情况,发现美国壁垒高、南美动荡、欧洲利润薄、东南亚支付弱。只有中东,尤其是沙特、阿联酋、卡塔尔等国,正释放出乐观的信号,是个“高价值市场”。

看清趋势只是第一步,真正考验的是如何在中东活下来。

在中东市场,Neuxnet很快遇到了三类竞争对手:美国人掌握顶尖技术、欧洲人渗透到各行各业、印度人以极低成本包揽工作流开发。中国公司如果拼成本、拼沟通,根本毫无胜算。

Neunext很快展示出中国企业惯有的灵活身段。它们不做印度人擅长的“工作流定制”,而是专注技术门槛高的通信和AI应用;不做欧美人的标准化产品,而是以“中台+定制”快速响应客户需求;甚至愿意共享部分知识产权和代码,建立信任、降低使用门槛,构建了一整套的FDE合作模式。

这种前置工程师+技术中台的打法很快奏效。尤其是在AI模型层面,随着DeepSeek、千问等开源模型的崛起,中国团队在模型侧逐渐能够与欧美持平,而在应用侧——凭借更贴近客户、更灵活迭代的能力——甚至实现了反超。

目前,Neuxnet的核心业务聚焦于两大板块:一是通信技术方向,主要体现为超级APP的构建与运营;二是EAIP(Enterprise AI Platform)企业级AI能力平台,为企业提供从模型选择、多模型架构、算力统筹、企业知识平台、企业智能体等产品和技术,能将业务流程中的人工干预率降低50%以上。

Neuxnet Agentic Architecture(图源/企业)

Neuxnet的产品已深入应用于中东基建、文旅和金融等核心领域,在B端大客户的名单中,有了一席之地。

此外,除中东主战场外,Neuxnet的业务也已逐步扩展至中亚、东南亚、日本、新加坡等多个国家和地区,并在全球北京、南京、利雅得设有三个研发中心及五个分支机构(沙特、阿联酋、卡塔尔、香港、日本)。

 

重注AI

在全球竞争中,Neuxnet充分发挥出中国公司的AI实力。接下来,公司也将重心全面转向AI。

Neuxnet的EAIP企业级AI能力平台体系分明,涵盖三大产品序列:EnterpriseLLM构建多模型架构,解决模型选型与数据治理问题;EnterpriseGPT提供企业级效率工具,涵盖智能搜索、知识库和多种企业数字化工具;EnterpriseAgent则直接嵌入业务流程,替代传统人工环节,已成为增长最快的业务板块。

其中,在EnterpriseAgent领域,Neuxnet又将其落地分为三类:通用型Agent(如市场分析与数据分析)、工作流型Agent(深入业务流程)和职能型Agent(如HR、财务)。

张磊表示,目前最具实效的是工作流型Agent。举例来看,Neuxnet已在中东第一大零售银行的信用卡审核流程中成功部署AI方案,识别准确率从30%提升至80%,自动化率从20%提升至50%以上;同时,Neuxnet也在帮助当地某大型保险公司处理每日数千起起车险报案,通过AI视频研判替代传统人工勘查,预计可将人工负荷率从70%大幅压缩至20%以下。

Neuxnet的Agent方案已经初步具备了AI数字化员工的雏形,未来将会大规模应用于替代低效人力的工作领域。

Neuxnet Enterprise AI Agent Platform Overview(图源/企业)

虽然在中东市场有不错成绩,Neuxnet仍然保持谨慎。

在市场层面,Neuxnet在中东只深度布局金融、基建与文旅三大行业,在东南亚仅保留金融和基建,欧洲则重点关注能源领域。

这是一种典型的“钉子”战略。事实上,企业级AI的成功关键在于行业知识的深度而非广度。通过深耕特定垂直领域,公司能够不断积累行业特有的流程、数据和合规性知识,从而构建起难以被轻易复制的竞争壁垒。

此外,Neuxnet重点明确:只做AI应用层的业务,并逐渐关停非AI业务,推动所有产品AI化转型。

这一定位使其可以避开与科技巨头在基础大模型领域的“军备竞赛”,转而将全部精力集中于自身最擅长的领域——将前沿的AI技术转化为解决行业痛点的标准化方案。

在AI概念被过度炒作的市场环境中,这种克制是可贵的。它意味着公司并非盲目追逐短期热点,而是对行业的专业性、技术的长期性有所追求。

AI技术从实验室走向规模化商用,核心挑战已来到了关键的工程化落地。张磊认为,未来十年都将是AI大发展的时期,每个阶段都有大量工程难题需要解决。

同时,AI技术在全球落地的情况并不均等。广阔的发展空间意味着巨大的机遇。Neuxnet这样的全球化AI公司,眼前将是一片旷野。

C#从数组到集合的演进与最佳实践

作者 烛阴
2025年11月25日 09:54

一、 数组 (Array)

数组是C#中最基础、最原始的数据集合形式。理解它,是理解所有高级集合的起点。

1. 数组的本质:连续的内存空间

当你声明一个数组,如 int[] numbers = new int[5];,你实际上是在内存中请求了一块连续的、未被分割的空间,其大小足以存放5个int类型的数据。

  • 连续性 (Contiguous):这是数组最重要的特性。数据肩并肩地存储在一起,就像一排有编号的停车位。
  • 固定大小 (Fixed-Size):一旦数组被创建,其大小就不能再改变。你不能让一个长度为5的数组突然容纳第6个元素。
  • 类型统一 (Homogeneous):一个数组只能存储相同类型的元素。

2. 数组的声明与使用

// 1. 声明并初始化大小
int[] scores = new int[5]; // 在内存中分配了5个int的空间,默认值为0

// 2. 通过索引访问 (从0开始)
scores[0] = 95;
scores[1] = 88;
int firstScore = scores[0];

// 3. 声明并直接初始化内容
string[] names = new string[] { "Alice", "Bob", "Charlie" };
// 或者更简洁的语法
string[] namesShort = { "Alice", "Bob", "Charlie" };

// 4. 遍历数组
foreach (string name in names)
{
    Console.WriteLine(name);
}

// 5. 获取长度
int nameCount = names.Length; // 结果是 3

3. 数组的优缺点

优点:

  • 极高的访问性能:由于内存是连续的,通过索引 array[i] 访问元素是一个 O(1) 操作。CPU可以直接通过 基地址 + i * 单个元素大小 的公式计算出内存地址,无需任何遍历。这使得数组在需要频繁随机读取的场景下无与伦比。
  • 内存效率高:除了数据本身,几乎没有额外的开销。

缺点:

  • 大小不可变:这是数组最大的“原罪”。在创建时必须知道所需空间,这在许多动态场景下是不现实的。
  • 插入和删除效率低下:在数组中间插入或删除一个元素,需要移动该位置之后的所有元素来填补空位或腾出空间,这是一个 O(n) 操作,非常耗时。

二、List<T> - 动态数组

1. List<T> 初识

List<T> 的内部其实就封装了一个数组。它之所以能“动态”增长,是因为它实现了一套巧妙的**容量管理(Capacity Management)**机制。

  • Capacity vs. Count:
    • Count: 列表实际包含的元素数量。
    • Capacity: 内部数组能够容纳的元素数量。Capacity >= Count 恒成立。

当你向一个List<T>添加元素时,如果Count即将超过CapacityList<T>会自动执行以下操作:

  1. 创建一个新的、更大的数组(通常是当前容量的两倍)。
  2. 将旧数组中的所有元素复制到新数组中。
  3. 丢弃旧数组,将内部引用指向新数组。
  4. 在新数组的末尾添加新元素。
List<int> numbers = new List<int>(); // 初始Capacity通常为0
Console.WriteLine($"Count: {numbers.Count}, Capacity: {numbers.Capacity}");

numbers.Add(1); // 添加元素
Console.WriteLine($"Count: {numbers.Count}, Capacity: {numbers.Capacity}");  // 创建Capacity为4的容器
numbers.Add(2);
numbers.Add(3);
numbers.Add(4);
numbers.Add(5); // 此时会触发内部数组的扩容和复制!此时Capacity为8

Console.WriteLine($"Count: {numbers.Count}, Capacity: {numbers.Capacity}");

2. List<T> 的使用

List<T> 提供了比数组更加丰富的API方法:

1. 添加和插入元素 (Adding and Inserting Elements)

方法 (Method) 说明 (Description) 示例 (Example) List<string> fruits = new List<string>();
Add(T item) 在列表的末尾添加一个元素。 fruits.Add("Apple"); // ["Apple"]
AddRange(IEnumerable<T> collection) 将一个集合中的所有元素添加到列表的末尾。 var moreFruits = new[] { "Banana", "Cherry" };
fruits.AddRange(moreFruits); // ["Apple", "Banana", "Cherry"]
Insert(int index, T item) 在列表的指定索引处插入一个元素。 fruits.Insert(1, "Orange"); // ["Apple", "Orange", "Banana", "Cherry"]
InsertRange(int index, IEnumerable<T> collection) 在列表的指定索引处插入一个集合的所有元素。 var tropical = new[] { "Mango", "Pineapple" };
fruits.InsertRange(2, tropical);
Contains(T item) 判断列表中是否包含指定的元素。返回 bool bool hasApple = fruits.Contains("Apple"); // true
Exists(Predicate<T> match) 判断列表中是否存在满足指定条件的元素。使用Lambda表达式。 bool hasShortName = fruits.Exists(f => f.Length < 6); // true ("Apple")
Find(Predicate<T> match) 搜索满足指定条件的第一个元素并返回它。如果找不到,返回该类型的默认值(如引用类型为null)。 string bFruit = fruits.Find(f => f.StartsWith("B")); // "Banana"
FindAll(Predicate<T> match) 检索所有满足指定条件的元素,并返回一个包含它们的新 List<T> var longNameFruits = fruits.FindAll(f => f.Length > 5); // ["Orange", "Banana"]
IndexOf(T item) 搜索指定元素,并返回其第一次出现的索引。如果找不到,返回 -1 int index = fruits.IndexOf("Banana"); // 2
LastIndexOf(T item) 搜索指定元素,并返回其最后一次出现的索引。如果找不到,返回 -1 // If fruits = ["A", "B", "A"], LastIndexOf("A") is 2
Remove(T item) 从列表中移除第一次出现的指定元素。成功移除返回 true fruits.Remove("Apple"); // ["Orange", "Banana", "Apple"]
RemoveAt(int index) 移除列表中指定索引处的元素。 fruits.RemoveAt(1); // ["Apple", "Banana", "Apple"]
RemoveAll(Predicate<T> match) 移除所有满足指定条件的元素。返回被移除的元素数量。 int removedCount = fruits.RemoveAll(f => f == "Apple"); // ["Orange", "Banana"]
RemoveRange(int index, int count) 从指定索引开始,移除指定数量的元素。 fruits.RemoveRange(1, 2); // ["Apple", "Apple"]
Clear() 从列表中移除所有元素。Count 变为 0。 fruits.Clear(); // []
Sort() 使用默认比较器对列表中的元素进行就地排序(In-place sort)。 fruits.Sort(); // ["Apple", "Banana", "Cherry"]
Sort(Comparison<T> comparison) 使用指定的委托(通常是Lambda)对元素进行就地排序 fruits.Sort((a, b) => a.Length.CompareTo(b.Length)); // Sort by length
Reverse() 将列表中的元素顺序进行就地反转 fruits.Reverse(); // ["Banana", "Apple", "Cherry"]
ForEach(Action<T> action) 对列表中的每个元素执行指定的操作。 fruits.ForEach(f => Console.WriteLine(f.ToUpper()));
ToArray() 将列表中的元素复制到一个新的数组中。 string[] fruitArray = fruits.ToArray();
GetRange(int index, int count) 创建一个新列表,其中包含源列表中从指定索引开始的指定数量的元素。 var subList = fruits.GetRange(0, 1); // New List containing ["Apple"]

三、Dictionary等特殊集合

1. Dictionary<TKey, TValue> -- 字典

当你需要通过一个唯一的“键”(Key)来快速查找一个“值”(Value)时,Dictionary是你的首选。

  • 本质:基于哈希表(Hash Table)实现。它通过一个哈希函数将Key转换成一个索引,从而实现近乎O(1)的查找、插入和删除性能。
  • 核心特性:Key必须是唯一的,且不可为null
Dictionary<string, int> studentAges = new Dictionary<string, int>();

// 添加键值对
studentAges.Add("Alice", 20);
studentAges["Bob"] = 22; // 更方便的索引器语法

// 查找
if (studentAges.TryGetValue("Alice", out int age))
{
    Console.WriteLine($"Alice's age is {age}"); // 推荐用法,避免异常
}

// 遍历
foreach (var pair in studentAges)
{
    Console.WriteLine($"Key: {pair.Key}, Value: {pair.Value}");
}

2. HashSet<T> - 唯一数组

当你只关心一个元素是否存在于集合中,并且需要保证集合中没有重复元素时,HashSet<T>是完美的选择。

  • 本质:同样基于哈希表,但只存储Key(元素本身),没有Value。
  • 核心特性:元素唯一,查找效率极高(O(1))。支持高效的集合运算(交集、并集、差集)。
HashSet<string> uniqueNames = new HashSet<string>();
uniqueNames.Add("Alice");
uniqueNames.Add("Bob");
uniqueNames.Add("Alice"); // 添加失败,因为 "Alice" 已存在

Console.WriteLine(uniqueNames.Count); // 输出: 2

bool hasBob = uniqueNames.Contains("Bob"); // 极快

3. Queue<T>Stack<T> - 队列和堆栈

  • Queue<T> (队列)先进先出 (FIFO - First-In, First-Out)

    • Enqueue(): 入队(添加到队尾)。
    • Dequeue(): 出队(从队首移除并返回)。
  • Stack<T> (栈)后进先出 (LIFO - Last-In, First-Out)

    • Push(): 入栈(添加到栈顶)。
    • Pop(): 出栈(从栈顶移除并返回)。

结语

点个赞,关注我获取更多实用 C# 技术干货!如果觉得有用,记得收藏本文

创业板指涨超2%

2025年11月25日 09:53
36氪获悉,创业板指涨超2%,深成指涨1.4%,沪指涨0.6%。教育、游戏、贵金属板块涨幅居前。

Flutter 3.38 真的更快吗?基准测试与对比

作者 JarvanMo
2025年11月25日 09:52

将营销炒作与可衡量的现实区分开来——基准测试实际揭示了 Flutter 最新版本哪些信息

Flutter 3.38 于 2025 年 11 月 12 日发布,包含 145 位贡献者的 825+ 次提交。当然,有些文章声称它会带来立竿见影的巨大性能优势,但实际体验如何呢?营销炒作可以天马行空,但在可衡量的现实面前却并非如此。

欢迎关注我的公众号:OpenFlutter,感谢

🗣️ 现实检验:别光听宣传!

Flutter 3.38 到底有没有变快?官方文档没把性能提升当成这次发布会主打的卖点

跟以前的版本不一样:

  • Flutter 3.0 主打渲染优化,让界面跑得更快。
  • Flutter 3.7 专注于内存管理,让 App 占用更小。

这次 3.38 主要关注的是这些方面:

  • Dart 3.10 的“点”语法: 让代码写起来更简洁。
  • 小组件预览器(Widget Previewer)加速: 让你开发 UI 更快。
  • 系统兼容性: 支持最新的 iOS 18(UIScene)和 Android 的 16KB 页面大小。
  • 颜色和覆盖层: 引入了广色域支持和 Overlay 管理的一些重要变动。

📊 实际数字到底说明了什么?

🔧 编译性能 (Build Performance)

虽然有些文章提到了构建系统有改进(主要是 Linux 上的 GTK 渲染),但官方没有正式的基准测试数据,来证明那些“编译速度快 40%”之类的说法是真的。

实际开发者测试结果:

  • 从 3.35 升级到 3.38 后,首次冷编译时间基本没差(差异在 5% 以内)。
  • **热重载(Hot Reload)热重启(Hot Restart)**的速度也差不多。

🏃 运行时性能 (Runtime Performance)

3.38 在底层引擎确实做了一些优化:

  • 改进了 Linux 桌面应用的 GTK 渲染
  • 优化了 OpenGL 的合成
  • Impeller 渲染引擎继续在进步(但还在完善中)。

影响分析: 这些改进主要对 Linux 桌面应用有帮助。对于我们日常使用的 Android 和 iOS 手机应用来说,运行速度的提升感受不明显

💾 内存管理 (Memory Management)

3.38 修复了一些图片加载和释放的 Bug,可以防止某些场景下的内存泄漏。但 App 基础的内存占用量,和以前的版本是持平的


📈 纵观 Flutter 性能变化史

我们来看看 3.38 在性能系列中的位置:

  • Flutter 3.0 (2022 年 5 月): 宣布 Web 渲染性能提升,FPS 提升 11%–28% 。这是最后一次“大”性能版本
  • Flutter 3.7 (2023 年 1 月): 通过内存优化,App 占用体积减少了 10%–15%
  • Flutter 3.10–3.35 系列: 主要是性能微调、稳定性和 Bug 修复。
  • Flutter 3.38 (2025 年 11 月): 更注重开发者效率和平台集成,而不是追求更高的原始性能。

✨ 真正重要的性能改进

3.38 没有承诺大幅提高运行速度,但它增加了一些可以间接提升开发体验的功能:

  1. 小组件预览器(Widget Previewer)升级: 速度更快,功能更多。你可以瞬间看到 UI 变化,不需要完整重新编译。这能节省大量迭代设计的时间
  2. “点”语法(Dot Shorthand): Dart 3.10 引入的这个语法,让常用代码的冗余度减少了大约 20% 。代码更清晰,自然就更容易理解和维护——这也是另一种形式的“性能”提升!
// Before
Column(
  mainAxisAlignment: MainAxisAlignment.start,
  crossAxisAlignment: CrossAxisAlignment.center,
)

// After (Flutter 3.38 + Dart 3.10)
Column(
  mainAxisAlignment: .start,
  crossAxisAlignment: .center,
)

3. 平台稳定性

为了让应用能在最新的 iOS 18Android 设备上正常运行,减少崩溃,并表现得更流畅——可靠性本身也是一种性能

基准测试您的应用

所以,问题来了:Flutter 3.38 真的优化了您的应用性能吗?以下是衡量方法:

# Benchmark frame rendering
flutter run --profile --trace-skia

# Measure app startup time
flutter run --profile --trace-startup

# Memory profiling
flutter run --profile
# Then use DevTools Observatory

保持代码和测试条件在不同版本间的恒定

在运行 Flutter 3.35(或您现有的版本)和 3.38 时,保持代码和测试条件恒定不变,然后对比结果。

结论

Flutter 3.38 更快吗?没有戏剧性的提升,但对大多数开发者来说肯定有一点点

  • Linux 桌面应用: 在某些渲染场景中有所改进。
  • Android/iOS 应用: 基本上与 3.35–3.37 相同
  • Web 应用: 没有显著变化。
  • 开发流程: 通过 Widget Previewer简洁的语法实现了“页面优先”的开发。

Flutter 3.38 是一个可靠、完善的版本,它将开发者体验平台兼容性置于纯粹的性能之上。如果您期望性能有 40% 的飞跃,那么您可能会失望。但是,如果您追求整洁的代码、更好的工具稳定的平台支持,那么您会感到满意。

您应该升级吗?

如果满足以下条件,请升级:

  • 需要 iOS 18Android 上的 16KB 页面大小支持。
  • 需要开发者工具的改进(例如 Widget Previewer)。
  • 您正在构建 Linux 桌面应用

如果满足以下条件,请等待:

  • 您目前使用的稳定版本对您来说运行良好。
  • 性能是您唯一的顾虑
  • 您使用的 API 需要迁移(需要更多时间)。

Flutter 团队的工作仍在稳步推进中。3.38 在性能方面算不上一次革命,但它是框架发展过程中值得信赖的一步。最好的更新有时就是那些仅仅是让一切运行得更好的更新,即便这些改进不足以登上新闻头条。

欧洲央行管委内格尔:欧洲央行密切关注高企的食品和服务业通胀

2025年11月25日 09:50
欧洲央行管委内格尔表示,欧洲央行必须继续关注新冠疫情后通胀飙升的余波,包括食品价格上涨和服务业成本刚性问题。内格尔指出,尽管价格涨幅目前徘徊在2%的目标水平附近,并预计在中期内保持稳定,但危机后的影响“在某些情况下仍然明显”。(新华财经)

证券分析师外部评选规范迎重磅新规,中证协拟推主办方承诺函制度,强化源头把关

2025年11月25日 09:37
近期,中国证券业协会结合近年证券分析师外部评选组织情况,再次组织修订了《证券分析师参加外部评选规范》,目前正就征求意见稿征求行业意见。据悉,此次修订旨在进一步规范证券公司及其分析师参与外部评选活动,净化评选生态,维护分析师职业声誉。此次修订是自2019年10月《规范》发布、2023年9月首次修订后的再次重要调整,意见反馈截止日期为2025年12月8日。此次修订首次明确要求将分析师履行社会责任、践行中国特色金融文化纳入考核体系,并建立起主办方承诺函制度等创新监管机制。(中证报)

用 Gemini 3 复刻了 X 上爆火的复古拍立得,AI 也能写小程序了?

作者 对角
2025年11月25日 09:36

最近看到 X 上有位小姐姐使用 Gemini 3 做了一个复古拍立得相机,被 Gemini 3 的前端能力震撼到了。然后又看到了很多复刻的版本,但做的都是 web 版,在和朋友聊得时候,他说做个小程序版就好了,小程序更容易疯传。

PixPin_2025-11-25_09-35-49.png

之前一直怀疑 AI 的小程序代码能力,毕竟外国人搞出来的东西,能学习到我们国人的精髓吗?肯定会水土不服。趁着这个机会,刚好来试一下。

说干就干,使用宝玉的 prompt 我微调了一下(其实就只是改了技术栈)。

Please generate a single-file React application for a "Retro Camera Web App" with the following specifications:

1. Visual Layout & Container Strategy
- Theme: Retro aesthetic with a "Handwritten" font style for all text.
- Title: "Bao Retro Camera" displayed at the top center.
- Instructions: Display usage instructions at the bottom right.
- Main Camera Container: 
    - Create a fixed wrapper `div` that acts as the parent for the camera image, viewfinder, shutter button, and photo ejection slot.
    - Positioning: This container must be fixed at exactly 64px from the bottom and 64px from the left of the viewport (`bottom: 64px; left: 64px;`).
    - Dimensions: Width 450px, Height 450px.
    - Z-index: 20
    - All subsequent positioning coordinates (percentages) for camera elements are relative to this container.
- Background Image within Container:
    - Image Source: `https://s.baoyu.io/images/retro-camera.webp`
    - Size: 100% width and height of the container.
    - Position: Left 0, Bottom 0

2. Camera Functionality (The Viewfinder)
- Access the user's webcam.
- Viewfinder Position: The live video feed must be masked to a circle and positioned exactly over the camera lens.
- CSS for Video (Relative to Container): `bottom: 32%; left: 62%; transform: translateX(-50%); width: 27%; height: 27%; border-radius: 50%;z-index: 30`.
- Layering: The video must sit *above* the camera base image visually but within the container.

3. Shutter & Photo Interaction
- Shutter Button: Create an invisible clickable area over the camera's button.
- CSS for Button (Relative to Container): `bottom: 40%; left: 18%; width: 11%; height: 11%; cursor: pointer;z-index: 30`.
- Action: When clicked, play a shutter sound effect and trigger the "Photo Ejection" animation.

4. Photo Ejection & Development Animation
- Aspect Ratio: The generated Polaroid-style photo card must strictly follow a 3:4 portrait aspect ratio (Vertical).
- Ejection Animation: The photo paper slides UPWARDS (negative Y) from behind the camera body.
- Layering: The photo must appear to emerge from *inside* the camera (start with z-index(10) lower than camera body, then animate out).
- Ejection Container Origin CSS: `transform: translateX(-50%); top: 0; left: 50%; width: 35%;height: 100%;` (start position relative to the camera container).
- Ejection Container anmiation position from: ` translateY(0)` to ` translateY(-40%)`
- Developing Effect: Once the photo is taken, the image on the paper should transition from white/blurry to clear/sharp over a few seconds.

5. Drag & Drop "Photo Wall"
- Interaction: The user must be able to drag the ejected photo *out* of the camera slot and drop it anywhere on the rest of the screen (the "Photo Wall").
- Drag Handle: The entire Polaroid card (the white frame and the photo) must be interactive. The user should be able to click and drag from any part of the card to move it.
- Logic: While developing, the photo is attached to the camera container. Once dragged, it becomes absolutely positioned on the main screen body.
- Freedom: Once on the wall, photos can be dragged and repositioned freely.

6. AI Integration & Text Interactions
- Caption Generation: Use the Gemini Flash API to analyze the captured image content.
- Prompt: Generate a warm, short blessing or nice comment based on the photo content.
- Language Requirement: The generated text language must match the user's browser language.
- Footer Layout: The bottom of the Polaroid paper (below the image) should display the current date and the AI-generated text.
- Text Interaction & Icons:
    - When hovering specifically over the text area, display two small icons:
        1. Pencil Icon: Enters edit mode.
        2. Refresh Icon: Re-triggers the AI generation for that specific photo to get a new caption.
- Editing Logic:
    - Trigger: Edit mode can be entered by clicking the Pencil icon OR by double-clicking the text itself.
    - Behavior: When editing, replace the text display with an input/textarea showing the raw text.
    - Controls: Pressing Enter saves the changes. Pressing Esc cancels the edit and reverts to the previous text.

7. Photo Controls (Card Level)
- Hover Actions: When hovering over a developed photo card on the wall (general hover), show a small toolbar at the top of the photo with:
    - Download Button: When clicked, this must render the entire Polaroid card (including the white frame, the photo, the date, and the handwritten caption) into a single image file and download it. (Recommended: use `html2canvas` or similar logic).
    - Delete Button: Removes the photo from the screen.

Technical Stack
- uni-app + vue3 + typescript.
- canvas.

使用这个 prompt,其实 one shot 就已经跑通了整体的流程,后续我微调了一下样式(果然有点水土不服),还增加了相纸选择和分享的功能,然后上线了小程序,最终的效果可以扫码查看一下:

代码也放到 github 上了,100% AI 打造:github.com/HeftyKoo/re…

A股三大指数集体高开,光模块领涨

2025年11月25日 09:26
36氪获悉,A股三大指数集体高开,沪指高开0.36%,深成指高开0.85%,创业板指高开1.42%;光模块、锂电概念领涨,德科立涨近7%,中际旭创、长光华芯、德方纳米涨超4%;海运、航天军工板块跌幅居前,北方长龙跌超5%,招商轮船跌超2%。

恒指开盘涨0.9%,恒生科技指数涨1.43%

2025年11月25日 09:22
36氪获悉,恒指开盘涨0.9%,恒生科技指数涨1.43%;有色金属、硬件设备板块领涨,禾赛涨超12%,洛阳钼业、中广核集团涨超4%,娇俏科技、鸿腾精密涨超3%;电信服务、交通运输板块跌幅居前,中远海能跌超2%。
❌
❌