{"id":26214423,"url":"https://github.com/ljunb/rnprojectplayground","last_synced_at":"2025-04-06T09:08:39.113Z","repository":{"id":92176819,"uuid":"114863318","full_name":"ljunb/RNProjectPlayground","owner":"ljunb","description":"🍨React Native 相关，涉及 MobX、MST使用，原生简易导航模块、列表组件封装，一些动画尝试，以及 HOC 应用。","archived":false,"fork":false,"pushed_at":"2025-01-04T09:40:45.000Z","size":10514,"stargazers_count":97,"open_issues_count":1,"forks_count":24,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-03-30T08:08:33.783Z","etag":null,"topics":["animation","demo","hoc","mobx-mst","react-native","sectionlist"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ljunb.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-12-20T08:37:10.000Z","updated_at":"2025-01-13T14:09:00.000Z","dependencies_parsed_at":null,"dependency_job_id":"a3487ef2-40fb-427e-ab0d-145f69502f5c","html_url":"https://github.com/ljunb/RNProjectPlayground","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ljunb%2FRNProjectPlayground","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ljunb%2FRNProjectPlayground/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ljunb%2FRNProjectPlayground/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ljunb%2FRNProjectPlayground/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ljunb","download_url":"https://codeload.github.com/ljunb/RNProjectPlayground/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247457802,"owners_count":20941906,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["animation","demo","hoc","mobx-mst","react-native","sectionlist"],"created_at":"2025-03-12T10:16:57.350Z","updated_at":"2025-04-06T09:08:39.088Z","avatar_url":"https://github.com/ljunb.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"## 目录\n- [概览](#概览)\n- [导航功能](#导航功能)\n- [与JavaScript的事件交互](#与javascript的事件交互)\n- [Demo目录](#demo目录)\n  - [类朋友圈查看图片](#类朋友圈查看图片)\n  - [新手引导装饰器](#新手引导装饰器)\n  - [浮动文本动画输入框](#浮动文本动画输入框)\n  - [类Path菜单动画](#类path菜单动画)\n  - [常见支付密码输入框](#常见支付密码输入框)\n  - [类WhatsApp转场动画](#类whatsapp转场动画)\n  - [带索引SectionList](#带索引sectionlist)\n  - [粘性TabBar](#粘性tabbar)\n  - [轮播图动画指示器](#轮播图动画指示器)\n- [组件](#组件)\n  - [PullRefreshListView](#pullrefreshlistview)\n- [HOC应用（网络占位图处理）](#关于hoc应用)\n  - [代码概览](#代码概览)\n  - [代码梳理](#代码梳理)\n  - [使用方式](#使用方式)\n  - [其他思考](#其他思考)\n\n## 概览\n这是一个自己随意玩耍的仓库，主要涉及的东西有以下几部分：\n* 基于 [MobX](https://github.com/mobxjs/mobx) 和 [MST](https://github.com/mobxjs/mobx-state-tree) 重新实现了 React Native 版食物派的个别页面\n* 通过 UINavigationController 和 Activity ，实现导航功能：push、pop、popTo、popToRoot。每个页面，React Native 都只作为 View 的角色存在\n* 收集一些自己的练习 Demo、组件，或是项目实践中的想法\n\n## 导航功能\n口袋蜜蜂（[AppStore](https://itunes.apple.com/cn/app/%E5%8F%A3%E8%A2%8B%E8%9C%9C%E8%9C%82/id1268533784?mt=8) | [小米应用商店](http://app.mi.com/details?id=cn.com.pcauto.pocket)）是混编 App，在项目启动的前期，跟同事一起尝试了原生与 React Native 页面之间的各种导航场景，在此过程中也尝试了不同的几个 React Native 导航组件，略去其中细节，一番尝试后，回过头来想：既然是原生为主导，为何不就地取材，直接用原生的导航功能？React Native 本来就应该只承担 `View` 层的角色，数据的流转，实际仍是在原生层面。\n\n因此，每次跳转的起始或最终界面，不管是原生，还是 React Native 页面，实际上都是原生到原生的导航。React Native 可通过注册多个 `Component` 的形式来加载多个页面，而口袋在几个版本的迭代下来之后，我们总结了较为推荐的方式是：\n\n\u003e * 共同：只注册一个 `Component`，不同页面在初始参数中添加标识位区分\n\u003e * iOS：采用单例 `RCTBridge`，并通过 `- initWithBridge: moduleName: initialProperties:` 的方式来创建 `RCTRootView`，然后在 `initialProperties` 这个初始化参数字典中，传入页面标识位和其他必要数据。\n\u003e * Android：通过一个 [ReactActivityManager](https://github.com/ljunb/RNProjectPlayground/blob/master/android/app/src/main/java/com/rnprojectplayground/ReactActivityManager.java) 来模拟 Activity 栈的管理，可以实现与 iOS 一样的 `popTo` 功能。在传递 `Bundle` 数据的时候，需注意的是 `Map` 到 `Bundle` 的转换处理。因为在 React Native 端调用 `push(pageName, params)` 时，带参情况传入的 `params` 为字典，映射到原生端的 `Map`，`Bundle` 对象存入数据时需按对应类型来进行获取。\n\n在这个模式中，不同 React Native 页面之间的通知事件可正常使用，也可以按需在项目中集成 Redux 或是 MobX。口袋中集成了 MobX，类似代码在 [App.js](https://github.com/ljunb/RNProjectPlayground/blob/master/App.js) 文件中：\n```javascript\n// App.js\nimport { Provider } from 'mobx-react';\nimport Router from './src/routers';\nimport stores from './src/stores';\n\nexport default (props) =\u003e {\n  const { pageName: routerKey } = props;\n  const Page = Router[routerKey].default;\n  return (\n    \u003cProvider {...stores}\u003e\n      \u003cPage {...props} /\u003e\n    \u003c/Provider\u003e\n  );\n};\n```\n\n`store` 的注入与普通的纯 React Native 项目一致，在相关页面通过 `inject` 按需检出子树即可。`Router` 是路由配置，页面标识位和页面文件路径是字典中 `key` 和 `value` 的关系：\n```javascript\n// routers.js\n\nexport default {\n  'main_tab': require('./pages'),\n  'home': require('./pages/home'),\n  'search': require('./pages/home/Search'),\n  ...\n}\n```\n所以只要在 [routers](https://github.com/ljunb/RNProjectPlayground/blob/master/src/routers.js) 中配置好关系，通过 `props` 的 `pageName`，即可匹配到不同的 React Native 页面。\n\n[↑ 返回顶部](#目录)\n\n## 与JavaScript的事件交互\n既然是混编的 App ，那就免不了原生与 JavaScript 之间的事件交互。为了更方便地进行两端的发布\u0026订阅，封装一个 [CJNotification](https://github.com/ljunb/RNProjectPlayground/blob/master/src/utils/CJNotification.js) 的工具类。从 JavaScript 到原生端这一块的交互，当前工具类是不提供相关方法的，只是处理原生到 JavaScript 和 不同 React Native 页面之间的事件发布。工具类概览：\n```javascript\nimport {\n  NativeEventEmitter,\n  NativeModules,\n  Platform,\n  DeviceEventEmitter,\n} from 'react-native';\n\nconst { CJNotificationCenter } = NativeModules;\nconst emitter = Platform.OS === 'android' ? new NativeEventEmitter() : new NativeEventEmitter(CJNotificationCenter);\nconst NativeEventName = 'NATIVE_TO_RN';\n\nclass Emitter {\n  /**\n   * 监听从 Native 发来的事件\n   * @param event 事件名称\n   * @param callback 监听回调\n   * @function dispose 销毁监听对象\n   */\n  static addNativeListener = (event, callback) =\u003e {\n    const subscription = emitter.addListener(\n      NativeEventName,\n      reminder =\u003e {\n        const { eventName, body } = reminder;\n        if (eventName !== event) return;\n        callback \u0026\u0026 callback(body);\n      },\n    );\n    subscription.dispose = () =\u003e subscription \u0026\u0026 subscription.remove();\n    return subscription;\n  };\n\n  /**\n   * 监听不同 RN 页面的通知事件\n   * @param event 事件名称\n   * @param callback 监听回调\n   * @function dispose 销毁监听对象\n   */\n  static addRNListener = (event, callback) =\u003e {\n    const subscription = DeviceEventEmitter.addListener(\n      event,\n      reminder =\u003e callback \u0026\u0026 callback(reminder),\n    );\n    subscription.dispose = () =\u003e subscription \u0026\u0026 subscription.remove();\n    return subscription;\n  };\n\n  /**\n   * 发送 RN 页面之间的通知\n   * @param event 事件名称\n   * @param body 发送内容\n   */\n  static sendRNEvent = (event, body) =\u003e DeviceEventEmitter.emit(event, body);\n}\n\nexport default Emitter;\n```\n这里是原生端 [iOS](https://github.com/ljunb/RNProjectPlayground/blob/master/ios/Utils/CJNotificationCenter.m) 和 [Android](https://github.com/ljunb/RNProjectPlayground/blob/master/android/app/src/main/java/com/rnprojectplayground/CJNotification.java) 的对应实现。\n\n使用方式示例：\n```javascript\nimport CJNotification from '../utils/CJNotification';\n\nexport default class TestPage extends Component {\n  componentDidMount() {\n    this.addNativeListener();\n    this.addRNListener();\n  }\n\n  addNativeListener = () =\u003e {\n    this.nativeListener = CJNotification.addNativeListener('updateUserInfo', userInfo =\u003e {\n      const { name } = userInfo;\n      // todo sth\n    });\n  };\n\n  addRNListener = () =\u003e {\n    this.rnListener = CJNotification.addRNListener('updateFeedList', () =\u003e {\n      // todo sth\n    });\n  };\n\n  componentWillUnmount() {\n    this.nativeListener.dispose();\n    this.rnListener.dispose();\n  }\n\n  render() {\n    ...\n  }\n}\n```\niOS 端原生发送事件示例：\n```obj-c\n#import \"CJNotificationCenter.h\"\n\n// 发送事件到 JavaScript\n[[CJNotificationCenter center] sendRNEventWithName:@\"updateUserInfo\" body:@{@\"name\": @\"cookiej\"}];\n```\nAndroid 端：\n```java\n// 发送事件到 JavaScript\nWritableMap body = Arguments.createMap();\nbody.putString(\"name\", \"cookiej\");\nCJNotification.sendRNEvent(\"updateUserInfo\", body);\n```\n不同 React Naitve 页面之间：\n```javascript\nimport CJNotification from '../utils/CJNotification';\n\nexport default class OtherPage extends Component {\n\n  handleUpdateTestPageFeedList = () =\u003e CJNotification.sendRNEvent(\"updateFeedList\");\n\n  render() {\n    ...\n  }\n}\n\n```\n\n[↑ 返回顶部](#目录)\n\n## Demo目录\n这里主要是一些平时在有意无意中看到一些效果时，而做的 Demo 实践。没有一一罗列，更多的 Demo 可 `clone` 项目到本地查看。\n### 类朋友圈查看图片\n[该效果](https://github.com/ljunb/RNProjectPlayground/blob/master/src/pages/demos/gallery/index.js) \n 是类朋友圈查看图片效果的尝试，不过页码切换有所不一样，支持设置形变动画。运行示例：\n![demo](https://github.com/ljunb/screenshots/blob/master/gallery.gif)\n\n### 新手引导装饰器\n该 [Demo](https://github.com/ljunb/RNProjectPlayground/blob/master/src/pages/demos/guidance/NewGuidePage.js) \n是 `Decorator` 的简易应用，主要是实现一个快速为 React Native App 添加新手引导遮盖的需求，方便快捷易使用，[相应组件地址](https://github.com/ljunb/rn-beginner-guidance-decorator)。\n\n### 浮动文本动画输入框\n该 [效果]((https://github.com/ljunb/RNProjectPlayground/blob/master/src/pages/demos/animation/textinput.js)) 其实是属于 Google 的 Material 系列中的交互效果，上周有简单玩了下 [Flutter](https://github.com/flutter/flutter) ，发现里面的输入框组件，就是默认这种交互效果。而 React Native 相关的，其实网上也有类似组件，这里是自己看到效果后，做个简易版实现。运行示例：\n\n![demo](https://github.com/ljunb/screenshots/blob/master/floating.gif)\n\n### 类Path菜单动画\n该 [Demo](https://github.com/ljunb/RNProjectPlayground/blob/master/src/pages/demos/animation/path.js) 是仿 Path 的菜单动画效果：\n\n![demo](https://github.com/ljunb/screenshots/blob/master/path.gif)\n\n### 常见支付密码输入框\n该 [Demo](https://github.com/ljunb/RNProjectPlayground/blob/master/src/pages/demos/pay/PasswordInput.js) 是与支付宝类似的密码输入框：\n\n![demo](https://github.com/ljunb/screenshots/blob/master/password_input.gif)\n\n### 类WhatsApp转场动画\n该 [Demo](https://github.com/ljunb/RNProjectPlayground/blob/master/src/pages/demos/animation/uimovements/index.js) 是自己在偶然之中，发现一位国外开发者的 [仓库](https://github.com/kiok46/ReactNative-Animation-Challenges)，里面是参考 [UI Movement](https://uimovement.com/) 上的动画而做的 React Native 实现，自己看完也是跃跃欲试，所以写了这个动画 Demo。运行示例：\n\n![demo](https://github.com/ljunb/screenshots/blob/master/uimovement.gif)\n\n[↑ 返回顶部](#目录)\n\n### 带索引SectionList\n口袋项目中有一个选择汽车的分组列表，在指压并滑动索引时会有动画，项目启动时评估过 React Native 实现的性能问题，最终还是选择了原生实现。恰巧早上写完了家居的业务功能，想着用纯 React Native 来实现这个列表：\n\n![demo](https://github.com/ljunb/screenshots/blob/master/atoz.gif)\n\niOS 在模拟器上的效果如上所示，JavaScript 线程掉帧还是挺严重的，UI FPS 看起来倒是正常，实际滑动起来表现并不卡。Android 端在模拟器上表现一般般，没有在真机中测试，并且还需要处理 `overflow` 的问题，所以到时布局还需根据平台做适配处理。\n\n其实之前用官方自带的 SectionList 实现过这个模块，但是效果挺差的，分组跨度较大时，点击索引滚动时会出现白屏（只在 iOS 模拟器下调试，Android 没做进一步尝试）。当前 [Demo](https://github.com/ljunb/RNProjectPlayground/blob/master/src/pages/demos/largelist/index.js) 基于 [react-native-largelist](https://github.com/bolan9999/react-native-largelist) 实现（自己只在该示例中使用了该组件，并未集成到商业项目中）。\n\n[↑ 返回顶部](#目录)\n\n### 粘性TabBar\n可能存在于某些商城类 App 中，在页面滚动至顶部时，分段菜单停留在导航栏底部，表现为粘性效果，并可点击菜单项滚动到对应的分组。在 iPhone 效果如下：\n\n![demo](https://github.com/ljunb/screenshots/blob/master/sticky.gif)\n\n不过比较意外的是，iPhone 上运行时，滑动过程中设置了粘性的子组件老是会跳动，Android 反而表现良好……当前 [Demo](https://github.com/ljunb/RNProjectPlayground/blob/master/src/pages/demos/stickytabbar/index.js) 没有集成下拉刷新，可能仍需基于某些第三方来做定制。\n\n[↑ 返回顶部](#目录)\n\n### 轮播图动画指示器\n暂时做了流动样式，后面考虑再做个 `scale` 渐变样式：\n![demo](https://github.com/ljunb/screenshots/blob/master/indicator.mov)\n\n[↑ 返回顶部](#目录)\n\n## 组件\n### PullRefreshListView\n[PullRefreshListView](https://github.com/ljunb/RNProjectPlayground/blob/master/src/components/PullRefreshListView.js)\n是对 [react-native-smart-pull-to-refresh-listview](https://github.com/react-native-component/react-native-smart-pull-to-refresh-listview) \n的二次封装，可自定义下拉刷新、上拖加载更多的样式，也添加了空列表、数据加载出错时（分有数据和无数据）的样式定制，更适用于商业项目使用。简单使用示例：\n```javascript\n\nimport PullRefreshListView from './PullRefreshListView';\n\nexport default class MsgList extends Component {\n  pageNo = 1;\n  msgList = [];\n\n  componentDidMount() {\n    this.listView \u0026\u0026 this.listView.beginRefresh();\n  }\n  \n  fetchMsgList = async() =\u003e {\n    try {\n      const responseData = await fetch(url).then(res =\u003e res.json());\n      \n      const result = this.pageNo === 1 ? [...responseData.list] : [...this.msgList, ...responseData.list];\n      this.msgList = result;\n      const isLoadAll = this.msgList.length \u003e= responseData.total;\n      this.listView \u0026\u0026 this.listView.setData(result, this.pageNo, isLoadAll);\n    } catch (e) { \n      this.listView \u0026\u0026 this.listView.setError();\n      // and log error message\n    }\n  };\n  \n  handleRefresh = () =\u003e {\n    this.pageNo = 1;\n    this.fetchMsgList();\n  };\n  \n  handleLoadMore = () =\u003e {\n    this.pageNo++;\n    this.fetchMsgList();\n  };\n  \n  /**\n   *  setError 调用时触发，情况为：\n   *  1 第一次进入列表出错时，pageNo 从 1 减至为 0，重置为 1\n   *  2 加载更多时出错，此时页码已经加了 1 ，需要减 1，确保再次加载更多时的页码正确\n   */\n  handleLoadError = () =\u003e {\n    this.pageNo--;\n    if (this.pageNo \u003c 1) {\n      this.pageNo = 1;\n    }\n  };\n\n  render() {\n    return (\n      \u003cPullRefreshListView\n        ref={r =\u003e this.listView = r}\n        onRefresh={this.handleRefresh}\n        onLoadMore={this.handleLoadMore}\n        onSetError={this.handleLoadError}\n      /\u003e\n    );\n  }\n}\n```\n\n[↑ 返回顶部](#目录)\n\n## 关于HOC应用\n基本上，每个页面都会存在首屏渲染和网络出错的占位图，大部分情况下，我们会发现其中的实现逻辑大同小异，所以看到这些页面，自己经常觉得代码很冗余，一直想着有没一些优化的方法。\n\n较早之前写过一个关于新手引导的 [组件](https://github.com/ljunb/rn-beginner-guidance-decorator)，是对 HOC 的简单应用，大抵是抽取公用的代码逻辑做为上一层的封装，新手引导内容则由具体组件去负责。基于这种思路，尝试对网络请求的通用业务需求做一次解耦简化，期望是通过一次编写 HOC ，然后不再涉及首屏渲染，或是网络出错这些状态处理的编写逻辑，并支持动态配置不同的占位组件。\n\n于是，有了这个 [尝试](https://github.com/ljunb/RNProjectPlayground/blob/master/src/pages/demos/decorators/index.js) 。\n\n### 代码概览\n罗列的代码中，将省略部分不必要内容：\n```javascript\n// HOCUtils.js\n\nconst enhanceFetch = (WrappedComponent, options) =\u003e class extends Component {\n  static propTypes = {\n    requestQueues: PropTypes.array.isRequired, // A.1\n  }\n  \n  constructor(props) {\n    super(props)\n    this.state = {\n      isLoading: true,\n      isLoadError: false,\n      data: null,\n    }\n  }\n\n  componentDidMount() {\n    this.fetchData()\n  }\n\n  fetchData = async () =\u003e {\n    try {\n      const { requestQueues } = this.props\n      const requestHandlers = []\n\n      requestQueues.map(request =\u003e requestHandlers.push(this.convertHandler(request)))\n      const requestResults = await Promise.all(requestHandlers) // A.2\n      this.setState({\n        isLoading: false,\n        data: requestResults.length === 1 ? requestResults[0] : requestResults,\n      })\n    } catch (e) {\n      this.setState({\n        isLoading: false,\n        isLoadError: true,\n      })\n    }\n  }\n\n  convertHandler = ({url, options = {}}) =\u003e {\n    return new Promise((resolve, reject) =\u003e {\n      fetch(url, options)\n        .then(res =\u003e res.json())\n        // TODO：实际上这里还应有接口响应 code 的判断，eg：code === 1 → success\n        // 具体跟接口同事协商即可\n        .then(responseData =\u003e resolve(responseData))\n        .catch(err =\u003e reject(err))\n    })\n  }\n\n  handleReload = () =\u003e this.setState({ isLoading: true, isLoadError: false }, this.fetchData)\n\n  handleUpdateData = data =\u003e this.setState({ data })\n\n  render() {\n    const { style, ...rest } = this.props\n    const { isLoadError, isLoading, data } = this.state\n    const isShowContent = !isLoading \u0026\u0026 !isLoadError\n    const ShowedLoading = options \u0026\u0026 options.loading || DefaultLoading\n    const ShowedNetError = options \u0026\u0026 options.error || DefaultNetError\n\n    return (\n      \u003cView style={[styles.root, style]}\u003e\n        {isLoading \u0026\u0026 \u003cShowedLoading /\u003e}\n        {isLoadError \u0026\u0026 \u003cShowedNetError onReload={this.handleReload} /\u003e}\n        {isShowContent \u0026\u0026\n          \u003cWrappedComponent\n            {...rest}\n            data={data}\n            fetchData={this.fetchData}\n            updateData={this.handleUpdateData}\n          /\u003e\n        }\n      \u003c/View\u003e\n    )\n  }\n}\n\nexport { enhanceFetch }\n```\n### 代码梳理\n* HOC 负责 `isLoading`、`isLoadError` 的管理，完成不同占位图的渲染\n* 暴露 `enhanceFetch(component: ReactComponent, options: object)` 的接口，根据需要在 `options` 中配置 `loading` 和 `error`。如无设置，则使用默认的占位图\n\n关于 `props`：\n* `A.x` → `requestQueues`：这里主要是接收多个请求的配置及其接口响应处理。每个请求将保持 `{url: ‘’, options: {}}` 的格式，触发请求之前会进行 `Promise` 化，然后基于 `Promise.all()` 进行并发。单请求将返回一个结果，并发请求将返回一个结果数组，与传入的请求参数顺序一一对应\n\n关于 `WrappedComponent` 的 `props`：\n* `data`：接口响应的数据\n* `fetchData`：如果页面需要重新请求数据，通过 `this.props.fetchData()` 的方式触发\n* `updateData`：单纯的进行本地数据更新，可采用 `this.props.updateData(newData)` 的方式，`newData` 为最新数据，格式应与旧数据保持一致\n\n### 使用方式：\n```javascript\nimport { enhanceFetch } from './HOCUtils'\n\nclass TargetList extends PureComponent {\n\n  handleUpdateData = () =\u003e this.props.updateData \u0026\u0026 this.props.updateData([0, 1, 2, 3])\n\n  handleFetchData = () =\u003e this.props.fetchData \u0026\u0026 this.props.fetchData()\n\n  renderContent = (item, index) =\u003e {\n    return (\n      \u003cView key={`Content_${index}`} style={styles.item}\u003e\n        {item.keywords \u0026\u0026 \u003cText\u003e热搜词：{item.keywords}\u003c/Text\u003e}\n        {item.group_count \u0026\u0026 \u003cText\u003e分组数量：{item.group_count}\u003c/Text\u003e}\n        {Number.isInteger(item) \u0026\u0026 \u003cText\u003eReload data: {item}\u003c/Text\u003e}\n      \u003c/View\u003e\n    )\n  }\n\n  render() {\n    const { data = null } = this.props\n\n    return (\n      \u003cView style={styles.root}\u003e\n        \u003cView\u003e\n          {data \u0026\u0026 data.map(this.renderContent)}\n        \u003c/View\u003e\n        \u003cView style={styles.btnWrapper}\u003e\n          \u003cTouchableOpacity onPress={this.handleUpdateData}\u003e\n            \u003cText\u003eUpdate Data\u003c/Text\u003e\n          \u003c/TouchableOpacity\u003e\n          \u003cTouchableOpacity onPress={this.handleFetchData}\u003e\n            \u003cText\u003eFetch Data\u003c/Text\u003e\n          \u003c/TouchableOpacity\u003e\n        \u003c/View\u003e\n      \u003c/View\u003e\n    )\n  }\n}\n\nconst CustomerLoading = () =\u003e {\n  return (\n    \u003cText\u003eCustomer Loading...\u003c/Text\u003e\n  )\n}\n\n// 进行修饰\nconst FinalList = enhanceFetch(TargetList, { loading: CustomerLoading })\n\nexport default () =\u003e {\n  const requestQueues = [\n    {url: 'http://food.boohee.com/fb/v1/keywords', options: {}},\n    {url: 'http://food.boohee.com/fb/v1/categories/list', options: {}}\n  ]\n  return \u003cFinalList requestQueues={requestQueues} /\u003e\n}\n```\n很明显，其实 `FinalList` 就是智能组件，用于进行占位图、网络请求的配置，或者还有其他配置；而 `TargetList` 则是木偶组件，无须感知与 UI 无关的其他东西。到这一步，假如要新建业务页面，那么需要做的工作，就是做好接口和占位图的按需配置，然后直接进行 UI 的编码工作即可，无须再处理首屏渲染和网络出错逻辑。\n\n### 其他思考\n* 列表下拉刷新、加载更多支持？\n\u003e 1. 为 `WrappedComponent` 增加 `enableRefresh`、`enableLoadMore` 的 `props`，来开启或忽略这些功能。但是页码的参数名？page？亦或pageNo？\n\u003e 2. 目前项目中的列表基于 [react-native-smart-pull-to-refresh-listview](https://github.com/react-native-component/react-native-smart-pull-to-refresh-listview) 做了二次封装，满足通用的首屏渲染和网络出错的处理，不过该组件目前仍然未采用 `FlatList` 实现\n\n* 其他暂未想到\n\n[↑ 返回顶部](#目录)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fljunb%2Frnprojectplayground","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fljunb%2Frnprojectplayground","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fljunb%2Frnprojectplayground/lists"}