扩展 React Native

我们爱 React Native ,是因为她提供了大量的原生能力和原生组件,原生能力比较好理解,类似 Apache Cordova 也提供通过 Javascript 访问设备 API 的能力,我们看看 React Native 的工具箱里都有哪些东西?

原生方法:

  • AccessibilityInfo
  • ActionSheetIOS
  • AdSupportIOS
  • Alert
  • AlertIOS
  • Animated
  • AppRegistry
  • AppState
  • AsyncStorage
  • BackHandler
  • CameraRoll
  • Clipboard
  • DatePickerAndroid
  • Dimensions
  • Easing
  • Geolocation
  • ImageEditor
  • ImagePickerIOS
  • ImageStore
  • InteractionManager
  • Keyboard
  • LayoutAnimation
  • Linking
  • NetInfo
  • PanResponder
  • PermissionsAndroid
  • PixelRatio
  • PushNotificationIOS
  • Settings
  • Share
  • StatusBarIOS
  • StyleSheet
  • Systrace
  • TimePickerAndroid
  • ToastAndroid
  • Vibration
  • VibrationIOS

原生界面:

  • ActivityIndicator
  • Button
  • DatePickerIOS
  • DrawerLayoutAndroid
  • FlatList
  • Image
  • KeyboardAvoidingView
  • ListView
  • MaskedViewIOS
  • Modal
  • NavigatorIOS
  • Picker
  • PickerIOS
  • ProgressBarAndroid
  • ProgressViewIOS
  • RefreshControl
  • ScrollView
  • SectionList
  • SegmentedControlIOS
  • Slider
  • SnapshotViewIOS
  • StatusBar
  • Switch
  • TabBarIOS
  • TabBarIOS.Item
  • Text
  • TextInput
  • ToolbarAndroid
  • TouchableHighlight
  • TouchableNativeFeedback
  • TouchableOpacity
  • TouchableWithoutFeedback
  • View
  • ViewPagerAndroid
  • VirtualizedList
  • WebView

React Native 除了提供丰富的访问设备 API 的能力,还提供了丰富的原生组件和基于原生组件的 Javascript 组件。两者的差别在于原生组件相当于积木的最小单元,而基于原生组件的 Javascript 组件则是用最小单元拼装出的可重用单元。

原生组件:

  • View

Javascript 组件:

  • FlatList

就此看出,React Native 自己本身也是扩展者本身,所有的一切建立在 React Native 这个内核中。 我们平时构建 App 的过程就是使用这些积木拼装出多姿多彩的界面,又通过原生 API 让界面产生不可思议的功能来帮助我们完成日常生活和娱乐需求。大部分时候我们用前面介绍的这些组件就可以完成所有的工作了,但是两个原因我们需要「扩展」她。

  1. 基于组件化开发思想
  2. 现有原生组件不能满足需求

上图表达了三层意思,首先 Javascript 组件可以通过 Native 组件组合而成,再则 Javascript 组件也可以通过 Javascript 组件组合而成,而 App 则由这三种组建组合而成。每一次组合都是开发者对 React Native 的一次扩展,我们平时在社区中(例如:github.com)看到的开源组件甚至开源的组件库都是这样的一次扩展尝试,他们扩充了 React Native 的工具箱,让我们得以重用别人的劳动成果,站在巨人的肩膀上创新,构建自己的商业帝国。

而另一个触发我们扩展 React Native 的动机是「现有原生组件不能满足需求」

扩展的时机

在开始进行扩展前我们讨论一些形而上的东西,聊聊「扩展的时机」,React Native 有两个「扩展点」:

  • 原生方法
  • 原生组件

原生方法的扩展时机比较好把握,一般来讲缺少 API 的时候我们第一个想到的就是增加相应的模块去扩展,比如像 github.com/invertase/react-native-firebase 这个项目就是实现了 firebase 的原生方法到 React Native 的映射。但是在设计 API 的时候却可以好好的去讨论一番,那我们看看他们一般是如何设计 API 的?

  • 直接使用原生 API 的规范,简单通过 Javascript 进行 API 映射。类似 github.com/esseak/rn-umeng
  • 兼容 Web API 的规范。类似 github.com/invertase/react-native-firebase 没有直接使用 firebase 原生 API 的规范,而是当 Web API 有相同 API 的时候遵循 Web API 的规范,这样我们在同时开发 React Native 和 React 项目的时候可以重用大量代码。

原生组件的扩展时机,是个很好的开放性话题,一般来讲会有四种层次的扩展。

  • 将某个 Web 标准迁移到 React Native 中来
  • 将某个 Web 组件迁移到 React Native 中来
  • 实现某个通用的组件
  • 实现某个特定的组件

我们来分别举例子来说明:

将某个 Web 标准迁移到 React Native 中来

github.com/react-native-community/react-native-svg 是最有代表性的项目之一,笔者之前在项目中需要使用图表的时候,是通过把 iOS 和 Android 上面的原生图表控件库映射到 React Native 中来,但是这个项目最大的亮点在于把 SVG 这个 W3C 的矢量图标准移植到了 React Native ,这样海量的 JS 图表控件项目就可以基于这个项目以很低的成本在 React Native 中使用,从而在更「底层」解决了图表控件的问题,不仅仅如此、其他很多有创意的基于 SVG 的 JS 项目也有机会被移植过来。

前文提到的几个基于 react-native-svg 的图表控件项目:

  • github.com/FormidableLabs/victory-native
  • github.com/capitalone/react-native-pathjs-charts

将某个 Web 组件迁移到 React Native 中来

github.com/react-native-community/react-native-video 是着其中比较有代表性的开源项目,后续在如果扩展原生界面小节我们会尝试实现一个简单的 Android 版本。就如他的项目描述一样

A <Video /> component for react-native

该项目把 Video 标签带到 React Native 中来,并尽量实现和 Web 类似的 API,但因为在原生应用中场景要比 Web 环境中复杂,所以会有比较多的 API 和事件不相同,但其实 API 设计类似并不是为了在不同的平台中重用代码,而是为了降低学习成本,关于重用的话题会在本书的最后一章进行讨论,这里不多赘述。

实现某个通用的组件或者 API

github.com/itinance/react-native-fs 是其中比较有代表性的开源项目,这部分其实放在 React Native 主项目本身也不为过,如其名该项目帮助开发者访问原生设备的本地文件系统、网络下载、文件系统路径别名等等,这里不赘述,有需要的读者可以去项目主页查看。

实现某个特定的组件

github.com/buhe/react-native-pili 这个项目是笔者在七牛工作期间帮助 PILI 项目组封装的直播 SDK 项目,这种项目就是抽象程度最低的扩展,只是为了满足某个特定的需求或者功能,甚至某些不太容易在 React Native 的 JS 层实现的界面,对生态的推动作用是比较有限的。

如何扩展

React Native 在原生扩展上有两个「扩展点」,下面以 Android 平台为例,这两个扩展点包含在 ReactPackage.java 中:

public interface ReactPackage {

  List<NativeModule> createNativeModules(ReactApplicationContext reactContext);

  List<ViewManager> createViewManagers(ReactApplicationContext reactContext);
}

createNativeModules

这个方法返回一个 NativeModule 列表,每个 NativeModule 就是一个原生方法的模块、里面包含了若干原生方法集合,我们可以通过继承 BaseJavaModule 或者 ReactContextBaseJavaModule ,这两个是 NativeModule 的抽象实现,用来快速帮助我们实现 Java 实现的原生方法模块。他们唯一没有实现的方法是 getName() ,简单实现如下:

@ReactModule(name = "Hello")
public class HelloModule extends BaseJavaModule {

    @Override
    public String getName() {
        return "Hello";
    }

}

getName 方法的返回值,会在 JS 层使用,是这个 NativeModule 的唯一标示。但是显然,她完成不了任何功能,因为他没有任何「原生方法」,换句话说,他只是个「空集合」而已。

@ReactModule(name = "Hello")
public class HelloModule extends BaseJavaModule {

    @Override
    public String getName() {
        return "Hello";
    }

    @ReactMethod
    public void hello(Callback successCallback) {
        successCallback.invoke("Hello native moudles :)");
    }

}

我们给他加上一个 hello 方法,让他可以对世界问好,这里面有几个需要注意的地方, 首先,@ReactMethod 是什么?他表示这个方法可以被导出到 JS 层,这也是我们希望的,在同一个原生模块中不能同时出现两个相同名字的方法,虽然我们都知道这在 Java 中是被允许的,举个例子,我们不能再添加类似的方法了,即使参数不同:

    @ReactMethod
    public void hello(String content, Callback successCallback) {
        successCallback.invoke("Hello native moudles :) " + content);
    }

再则,Callback successCallback是什么?在 JS 层中,JS 只有一个线程,意味着我们不能去阻塞她,阻塞她会导致其他任务和响应不能被执行,用户的感受就是「卡」。那怎么得到结果呢?原生方法被要求只能返回 void 类型,他提供了 CallbackPromise 两个类型的参数,从名字可以大概猜出来,和 JS 中的 callback 和 promise 是一个意思,我们在下面调用的例子中会介绍具体用法。

最后,我们把我们实现的 HelloModule 放进我们的 HelloPackage 中,最后的最后还有一点不要忘记,要注册在我们的「主入口」上。

HelloPackage.java

public class HelloPackage implements ReactPackage {

  @Override
  public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
    return Collections.emptyList();
  }

  @Override
  public List<NativeModule> createNativeModules(
                              ReactApplicationContext reactContext) {
    List<NativeModule> modules = new ArrayList<>();

    modules.add(new HelloModule());

    return modules;
  }

}

MainApplication.java

protected List<ReactPackage> getPackages() {
    return Arrays.<ReactPackage>asList(
            new MainReactPackage(),
            new HelloPackage()); 
}

至此,我们的应用就具备了用原生方法对这个世界说 hello 的能力,我们赶紧去使用一下吧!

import { NativeModules } from 'react-native';
let hello = NativeModules.Hello;
hello.hello('bingo',(content) => { console.log('content : ' + content) })

介绍了原生方法这个扩展点后,我们再来聊聊,原生界面这个扩展点,类似 react-native-video 这个项目,她提供了 react-native 不具备的播放音视频的能力,她不仅仅提供了原生方法:播放和停止等,还提供了播放界面,而且不同的应用需要提供不同的视觉效果,我们还需要对播放界面进行样式的定义,非常适合使用原生界面这个扩展点,下面我们就通过实现了一个简单的 react-native-video 来看看原生界面是怎么玩的!

createViewManagers

这个方法返回一个 ViewManager 列表,每个 ViewManager 都是一个 View 的管理器和工厂,他是原生界面的「扩展点」,为什么是 ViewManager 做为「扩展点」而不是 View 做为「扩展点」,其实很容易理解,View 可以在页面中出现多次而非「单例模式」,所以这里 React Native 让我们实现了 View 的工厂,在适当的时候会去让我们创建 View 的实例。而前面的 NativeMoudle 「扩展点」就可以使用「单例模式」达到全局唯一的目的。

public class ReactVideoViewManager extends SimpleViewManager<ReactVideoView> {

    public static final String REACT_CLASS = "RCTVideo";

    public static final String PROP_SRC = "src";
    public static final String PROP_PAUSED = "paused";
    public static final String PROP_SRC_URI = "uri";

    @Override
    public String getName() {
        return REACT_CLASS;
    }

    @Override
    protected ReactVideoView createViewInstance(ThemedReactContext themedReactContext) {
        return new ReactVideoView(themedReactContext);
    }

    @Override
    public void onDropViewInstance(ReactVideoView view) {
        super.onDropViewInstance(view);
        view.cleanupMediaPlayerResources();
    }


    @ReactProp(name = PROP_SRC)
    public void setSrc(final ReactVideoView videoView, @Nullable ReadableMap src) {
            videoView.setSrc(
                    src.getString(PROP_SRC_URI)
            );
    }

    @ReactProp(name = PROP_PAUSED, defaultBoolean = false)
    public void setPaused(final ReactVideoView videoView, final boolean paused) {
        videoView.setPausedModifier(paused);
    }

}

为了大家更容易聚焦在主要逻辑上和扩展方法上,我们简化了 react-native-video 的代码,如果想看完整版可以参考链接:https://github.com/react-native-community/react-native-video

首先,我们继承了 SimpleViewManager,并提供了 ReactVideoView 范型,这个是 createViewInstance 方法的返回值。也是我们实现的 ViewManager 创建的 View 类型。

然后,我们实现了 getName 方法,这个方法的返回值会在 JS 层使用,是这个 ViewManager 的唯一标示,会在 createReactNativeComponentClass 中被使用。

再则,我们有两个重要的方法:createViewInstanceonDropViewInstance 这两个方法其实比较好理解,前面我们大略说过,当 React Native 需要真实的创建这个原生界面之前会调用 createViewInstance 方法创建一个原生界面的实例。反正,当 React Native 需要删除这个原生界面的时候 React Native 会回调onDropViewInstance 这个方法给我们做一些资源的释放操作的机会。

我们发现,原生界面的「扩展点」中没有类似 @ReactMethod 的标注,不能去通过调用一个方法来影响一个组件,这其实也比较好理解,最终这些原生界面映射到 JS 层之后的表现形式是类似 <Video /> 这样的 jsx 标签,而这些标签只有「属性(props)」可以被设置,并不能调用实例方法,所以原生实现中对应的 ViewManager 也不会提供实例方法而只有「属性(props)」。这里面声明了两个属性:src 和 paused;src 是一个字典,里面包含了 uri 的 key ,paused 则是个简单的布尔类型。

在 JS 层,我们可以使用如下方式来使用她:

<Video src={{uri:"videoUrlPlaceholder"}} paused={true} />

表示从 videoUrlPlaceholder 链接加载这个视频内容,并且当前处于暂停状态,我们通过在 JS 层改变 paused 的值来播放和暂停视频,这也是「React 方式」的实践。

ViewManager 提供了 jsx 和原生界面的桥梁,具体的实现还要委托给 View 来做,这方面 React Native 的适配性非常强,只要 createViewInstance 的返回值继承自 Android 的 View 类型就可以。

    public class ReactVideoView extends View {

    private ThemedReactContext mThemedReactContext;
    private String mSrcUriString = null;
    private boolean mPaused = false;

    public ReactVideoView(ThemedReactContext themedReactContext) {
        super(themedReactContext);
        mThemedReactContext = themedReactContext;
        initializeMediaPlayerIfNeeded();
    }

    private void initializeMediaPlayerIfNeeded() {
        if (mMediaPlayer == null) {
            mMediaPlayerValid = false;
            mMediaPlayer = new MediaPlayer();
            mMediaPlayer.setScreenOnWhilePlaying(true);
        }
    }

    public void cleanupMediaPlayerResources() {
        if ( mediaController != null ) {
            mediaController.hide();
        }
        if ( mMediaPlayer != null ) {
            mMediaPlayerValid = false;
            release();
        }
    }

    public void setSrc(final String uriString) {
        initializeMediaPlayerIfNeeded();
        mMediaPlayer.reset();
        try {

                Uri parsedUrl = Uri.parse(uriString);
                Uri.Builder builtUrl = parsedUrl.buildUpon();
                setDataSource(uriString);
        } catch (Exception e) {
            e.printStackTrace();
            return;
        }

        try {
          prepareAsync(this);
        } catch (Exception e) {
          e.printStackTrace();
        }
    }

    public void setPausedModifier(final boolean paused) {
        mPaused = paused;
        if (mPaused) {
            if (mMediaPlayer.isPlaying()) {
                pause();
            }
        } else {
            if (!mMediaPlayer.isPlaying()) {
                start();
            }
        }
    }

}

同样去掉了大量的代码,需要查看完整版请去查看原始地址。

这部分其实不包含任何 React Native 代码,完全就是实现一个自定义的 View,因为与本书主题无关这里就不展开详细描述了。之后就是把她放进 VideoPackage 的 createViewManagers 方法返回值中,再把 VideoPackage 注册进我们的程序入口,这部分和 NativeModule 完全一样就不赘述了。

不同于 NativeModule ,现在我们还不能马上在 JS 层使用,在 JS 层还需要把 RCTVideo 包裹起来。因为现在的 ViewManager 还只是一个「特殊的 NativeModule」,本不是一个 React 组件,我们还需要在 JS 层进行加工包裹。

export default class Video extends Component {

  constructor(props) {
    super(props);
  }

  render() {
    let uri = this.props.source.uri || '';
    const nativeProps = Object.assign({}, this.props);
    Object.assign(nativeProps, {
      src: {
        uri,
      },
    });

    return (
      <RCTVideo
        {...nativeProps}
      />
    );
  }
}

Video.propTypes = {
  /* Native only */
  src: PropTypes.object,
  /* Wrapper component */
  source: PropTypes.oneOfType([
    PropTypes.shape({
      uri: PropTypes.string
    })
  ]),
  paused: PropTypes.bool,
  ...View.propTypes,
};
const RCTVideo = requireNativeComponent('RCTVideo', Video});

requireNativeComponent 方法传入的第一个参数是 RCTVideo ,就是我们实现 ViewManager 中的 getName 方法的返回值,第二个参数是 Video ,是所谓的「ComponentInterface」,返回的结果是一个 React 组件,他的 propTypes 和 Video 的 propTypes 一致,并继承自 ReactNativeBaseComponent,最后被 「NativeRender」渲染出来,具体细节请参考第二章。

扩展的原理

在第二章,我们知道了从 React 的 JSX 到渲染出原生界面的全过程,但是确刻意「忽略」了一个重要的技术细节。JS层 和 Naive 层的互操作,这也是 React Native 中最重要的组成部分之一。

  1. JS 层是怎么「操作」Native 层的原生方法的?
  2. Native 的事件如何传递到 JS 层的事件处理器的?

results matching ""

    No results matching ""