有沒有在一篇文章的時候, 覺得似曾相識? 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.
這是一個大的社區.很多的解決辦法,組件,結構等等.在你開發的時候你可能會犯很多錯誤. 所以我希望這篇文章能幫助你避免一些錯誤.