我在使用React Native/Redux开发中犯过的11个错误

开发 前端
在使用React Native近一年之后,是时候分享一下我刚开始用RN开发项目时犯过的错误了。有可能你对第一个React Native(RN)应用程序的预估是完全错误的!

在使用React Native近一年之后,是时候分享一下我刚开始用RN开发项目时犯过的错误了。

1.错误的估计

有可能你对***个React Native(RN)应用程序的预估是完全错误的!

  • 1)你需要分别考虑iOS和Android版本的布局!在布局的时候,有很多组件可以重复使用;如果ios和Android的页面结构不同,就需要对他们分开单独布局。
  • 2)对form进行评估时,***也考虑一下数据层验证。
  • 3)了解数据库结构,有助于正确地规划redux

2.尽量使用基础组件(buttons,footers,headers,inputs,texts)

google搜索RN的基础组件,你会发现有很多现有组件可以方便的用到项目中,如buttons,footers等。如果项目中没有特别的布局设计,只需要使用这些基础组件就可以构建一个页面。如果有一些特殊的设计,例如,特殊的button样式,你就需要为每个按钮设置自定义样式。你可以封装已经构建好的组件,并为它们定制样式。但是我觉得使用View,Text,TouchableOpacity和RN的其他组件来构建自己的组件更加有意义。因为你会有更多的rn实践,并且深刻理解如何使用RN。最重要的一点,你可以确定你自己构建的组件版本不会被改变。

3.不要把iOS和Android的布局分开

如果iOS和Android布局大致一样,只有一小部分不同,你可以简单地使用RN提供的Platform API根据设备平台进行区分。

如果布局完全不同 – ***分散在不同的文件中单独布局。

如果你给一个文件命名为index.ios.js – 程序打包时,在iOS中将使用这个文件显示iOS布局。 index.android.js也是一样的道理。

你可能会问:“代码怎么复用呢?” 你可以将重复的代码移动到助手函数中。需要的时候只复用这些助手函数。

4.错误的redux store规划。

初学者经常会犯的一个很大的错误就是,当你在规划你的应用程序时,你可能会考虑很多布局相关的问题,却很少考虑关于数据的处理。

Redux能够帮助我们正确地存储数据。如果redux规划的好 – 它将是管理应用程序数据的强大工具。

当我刚刚开始构建RN应用程序时,我曾考虑将reducers作为每个container的数据存储。所以,如果你有登录,忘记密码,待办事项列表页面 – 使用他们的缩写比较简单:SignIn, Forgot, ToDoList.

在进行了一段工作后,我发现管理数据没有想象中的容易。

当我从ToDo列表中选择项目时 – 我需要将数据传递给ToDoDetails reducer。这意味着使用了额外的操作来发送数据到reducer。

在做了一些调查之后,我决定以不同的方式规划结构。一个例子:

  1. Auth
  2. Todos
  3. Friends

Auth用于存储认证的token。

而ToDos和Friends reducers用于存储实体,当我去ToDo Detail页面时 – 我只需要通过ID搜索所有的ToDos。

对于更复杂的结构,我推荐使用这种规划,你可以快速的定位到你想找到的。

5.错误的项目结构

作为初学者时,总是规划不好项目结构。

首先 ,需要分析你的项目是不是足够大?

你的应用程序中有多少个页面? 20?30?10?5?还是只有一个”Hello World”页面?

我遇到并开始实施的***个结构是这样的:

 

图0:[外文翻译]我在使用React Native / Redux开发中犯过的11个错误

如果你的项目不超过10个页面,使用上面这种结构是没有问题的。但是如果项目特别大 – 你可以考虑这样规划结构:

 

图1:[外文翻译]我在使用React Native / Redux开发中犯过的11个错误

区别在于,***种类型建议我们将actions和reducers与container分开存储。第二种- 把它们存储在一起。如果应用程序很小 – 将redux模块从container中分离出来会更加有用。

如果你有通用的样式(如Header、Footer、Buttons) – 你可以单***建一个名为“styles”的文件夹,在那里设置一个index.js文件并在其中写入通用样式。然后在每个页面上重复使用它们。

实际项目中会有很多不同的结构。你应该了解使用哪种结构更适合你的需求。

6.错误的container结构。没有从一开始就使用smart/dumb组件

当你开始使用RN并初始化项目时,index.ios.js文件中已经有一些代码,存储在一个单独的对象中。

在实际开发项目中,你将需要使用很多组件,不仅仅是由RN提供的,还有自己构建的一些组件。构建container时,可以重用它们。

考虑该组件:

 

  1. import React, { Component } from ‘react’; 
  2. import { 
  3.    Text, 
  4.    TextInput, 
  5.    View
  6.    TouchableOpacity 
  7. from ‘react-native’; 
  8. import styles from ‘./styles.ios’; 
  9.  
  10. export default class SomeContainer extends Component { 
  11.    constructor(props){ 
  12.        super(props);        
  13.        this.state = { 
  14.            username:null 
  15.        } 
  16.    } 
  17.    _usernameChanged(event){        
  18.        this.setState({ 
  19.            username:event.nativeEvent.text 
  20.        }); 
  21.     } 
  22.    _submit(){        
  23.        if(this.state.username){            
  24.            console.log(`Hello, ${this.state.username}!`); 
  25.        }        
  26.        else{            
  27.            console.log(‘Please, enter username’); 
  28.        } 
  29.     } 
  30.                 
  31.     render() {         
  32.       return (             
  33.         <View style={styles.container}>             
  34.             <View style={styles.avatarBlock}>                 
  35.                 <Image 
  36.                         source={this.props.image}  
  37.                         style={styles.avatar}/>             
  38.             </View>             
  39.             <View style={styles.form}> 
  40.                 <View style={styles.formItem}> 
  41.                    <Text>Username</Text>  
  42.                    <TextInput 
  43.                          onChange={this._usernameChanged.bind(this)} 
  44.                          value={this.state.username} /> 
  45.                  </View
  46.             </View
  47.             <TouchableOpacity onPress={this._submit.bind(this)}>                 
  48.                 <View style={styles.btn}>                     
  49.                     <Text style={styles.btnText}> 
  50.                             Submit                         
  51.                     </Text> 
  52.                 </View>  
  53.             </TouchableOpacity> 
  54.         </View
  55.         ); 
  56.     } 

所有样式都存储在一个单独的模块中。

包裹在TouchableOpacity中的button组件应该单独分离出来,这样才能方便我们以后重复使用它。Image组件,以后也可能重复使用,所以也应该把它分离出来。

做了一些改变之后的样子:

 

  1. import React, { Component, PropTypes } from 'react'
  2. import { 
  3.     Text, 
  4.     TextInput, 
  5.     View
  6.     TouchableOpacity 
  7. from 'react-native'
  8. import styles from './styles.ios'
  9.  
  10. class Avatar extends Component{ 
  11.     constructor(props){ 
  12.         super(props); 
  13.     } 
  14.     render(){         
  15.         if(this.props.imgSrc){             
  16.             return(                 
  17.                 <View style={styles.avatarBlock}> 
  18.                     <Image 
  19.                         source={this.props.imgSrc} 
  20.                         style={styles.avatar}/> 
  21.                 </View
  22.              ) 
  23.          } 
  24.          return null
  25.     } 
  26. Avatar.propTypes = { 
  27.     imgSrc: PropTypes.object 
  28.  
  29. class FormItem extends Component{ 
  30.     constructor(props){ 
  31.         super(props); 
  32.     } 
  33.     render(){ 
  34.         let title = this.props.title; 
  35.         return(  
  36.             <View style={styles.formItem}> 
  37.                 <Text> 
  38.                     {title}                
  39.                 </Text> 
  40.                <TextInput 
  41.                    onChange={this.props.onChange} 
  42.                    value={this.props.value} /> 
  43.             </View
  44.         ) 
  45.     } 
  46. FormItem.propTypes = { 
  47.     title: PropTypes.string, 
  48.     value: PropTypes.string, 
  49.     onChange: PropTypes.func.isRequired 
  50.  
  51. class Button extends Component{ 
  52.     constructor(props){ 
  53.         super(props); 
  54.     } 
  55.     render(){ 
  56.         let title = this.props.title; 
  57.         return(             
  58.             <TouchableOpacity onPress={this.props.onPress}> 
  59.                 <View style={styles.btn}> 
  60.                     <Text style={styles.btnText}> 
  61.                         {title}                     
  62.                     </Text> 
  63.                 </View
  64.             </TouchableOpacity> 
  65.         ) 
  66.     } 
  67.              
  68. Button.propTypes = { 
  69.     title: PropTypes.string, 
  70.     onPress: PropTypes.func.isRequired 
  71. export default class SomeContainer extends Component { 
  72.     constructor(props){ 
  73.         super(props); 
  74.         this.state = { 
  75.             username:null 
  76.         } 
  77.     } 
  78.     _usernameChanged(event){ 
  79.         this.setState({ 
  80.             username:event.nativeEvent.text  
  81.         }); 
  82.     } 
  83.     _submit(){ 
  84.         if(this.state.username){ 
  85.             console.log(`Hello, ${this.state.username}!`); 
  86.         } 
  87.         else
  88.             console.log('Please, enter username'); 
  89.         } 
  90.     } 
  91.     render() { 
  92.         return (                                  
  93.         <View style={styles.container}> 
  94.                 <Avatar imgSrc={this.props.image} /> 
  95.                 <View style={styles.form}> 
  96.                     <FormItem 
  97.                       title={"Username"
  98.                       value={this.state.username} 
  99.                       onChange={this._usernameChanged.bind(this)}/> 
  100.                 </View
  101.                 <Button 
  102.                     title={"Submit"
  103.                     onPress={this._submit.bind(this)}/> 
  104.             </View
  105.         ); 
  106.     } 

现在的代码看起来更多了 – 因为我们为Avatar,FormItem和Button组件添加了包装器,但现在我们可以在需要的地方重复使用这些组件。我们可以将这些组件移动到单独的模块中,并导入我们需要的任何地方。我们也可以添加其他一些Props,例如style,TextStyle,onLongPress,onBlur,onFocus。而且这些组件是完全可以定制的。

注意,一定不要深度定制一个小组件, 这样会使组件过于繁琐,代码会变的很难阅读。即使现在添加新属性的想法看起来像是解决任务的最简单的方法,将来这个小小的属性在阅读代码时可能会引起困惑。

关于理想的smart/dumb组件,看一下这个:

 

  1. class Button extends Component{ 
  2.     constructor(props){ 
  3.         super(props); 
  4.     } 
  5.     _setTitle(){         
  6.         const { id } = this.props;         
  7.         switch(id){             
  8.             case 0:                 
  9.                 return 'Submit';             
  10.             case 1:                 
  11.                 return 'Draft';             
  12.             case 2:                 
  13.                 return 'Delete';             
  14.             default:                 
  15.                 return 'Submit'
  16.          } 
  17.     } 
  18.                  
  19.     render(){            
  20.         let title = this._setTitle();              
  21.         return(             
  22.             <TouchableOpacity onPress={this.props.onPress}> 
  23.                 <View style={styles.btn}> 
  24.                     <Text style={styles.btnText}> 
  25.                         {title}                     
  26.                     </Text> 
  27.                </View
  28.            </TouchableOpacity> 
  29.         ) 
  30.     } 
  31. Button.propTypes = { 
  32.     id: PropTypes.number, 
  33.     onPress: PropTypes.func.isRequired 
  34. export default class SomeContainer extends Component { 
  35.     constructor(props){ 
  36.         super(props); 
  37.         this.state = { 
  38.             username:null 
  39.         } 
  40.     } 
  41.     _submit(){ 
  42.         if(this.state.username){ 
  43.             console.log(`Hello, ${this.state.username}!`); 
  44.         } 
  45.         else
  46.             console.log('Please, enter username'); 
  47.         } 
  48.     } 
  49.     render() { 
  50.         return (             
  51.             <View style={styles.container}> 
  52.                 <Button 
  53.                     id={0} 
  54.                     onPress={this._submit.bind(this)}/> 
  55.             </View
  56.          
  57.     } 

我们已经“升级”了Button组件。用一个叫做“id”的新属性来替换属性“title”。现在Button组件就变的“灵活”了。传0 – button组件会显示“submit”。传2 – 显示“delete”。但这可能会有一些问题。

Button被创建为一个dumb组件 – 只是为了显示数据,传递数据这件事由它的更高一级的组件来完成。

如果我们将5作为id传递给这个组件,我们就需要更新组件,以便让它适应这个改动。dumb组件,就是细分的小组件,它只要接收props就好了,如果有state也应该与全局的无关。

7.行内样式

在使用RN布局之后,我遇到了行内样式的写作风格问题。类似这样:

 

  1. render() {     
  2.     return (         
  3.         <View style={{flex:1, flexDirection:'row', backgroundColor:'transparent'}}> 
  4.             <Button 
  5.                 title={"Submit"
  6.                 onPress={this._submit.bind(this)}/> 
  7.         </View
  8.     ); 

当你这样写的时候,你会想:“暂时这样写,等我在模拟器中运行之后 – 如果布局没问题,再把样式移动到单独的模块。”也许这是一个好的想法。但是..不幸的是,你往往会选择性忽略行内样式…

一定要在独立的模块中编写样式,远离行内样式。

8.使用redux验证表单

要使用redux来验证表单,我需要在reducer中创建action,actionType单独的字段,这样做很麻烦。

所以我决定只借助state来完成验证。没有reducers,types等等,只是在container级别上的纯功能函数。从action和reducer文件中删除不必要的函数,这个策略对我帮助很大。

9.过于依赖zIndex

很多人从web开发转到RN开发。在web中有一个css属性z-index,它可以帮助我们在需要的层级显示我们想要的内容。

在RN中,一开始没有这样的特性。但后来又被添加进来了。起初,使用起来还挺简单的。只需为元素设置zIndex属性,它就会按照任何你想要的图层顺序来渲染。但是在Android上测试之后…现在我只用zIndex来设置展示层的结构。

10.不仔细阅读外部组件的源码

你可以引入外部组件来节省你的开发时间。

但有时这个模块可能会中断,或者不像描述的那样工作。阅读源码你才会明白哪里出现了错误。也许这个模块本身就有问题,或者你只是用错了。另外 – 如果你仔细阅读其他模块的源码,你将会学习到如何构建自己的组件。

11.要小心手势操作和Animated API。

RN为我们提供了构建完全原生应用程序的能力。怎么让用户感觉是原生应用?页面布局,滑动手势,还是展示动画?

当你使用View,Text,TextInput和其他RN提供的默认模块时,手势和动画应该由PanResponder和Animated API来处理。

如果你是从web转过来的rn开发工程师,获取用户的手势操作可能会有些困难,你需要区分什么时候开始,什么时候结束,长按,短按。你可能还不够清楚怎么在RN中模拟这些动画操作。

这是我用PanResponder和Animated建立的Button组件。这个button是为了捕捉用户手势而构建的。例如 – 用户按下项目,然后将手指拖到一边。在按下按钮时,借助于动画API,构建button按压下的不透明度的变化:

 

  1. 'use strict'
  2. import React, { Component, PropTypes } from 'react'
  3. import { Animated, View, PanResponder, Easing } from 'react-native'
  4. import moment from 'moment'
  5. export default class Button extends Component { 
  6.     constructor(props){ 
  7.         super(props);                 
  8.         this.state = { 
  9.             timestamp: 0 
  10.         };         
  11.         this.opacityAnimated = new Animated.Value(0);                 
  12.         this.panResponder = PanResponder.create({ 
  13.    onMoveShouldSetPanResponderCapture: (evt, gestureState) => true
  14.    onStartShouldSetResponder:() => true
  15.    onStartShouldSetPanResponder : () => true
  16.    onMoveShouldSetPanResponder:(evt, gestureState) => true
  17.    onPanResponderMove: (e, gesture) => {},  
  18.    onPanResponderGrant: (evt, gestureState) => {    
  19.         /**THIS EVENT IS CALLED WHEN WE PRESS THE BUTTON**/ 
  20.        this._setOpacity(1);        
  21.        this.setState({ 
  22.            timestamp: moment() 
  23.        });        
  24.        this.long_press_timeout = setTimeout(() => {             
  25.            this.props.onLongPress(); 
  26.        }, 1000); 
  27.    }, 
  28.    onPanResponderStart: (e, gestureState) => {}, 
  29.    onPanResponderEnd: (e, gestureState) => {}, 
  30.    onPanResponderTerminationRequest: (evt, gestureState) => true
  31.    onPanResponderRelease: (e, gesture) => {    
  32.        /**THIS EVENT IS CALLED WHEN WE RELEASE THE BUTTON**/ 
  33.        let diff = moment().diff(moment(this.state.timestamp));        
  34.        if(diff < 1000){            
  35.            this.props.onPress(); 
  36.        } 
  37.        clearTimeout(this.long_press_timeout);        
  38.        this._setOpacity(0);        
  39.        this.props.releaseBtn(gesture); 
  40.    } 
  41.      }); 
  42.     } 
  43.     _setOpacity(value){     
  44.      /**SETS OPACITY OF THE BUTTON**/ 
  45.         Animated.timing(         
  46.         this.opacityAnimated, 
  47.         { 
  48.             toValue: value, 
  49.             duration: 80, 
  50.         } 
  51.         ).start(); 
  52.     } 
  53.          
  54.     render(){         
  55.         let longPressHandler = this.props.onLongPress, 
  56.             pressHandler = this.props.onPress, 
  57.             image = this.props.image, 
  58.             opacity = this.opacityAnimated.interpolate({ 
  59.               inputRange: [0, 1], 
  60.               outputRange: [1, 0.5] 
  61.             });         
  62.          
  63.         return(             
  64.             <View style={styles.btn}> 
  65.                 <Animated.View 
  66.                    {...this.panResponder.panHandlers}                   style={[styles.mainBtn, this.props.style, {opacity:opacity}]}> 
  67.                     {image}                
  68.                 </Animated.View
  69.             </View
  70.         ) 
  71.     } 
  72. Button.propTypes = { 
  73.     onLongPress: PropTypes.func, 
  74.     onPressOut: PropTypes.func, 
  75.     onPress: PropTypes.func, 
  76.     style: PropTypes.object, 
  77.     image: PropTypes.object 
  78. }; 
  79. Button.defaultProps = { 
  80.     onPressOut: ()=>{ console.log('onPressOut is not defined'); }, 
  81.     onLongPress: ()=>{ console.log('onLongPress is not defined'); }, 
  82.     onPress: ()=>{ console.log('onPress is not defined'); }, 
  83.     style: {}, 
  84.     image: null 
  85. }; 
  86. const styles = { 
  87.     mainBtn:{ 
  88.         width:55, 
  89.         height:55, 
  90.         backgroundColor:'rgb(255,255,255)',   
  91.     } 
  92. }; 

首先,初始化PanResponder对象实例。它有一套不同的操作句柄。我感兴趣的是onPanResponderGrand(当用户触摸按钮时触发)和onPanResponderRelease(当用户从屏幕上移开手指时触发)两个句柄;

我还设置了一个动画对象实例,帮助我们处理动画。将其值设置为零;然后我们定义_setOpacity方法,调用时改变this.opacityAnimated的值。在渲染之前,我们插入this.opacityAnimated为正常的opacity值。我们不使用View而是使用Animated.View模块为了使用动态变化的opacity值。

通过上面的例子,你会发现Animated API不难理解,你只需要阅读相关的API文档,以确保你的应用程序***运行。希望这个例子能帮你开个好头。

在使用React Native开发时可能会遇到很多问题,希望这篇文章能帮助你避免一些错误。

责任编辑:未丽燕 来源: 前端工坊公众号
相关推荐

2021-12-16 06:52:33

Ceph分布式对象

2017-10-17 16:23:58

函数式编程ReduxReact

2022-06-10 08:01:17

ReduxReact

2020-10-08 18:12:36

数据科学职位面试数据科学家

2016-08-12 13:55:06

2017-09-11 14:35:34

编辑器开发环境React

2020-04-20 18:15:46

开发自信技术

2021-07-25 21:36:24

Windows操作系统功能

2020-05-17 16:10:36

开发人员软件开发开发

2016-11-23 16:48:20

react-nativandroidjavascript

2016-10-31 11:26:13

ReactRedux前端应用

2017-03-21 21:37:06

组件UI测试架构

2015-04-17 09:47:57

2015-03-02 15:30:11

2018-07-06 15:00:50

码农科技开发

2022-04-02 15:11:04

工具APIUI

2015-08-06 17:15:28

2020-12-22 13:49:23

开发编码框架

2017-01-04 10:18:00

React NativScrollViewAndroid

2019-08-29 09:00:55

开发Flutter框架
点赞
收藏

51CTO技术栈公众号