翻译|11 mistakes I’ve made during React Native / Redux app development

有没有在一篇文章的时候, 觉得似曾相识? javascript很简单,但是其实水也深着呢!马云说:”要从失败的地方学”.对于React的开发更是意义重大,React现在的生态系统太庞大了,稍不注意就会出错,有语法问题,有结构问题,有设计问题.所以如果能从高手的文章中学习一点对错误的总结,那么我会少走很多的弯路.

那么看看这篇文章吧11个 React-native app 开发中的错误

译文开始:

我在 React-Native app开发中曾经犯过的11个错误

经过差不多一年的 React Native 的开发后,我决定把我自打新手开始所犯的错误总结一下.


1. 错误的预计

真的!开始设想的 React Native(RN)的应用是完全错误的.彻底的错误.

  • 你需要单独考虑 iOS 和 Android版本的布局.当然,有很多的组件是可以重用的,但是他们有不同的布局考虑.甚至他们之间的应用结构页面也都是不同的.
  • 当你在预测 form的时候-你最好要一并考虑一下数据验证层.例如,当你使用React Native开发应用程序的时候,你会比使用Cordova时写更多的代码.
  • 如果你需要在已经已经开发完毕,并且已经有后端(所以,你可以使用现存的API)的webapp基础上创建一个app-要确保检查每个后端提供的数据点.因为你需要在app中处理逻辑,编码应该要恰如其分.理解数据库的结构,实体之间的连接关系等等.如果你理解了数据库的结构,你可以正确的规划你的redux store(后面会讲到).(译注:分离关注点,引入了Redux,React的逻辑处理权交到了Redux手中.意识到这一点对于Redux和React的结合使用非常重要.)

2. 尽量使用已经构建好的组件(buttons,footers,headers,inputs,text)-仅仅是我个人的观点.

如果你搜索Google里面的已有React组件,可以搜到很多,例如 buttons,footers等等,有很多可以使用的组件库.如果你没有特别的布局设计,使用这些组件库将会非常有用.就用这些组件就可以了.但是如果你有特别的设计,在这个设计中
button看起来不同,你需要定制每个组件.这需要一些技巧.当然你也可以包装已经构建好的组件,定制样式就可以了.但是我认为使用使用RN的View,Text,TouchableOpaticy组件来构建自己的组件很容易,也有很大的价值.通过自己的包装过程,你可以理解怎么和RN融洽工作.也会积累更多的经验.由于是自己构建的组件,可以确保组件的版本不会被改变.所以,不要依赖外部的模块.

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

如果你只是在iOS和Android之间使用不同的布局,这个方法会非常有用.如果布局一样,仅仅使用RN提供的Platform API,可以根据设备平台的不同来做小小的检测.
如果布局完全不同-最好是分散到不同的文件中完成(译注:RN可以识别 fileName.ios.js 和 fileName.android.js).

如果你命名未见为index.ios.js,程序打包的时候就会在iOS中使用这个文件.类似的,在Android打包的时候会使用indexn.android.js.(译注:具体做法可以参考F8 APP的做法).

你可能会问”代码怎么复用?”.你可以把复用的代码放到助手函数中,需要的地方仅仅复用助手函数.

4. 错误的Redux store规划

可能会犯大错误的地方.

当你在设计应用的时候,你可能更多的考虑表现层.很少考虑到数据操作.
Redux帮助我们正确的存储数据.如果Redux store规划的好,将会是一个一个非常有力的data管理工具.如果没有规划好,会把事情弄的一团糟.

当我刚开始构建RN app的时候,我只把reducers作为每一个container的数据容器.所以如果你有登录,密码找回,ToDO list页面-reducer应该是比较简单-:SigIn,Forgot,ToDoList.
在经过一段时间的store规划以后,我发现在我的程序中不太好管理数据了.我已经有了一个ToDo 详情页面.使用上面的想法,store需要一个ToDoDetails reducer是吗?这是一个巨大的错误!为什么?

当我从ToDo List中选择出需要传递到ToDoDetail reducer的一项.这意味着使用了额外的actions 发送数据到reducer.非常的不合适.

经过一点研究之后,我决定做点改变.结构想下面这样:

  • Auth
  • ToDos
  • Friends
    Auth用于存储认证的token.仅仅如此.
    ToDos和Friends reducers用于储存实体,从名字很容易知道他们是干什么的.当我进入到ToDo Detail页面中-我只需要根据id来搜索所有的ToDos.
    如果有更多的复杂结构,我建议使用这个计划.你会明白什么是什么.在哪里找到他们.

5. 错误的项目结构

当你是一个新手的时候,规划项目结构很难.
首先要理解你的项目有多大? 大?真的很大?巨大?还是很小?

应用中有多少页面?20?30?10?5?还是只有一个hello world页面

开始的时候,我的项目实施的结构像这样:

还好,如果你的应用不是大项目,例如最多十个页面.如果比这个规模更大,可以考虑使用:


有什么不同吗?如你所见,首要的目的是建议我们为每个container分开存储actions和reducers.如果应用较小,把Redux 模块和container分离开可能有用.如果redux Reducer和container放到一起,你可以很容易的知道哪个action和这个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>
        );
    }
}

看起来怎么样?
正如你看到的,所有的样式都放在独立的模块中-好的.没有代码复制(目前为止).
但是我们到底多长时间才在表单中使用一个字段?我不确定频率到底多少.button组件也是如此-包装在TouchableOpatcity中-应该被分离出来,便于我们在将来复用他.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组件上有一些灵活性.传递 o,Button组件将会显示”Submit”,传递 2-“Delete”.但是这很成问题.

Button作为dumb组件创建,为的是仅仅展示传递的数据.传递数据这件事由他的更高一级的组件来完成. Dumb组件不应该知道周围的任何环境因素.仅仅只要执行和展示他们被告知的数据.经过这次”升级”之后.但是这个做法并不好,为什么?

如果我们把5作为id传递给组件,会发生什么?我们需要更新组件,能让他可以适应这个选项.等等,等等.Dumb组件应该仅仅展示他们被告知的数据.这就是Dumb组件要做的全部.

7. inline styles

使用RN一段时间以后,我面临一个行内书写样式的问题,像这样:

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

当你刚开始这么写的时候,你会想:”好了”,等我在模拟器里检查了布局以后,如果演示可以,我就会把样式转移到独立的模块中.或许这是个好的愿景,但是不幸的是,这件事不会发生.没有人这么做,除非有人提醒.

一定要把样式分到独立的模块中.这会让你远离行内样式.

8.使用redux来验证表单

这是我的项目中的错误.希望能对你有帮助.

为了由Redux协助验证表单,我需要创建action,actionType,reducer里分离字段.这让人有点恼火.
所以我决定仅借助state来完成验证过程,没有reducers,types等等.仅仅在container水平上的纯函数.这个策略对我帮助很大,从action和reducer里去掉了不必要的函数,不要操作store.

9. 过度的依赖zIndex

很多人从web开发转移到RN开发.在web开发中,有一个css 属性是z-index.它帮助我们展示我们需要的内容,在web中,这么做很酷.
在RN中,一开始是没有这个特性的,但是后来被添加进来了.起初还挺容易使用的, 要按照你想要的顺序来渲染展示层,只需要把z-Index属性作为style就可以了.
工作正常,但是经过Android测试以后… 现在我只用z-Index来设置展示层的结构.这就是zIndex能做的.

10.不读外部模块的代码

当你想节约时间,你可以使用外部的模块.通常他们都要文档.你可以从文档中获取信息并使用外部模块.
但有时,模块会崩溃.或者不像描述的那样工作.这就是你为什么需要读源码.通过读源码,你可以理解错误在哪里.或许模块是很坏的.或是是你使用的方法不对.另外就是-如果你读了其他模块的代码,你会了解到如何构建你自己的模块.

11. 要小心手势操作和动画 API

RN让我们有能力构建原生的应用.怎么让应用感觉像是原生应用.展示层,手势,还是动画?
当你使用View,Text,TextInput和其他的RN默认提供的模块的时候,手势和动画应该由PanResponder和动画API来操作.

如果你和我一样是从web转过来的RN开发者,获取用户的手势操作可能多少有点吓人-什么时间开始,何时结束,长点击,短点击.过程不是太清晰,怎么在RN中模拟这些操作?

这里是一个Button组件由PanResponder和动画来协助.创建这个组件来捕获用户的手势操作.例如,用户按压项目,然后手指拖动到另一边.在动画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的对象实例.它有一套不同的操作句柄,我们感兴趣的是 onPanResonderGrand (用户触摸按钮是触发)和 onPanResponderRelase(用户从屏幕中移开手指是触发),两个句柄.

我们也初始化动画对象的实例,帮助我们使用动画.设定值为0,然后我们定义_setOpacity方法,调用时改变this.opacityAnimated的值.在渲染之前我们插值处理this.opacityAnimated到正常的opacity值.我们没有使用View,而是使用了Animated.View模块为了使用动态变化的opacity值.
搞定了.

正如你所见,不是很难理解具体是怎么回事.当然你需要读相关API的文档,确保你的app的完美运行.但是我希望找个例子能够帮助你开个好头.


React Native太棒了,你可以用它做几乎任何事情.如果没有RN,你要做这些事情需要 Swift/Objective C或者JAVA.然后关联到React Native.

这是一个大的社区.很多的解决办法,组件,结构等等.在你开发的时候你可能会犯很多错误. 所以我希望这篇文章能帮助你避免一些错误.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容