翻譯|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.

這是一個大的社區.很多的解決辦法,組件,結構等等.在你開發的時候你可能會犯很多錯誤. 所以我希望這篇文章能幫助你避免一些錯誤.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容