RN的生态圈很火爆,但是很难找到一个开箱即用的 React Native APP Demo。目前存在的 Demo 要么过于简单,比如 React Native 官网提供的 Demo AwesomeProject ,这个 Demo 只提供了最简功能,对于路由(导航组件)、状态管理等并没有涉及。虽然 React Native 教程中对于复杂应用应如何选择组件及第三方库都有提及,但并没有给出完整示例。还有一些demo可能版本比较旧,对于新手来说,语法和代码组织方式都有变化,结合官方api看的话,会比较懵逼,哪哪对不上的赶脚。而另一方面,又有很多 React Native APP 虽已开源,但都是用于特定场合的完整 APP,有些 APP 的目录结构本身就不友好,并且也没有完整的说明文档。 其次,相对于vue,React 本身的学习曲线就相对陡峭,尤其涉及状态管理部分,很难找到可以直接 copy-paste 的代码,除此之外原生 App 本身还有很多区别于 web 的需求。
找了很多demo,L小庸的demo真的很棒,个人没有直接download小庸的github代码,基本上市按照步骤自己敲或者copy一部分代码,先让整个demo可以跑起来,再慢慢研究相关的功能和语法,API等,虽然敲的过程也遇到了很多麻烦,运行不起来等问题,但全程撸完一遍代码,有一个比较完整的demo实现,也算有一点点成就感。
个人的代码还未更新到github上,按照本文的步骤,step by step就运行起来。后续会掺杂一些对某部分内容的额外的理解或更多使用场景的demo,在代码里会写比较详细的注释,后续都更新到github上。
鉴于以上原因,所以决定写篇文章记录下学习过程,再次感谢L小庸的文章和demo, 内容较多,这部分主要内容为:
- react navigation 作为路由(导航)组件的初步使用
- 自定义组件
- 通过 fetch API 发送网络请求
- 集成 redux,并实现 redux 状态的持久化存储
一 准备工作
使用自己喜欢的编辑器,安装RN相关插件,个人使用的sublime text3,配置了插件后,使用起来也还是不比较顺手的。
二 官方 Demo 下载及介绍
官方 demo 虽然不完整,但却是一个很好的开始。介绍完官方 Demo(包括环境配置),后文会一步步介绍如何从这个不完整的官方 Demo 改造成可用于生产的 APP。
2.1 环境配置
下载官方 Demo:AwesomeProject,然后运行。
所需的环境配置官方文档讲的很清楚,这里不在赘述。需要指出的是 React Native 对于运行 Demo 提供了两种方法:一种是在 Expo 客户端中运行,另一种是编译成原生代码(安卓编译成 Java,iOS 编译成 objective-C)后在模拟器或者在真机上运行。推荐直接使用第二种,如果想发布 APP 这也是绕不过去的。
如果之前没有开发过原生 APP,还需要熟悉下原生 APP 的开发工具:安卓使用 Android Studio,iOS 使用 Xcode。它们如何配合 React Native 使用在 官方文档有说明,遇到问题自行谷歌一般都有解决方案。
需要说明的是 Android Studio 很多依赖更新需要访问谷歌服务,所以请自备梯子。
这段完全copy自L小庸的文章,个人没有mac,所以很多细节并不了解,也先记录着,方便后续采坑参考。
2.2 官方 Demo 目录介绍
上面的目录结构说明如下,重要的有:
android/ ios/ index.js App.js
上面是最重要的四个目录/文件,其他说明如下:
app.json package.json node_modules yarn.lock
// App.js import React, { Component } from 'react'; import App from './src/config/route' export default class RootApp extends Component { constructor(props) { super(props); } render() { // 渲染页面 return <App />; } } 复制代码
/** * ScreenHome/index.js */ import React, {Component} from 'react'; import view from './view' export default class ScreenHome extends Component { // 自定义当前页面路由配置,后面用到的createBottomTabNavigator也使用这个对象中的属性 static navigationOptions = { title: '首页', }; constructor(props) { super(props); this.navigation = props.navigation; } render() { return view(this); } } 复制代码
// 引入依赖 import React, {Component} from 'react'; import {Text, View, Button} from 'react-native' export default self => ( <View> <Text style={{ fontSize: 36 }}>home</Text> <Button title="ScreenSome1" // 路由跳转 onPress={() => self.navigation.navigate("ScreenSome1")} /> </View> ); 复制代码
3.2 createTabNavigator实现页面底部 tab 切换
/** * ScreenTab1/index.js */ import React, {Component} from 'react'; import {Text, View, Button} from 'react-native' export default class ScreenSome1 extends Component { // 自定义当前页面路由配置,后面用到的createBottomTabNavigator也使用这个对象中的属性 static navigationOptions = { // 设置 title title: "TAB1" }; constructor(props) { super(props); this.navigation = props.navigation; } render() { return( <View> <Text style={{ fontSize: 36 }}>TAB1</Text> </View> ); } } 复制代码
此时只需要配置 ScreenBottomTab
里面的 index.js
文件就好,如下:
/** * ScreenBottomTab/index.js */ import { createBottomTabNavigator } from 'react-navigation' import ScreenHome from '../../screens/ScreenHome'; import ScreenTab1 from '../../screens/ScreenTab1'; import ScreenTab2 from '../../screens/ScreenTab2'; import ScreenTab3 from '../../screens/ScreenTab3'; const ScreenTab = createBottomTabNavigator( // 配置 tab 路由 { ScreenHome: ScreenHome, ScreenTab1: ScreenTab1, ScreenTab2: ScreenTab2, ScreenTab3: ScreenTab3, }, // 其他配置选项 { tabBarPosition: "bottom" } ); export default ScreenTab;复制代码
// 引入依赖 import { createStackNavigator, createAppContainer } from 'react-navigation' // 引入页面组件 import ScreenBottomTab from '../screens/ScreenBottomTab'; // 配置路由 const navigator = createStackNavigator({ ScreenBottomTab: ScreenBottomTab, }) const App = createAppContainer(navigator) export default App复制代码
/** * ScreenHome/index.js */ import React, {Component} from 'react'; import { Image } from 'react-native' import view from './view' export default class ScreenHome extends Component { static navigationOptions = { title: '首页', tabBarIcon: ({ focused }) => { const icon = focused ? require('../../assets/images/tab_home_active.png') : require('../../assets/images/tab_home.png'); return <Image source={icon} style={{ height: 22, width: 22 }} />; }, }; constructor(props) { super(props); this.navigation = props.navigation; } render() { return view(this); } } 复制代码
四 自定义组件
react native 已经封装了很多常用组件,但有时我们仍然需要在次基础上进行封装,比如某些组件需要大量复用而原生组件样式或者交互逻辑不符合需求。
这里只介绍目录结构的调整,具体代码可参考 Github 上项目代码,因为自定义组件的需求千差万别,具体编写过程也有很多教程,这里不再具体介绍,只添加了自定义 Toast 组件。目录结构调整如下:
文件 config/pxToDp.js
用于尺寸自适应,在 XgToast.js
中有使用,这段组件从L小庸的代码中拷贝而来,具体功能可自行查看
五 网络请求
react native 使用上有个最大的好处是可以不用考虑新语法兼容性的问题,既然如此,自然使用设计更加优良的 API,在网络请求方面,本项目使用fetch API。
5.1 配置 fetch api
/** * xgHttp.js */ // 请求服务器host const host = "http://api.juheapi.com"; export default async function( method, url, { bodyParams = {}, urlParams = {} } ) { const headers = new Headers(); headers.append("Content-Type", "application/json"); // 将url参数写入URL let urlParStr = ""; const urlParArr = Object.keys(urlParams); if (urlParArr.length) { Object.keys(urlParams).forEach(element => { urlParStr += `${element}=${urlParams[element]}&`; }); urlParStr = `?${urlParStr}`.slice(0, -1); } const res = await fetch( new Request(`${host}${url}${urlParStr}`, { method, headers, // 如果是 get 或者 head 方法,不添加请求头部 body: method === ("GET" || "HEAD") ? null : JSON.stringify(bodyParams) }) ); if (res.status < 200 || res.status > 299) { console.log(`出错啦:${res.status}`); } else { return res.json(); } }复制代码
5.2 请求 api 编写及使用
具体 api 请求代码我放在了 xgRequest.js
文件中,以 get
请求为例, xgRequest.js
代码如下:
/** * xgRequest.js */ import XgHttp from "./xgHttp"; export default { todayOnHistory: urlPar => XgHttp("GET", "/japi/toh", { urlParams: urlPar }) }; 复制代码
接口调用是在页面文件的 index.js
中进行的,以 ScreenTab1/index.js
为例:
/** * ScreenTab1/index.js */ const urlPar = { // 大佬们,这个是我申请的聚合数据应用的key,每天只有100免费请求次数 key: '7606e878163d494b376802115f30dd4e', v: '1.0', month: Number(this.state.inputMonthText), day: Number(this.state.inputDayText), }; // 拿到返回数据后就可以进一步操作了 const todayOnHistoryInfo = await XgRequest.todayOnHistory(urlPar);复制代码
首先是在 index.js
中把需要动态展示的数据先写入 state
:
/** * ScreenTab1/index.js */ // 将需要动态更新的数据放入 state this.state = { todayOnHistoryInfo: {} };复制代码
import React, { Component } from 'react'; import { Image,Alert } from 'react-native'; import view from './view'; import XgRequest from '../../config/xgRequest'; export default class ScreenTab1 extends Component { static navigationOptions = { title: '网络请求(TAB1)', tabBarIcon: ({ focused }) => { const icon = focused ? require('../../assets/images/tab_home_active.png') : require('../../assets/images/tab_home.png'); return <Image source={icon} style={{ height: 22, width: 22 }} />; }, }; constructor(props) { super(props); this.navigation = props.navigation; // 将需要动态更新的数据放入 state this.state = { todayOnHistoryInfo: {}, inputMonthText: '', inputDayText: '', }; } async getTodayOnHistoryInfo() { if (!this.state.inputMonthText || !this.state.inputDayText) { this.xgToast.show('请输入有效数据', 2000, 'error'); return; } try { const urlPar = { // 大佬们,这个是我申请的聚合数据应用的key,每天只有100免费请求次数 key: '7606e878163d494b376802115f30dd4e', v: '1.0', month: Number(this.state.inputMonthText), day: Number(this.state.inputDayText), }; const todayOnHistoryInfo = await XgRequest.todayOnHistory(urlPar); // 捕获错误,具体捕获过程需与写api的同学商量确定 if (todayOnHistoryInfo.error_code) { this.xgToast.show(todayOnHistoryInfo.reason, 2000, 'error'); } else { // 更新state,render函数自动重新渲染DOM中变化了的那部分 this.setState({ todayOnHistoryInfo }); } } catch (e) { console.log(e); } } render() { return view(this); } } 复制代码
/** * ScreenTab1/view.js */ { /* 查询 */ } <Button title="查询" onPress={() => self.getTodayOnHistoryInfo()} />; { /* 展示查询数据 */ } <Text> 发生了啥事:{self.state.todayOnHistoryInfo.result ? self.state.todayOnHistoryInfo.result[0].des : "暂无数据"} </Text>;复制代码
view.js完整代码,其中style.js可直接copy先看效果
import React from 'react'; import { View, Button, Text, TextInput } from 'react-native'; import styles from './style'; // 引入 toast 组件 import XgToast from '../../components/XgToast'; export default self => ( <View style={{ alignItems: 'center' }}> <Text style={{ fontSize: 24 }}>历史上的今天</Text> <TextInput style={[styles.input]} placeholder="month" onChangeText={text => self.setState({ inputMonthText: text })} /> <TextInput style={[styles.input]} placeholder="day" onChangeText={text => self.setState({ inputDayText: text })} /> <Button title="查询" onPress={() => self.getTodayOnHistoryInfo()} /> <Text> 发生了啥事:{self.state.todayOnHistoryInfo.result ? self.state.todayOnHistoryInfo.result[0].des : '暂无数据'} </Text> <XgToast ref={(element) => { self.xgToast = element; }} /> </View> );复制代码
import { StyleSheet } from 'react-native'; import pxToDp from '../../config/pxToDp'; export default StyleSheet.create({ inputContainer: { height: pxToDp(100), paddingTop: pxToDp(20), borderBottomWidth: pxToDp(1), borderBottomColor: '#ddd', }, input: { textAlign: 'center', height: pxToDp(80), width: pxToDp(600), marginTop: pxToDp(30), marginBottom: pxToDp(30), color: '#000', fontSize: pxToDp(30), borderBottomColor: '#000', borderBottomWidth: pxToDp(0.5), }, }); 复制代码
六 集成 redux
在 App 中有一些全局状态是所有页面共享的,比如登录状态,或者账户余额(购买商品后所有展示余额的页面都要跟着更新)。在本项目中,使用 Redux 进行状态管理。
如果对 redux 毫无概念,可以看下这篇文章 Redux 入门教程
按照小庸的demo敲了之后,发现Redux 实际上是非常难用的,,,如果之前使用过vuex的话,在使用 Redux 的过程中,会发现需要自己配置的东西太多(不喜勿喷,只是表达个人想使用感受而已),为了简化 Redux 的操作, Redux 作者开发了 react-redux ,虽然使用的便捷性上还没法和 vuex 比,但总算是比直接使用 Redux 好用很多。
在集成 Redux 进行状态管理之前我们先思考一个问题:集成过程中难点在哪?
因为在一个 App 中 Redux 只有一个 Store,这个 Store 应该为所有(页面)组件共享,所以,集成的难点就是 如何使所有(页面)组件可以访问到这个唯一的 store,并且可以触发 action 。为此,redux-react 引入了 connect
函数和 Provide
组件,他们必须配合使用才能实现 redux 的集成。
通过这 connect
和 Provide
实现 store 在组件间共享的思想是:
- Redux store 可以(注意是“可以”,并不是“一定”,需要配置,见第 2 条)对
connect
方法可见,所以在组件中可以通过调用connect
方法实现对 store 数据的访问; - 实现 Redux store 对
connect
的可见的前提条件是, 需要保证这个组件为Provide
组件的子组件 ,这样通过将 store 作为Provide
组件的 props,就可以层层往下传递给所有子组件; - 但子组件必须通过
connect
方法实现对 store 的访问,而无法直接访问。
6.1 引入依赖
首先是安装依赖 redux,react-redux:
yarn add redux react-redux复制代码
6.2 配置 redux
这里指的是配置 actions
, reducers
和 store
。
据说应用大了,最好将 redux 分拆,但现在项目还小,暂时没有做拆分。
- 配置
actions
/** * actions.js */ export function setUserInfo(userInfo) { return { // action 类型 type: "SET_USER_INFO", // userinfo 是传进来的参数 userInfo }; } export function clearReduxStore() { return { type: "CLEAR_REDUX_STORE" }; } 复制代码
- 配置
reducers
/** * reducers.js */ import { initialState } from "./store"; function reducer(state = initialState, action) { switch (action.type) { case "SET_USER_INFO": // 合并 userInfo 对象 action.userInfo = Object.assign({}, state.userInfo, action.userInfo); // 更新状态 return Object.assign({}, state, { userInfo: action.userInfo }); case "CLEAR_REDUX_STORE": // 清空 store 中的 userInfo 信息 return { userInfo: {} }; default: return state; } } export default reducer; 复制代码
注意 SET_USER_INFO
这条路径下的代码,使用了 Object.assign()
。这是因为 reducer
函数每次都会返回全新的 state
对象, 这意味着如果 state
对象含有多个属性而在 reducer
函数返回时没有合并之前的 state
,可能会导致 state
对象属性丢失 。
这是一个很常见的错误,因为通常我们在触发 actions
时只需要传入更改的那部分 state
属性,而不是将整个 state
再传一遍。
redux 经典计数器教程在触发 state
变化时通常这样写 return { defaultNum: state.defaultNum - 1 };
,因为计数器例子中只有一个属性,即 defaultNum
,所以合并之前的 state
就没有意义了,但生产环境中的应用 state
对象中往往不止一个属性,此时上述的写法就会出错。
- 配置
store
/** * store.js */ import { createStore } from "redux"; import reducers from "./reducers"; // 定义初始值 const initialState = { userInfo: { name: "小光", gender: "男" } }; const store = createStore(reducers, initialState); export default store;复制代码
6.3 组件中使用
配置完 redux,接下来就是使用了。
- 配置
index.js
在配置 index.js
中 主要是配置 Provide
作为根组件,并传入 store
作为其属性,为接下来组件使用 redux 创造条件。
/** * index.js */ import React from "react"; import { AppRegistry } from "react-native"; import { Provider } from "react-redux"; import App from "./App"; import store from "./src/redux/store"; const ReduxApp = () => ( // 配置 Provider 为根组件,同时传入 store 作为其属性 <Provider store={store}> <App /> </Provider> ); AppRegistry.registerComponent("AwesomeProject", () => ReduxApp); 复制代码
- 配置组件
这里以 ScreenTab2
为例,注意,引入的style.js可直接copy使用
首先,在 index.js
中关联 redux
/** * ScreenTab2/index.js */ // redux 依赖 import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import * as actionCreators from '../../redux/actions'; import React, { Component } from 'react'; import { Image } from 'react-native'; import view from './view'; class ScreenTab2 extends Component { static navigationOptions = { title: 'Redux(TAB2)', tabBarIcon: ({ focused }) => { const icon = focused ? require('../../assets/images/tab_home_active.png') : require('../../assets/images/tab_home.png'); return <Image source={icon} style={{ height: 22, width: 22 }} />; }, }; constructor(props) { super(props); this.navigation = props.navigation; } changeReduxStore(userInfo) { // 设置 redux store this.props.setUserInfo(userInfo); } render() { return view(this); } } // 将 store 中的状态映射(map)到当前组件的 props 中 function mapStateToProps(state) { return { userInfo: state.userInfo }; } // 将 actions 中定义的方法映射到当前组件的 props 中 function mapDispatchToProps(dispatch) { return bindActionCreators(actionCreators, dispatch); } // 将 store 和 当前组件连接(connect)起来 export default connect(mapStateToProps, mapDispatchToProps)(ScreenTab2); 复制代码
然后,就是在 view 中控制具体改变的数据
import React from 'react'; import { View, Text, Button } from 'react-native'; import pxToDp from '../../config/pxToDp'; import styles from './style'; export default self => ( <View> <View> <Text style={{ fontSize: pxToDp(36) }}>名字:{self.props.userInfo.name}</Text> <Text style={{ fontSize: pxToDp(36) }}>性别:{self.props.userInfo.gender}</Text> </View> <View style={{ alignItems: 'center' }}> <View style={styles.buttonContainer}> <Button title="改变名字" onPress={() => self.changeReduxStore({ name: 'vince' })} /> </View> <View style={styles.buttonContainer}> <Button style={styles.buttonContainer} title="改变性别" onPress={() => self.changeReduxStore({ gender: '女' })} /> </View> <View style={styles.buttonContainer}> <Button style={styles.buttonContainer} title="还原" onPress={() => self.changeReduxStore({ name: '小光', gender: '男' })} /> </View> </View> </View> ); 复制代码
style.js
import { StyleSheet } from 'react-native'; export default StyleSheet.create({ buttonContainer: { margin:20 }, }); 复制代码
最终效果图如下:
6.4 持久化存储
手机 App 一般都有这样的需求: 除非用户主动退出,不然即便 App 进程被杀死,App 重新打开后登录信息依旧会保存 。
在本项目中,为了便于各组件共享登录状态,我把登录状态写在了 redux store 中,但原生 redux 有个特性:页面刷新后 redux store 会回恢复初始状态。为了达到上述需求,就需要考虑 redux store 持久化存储方案。本项目中使用了 redux-persist ,下面介绍如何配置:
- 引入依赖
yarn add redux-persist复制代码
- 修改 redux 配置
store.js
。
除了引入 redux-persist
外,这里使用了 react native 提供的AsyncStorage 作为持久化存储的容器。另外,初始化 state
移到了 reducers.js
中。
/** * store.js * 更改为持久化存储 */ import { createStore } from "redux"; // 引入 AsyncStorage 作为存储容器 import { AsyncStorage } from "react-native"; // 引入 redux-persist import { persistStore, persistCombineReducers } from "redux-persist"; import reducers from "./reducers"; // 持久化存储配置 const config = { key: "root", storage: AsyncStorage }; const persistReducers = persistCombineReducers(config, { reducers }); const configureStore = () => { const store = createStore(persistReducers); const persistor = persistStore(store); return { persistor, store }; }; export default configureStore; 复制代码
2)修改 reducers.js
只是将初始化 state
移入。至于为什么要将初始化 state
从 store.js
移入 reducers.js
实在是无奈之举:不然在 store.js
中创建 store
报错,后续再填坑,暂时先放在 reducers.js
中。
/** * reducers.js * 更改为持久化存储 */ //import { initialState } from "./store"; // 初始化 state 放在这里 const initialState = { userInfo: { name: "小光", gender: "男" } }; function reducer(state = initialState, action) { switch (action.type) { case "SET_USER_INFO": // 合并 userInfo 对象 action.userInfo = Object.assign({}, state.userInfo, action.userInfo); // 更新状态 return Object.assign({}, state, { userInfo: action.userInfo }); case "CLEAR_REDUX_STORE": // 清空 store 中的 userInfo 信息 return { userInfo: {} }; default: return state; } } export default reducer; 复制代码
- 修改使用 redux 的文件
index.js
:
/** * index.js * 更改为持久化存储 */ import React from "react"; import { PersistGate } from "redux-persist/es/integration/react"; import configureStore from "./src/redux/store"; import { AppRegistry } from "react-native"; import { Provider } from "react-redux"; import App from "./App"; const { persistor, store } = configureStore(); const ReduxApp = () => ( // 配置 Provider 为根组件,同时传入 store 作为其属性 <Provider store={store}> <PersistGate persistor={persistor}> <App /> </PersistGate> </Provider> ); AppRegistry.registerComponent("AwesomeProject", () => ReduxApp);复制代码
2)因为修改为持久化存储的过程过程中把初始化的 state
存在了 reducers.js
中,所以在页面组件映射 state
到当前页面时需要还需要修改对应属性的引入地址,依然以 ScreenTab2
为例:
//修改前 // 将 store 中的状态映射(map)到当前组件的 props 中 /*function mapStateToProps(state) { return { userInfo: state.userInfo }; }*/ // 修改后 function mapStateToProps(state) { // 引用 state.reducers.userInfo return { userInfo: state.reducers.userInfo }; }复制代码
经过上述修改,便可以实现 redux 的持久化存储:初始化姓名是 小光
,更改为 vince
后重新加载页面,姓名还是 vince
(而非初始状态 小光
)。效果图如下:
七 小结
经过这部分介绍,App 框架基本构建完成,
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 开发 React Native APP —— 从改造官方Demo开始(2)
- 告别 Windows 终端的难看难用,从改造 PowerShell 的外观开始
- 项目容器化改造心得
- 遗留系统改造-开篇
- 全链路压测改造
- 单体应用微服务改造实践
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。