我在使用React Native/Redux開發(fā)中犯過的11個錯誤
在使用React Native近一年之后,是時候分享一下我剛開始用RN開發(fā)項(xiàng)目時犯過的錯誤了。
1.錯誤的估計
有可能你對***個React Native(RN)應(yīng)用程序的預(yù)估是完全錯誤的!
- 1)你需要分別考慮iOS和Android版本的布局!在布局的時候,有很多組件可以重復(fù)使用;如果ios和Android的頁面結(jié)構(gòu)不同,就需要對他們分開單獨(dú)布局。
- 2)對form進(jìn)行評估時,***也考慮一下數(shù)據(jù)層驗(yàn)證。
- 3)了解數(shù)據(jù)庫結(jié)構(gòu),有助于正確地規(guī)劃redux
2.盡量使用基礎(chǔ)組件(buttons,footers,headers,inputs,texts)
google搜索RN的基礎(chǔ)組件,你會發(fā)現(xiàn)有很多現(xiàn)有組件可以方便的用到項(xiàng)目中,如buttons,footers等。如果項(xiàng)目中沒有特別的布局設(shè)計,只需要使用這些基礎(chǔ)組件就可以構(gòu)建一個頁面。如果有一些特殊的設(shè)計,例如,特殊的button樣式,你就需要為每個按鈕設(shè)置自定義樣式。你可以封裝已經(jīng)構(gòu)建好的組件,并為它們定制樣式。但是我覺得使用View,Text,TouchableOpacity和RN的其他組件來構(gòu)建自己的組件更加有意義。因?yàn)槟銜懈嗟膔n實(shí)踐,并且深刻理解如何使用RN。最重要的一點(diǎn),你可以確定你自己構(gòu)建的組件版本不會被改變。
3.不要把iOS和Android的布局分開
如果iOS和Android布局大致一樣,只有一小部分不同,你可以簡單地使用RN提供的Platform API根據(jù)設(shè)備平臺進(jìn)行區(qū)分。
如果布局完全不同 – ***分散在不同的文件中單獨(dú)布局。
如果你給一個文件命名為index.ios.js – 程序打包時,在iOS中將使用這個文件顯示iOS布局。 index.android.js也是一樣的道理。
你可能會問:“代碼怎么復(fù)用呢?” 你可以將重復(fù)的代碼移動到助手函數(shù)中。需要的時候只復(fù)用這些助手函數(shù)。
4.錯誤的redux store規(guī)劃。
初學(xué)者經(jīng)常會犯的一個很大的錯誤就是,當(dāng)你在規(guī)劃你的應(yīng)用程序時,你可能會考慮很多布局相關(guān)的問題,卻很少考慮關(guān)于數(shù)據(jù)的處理。
Redux能夠幫助我們正確地存儲數(shù)據(jù)。如果redux規(guī)劃的好 – 它將是管理應(yīng)用程序數(shù)據(jù)的強(qiáng)大工具。
當(dāng)我剛剛開始構(gòu)建RN應(yīng)用程序時,我曾考慮將reducers作為每個container的數(shù)據(jù)存儲。所以,如果你有登錄,忘記密碼,待辦事項(xiàng)列表頁面 – 使用他們的縮寫比較簡單:SignIn, Forgot, ToDoList.
在進(jìn)行了一段工作后,我發(fā)現(xiàn)管理數(shù)據(jù)沒有想象中的容易。
當(dāng)我從ToDo列表中選擇項(xiàng)目時 – 我需要將數(shù)據(jù)傳遞給ToDoDetails reducer。這意味著使用了額外的操作來發(fā)送數(shù)據(jù)到reducer。
在做了一些調(diào)查之后,我決定以不同的方式規(guī)劃結(jié)構(gòu)。一個例子:
- Auth
- Todos
- Friends
Auth用于存儲認(rèn)證的token。
而ToDos和Friends reducers用于存儲實(shí)體,當(dāng)我去ToDo Detail頁面時 – 我只需要通過ID搜索所有的ToDos。
對于更復(fù)雜的結(jié)構(gòu),我推薦使用這種規(guī)劃,你可以快速的定位到你想找到的。
5.錯誤的項(xiàng)目結(jié)構(gòu)
作為初學(xué)者時,總是規(guī)劃不好項(xiàng)目結(jié)構(gòu)。
首先 ,需要分析你的項(xiàng)目是不是足夠大?
你的應(yīng)用程序中有多少個頁面? 20?30?10?5?還是只有一個”Hello World”頁面?
我遇到并開始實(shí)施的***個結(jié)構(gòu)是這樣的:
![圖0:[外文翻譯]我在使用React Native / Redux開發(fā)中犯過的11個錯誤](https://s5.51cto.com/oss/201801/12/3b5d3a88d2ec1591fa953de59b0314f9.gif)
如果你的項(xiàng)目不超過10個頁面,使用上面這種結(jié)構(gòu)是沒有問題的。但是如果項(xiàng)目特別大 – 你可以考慮這樣規(guī)劃結(jié)構(gòu):
![圖1:[外文翻譯]我在使用React Native / Redux開發(fā)中犯過的11個錯誤](https://s2.51cto.com/oss/201801/12/e9da5c446ee853bf423c90b216b7e47d.gif)
區(qū)別在于,***種類型建議我們將actions和reducers與container分開存儲。第二種- 把它們存儲在一起。如果應(yīng)用程序很小 – 將redux模塊從container中分離出來會更加有用。
如果你有通用的樣式(如Header、Footer、Buttons) – 你可以單***建一個名為“styles”的文件夾,在那里設(shè)置一個index.js文件并在其中寫入通用樣式。然后在每個頁面上重復(fù)使用它們。
實(shí)際項(xiàng)目中會有很多不同的結(jié)構(gòu)。你應(yīng)該了解使用哪種結(jié)構(gòu)更適合你的需求。
6.錯誤的container結(jié)構(gòu)。沒有從一開始就使用smart/dumb組件
當(dāng)你開始使用RN并初始化項(xiàng)目時,index.ios.js文件中已經(jīng)有一些代碼,存儲在一個單獨(dú)的對象中。
在實(shí)際開發(fā)項(xiàng)目中,你將需要使用很多組件,不僅僅是由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>
- );
- }
- }
所有樣式都存儲在一個單獨(dú)的模塊中。
包裹在TouchableOpacity中的button組件應(yīng)該單獨(dú)分離出來,這樣才能方便我們以后重復(fù)使用它。Image組件,以后也可能重復(fù)使用,所以也應(yī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)在的代碼看起來更多了 – 因?yàn)槲覀優(yōu)锳vatar,F(xiàn)ormItem和Button組件添加了包裝器,但現(xiàn)在我們可以在需要的地方重復(fù)使用這些組件。我們可以將這些組件移動到單獨(dú)的模塊中,并導(dǎo)入我們需要的任何地方。我們也可以添加其他一些Props,例如style,TextStyle,onLongPress,onBlur,onFocus。而且這些組件是完全可以定制的。
注意,一定不要深度定制一個小組件, 這樣會使組件過于繁瑣,代碼會變的很難閱讀。即使現(xiàn)在添加新屬性的想法看起來像是解決任務(wù)的最簡單的方法,將來這個小小的屬性在閱讀代碼時可能會引起困惑。
關(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>
- }
- }
我們已經(jīng)“升級”了Button組件。用一個叫做“id”的新屬性來替換屬性“title”?,F(xiàn)在Button組件就變的“靈活”了。傳0 – button組件會顯示“submit”。傳2 – 顯示“delete”。但這可能會有一些問題。
Button被創(chuàng)建為一個dumb組件 – 只是為了顯示數(shù)據(jù),傳遞數(shù)據(jù)這件事由它的更高一級的組件來完成。
如果我們將5作為id傳遞給這個組件,我們就需要更新組件,以便讓它適應(yīng)這個改動。dumb組件,就是細(xì)分的小組件,它只要接收props就好了,如果有state也應(yīng)該與全局的無關(guān)。
7.行內(nèi)樣式
在使用RN布局之后,我遇到了行內(nèi)樣式的寫作風(fēng)格問題。類似這樣:
- render() {
- return (
- <View style={{flex:1, flexDirection:'row', backgroundColor:'transparent'}}>
- <Button
- title={"Submit"}
- onPress={this._submit.bind(this)}/>
- </View>
- );
- }
當(dāng)你這樣寫的時候,你會想:“暫時這樣寫,等我在模擬器中運(yùn)行之后 – 如果布局沒問題,再把樣式移動到單獨(dú)的模塊。”也許這是一個好的想法。但是..不幸的是,你往往會選擇性忽略行內(nèi)樣式…
一定要在獨(dú)立的模塊中編寫樣式,遠(yuǎn)離行內(nèi)樣式。
8.使用redux驗(yàn)證表單
要使用redux來驗(yàn)證表單,我需要在reducer中創(chuàng)建action,actionType單獨(dú)的字段,這樣做很麻煩。
所以我決定只借助state來完成驗(yàn)證。沒有reducers,types等等,只是在container級別上的純功能函數(shù)。從action和reducer文件中刪除不必要的函數(shù),這個策略對我?guī)椭艽蟆?/p>
9.過于依賴zIndex
很多人從web開發(fā)轉(zhuǎn)到RN開發(fā)。在web中有一個css屬性z-index,它可以幫助我們在需要的層級顯示我們想要的內(nèi)容。
在RN中,一開始沒有這樣的特性。但后來又被添加進(jìn)來了。起初,使用起來還挺簡單的。只需為元素設(shè)置zIndex屬性,它就會按照任何你想要的圖層順序來渲染。但是在Android上測試之后…現(xiàn)在我只用zIndex來設(shè)置展示層的結(jié)構(gòu)。
10.不仔細(xì)閱讀外部組件的源碼
你可以引入外部組件來節(jié)省你的開發(fā)時間。
但有時這個模塊可能會中斷,或者不像描述的那樣工作。閱讀源碼你才會明白哪里出現(xiàn)了錯誤。也許這個模塊本身就有問題,或者你只是用錯了。另外 – 如果你仔細(xì)閱讀其他模塊的源碼,你將會學(xué)習(xí)到如何構(gòu)建自己的組件。
11.要小心手勢操作和Animated API。
RN為我們提供了構(gòu)建完全原生應(yīng)用程序的能力。怎么讓用戶感覺是原生應(yīng)用?頁面布局,滑動手勢,還是展示動畫?
當(dāng)你使用View,Text,TextInput和其他RN提供的默認(rèn)模塊時,手勢和動畫應(yīng)該由PanResponder和Animated API來處理。
如果你是從web轉(zhuǎn)過來的rn開發(fā)工程師,獲取用戶的手勢操作可能會有些困難,你需要區(qū)分什么時候開始,什么時候結(jié)束,長按,短按。你可能還不夠清楚怎么在RN中模擬這些動畫操作。
這是我用PanResponder和Animated建立的Button組件。這個button是為了捕捉用戶手勢而構(gòu)建的。例如 – 用戶按下項(xiàng)目,然后將手指拖到一邊。在按下按鈕時,借助于動畫API,構(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對象實(shí)例。它有一套不同的操作句柄。我感興趣的是onPanResponderGrand(當(dāng)用戶觸摸按鈕時觸發(fā))和onPanResponderRelease(當(dāng)用戶從屏幕上移開手指時觸發(fā))兩個句柄;
我還設(shè)置了一個動畫對象實(shí)例,幫助我們處理動畫。將其值設(shè)置為零;然后我們定義_setOpacity方法,調(diào)用時改變this.opacityAnimated的值。在渲染之前,我們插入this.opacityAnimated為正常的opacity值。我們不使用View而是使用Animated.View模塊為了使用動態(tài)變化的opacity值。
通過上面的例子,你會發(fā)現(xiàn)Animated API不難理解,你只需要閱讀相關(guān)的API文檔,以確保你的應(yīng)用程序***運(yùn)行。希望這個例子能幫你開個好頭。
在使用React Native開發(fā)時可能會遇到很多問題,希望這篇文章能幫助你避免一些錯誤。