有沒有在一篇文章的時候, 覺得似曾相識? javascript很簡單,但是其實水也深著呢!馬云說:”要從失敗的地方學(xué)”.對于React的開發(fā)更是意義重大,React現(xiàn)在的生態(tài)系統(tǒng)太龐大了,稍不注意就會出錯,有語法問題,有結(jié)構(gòu)問題,有設(shè)計問題.所以如果能從高手的文章中學(xué)習(xí)一點對錯誤的總結(jié),那么我會少走很多的彎路.
那么看看這篇文章吧11個 React-native app 開發(fā)中的錯誤
譯文開始:
我在 React-Native app開發(fā)中曾經(jīng)犯過的11個錯誤
經(jīng)過差不多一年的 React Native 的開發(fā)后,我決定把我自打新手開始所犯的錯誤總結(jié)一下.
1. 錯誤的預(yù)計
真的!開始設(shè)想的 React Native(RN)的應(yīng)用是完全錯誤的.徹底的錯誤.
- 你需要單獨考慮 iOS 和 Android版本的布局.當(dāng)然,有很多的組件是可以重用的,但是他們有不同的布局考慮.甚至他們之間的應(yīng)用結(jié)構(gòu)頁面也都是不同的.
- 當(dāng)你在預(yù)測 form的時候-你最好要一并考慮一下數(shù)據(jù)驗證層.例如,當(dāng)你使用React Native開發(fā)應(yīng)用程序的時候,你會比使用Cordova時寫更多的代碼.
- 如果你需要在已經(jīng)已經(jīng)開發(fā)完畢,并且已經(jīng)有后端(所以,你可以使用現(xiàn)存的API)的webapp基礎(chǔ)上創(chuàng)建一個app-要確保檢查每個后端提供的數(shù)據(jù)點.因為你需要在app中處理邏輯,編碼應(yīng)該要恰如其分.理解數(shù)據(jù)庫的結(jié)構(gòu),實體之間的連接關(guān)系等等.如果你理解了數(shù)據(jù)庫的結(jié)構(gòu),你可以正確的規(guī)劃你的redux store(后面會講到).(譯注:分離關(guān)注點,引入了Redux,React的邏輯處理權(quán)交到了Redux手中.意識到這一點對于Redux和React的結(jié)合使用非常重要.)
2. 盡量使用已經(jīng)構(gòu)建好的組件(buttons,footers,headers,inputs,text)-僅僅是我個人的觀點.
如果你搜索Google里面的已有React組件,可以搜到很多,例如 buttons,footers等等,有很多可以使用的組件庫.如果你沒有特別的布局設(shè)計,使用這些組件庫將會非常有用.就用這些組件就可以了.但是如果你有特別的設(shè)計,在這個設(shè)計中
button看起來不同,你需要定制每個組件.這需要一些技巧.當(dāng)然你也可以包裝已經(jīng)構(gòu)建好的組件,定制樣式就可以了.但是我認(rèn)為使用使用RN的View,Text,TouchableOpaticy組件來構(gòu)建自己的組件很容易,也有很大的價值.通過自己的包裝過程,你可以理解怎么和RN融洽工作.也會積累更多的經(jīng)驗.由于是自己構(gòu)建的組件,可以確保組件的版本不會被改變.所以,不要依賴外部的模塊.
3. 不要把iOS和Andorid的布局分開
如果你只是在iOS和Android之間使用不同的布局,這個方法會非常有用.如果布局一樣,僅僅使用RN提供的Platform API,可以根據(jù)設(shè)備平臺的不同來做小小的檢測.
如果布局完全不同-最好是分散到不同的文件中完成(譯注:RN可以識別 fileName.ios.js 和 fileName.android.js).
如果你命名未見為index.ios.js,程序打包的時候就會在iOS中使用這個文件.類似的,在Android打包的時候會使用indexn.android.js.(譯注:具體做法可以參考F8 APP的做法).
你可能會問”代碼怎么復(fù)用?”.你可以把復(fù)用的代碼放到助手函數(shù)中,需要的地方僅僅復(fù)用助手函數(shù).
4. 錯誤的Redux store規(guī)劃
可能會犯大錯誤的地方.
當(dāng)你在設(shè)計應(yīng)用的時候,你可能更多的考慮表現(xiàn)層.很少考慮到數(shù)據(jù)操作.
Redux幫助我們正確的存儲數(shù)據(jù).如果Redux store規(guī)劃的好,將會是一個一個非常有力的data管理工具.如果沒有規(guī)劃好,會把事情弄的一團(tuán)糟.
當(dāng)我剛開始構(gòu)建RN app的時候,我只把reducers作為每一個container的數(shù)據(jù)容器.所以如果你有登錄,密碼找回,ToDO list頁面-reducer應(yīng)該是比較簡單-:SigIn,Forgot,ToDoList.
在經(jīng)過一段時間的store規(guī)劃以后,我發(fā)現(xiàn)在我的程序中不太好管理數(shù)據(jù)了.我已經(jīng)有了一個ToDo 詳情頁面.使用上面的想法,store需要一個ToDoDetails reducer是嗎?這是一個巨大的錯誤!為什么?
當(dāng)我從ToDo List中選擇出需要傳遞到ToDoDetail reducer的一項.這意味著使用了額外的actions 發(fā)送數(shù)據(jù)到reducer.非常的不合適.
經(jīng)過一點研究之后,我決定做點改變.結(jié)構(gòu)想下面這樣:
- Auth
- ToDos
- Friends
Auth用于存儲認(rèn)證的token.僅僅如此.
ToDos和Friends reducers用于儲存實體,從名字很容易知道他們是干什么的.當(dāng)我進(jìn)入到ToDo Detail頁面中-我只需要根據(jù)id來搜索所有的ToDos.
如果有更多的復(fù)雜結(jié)構(gòu),我建議使用這個計劃.你會明白什么是什么.在哪里找到他們.
5. 錯誤的項目結(jié)構(gòu)
當(dāng)你是一個新手的時候,規(guī)劃項目結(jié)構(gòu)很難.
首先要理解你的項目有多大? 大?真的很大?巨大?還是很小?
應(yīng)用中有多少頁面?20?30?10?5?還是只有一個hello world頁面
開始的時候,我的項目實施的結(jié)構(gòu)像這樣:
還好,如果你的應(yīng)用不是大項目,例如最多十個頁面.如果比這個規(guī)模更大,可以考慮使用:
有什么不同嗎?如你所見,首要的目的是建議我們?yōu)槊總€container分開存儲actions和reducers.如果應(yīng)用較小,把Redux 模塊和container分離開可能有用.如果redux Reducer和container放到一起,你可以很容易的知道哪個action和這個container關(guān)聯(lián).
如果你有通用的樣式(例如:Header,Footer,Buttons)-你可以單獨創(chuàng)建一個文件夾,叫做”styles”,之后創(chuàng)建index.js文件,編寫通用樣式,然后在每個頁面重用他們.
可能會用很多不同的結(jié)構(gòu),你應(yīng)該找到到底哪種是最適合你的.
6. 錯誤的container結(jié)構(gòu).沒有從一開始就使用smart/dumb組件
當(dāng)你初始化一個RN項目,在index.ios.js文件中已經(jīng)有了樣式,存儲在一個獨立的對象中.
在實際開發(fā)中,你需要使用很多的組件,不僅是由RN提供的,還有自己構(gòu)建的一些組件,在構(gòu)建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>
);
}
}
看起來怎么樣?
正如你看到的,所有的樣式都放在獨立的模塊中-好的.沒有代碼復(fù)制(目前為止).
但是我們到底多長時間才在表單中使用一個字段?我不確定頻率到底多少.button組件也是如此-包裝在TouchableOpatcity中-應(yīng)該被分離出來,便于我們在將來復(fù)用他.Image組件也可以依次來操作,移到一個獨立的組件中.
經(jīng)過變化以后,代碼的樣子:
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>
);
}
}
好的,或許現(xiàn)在有更多的代碼-因為我們添加了Avatar,FormItem.Button,組件的包裝器,但是現(xiàn)在我們重用這些組件.把這些組件移動到獨立的模塊中,可以到任何需要用到的地方來導(dǎo)入他們.我們也可以添加一些其他的Props,例如-style,TextStyle,onLongPress,onBlur,onFocus.這些組件可以充分的定制化.
但是要確保并不要深度定制一個小組件,這樣會讓組件的規(guī)模過大,這樣一來很難去讀懂代碼.確確實實是這樣.在需要添加一個新屬性的時候,似乎是解決問題的最簡單的辦法,在未來這個小舉動可能會在讀代碼的時候把你搞暈.
關(guān)于理想化的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”屬性.現(xiàn)在在我們的Button組件上有一些靈活性.傳遞 o,Button組件將會顯示”Submit”,傳遞 2-“Delete”.但是這很成問題.
Button作為dumb組件創(chuàng)建,為的是僅僅展示傳遞的數(shù)據(jù).傳遞數(shù)據(jù)這件事由他的更高一級的組件來完成. Dumb組件不應(yīng)該知道周圍的任何環(huán)境因素.僅僅只要執(zhí)行和展示他們被告知的數(shù)據(jù).經(jīng)過這次”升級”之后.但是這個做法并不好,為什么?
如果我們把5作為id傳遞給組件,會發(fā)生什么?我們需要更新組件,能讓他可以適應(yīng)這個選項.等等,等等.Dumb組件應(yīng)該僅僅展示他們被告知的數(shù)據(jù).這就是Dumb組件要做的全部.
7. inline styles
使用RN一段時間以后,我面臨一個行內(nèi)書寫樣式的問題,像這樣:
render() {
return (
<View style={{flex:1, flexDirection:'row', backgroundColor:'transparent'}}>
<Button
title={"Submit"}
onPress={this._submit.bind(this)}/>
</View>
);
}
當(dāng)你剛開始這么寫的時候,你會想:”好了”,等我在模擬器里檢查了布局以后,如果演示可以,我就會把樣式轉(zhuǎn)移到獨立的模塊中.或許這是個好的愿景,但是不幸的是,這件事不會發(fā)生.沒有人這么做,除非有人提醒.
一定要把樣式分到獨立的模塊中.這會讓你遠(yuǎn)離行內(nèi)樣式.
8.使用redux來驗證表單
這是我的項目中的錯誤.希望能對你有幫助.
為了由Redux協(xié)助驗證表單,我需要創(chuàng)建action,actionType,reducer里分離字段.這讓人有點惱火.
所以我決定僅借助state來完成驗證過程,沒有reducers,types等等.僅僅在container水平上的純函數(shù).這個策略對我?guī)椭艽?從action和reducer里去掉了不必要的函數(shù),不要操作store.
9. 過度的依賴zIndex
很多人從web開發(fā)轉(zhuǎn)移到RN開發(fā).在web開發(fā)中,有一個css 屬性是z-index.它幫助我們展示我們需要的內(nèi)容,在web中,這么做很酷.
在RN中,一開始是沒有這個特性的,但是后來被添加進(jìn)來了.起初還挺容易使用的, 要按照你想要的順序來渲染展示層,只需要把z-Index屬性作為style就可以了.
工作正常,但是經(jīng)過Android測試以后… 現(xiàn)在我只用z-Index來設(shè)置展示層的結(jié)構(gòu).這就是zIndex能做的.
10.不讀外部模塊的代碼
當(dāng)你想節(jié)約時間,你可以使用外部的模塊.通常他們都要文檔.你可以從文檔中獲取信息并使用外部模塊.
但有時,模塊會崩潰.或者不像描述的那樣工作.這就是你為什么需要讀源碼.通過讀源碼,你可以理解錯誤在哪里.或許模塊是很壞的.或是是你使用的方法不對.另外就是-如果你讀了其他模塊的代碼,你會了解到如何構(gòu)建你自己的模塊.
11. 要小心手勢操作和動畫 API
RN讓我們有能力構(gòu)建原生的應(yīng)用.怎么讓應(yīng)用感覺像是原生應(yīng)用.展示層,手勢,還是動畫?
當(dāng)你使用View,Text,TextInput和其他的RN默認(rèn)提供的模塊的時候,手勢和動畫應(yīng)該由PanResponder和動畫API來操作.
如果你和我一樣是從web轉(zhuǎn)過來的RN開發(fā)者,獲取用戶的手勢操作可能多少有點嚇人-什么時間開始,何時結(jié)束,長點擊,短點擊.過程不是太清晰,怎么在RN中模擬這些操作?
這里是一個Button組件由PanResponder和動畫來協(xié)助.創(chuàng)建這個組件來捕獲用戶的手勢操作.例如,用戶按壓項目,然后手指拖動到另一邊.在動畫API的協(xié)助下,構(gòu)建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 (用戶觸摸按鈕是觸發(fā))和 onPanResponderRelase(用戶從屏幕中移開手指是觸發(fā)),兩個句柄.
我們也初始化動畫對象的實例,幫助我們使用動畫.設(shè)定值為0,然后我們定義_setOpacity方法,調(diào)用時改變this.opacityAnimated的值.在渲染之前我們插值處理this.opacityAnimated到正常的opacity值.我們沒有使用View,而是使用了Animated.View模塊為了使用動態(tài)變化的opacity值.
搞定了.
正如你所見,不是很難理解具體是怎么回事.當(dāng)然你需要讀相關(guān)API的文檔,確保你的app的完美運行.但是我希望找個例子能夠幫助你開個好頭.
React Native太棒了,你可以用它做幾乎任何事情.如果沒有RN,你要做這些事情需要 Swift/Objective C或者JAVA.然后關(guān)聯(lián)到React Native.
這是一個大的社區(qū).很多的解決辦法,組件,結(jié)構(gòu)等等.在你開發(fā)的時候你可能會犯很多錯誤. 所以我希望這篇文章能幫助你避免一些錯誤.