React Native与原生模块、组件之间的关系浅析

前言

React Native的一大特点就是用户界面其实都是由原生组件所组成。JavaScript编写的React组件仅仅作为原生视图的抽象表现以及配置。所有React组件最后都会被渲染成原生UI组件。这使得我们可以使用熟悉的JavaScriptReact让用户界面开发变得很简单。

React Native还提供了许多暴露为NativeModules的库,来调用原生的API。这些方法可以让JavaScript调用,或者传播事件让JavaScript监听。原生模块函数从JavaScript端接收参数,经过桥接层后,参数被处理成恰当的类型并且可以直接使用。

本篇试图探究原生组件和原生模块的架构方式,以及如何开发它们。

原生组件

React Native开始渲染一个界面时,原生层会建立与该界面结构一样的镜像。桥接层把规则(组件属性)从React Native所描绘的UI层传递给原生层。最终在屏幕上看到的,就是React组件转译成相应原生组件的结果。以下我们将探究:
- ViewManager(“视图管理器”)如何管理原生UI组件;
- React组件属性如何传递给在原生层定义的函数并选择处理方式。

React Native中的JSX语法类似于我们所熟悉的React虚拟DOM,它就是用来编写设置原生视图的配置文件。

每一个JSX元素都与其所代表的原生组件的实例绑定到一起。两者总是与对方保持同步,React组件定义页面结构,原生UI组件就渲染出对应的UI结构。

React Native创建UI组件的过程概述:

React Native所创建的组件挂载之后,通知原生层根据页面结构绘制节点。UI组件的实例被创建。每个实例分配了一个标识符,并与JavaScript层共享。从此刻起,只要这些视图的属性发生了改变,或者将它们从屏幕上移除,这些改变集合会根据它们的身份(组件ID)和改变内容(任意属性)通知给原生层,原生层就会按照JavaScript层传来的信息调整布局。

剖析原生组件

一个原生组件主要由两部分组成:ViewManager ,以及实际的UI组件。

ViewManager扮演的角色负责连接React组件以及该组件代表的原生UI组件实例。ViewManager 是单例模式,对于指定的组件类型只有唯一一个视图管理器,它管理着指定类型的所有组件。

React组件挂载之后就会触发它的render方法,并返回它所维护的JSX组件树。这些树节点根据它们的类型被发送给对应的ViewManager(比如 节点会被发送给处理View实例的管理器)。元素的视图管理器创建出React组件代表的原生组件实例。如果该组件拥有属性,这些属性将由ViewManager 的函数接收,用来创建符合这些属性的原生组件实例。当属性发生变化后会进行同样的过程。

现在假设我们创建一个自定义的原生组件TestView:

class TestView extends View {
    ...
}

React Native端,它将显示成<TestView/>元素。

接下来我们就需要给TestViewh创建一个ViewManager

/**
 * 控制TestView的TestViewManager类
 */
class TestViewManager extends SimpleViewManager<TestView>{
    @Override
    public String getName();

    @Override
    protected TestView createViewInstance (ThemedReactContext context);

    @ReactProp(name = "someProp")
    public void setSomeProp(TestView testViewInstance, @Nullable String value);
}

我们实现了两个继承自父类的方法getNamecreateViewInstance ,以及自定义方法setSomeProp

  • getName方法返回组件名称,在JavaScript层被引用。
  • createViewInstance 方法用于在JavaScript层挂载React组件时创建TestView的实例。
  • setSomeProp方法在React组件属性包含初始值或新值时被调用。参数为相应的实例和属性值。

总体上看控制流从JavaScript层进入原生层。React组件负责渲染JSX组件,JSX组件的属性以及层次结构通过桥接层传递给原生层。

相应的ViewManager取得这些数据后,如果不存在已有实例,就调用createViewInstance 方法创建一个。

每个原生元素都有一个标识符,创建React组件的时候需要提供对应的标识符。这便是React Native能够映射两个层次结构的奥秘所在。

如果JSX组件定义了某些属性,它将寻找对应的函数(带有@ReactProp@ReactPropGroup 注释),并以相应的视图实例和属性值作为参数调用该方法。根据JavaScript组件定义的配置和布局,屏幕上就会渲染出最终的原生组件。



示意图

创建自定义原生组件

在接下来的示例中将用MapBox的SDK开发地图组件。先来编写React组件。

class MapDemo extends React.Component {
    constructor(props) {
        // 记得传入props
        // 我们终究希望原生层知道最初的状态
        super(props);

        // 起始坐标经纬度均为0
        var center = {latitude: 0, longitude: 0};

        this.state = {
            // 地图中心
            mapCenter: {
                ...center
            },
            // 编辑后的坐标(避免在用户输入时更新地图)
            editCenter: {
                ...center
            }
        };
    }

    /**
     * 地图位置移动后更新输入框
     */
    onCenterChanged({latitude, longitude}) {
        var editCenter = {latitude, longitude};
        this.setState({editCenter});
    }

    /**
     * 提交输入框内容后更新地图
     */
    updateMapCoordinates() {
        var latitude = parseFloat(this.state.editCenter.latitude);
        var longitude = parseFloat(this.state.editCenter.longitude);
        var mapCenter = {latitude, longitude};
        this.setState({mapCenter});
    }

    /**
     * 用户输入时更新单个输入框的内容
     */
    updateEditField(plain, value) {
        var editCenter = {
            ...this.state.editCenter,
            [plain]: value
        };
        this.setState({editCenter});
    }

    render() {
        return (
            <View>
                <MapBoxView
                    zoom={10}
                    center={this.state.mapCenter}
                    onRegionChanged={event =>this.onCenterChanged(event.nativeEvent.src)}
                />
                <TextInput
                    value={this.state.editCenter.latitude}
                    onChangeText={value =>updateEditField('latitude', value)}
                />
                <TextInput
                    value={this.state.editCenter.longitude}
                    onChangeText={value =>updateEditField('longitude', value)}
                />
                <TouchableHighlight onPress={() =>updateMapCoordinates()}>
                    <Text>Update</Text>
                    </TouchableHighlight>
            </View>
        );
    }
}

Android

接下来编写视图管理器类:

class ReactMapBoxViewManager extends SimpleViewManager<MapView>{
    public static final String REACT_CLASS = "ReactMapBoxView";
    public static final String REACT_PROP_CENTER = "center";
    public static final String REACT_PROP_ZOOM = "zoom";
    private static final String EVENT_REGION_CHANGED = "onRegionChanged";
    private String accessToken;

    ReactMapBoxViewManager(String accessToken) {
        this.accessToken = accessToken;
    }

    /**
     * React组件的名称
     */
    @Override
    public String getName() {
        return REACT_CLASS;
    }

    /**
     * 创建新的MapView实例
     * 该过程在新的React组件MapBox挂载到应用的时候执行
     */
    protected MapView createViewInstance(ThemedReactContexted themedReactContext) {
        MapView mapView = new MapView(themedReactContext, accessToken);
        // 必需的调用
        mapView.onCreate(null);
        return mapView;
    }

    @ReactProp(name = REACT_PROP_CENTER)
    public void setCenter(MapView view, @Nullable ReadableMap center) {
        if (center != null) {
            double latitude = center.getDouble("latitude");
            double longitude = center.getDouble("longitude");
            CameraPosition cameraPosition = new CameraPosition.Builder()
                .target(new LatLng(latitude, longitude))
                .build();
            view.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition));
        }
    }

    @ReactProp(name = REACT_PROP_ZOOM)
    public void setZoom(MapView view, double zoomLevel) {
        view.setZoom(zoomLevel);
    }

    @Override
    @Nullable
    public Map getExportedCustomDirectEventTypeConstants() {
        MapBuilder.Builder builder = MapBuilder.builder();
        builder.put(EVENT_REGION_CHANGED, MapBuilder.of("registrationName",
        EVENT_REGION_CHANGED));
        return builder.build();
    }

    @ReactMethod(name = EVENT_REGION_CHANGED, defaultBoolean = true)
    public void onRegionChangedListener(final MapView map, Boolean value) {
        view.addOnMapChangedListener(
            new MapView.onMapChangedListener() {
            @Override
            public void onMapChanged(int change) {
                if (change == MapView.REGION_DID_CHANGE ||
                change == MapView.REGION_DID_CHANGE_ANIMATED) {
                    WritableMap event = Arguments.createMap();
                    WritableMap location = Arguments.createMap();

                    location.putDouble("latitude", view.getCenterCoordinate().getLatitude());
                    location.putDouble("longitude", view.getCenterCoordinate().getLongitude());
                    location.putDouble("zoom", view.getZoom());
                    event.putMap("src", location);

                    ReactContext reactContext = (ReactContext) view.getContext();
                    context.getJSModule(RCTEventEmitter.class)
                        .receiveEvent(view.getId(), EVENT_REGION_CHANGED, event);
                }
            }
        });
    }
}

这个React组件会在某种方式下被转译成原生层的代码。MapBoxView将映射到ReactMapBoxViewManager 上,它有一个以@ReactProp(name = "center") 进行注释的方法。根据这种映射关系,React内部就可以作出以下判断。
- (1) 调用哪个方法。
- (2) 该方法需要的参数类型是什么。

onRegionChangedListener是处理用户移动地图的回调函数,React Native需要一种策略把事件的传播给绑定在React组件属性的函数上,这就需要重写getExportedCustomDirectEventTypeConstants方法,该方法负责定义事件、组件,以及绑定了回调函数的组件属性之间的映射关系。

它接受事件名称以及一个映射值registrationName:callbackPropertyName作为参数。这样React Native在分发事件给组件时就知道要查找的属性名称。

注意: JavaScript层接收了来自原生层的事件后,原生事件对象位于JavaScript事件对象的nativeEvent属性中。

JavaScript

原生组件已经就绪,下面要在JavaScript层为它定义React部分的接口。此处将会定义允许的属性类型。基本来讲,React组件只需要定义组件接口的部分,不需要定义完整的类。

// mapBox.js

import {PropTypes} from 'react';
import {requireNativeComponent, View} from 'react-native';

// 定义暴露给原生组件的接口
var iface = {
    name: 'MapBoxView',
    propTypes: {
        center: PropTypes.shape({
            latitude: PropTypes.number,
            longitude: PropTypes.number
        }),
        zoom: PropTypes.number,
        onRegionChanged: PropTypes.func,
        ...View.propTypes
    }
};

// export语句导出的接口,会被原生组件导入成JSX组件来使用
export default requireNativeComponent('ReactMapBoxView', iface);

任何原生层所定义的属性都必须在接口中定义好它们的类型。如果有任何遗漏,会出现如下图所示的红色警告界面通知你。



错误提示

小结

总体来看,React Native组件就是原生组件,只不过它还提供了一个代理来实现属性的映射,以及负责与JavaScript层交互的媒介。

原生模块的学习就留到下一篇,敬请期待…

猜你喜欢

转载自blog.csdn.net/r122555/article/details/80959361