第三章 创建第一个原生模块:项目结构解析、模块注册流程、Hello World模块实现

好,咱们直接动手。这一章我会带你走一遍完整的流程——从项目结构长什么样,到模块怎么注册,最后跑通一个 Hello World。我当年第一次写原生模块时,光找文件位置就花了半小时……嗯,这些坑我会帮你避开。

3.1 项目结构长什么样?

先别急着写代码。你得先知道文件都放哪。以 React Native 0.70+ 为例,一个标准的原生模块项目结构是这样的:

MyAwesomeModule/
├── android/
│   ├── src/main/java/com/myawesomemodule/
│   │   ├── MyAwesomeModule.java        // 模块实现
│   │   ├── MyAwesomePackage.java       // 模块注册
│   │   └── MyAwesomeModuleManager.java // 可选,管理类
│   ├── build.gradle
│   └── src/main/AndroidManifest.xml
├── ios/
│   ├── MyAwesomeModule.m               // 模块实现(ObjC)
│   ├── MyAwesomeModule.swift           // 或 Swift
│   └── MyAwesomeModuleBridge.m         // 桥接头文件
├── index.js                            // JS 入口
├── package.json
└── README.md

我个人习惯把 Java 文件放在 com.xxx.xxx 包路径下,这样后续维护时一眼就能找到。你想想看,如果所有文件都堆在根目录,那找起来多痛苦。

核心要点:

  • android/ 目录下放 Android 原生代码
  • ios/ 目录下放 iOS 原生代码
  • index.js 是 JS 端的入口,负责导出模块
  • package.json 定义模块名称、版本、依赖

3.2 模块注册流程——到底怎么连起来的?

很多新手会问:我写了个 Java 类,JS 怎么就能调用了?

说白了,中间有个「注册」环节。React Native 在启动时会扫描所有注册的原生模块,然后通过 Bridge 把方法暴露给 JS。流程大致如下:

  1. 创建模块类:继承 ReactContextBaseJavaModule(Android)或实现 RCTBridgeModule 协议(iOS)
  2. 创建 Package 类:实现 ReactPackage 接口,把模块注册进去
  3. 在 MainApplication 中注册 Package:把 Package 加到 getPackages() 列表里
  4. JS 端导入:通过 NativeModules 或 TurboModuleRegistry 获取模块

我在项目中遇到过一个问题:注册了模块但 JS 端一直报 undefined。查了半天,原来是 Package 类里忘了把模块加进 createNativeModules() 方法里。嗯,这种低级错误最容易犯。

小技巧: 如果你用 TypeScript,建议给模块写一个类型声明文件(.d.ts),这样调用时能有代码提示,省得每次都要翻文档。

3.3 Hello World 模块实现(Android 篇)

好,咱们直接写代码。先看 Android 端。

3.3.1 创建模块类

// MyHelloModule.java
package com.myhellomodule;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Promise;

public class MyHelloModule extends ReactContextBaseJavaModule {

    public MyHelloModule(ReactApplicationContext reactContext) {
        super(reactContext);
    }

    @Override
    public String getName() {
        return "MyHelloModule";  // 这个名称就是 JS 端调用的模块名
    }

    @ReactMethod
    public void sayHello(String name, Promise promise) {
        try {
            String result = "Hello, " + name + "! Welcome to native module.";
            promise.resolve(result);
        } catch (Exception e) {
            promise.reject("HELLO_ERROR", e.getMessage());
        }
    }
}

注意 getName() 返回的字符串,它就是 JS 端 NativeModules.MyHelloModule 里的那个名字。大小写敏感,别写错了。

3.3.2 创建 Package 类

// MyHelloPackage.java
package com.myhellomodule;

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 MyHelloPackage implements ReactPackage {

    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
        List<NativeModule> modules = new ArrayList<>();
        modules.add(new MyHelloModule(reactContext));  // 注册模块
        return modules;
    }

    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Collections.emptyList();  // 没有 UI 组件,返回空列表
    }
}

3.3.3 在 MainApplication 中注册

// MainApplication.java
@Override
protected List<ReactPackage> getPackages() {
    List<ReactPackage> packages = new PackageList(this).getPackages();
    packages.add(new MyHelloPackage());  // 加上这一行
    return packages;
}

注意: 如果你用的是 React Native 0.60+ 的自动链接(Autolinking),其实不需要手动改 MainApplication。但为了教学清晰,我建议你手动注册一次,理解流程后再用自动链接。

3.4 Hello World 模块实现(iOS 篇)

iOS 端稍微有点不同。我们用 Objective-C 来写,因为 Swift 还需要桥接头文件,步骤多一点。

3.4.1 创建模块实现文件

// MyHelloModule.m
#import <React/RCTBridgeModule.h>

@interface RCT_EXTERN_MODULE(MyHelloModule, NSObject)

RCT_EXTERN_METHOD(sayHello:(NSString *)name
                  resolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

@end

等等,上面这个是声明文件。真正的实现要写在 .swift 或 .mm 文件里。我个人习惯用 Swift,因为语法更现代。

// MyHelloModule.swift
import Foundation

@objc(MyHelloModule)
class MyHelloModule: NSObject {

    @objc
    func sayHello(_ name: String,
                  resolver resolve: @escaping RCTPromiseResolveBlock,
                  rejecter reject: @escaping RCTPromiseRejectBlock) {
        let result = "Hello, \(name)! Welcome to native module."
        resolve(result)
    }
}

3.4.2 创建桥接头文件

如果你用 Swift,还需要一个 MyHelloModule-Bridging-Header.h 文件:

// MyHelloModule-Bridging-Header.h
#import <React/RCTBridgeModule.h>

然后在 Xcode 的 Build Settings 里设置 Objective-C Bridging Header 路径指向这个文件。

避坑指南: 我曾经因为忘记设置桥接头文件,导致编译时一直报 RCTPromiseResolveBlock 未定义。嗯,这种问题查起来特别费时间,所以建议你一开始就配好。

3.5 JS 端调用

两端都写好了,JS 端怎么调?很简单:

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

const { MyHelloModule } = NativeModules;

// 调用原生方法
MyHelloModule.sayHello('React Native')
    .then(result => {
        console.log(result);  // 输出: Hello, React Native! Welcome to native module.
    })
    .catch(error => {
        console.error(error);
    });

如果你用 TypeScript,可以加个类型声明:

// types.ts
export interface MyHelloModuleInterface {
    sayHello(name: string): Promise<string>;
}

declare module 'react-native' {
    interface NativeModulesStatic {
        MyHelloModule: MyHelloModuleInterface;
    }
}

3.6 常见问题与排查

问题 原因 解决方案
NativeModules.MyHelloModule 为 undefined 模块未注册或名称不匹配 检查 getName() 返回的名称,以及 Package 是否已注册
调用方法时返回 Promise 未 resolve 原生方法未调用 resolve 或 reject 确保所有分支都调用了 Promise 回调
iOS 编译报错 RCTBridgeModule 未找到 缺少桥接头文件或未导入框架 检查 Bridging Header 配置,确保 #import <React/RCTBridgeModule.h>
Android 编译报错 ReactContextBaseJavaModule 找不到 缺少 React Native 依赖 检查 build.gradle 是否添加了 implementation 'com.facebook.react:react-native:+'

总结一下:

  • 项目结构要清晰,android/ 和 ios/ 分开
  • 模块注册三步走:创建模块类 → 创建 Package → 注册到应用
  • JS 端通过 NativeModules 调用,方法名和参数要一一对应
  • 遇到问题先检查名称是否匹配,再检查注册流程

好了,这一章的内容就到这里。你跟着敲一遍代码,应该就能跑通第一个原生模块了。下一章我们会深入讲参数传递和回调处理,到时候会涉及更复杂的类型——比如数组、Map、回调函数等。做好准备。