Skip to content

Repository files navigation

目录

概览

这是一个自己随意玩耍的仓库,主要涉及的东西有以下几部分:

  • 基于 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;

这里是原生端 iOS 和 Android 的对应实现。

使用方式示例:

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

新手引导装饰器

该 Demo 是 Decorator 的简易应用,主要是实现一个快速为 React Native App 添加新手引导遮盖的需求,方便快捷易使用,相应组件地址。

浮动文本动画输入框

该 效果 其实是属于 Google 的 Material 系列中的交互效果,上周有简单玩了下 Flutter ,发现里面的输入框组件,就是默认这种交互效果。而 React Native 相关的,其实网上也有类似组件,这里是自己看到效果后,做个简易版实现。运行示例:

demo

类Path菜单动画

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

demo

常见支付密码输入框

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

demo

类WhatsApp转场动画

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

demo

↑ 返回顶部

带索引SectionList

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

demo

iOS 在模拟器上的效果如上所示,JavaScript 线程掉帧还是挺严重的,UI FPS 看起来倒是正常,实际滑动起来表现并不卡。Android 端在模拟器上表现一般般,没有在真机中测试,并且还需要处理 overflow 的问题,所以到时布局还需根据平台做适配处理。

其实之前用官方自带的 SectionList 实现过这个模块,但是效果挺差的,分组跨度较大时,点击索引滚动时会出现白屏(只在 iOS 模拟器下调试,Android 没做进一步尝试)。当前 Demo 基于 react-native-largelist 实现(自己只在该示例中使用了该组件,并未集成到商业项目中)。

↑ 返回顶部

粘性TabBar

可能存在于某些商城类 App 中,在页面滚动至顶部时,分段菜单停留在导航栏底部,表现为粘性效果,并可点击菜单项滚动到对应的分组。在 iPhone 效果如下:

demo

不过比较意外的是,iPhone 上运行时,滑动过程中设置了粘性的子组件老是会跳动,Android 反而表现良好……当前 Demo 没有集成下拉刷新,可能仍需基于某些第三方来做定制。

↑ 返回顶部

轮播图动画指示器

暂时做了流动样式,后面考虑再做个 scale 渐变样式: demo

↑ 返回顶部

组件

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 && this.listView.setError();
      // and log error message
    }
  };
  
  handleRefresh = () => {
    this.pageNo = 1;
    this.fetchMsgList();
  };
  
  handleLoadMore = () => {
    this.pageNo++;
    this.fetchMsgList();
  };
  
  /**
   *  setError 调用时触发,情况为:
   *  1 第一次进入列表出错时,pageNo 从 1 减至为 0,重置为 1
   *  2 加载更多时出错,此时页码已经加了 1 ,需要减 1,确保再次加载更多时的页码正确
   */
  handleLoadError = () => {
    this.pageNo--;
    if (this.pageNo < 1) {
      this.pageNo = 1;
    }
  };

  render() {
    return (
      <PullRefreshListView
        ref={r => this.listView = r}
        onRefresh={this.handleRefresh}
        onLoadMore={this.handleLoadMore}
        onSetError={this.handleLoadError}
      />
    );
  }
}

↑ 返回顶部

关于HOC应用

基本上,每个页面都会存在首屏渲染和网络出错的占位图,大部分情况下,我们会发现其中的实现逻辑大同小异,所以看到这些页面,自己经常觉得代码很冗余,一直想着有没一些优化的方法。

较早之前写过一个关于新手引导的 组件,是对 HOC 的简单应用,大抵是抽取公用的代码逻辑做为上一层的封装,新手引导内容则由具体组件去负责。基于这种思路,尝试对网络请求的通用业务需求做一次解耦简化,期望是通过一次编写 HOC ,然后不再涉及首屏渲染,或是网络出错这些状态处理的编写逻辑,并支持动态配置不同的占位组件。

于是,有了这个 尝试 。

代码概览

罗列的代码中,将省略部分不必要内容:

// HOCUtils.js

const enhanceFetch = (WrappedComponent, options) => class extends Component {
  static propTypes = {
    requestQueues: PropTypes.array.isRequired, // A.1
  }
  
  constructor(props) {
    super(props)
    this.state = {
      isLoading: true,
      isLoadError: false,
      data: null,
    }
  }

  componentDidMount() {
    this.fetchData()
  }

  fetchData = async () => {
    try {
      const { requestQueues } = this.props
      const requestHandlers = []

      requestQueues.map(request => requestHandlers.push(this.convertHandler(request)))
      const requestResults = await Promise.all(requestHandlers) // A.2
      this.setState({
        isLoading: false,
        data: requestResults.length === 1 ? requestResults[0] : requestResults,
      })
    } catch (e) {
      this.setState({
        isLoading: false,
        isLoadError: true,
      })
    }
  }

  convertHandler = ({url, options = {}}) => {
    return new Promise((resolve, reject) => {
      fetch(url, options)
        .then(res => res.json())
        // TODO:实际上这里还应有接口响应 code 的判断,eg:code === 1 → success
        // 具体跟接口同事协商即可
        .then(responseData => resolve(responseData))
        .catch(err => reject(err))
    })
  }

  handleReload = () => this.setState({ isLoading: true, isLoadError: false }, this.fetchData)

  handleUpdateData = data => this.setState({ data })

  render() {
    const { style, ...rest } = this.props
    const { isLoadError, isLoading, data } = this.state
    const isShowContent = !isLoading && !isLoadError
    const ShowedLoading = options && options.loading || DefaultLoading
    const ShowedNetError = options && options.error || DefaultNetError

    return (
      <View style={[styles.root, style]}>
        {isLoading && <ShowedLoading />}
        {isLoadError && <ShowedNetError onReload={this.handleReload} />}
        {isShowContent &&
          <WrappedComponent
            {...rest}
            data={data}
            fetchData={this.fetchData}
            updateData={this.handleUpdateData}
          />
        }
      </View>
    )
  }
}

export { enhanceFetch }

代码梳理

  • HOC 负责 isLoading、isLoadError 的管理,完成不同占位图的渲染
  • 暴露 enhanceFetch(component: ReactComponent, options: object) 的接口,根据需要在 options 中配置 loading 和 error。如无设置,则使用默认的占位图

关于 props:

  • A.x → requestQueues:这里主要是接收多个请求的配置及其接口响应处理。每个请求将保持 {url: ‘’, options: {}} 的格式,触发请求之前会进行 Promise 化,然后基于 Promise.all() 进行并发。单请求将返回一个结果,并发请求将返回一个结果数组,与传入的请求参数顺序一一对应

关于 WrappedComponent 的 props:

  • data:接口响应的数据
  • fetchData:如果页面需要重新请求数据,通过 this.props.fetchData() 的方式触发
  • updateData:单纯的进行本地数据更新,可采用 this.props.updateData(newData) 的方式,newData 为最新数据,格式应与旧数据保持一致

使用方式:

import { enhanceFetch } from './HOCUtils'

class TargetList extends PureComponent {

  handleUpdateData = () => this.props.updateData && this.props.updateData([0, 1, 2, 3])

  handleFetchData = () => this.props.fetchData && this.props.fetchData()

  renderContent = (item, index) => {
    return (
      <View key={`Content_${index}`} style={styles.item}>
        {item.keywords && <Text>热搜词:{item.keywords}</Text>}
        {item.group_count && <Text>分组数量:{item.group_count}</Text>}
        {Number.isInteger(item) && <Text>Reload data: {item}</Text>}
      </View>
    )
  }

  render() {
    const { data = null } = this.props

    return (
      <View style={styles.root}>
        <View>
          {data && data.map(this.renderContent)}
        </View>
        <View style={styles.btnWrapper}>
          <TouchableOpacity onPress={this.handleUpdateData}>
            <Text>Update Data</Text>
          </TouchableOpacity>
          <TouchableOpacity onPress={this.handleFetchData}>
            <Text>Fetch Data</Text>
          </TouchableOpacity>
        </View>
      </View>
    )
  }
}

const CustomerLoading = () => {
  return (
    <Text>Customer Loading...</Text>
  )
}

// 进行修饰
const FinalList = enhanceFetch(TargetList, { loading: CustomerLoading })

export default () => {
  const requestQueues = [
    {url: 'http://food.boohee.com/fb/v1/keywords', options: {}},
    {url: 'http://food.boohee.com/fb/v1/categories/list', options: {}}
  ]
  return <FinalList requestQueues={requestQueues} />
}

很明显,其实 FinalList 就是智能组件,用于进行占位图、网络请求的配置,或者还有其他配置;而 TargetList 则是木偶组件,无须感知与 UI 无关的其他东西。到这一步,假如要新建业务页面,那么需要做的工作,就是做好接口和占位图的按需配置,然后直接进行 UI 的编码工作即可,无须再处理首屏渲染和网络出错逻辑。

其他思考

  • 列表下拉刷新、加载更多支持?
  1. 为 WrappedComponent 增加 enableRefresh、enableLoadMore 的 props,来开启或忽略这些功能。但是页码的参数名?page?亦或pageNo?
  2. 目前项目中的列表基于 react-native-smart-pull-to-refresh-listview 做了二次封装,满足通用的首屏渲染和网络出错的处理,不过该组件目前仍然未采用 FlatList 实现
  • 其他暂未想到

↑ 返回顶部

About

🍨React Native 相关,涉及 MobX、MST使用,原生简易导航模块、列表组件封装,一些动画尝试,以及 HOC 应用。

Topics

Resources

Stars

96 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages