51

[前端工坊]我在使用React Native / Redux开发中,犯过的11个错误

 6 years ago
source link: http://mp.weixin.qq.com/s/38y5YnFVurkqHsqe7L_NHw
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

[前端工坊]我在使用React Native / Redux开发中,犯过的11个错误

Original zmy / 译 前端工坊 2018-01-10 13:51 Posted on

在使用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"页面?

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

Image

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

Image

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

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

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

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

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

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

import React, { Component } from ‘react’;
import {
   Text,
   TextInput,
   View,
   TouchableOpacity
} from ‘react-native’;
import styles from ‘./styles.ios’;

export default class SomeContainer extends Component {   constructor(props){       super(props);      
      this.state = {           username:null       }   }   _usernameChanged(event){      
      this.setState({           username:event.nativeEvent.text       });    }   _submit(){      
      if(this.state.username){          
          console.log(`Hello, ${this.state.username}!`);       }      
      else{          
          console.log(‘Please, enter username’);       }    }
                 render() {        
     return (            
       <View style={styles.container}>            
           <View style={styles.avatarBlock}>                
               <Image
                       source={this.props.image}                        style={styles.avatar}/>            
           </View>            
           <View style={styles.form}>
               <View style={styles.formItem}>                   <Text>Username</Text>                   <TextInput                         onChange={this._usernameChanged.bind(this)}                         value={this.state.username} />                 </View>            </View>            <TouchableOpacity onPress={this._submit.bind(this)}>                
               <View style={styles.btn}>                    
                   <Text style={styles.btnText}>                            Submit                        
                   </Text>                </View>            </TouchableOpacity>        </View>        );    } }

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

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

import React, { Component, PropTypes } from 'react';
import {
    Text,
    TextInput,
    View,
    TouchableOpacity
} from 'react-native';
import styles from './styles.ios';

class Avatar extends Component{    constructor(props){        super(props);    }
   render(){                if(this.props.imgSrc){                        return(                                <View style={styles.avatarBlock}>                    <Image                        source={this.props.imgSrc}                        style={styles.avatar}/>                </View>             )         }         return null;    } }
Avatar.propTypes = {    imgSrc: PropTypes.object } class FormItem extends Component{    constructor(props){        super(props);    }    render(){        let title = this.props.title;        return(            <View style={styles.formItem}>                <Text>                    {title}                              </Text>               <TextInput                   onChange={this.props.onChange}                   value={this.props.value} />            </View>        )    } } FormItem.propTypes = {    title: PropTypes.string,    value: PropTypes.string,    onChange: PropTypes.func.isRequired } class Button extends Component{    constructor(props){        super(props);    }    render(){        let title = this.props.title;        return(                        <TouchableOpacity onPress={this.props.onPress}>                <View style={styles.btn}>                    <Text style={styles.btnText}>                        {title}                                        </Text>                </View>            </TouchableOpacity>        )    } }
            Button.propTypes = {    title: PropTypes.string,    onPress: PropTypes.func.isRequired } export default class SomeContainer extends Component {    constructor(props){        super(props);        this.state = {            username:null        }    }    _usernameChanged(event){        this.setState({            username:event.nativeEvent.text        });    }    _submit(){        if(this.state.username){            console.log(`Hello, ${this.state.username}!`);        }        else{            console.log('Please, enter username');        }    }    render() {        return (                                        <View style={styles.container}>                <Avatar imgSrc={this.props.image} />                <View style={styles.form}>                    <FormItem                      title={"Username"}                      value={this.state.username}                      onChange={this._usernameChanged.bind(this)}/>                </View>                <Button                    title={"Submit"}                    onPress={this._submit.bind(this)}/>            </View>        );    } }

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

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

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

class Button extends Component{
    constructor(props){
        super(props);
    }
    _setTitle(){        
       const { id } = this.props;        
       switch(id){            
           case 0:                
               return 'Submit';            
           case 1:                
               return 'Draft';            
           case 2:                
               return 'Delete';            
           default:                
               return 'Submit';         }    }
                   render(){                  let title = this._setTitle();                     return(                        <TouchableOpacity onPress={this.props.onPress}>                <View style={styles.btn}>                    <Text style={styles.btnText}>                        {title}                    
                   </Text>               </View>           </TouchableOpacity>        )    } } Button.propTypes = {    id: PropTypes.number,    onPress: PropTypes.func.isRequired } export default class SomeContainer extends Component {    constructor(props){        super(props);        this.state = {            username:null        }    }    _submit(){        if(this.state.username){            console.log(`Hello, ${this.state.username}!`);        }        else{            console.log('Please, enter username');        }    }    render() {        return (            
           <View style={styles.container}>                <Button                    id={0}                    onPress={this._submit.bind(this)}/>            </View>            } }

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

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

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

7.行内样式

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

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

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

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

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按压下的不透明度的变化:

'use strict';
import React, { Component, PropTypes } from 'react';
import { Animated, View, PanResponder, Easing } from 'react-native';
import moment from 'moment';
export default class Button extends Component {    constructor(props){        super(props);                
       this.state = {            timestamp: 0        };        
       this.opacityAnimated = new Animated.Value(0);                        this.panResponder = PanResponder.create({   onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,   onStartShouldSetResponder:() => true,   onStartShouldSetPanResponder : () => true,   onMoveShouldSetPanResponder:(evt, gestureState) => true,   onPanResponderMove: (e, gesture) => {},   onPanResponderGrant: (evt, gestureState) => {          /**THIS EVENT IS CALLED WHEN WE PRESS THE BUTTON**/       this._setOpacity(1);      
      this.setState({           timestamp: moment()       });      
      this.long_press_timeout = setTimeout(() => {            
          this.props.onLongPress();       }, 1000);   },   onPanResponderStart: (e, gestureState) => {},   onPanResponderEnd: (e, gestureState) => {},   onPanResponderTerminationRequest: (evt, gestureState) => true,   onPanResponderRelease: (e, gesture) => {  
      /**THIS EVENT IS CALLED WHEN WE RELEASE THE BUTTON**/       let diff = moment().diff(moment(this.state.timestamp));      
      if(diff < 1000){          
          this.props.onPress();       }       clearTimeout(this.long_press_timeout);      
      this._setOpacity(0);      
      this.props.releaseBtn(gesture);   }     });    }    _setOpacity(value){    
    /**SETS OPACITY OF THE BUTTON**/        Animated.timing(        
       this.opacityAnimated,        {            toValue: value,            duration: 80,        }        ).start();    }
           render(){        
       let longPressHandler = this.props.onLongPress,            pressHandler = this.props.onPress,            image = this.props.image,            opacity = this.opacityAnimated.interpolate({              inputRange: [0, 1],              outputRange: [1, 0.5]            });        

return(            
           <View style={styles.btn}>                <Animated.View                   {...this.panResponder.panHandlers}                   style={[styles.mainBtn, this.props.style, {opacity:opacity}]}>                    {image}              
               </Animated.View>            </View>        )    } } Button.propTypes = {    onLongPress: PropTypes.func,    onPressOut: PropTypes.func,    onPress: PropTypes.func,    style: PropTypes.object,    image: PropTypes.object }; Button.defaultProps = {    onPressOut: ()=>{ console.log('onPressOut is not defined'); },    onLongPress: ()=>{ console.log('onLongPress is not defined'); },    onPress: ()=>{ console.log('onPress is not defined'); },    style: {},    image: null }; const styles = {    mainBtn:{        width:55,        height:55,        backgroundColor:'rgb(255,255,255)',      } };

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

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

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

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

原文链接:

https://medium.com/dailyjs/11-mistakes-ive-made-during-react-native-redux-app-development-8544e2be9a9

Valentin Galkin

Image

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK