扩展 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 让界面产生不可思议的功能来帮助我们完成日常生活和娱乐需求。大部分时候我们用前面介绍的这些组件就可以完成所有的工作了,但是两个原因我们需要「扩展」她。
- 基于组件化开发思想
- 现有原生组件不能满足需求
上图表达了三层意思,首先 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
类型,他提供了 Callback
和 Promise
两个类型的参数,从名字可以大概猜出来,和 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
中被使用。
再则,我们有两个重要的方法:createViewInstance
和 onDropViewInstance
这两个方法其实比较好理解,前面我们大略说过,当 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 中最重要的组成部分之一。
- JS 层是怎么「操作」Native 层的原生方法的?
- Native 的事件如何传递到 JS 层的事件处理器的?