RNProjectPlayground
🍨React Native 相关,涉及 MobX、MST使用,原生简易导航模块、列表组件封装,一些动画尝试,以及 HOC 应用。
Install / Use
/learn @ljunb/RNProjectPlaygroundREADME
目录
概览
这是一个自己随意玩耍的仓库,主要涉及的东西有以下几部分:
- 基于 MobX 和 MST 重新实现了 React Native 版食物派的个别页面
- 通过 UINavigationController 和 Activity ,实现导航功能:push、pop、popTo、popToRoot。每个页面,React Native 都只作为 View 的角色存在
- 收集一些自己的练习 Demo、组件,或是项目实践中的想法
导航功能
口袋蜜蜂(AppStore | 小米应用商店)是混编 App,在项目启动的前期,跟同事一起尝试了原生与 React Native 页面之间的各种导航场景,在此过程中也尝试了不同的几个 React Native 导航组件,略去其中细节,一番尝试后,回过头来想:既然是原生为主导,为何不就地取材,直接用原生的导航功能?React Native 本来就应该只承担 View 层的角色,数据的流转,实际仍是在原生层面。
因此,每次跳转的起始或最终界面,不管是原生,还是 React Native 页面,实际上都是原生到原生的导航。React Native 可通过注册多个 Component 的形式来加载多个页面,而口袋在几个版本的迭代下来之后,我们总结了较为推荐的方式是:
- 共同:只注册一个
Component,不同页面在初始参数中添加标识位区分- iOS:采用单例
RCTBridge,并通过- initWithBridge: moduleName: initialProperties:的方式来创建RCTRootView,然后在initialProperties这个初始化参数字典中,传入页面标识位和其他必要数据。- Android:通过一个 ReactActivityManager 来模拟 Activity 栈的管理,可以实现与 iOS 一样的
popTo功能。在传递Bundle数据的时候,需注意的是Map到Bundle的转换处理。因为在 React Native 端调用push(pageName, params)时,带参情况传入的params为字典,映射到原生端的Map,Bundle对象存入数据时需按对应类型来进行获取。
在这个模式中,不同 React Native 页面之间的通知事件可正常使用,也可以按需在项目中集成 Redux 或是 MobX。口袋中集成了 MobX,类似代码在 App.js 文件中:
// App.js
import { Provider } from 'mobx-react';
import Router from './src/routers';
import stores from './src/stores';
export default (props) => {
const { pageName: routerKey } = props;
const Page = Router[routerKey].default;
return (
<Provider {...stores}>
<Page {...props} />
</Provider>
);
};
store 的注入与普通的纯 React Native 项目一致,在相关页面通过 inject 按需检出子树即可。Router 是路由配置,页面标识位和页面文件路径是字典中 key 和 value 的关系:
// routers.js
export default {
'main_tab': require('./pages'),
'home': require('./pages/home'),
'search': require('./pages/home/Search'),
...
}
所以只要在 routers 中配置好关系,通过 props 的 pageName,即可匹配到不同的 React Native 页面。
与JavaScript的事件交互
既然是混编的 App ,那就免不了原生与 JavaScript 之间的事件交互。为了更方便地进行两端的发布&订阅,封装一个 CJNotification 的工具类。从 JavaScript 到原生端这一块的交互,当前工具类是不提供相关方法的,只是处理原生到 JavaScript 和 不同 React Native 页面之间的事件发布。工具类概览:
import {
NativeEventEmitter,
NativeModules,
Platform,
DeviceEventEmitter,
} from 'react-native';
const { CJNotificationCenter } = NativeModules;
const emitter = Platform.OS === 'android' ? new NativeEventEmitter() : new NativeEventEmitter(CJNotificationCenter);
const NativeEventName = 'NATIVE_TO_RN';
class Emitter {
/**
* 监听从 Native 发来的事件
* @param event 事件名称
* @param callback 监听回调
* @function dispose 销毁监听对象
*/
static addNativeListener = (event, callback) => {
const subscription = emitter.addListener(
NativeEventName,
reminder => {
const { eventName, body } = reminder;
if (eventName !== event) return;
callback && callback(body);
},
);
subscription.dispose = () => subscription && subscription.remove();
return subscription;
};
/**
* 监听不同 RN 页面的通知事件
* @param event 事件名称
* @param callback 监听回调
* @function dispose 销毁监听对象
*/
static addRNListener = (event, callback) => {
const subscription = DeviceEventEmitter.addListener(
event,
reminder => callback && callback(reminder),
);
subscription.dispose = () => subscription && subscription.remove();
return subscription;
};
/**
* 发送 RN 页面之间的通知
* @param event 事件名称
* @param body 发送内容
*/
static sendRNEvent = (event, body) => DeviceEventEmitter.emit(event, body);
}
export default Emitter;
使用方式示例:
import CJNotification from '../utils/CJNotification';
export default class TestPage extends Component {
componentDidMount() {
this.addNativeListener();
this.addRNListener();
}
addNativeListener = () => {
this.nativeListener = CJNotification.addNativeListener('updateUserInfo', userInfo => {
const { name } = userInfo;
// todo sth
});
};
addRNListener = () => {
this.rnListener = CJNotification.addRNListener('updateFeedList', () => {
// todo sth
});
};
componentWillUnmount() {
this.nativeListener.dispose();
this.rnListener.dispose();
}
render() {
...
}
}
iOS 端原生发送事件示例:
#import "CJNotificationCenter.h"
// 发送事件到 JavaScript
[[CJNotificationCenter center] sendRNEventWithName:@"updateUserInfo" body:@{@"name": @"cookiej"}];
Android 端:
// 发送事件到 JavaScript
WritableMap body = Arguments.createMap();
body.putString("name", "cookiej");
CJNotification.sendRNEvent("updateUserInfo", body);
不同 React Naitve 页面之间:
import CJNotification from '../utils/CJNotification';
export default class OtherPage extends Component {
handleUpdateTestPageFeedList = () => CJNotification.sendRNEvent("updateFeedList");
render() {
...
}
}
Demo目录
这里主要是一些平时在有意无意中看到一些效果时,而做的 Demo 实践。没有一一罗列,更多的 Demo 可 clone 项目到本地查看。
类朋友圈查看图片
该效果
是类朋友圈查看图片效果的尝试,不过页码切换有所不一样,支持设置形变动画。运行示例:

新手引导装饰器
该 Demo
是 Decorator 的简易应用,主要是实现一个快速为 React Native App 添加新手引导遮盖的需求,方便快捷易使用,相应组件地址。
浮动文本动画输入框
该 效果 其实是属于 Google 的 Material 系列中的交互效果,上周有简单玩了下 Flutter ,发现里面的输入框组件,就是默认这种交互效果。而 React Native 相关的,其实网上也有类似组件,这里是自己看到效果后,做个简易版实现。运行示例:

类Path菜单动画
该 Demo 是仿 Path 的菜单动画效果:

常见支付密码输入框
该 Demo 是与支付宝类似的密码输入框:

类WhatsApp转场动画
该 Demo 是自己在偶然之中,发现一位国外开发者的 仓库,里面是参考 UI Movement 上的动画而做的 React Native 实现,自己看完也是跃跃欲试,所以写了这个动画 Demo。运行示例:

带索引SectionList
口袋项目中有一个选择汽车的分组列表,在指压并滑动索引时会有动画,项目启动时评估过 React Native 实现的性能问题,最终还是选择了原生实现。恰巧早上写完了家居的业务功能,想着用纯 React Native 来实现这个列表:

iOS 在模拟器上的效果如上所示,JavaScript 线程掉帧还是挺严重的,UI FPS 看起来倒是正常,实际滑动起来表现并不卡。Android 端在模拟器上表现一般般,没有在真机中测试,并且还需要处理 overflow 的问题,所以到时布局还需根据平台做适配处理。
其实之前用官方自带的 SectionList 实现过这个模块,但是效果挺差的,分组跨度较大时,点击索引滚动时会出现白屏(只在 iOS 模拟器下调试,Android 没做进一步尝试)。当前 Demo 基于 react-native-largelist 实现(自己只在该示例中使用了该组件,并未集成到商业项目中)。
粘性TabBar
可能存在于某些商城类 App 中,在页面滚动至顶部时,分段菜单停留在导航栏底部,表现为粘性效果,并可点击菜单项滚动到对应的分组。在 iPhone 效果如下:

不过比较意外的是,iPhone 上运行时,滑动过程中设置了粘性的子组件老是会跳动,Android 反而表现良好……当前 Demo 没有集成下拉刷新,可能仍需基于某些第三方来做定制。
轮播图动画指示器
暂时做了流动样式,后面考虑再做个 scale 渐变样式:
组件
PullRefreshListView
PullRefreshListView 是对 react-native-smart-pull-to-refresh-listview 的二次封装,可自定义下拉刷新、上拖加载更多的样式,也添加了空列表、数据加载出错时(分有数据和无数据)的样式定制,更适用于商业项目使用。简单使用示例:
import PullRefreshListView from './PullRefreshListView';
export default class MsgList extends Component {
pageNo = 1;
msgList = [];
componentDidMount() {
this.listView && this.listView.beginRefresh();
}
fetchMsgList = async() => {
try {
const responseData = await fetch(url).then(res => res.json());
const result = this.pageNo === 1 ? [...responseData.list] : [...this.msgList, ...responseData.list];
this.msgList = result;
const isLoadAll = this.msgList.length >= responseData.total;
this.listView && this.listView.setData(result, this.pageNo, isLoadAll);
} catch (e) {
this.listView
