11 اشتباه رایج در طی توسعه برنامه‌های React Native / Redux
ﺯﻣﺎﻥ ﻣﻄﺎﻟﻌﻪ: 12 دقیقه

11 اشتباه رایج در طی توسعه برنامه‌های React Native / Redux

پس از کار کردن با React Native به مدت تقریبا یک سال، ‌تصمیم گرفتم اشتباهات رایجی که معمولا تازه‌کاران انجام می‌دهند را لیست کنم. در این مثاله، با ۱۱ مورد از آن‌ها آشنا خواهید شد.

1. ارزیابی اشتباه

۱) باید طرح مربوط به iOS و اندروید را به صورت جداگانه ارزیابی کنید. درست است که تعداد زیادی کامپوننت‌های مجددا استفاده شده وجود خواهند داشت، اما همچنین طراح‌های مختلفی می‌توانند وجود داشته باشند. یا حتی ساختارهای مربوطه به صفحات مختلف برنامه برای iOS و اندروید می‌توانند به کلی تفاوت داشته باشند.

۲) وقتی که فرم‌ها را ارزیابی می‌کنید، بهتر است که طرح اعتبارسنجی را نیز ارزیابی کنید. وقتی که در حال توسعه برنامه‌ها در React Native هستید، باید کد بیشتری نسبت به موقعی که مثلا در حال توسعه برنامه‌های دورگه با استفاده از Cordova می‌نویسید، بنویسید.

۳) اگر می‌خواهید برنامه‌ای بر پایه یک وب‌اپلیکیشن بسازید که از قبل یک backend را دارد، (و در نتیجه باید از یک API از قبل ساخته شده استفاده کنید) مطمئن شوید که تمام endointها توسط آن backend فراهم شده‌اند. زیرا شما قرار است که منطق برنامه خود را مدیریت کنید، و بهتر است که به درستی کدنویسی شود. ساختار دیتابیس، نحوه اتصال موجودیت‌ها و... را درک کنید. اگر آن را درک کنید، می‌توانید مخزن Redux خود را به درستی بسازید.

2. استفاده از کامپوننت‌های از پیش ساخته شده (دکمه‌ها، footerها، headerها، ورودی‌ها، متون)

اگر در گوگل به دنبال کامپوننت‌های از پیش ساخته شده مانند دکمه‌ها، footerها و... بگردید، خواهید دید که تعداد زیادی کتابخانه وجود دارند که می‌توانید استفاده کنید. اگر هیچ طرح خاصی ندارید، این کتابخانه‌ها می‌توانند بسیار پرکاربرد باشند. فقط با استفاده از این بلوک‌ها یک صفحه بسازید، و کار شما به اتمام می‌رسد. اما اگر طرح خاصی دارید و دکمه‌ها در این طرح متفاوت هستند، باید برای هر دکمه استایل‌های سفارشی تنظیم کنید. این مسئله گاهی می‌تواند گیج کننده باشد. البته، می‌توانید کامپوننت‌های از پیش ساخته شده را در کامپوننت‌های خود جمع‌بندی کنید و استایل‌های سفارشی خود را از آن‌جا تنظیم کنید. اما به نظر من ساخت کامپوننت‌های خود با استفاده از View، Text، TouchableOpacity و دیگر ماژول‌های React Native بهتر است؛ زیرا نحوه کار با آن را درک خواهید کرد، و همچنین تمرین بیشتری خواهید داشت. همچنین، به ماژول‌های خارجی وابسته نخواهید بود.

3. جدا نکردن طرح‌ها برای iOS و اندروید

این نکته فقط وقتی کاربردی است که طرح‌های مختلفی برای نسخه‌های iOS و اندروید داشته باشید. اگر اینطور نیست، می‌توانید به سادگی از اِی‌پی‌آی Platform فراهم شده توسط React Native استفاده کنید و بر حسب پلتفرم دستگاه، بررسی‌های کوچکی انجام دهید. اگر طرح‌ها کاملا متفاوت هستند، بهتر است آن‌ها را در فایل‌های مختلفی قرار دهید.

اگر فایلی را index.ios.js نامگذاری کنید، React Native در هنگام مونتاژ برنامه از این فایل برای نمایش طرح iOS استفاده خواهد کرد. همین اتفاق برای فایل index.android.js هم می‌افتد.

شاید بپرسید: «کدهای تکراری چه طور؟» می‌توانید کدهای تکراری خود را در helperها قرار دهید و سپس به راحتی از این helperها مجددا استفاده کنید.

4. ذخیره سازی Redux اشتباه

یک اشتباه بزرگ!

وقتی که برنامه خود را نقشه‌کشی می‌کنید، بیشتر به طرح آن، و کمتر به مدیریت داده‌ها فکر می‌کنید.

Redux به شما کمک می‌کند تا داده‌ها را به درستی ذخیره کنید. اگر مخزن Redux به درستی برنامه‌ریزی شده باشد، می‌تواند ابزار قدرتمند برای مدیریت داده‌های برنامه باشد. در غیر این صورت،‌ می‌تواند همه چیز را به هم بریزد.

5. ساختار پروژه اشتباه

همیشه وقتی که یک تازه‌کار هستید، برنامه‌ریزی برای ساختار پروژه کار سختی است.

اول از همه، ببینید که آیا برنامه شما بزرگ است؟ آیا خیلی بزرگ است؟ یا آیا کوچک است؟

چند صفحه در برنامه خود دارید؟ ۲۰؟ ۳۰؟ ۱۰؟ ۵؟ فقط صفحه «سلام دنیا»؟

اولین ساختاری که دیدم و شروع به پیاده‌سازی آن کردم، این ساختار بود:

خب، اگر برنامه بزرگی ندارید، این ساختار خوب است. برای مثال، حداکثر 10 صفحه. اگر از این مقدار بزرگ‌تر است، چنین ساختاری را در نظر داشته باشید:

تفاوت آن‌ها در چیست؟ همانطور که می‌توانید ببینید، اولین ساختار پیشنهاد می‌کند که actionها و reducerها را جدا از محفظه ذخیره کنیم. ساختار دوم، می‌گوید که آن‌ها را با هم ذخیره کنیم. اگر برنامه شما کوچک است، ذخیره کردن ماژول‌های Redux جدا از محفظه، پرکاربردتر است.

اگر استایل‌های رایجی دارید، (مانند Header، Footer، Buttons) می‌توانید پوشه جدایی به نام «styles» بسازید، در آن فایلی به نام index.js‌ بسازید و این استایل‌ها را در آن قرار دهید. سپس هم می‌توانید از آن‌ها بر روی هر صفحه‌ای که می‌خواهید استفاده کنید.

ساختارهای مختلفی وجود دارند. باید ببینید که کدام مورد بیشتر مناسب خواسته شماست.

6. ساختار محفظه اشتباه

وقتی که کار با React Native را شروع می‌کنید و اولین برنامه خود را راه‌اندازی می‌کنید، از قبل مقداری کد در فایل index.ios.js وجود دارد. اگر آن را بررسی کنید، خواهید دید که استایل‌ها در آبجکت‌های متفاوتی ذخیره شده‌اند. هیچ ارزیابی‌ای در متد رندر وجود ندارد و همه چیز با استفاده از ماژول‌های فراهم شده توسط React Native (View، Text) ساخته شده است.

در یک پروژه واقعی، نیاز به استفاده از تعداد زیادی کامپوننت‌ خواهید داشت، که صرفا توسط React Native فراهم نشده‌اند. همچنین در هنگام ساخت محفظه‌ها، تعداد زیادی کامپوننت ساخته، و مجدا استفاده خواهید کرد.

این کامپوننت را در نظر بگیرید:

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) که در TouchableOpacity جمع‌بندی کردیم، می‌تواند جدا شود تا در آینده بتوانیم مجددا از آن استفاده کنیم. پس 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>
        );
    }
}

البته شاید هنوز هم مقداری کد باقی مانده باشد؛ زیرا ما wrapperهایی برای کامپوننت‌های Avatar، FormItem و Button اضافه کردیم، و حال می‌توانیم از این کامپوننت‌ها در هر کجا که می‌خواهیم مجددا استفاده کنیم. می‌توانیم آن‌ها را به ماژول‌های جداگانه منتقل کرده، و در هر کجا که می‌خواهیم وارد کنیم. می‌توانیم برخی ویژگی‌های دیگر مانند style، textStyle، onLongPress، onBlur و onFocus را به آن‌ها اضافه کنیم. این کامپوننت‌ها، کاملا قابل سفارشی‌سازی هستند.

دقت کنید که زیاد به سفارشی‌سازی یک کامپوننت وارد نشوید؛ زیرا اگر بیش از حد این کار را انجام دهید، حجم این کامپوننت‌ بیش از حد خواهد شد، و در نتیجه خواندن آن بد و سخت خواهد بود. با این که در لحظه فکر اضافه کردن چند ویژگی ممکن است راحت‌ترین روش برای رفع مشکلات به نظر برسد، در آینده این ویژگی کوچک ممکن است هنگام خواندن کد گیج کننده باشد.

به این کد نگاهی داشته باشید:

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>
        );
    }
}

همانطور که می‌توانید ببینید، ما کامپوننت Buttun خود را ارتقا داده‌ایم. چه چیزی را تغییر داده‌ایم؟ ما ویژگی «title» را با یک ویژگی جدید به نام «id» جایگزین کردیم، و حال مقداری انعطاف در این کامپوننت داریم. اگر مقدار «0» را رد کنید، نتیجه «Submit» را نمایش خواهد داد، و اگر مقدار «2» را رد کنید، نتیجه «Delete» را نمایش خواهد داد. اما این بسیار بد است.

کامپوننت Button به عنوان یک کامپوننت Dumb ساخته شده بود، که یعنی فقط برای نمایش داده‌هایی بود که به آن منتقل شده بودند. کامپوننت‌های Dumb نباید چیزی درباره آن چیزی که احاطه‌شان می‌کند بدانند؛ بلکه فقط آنچه به آن‌ها گفته بودید را نشان داده، و انجام دهند. وقتی که ما آن را ارتقا دادیم، آن را هوشمند کردیم؛ اما این مسئله بد است. چرا؟

اگر مقدار «5» را به عنوان id به این کامپوننت منتقل کنیم چه می‌شود؟ در این صورت مجبوریم که مجددا آن را بروزرسانی کنیم، تا با این گزینه نیز کار کند. و به همین صورت، کامپوننت‌های Dumb بهتر است که فقط کاری که به آن‌ها می‌گویید را انجام دهند.

7. استایل‌های خطی

پس مدتی کار کردن با طرح‌ها در React Native، به مشکلی در نوشتن استایل‌های خطی به مانند این مورد بر خوردم:

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

وقتی که به این صورت استایل‌های خطی می‌نویسید، در ابتدا با خود فکر می‌کنید: «پس از این که طرح را در یک شبیه‌ساز بررسی کردم، آن را به یک ماژول جداگانه منتقل خواهم کرد.» و شاید این دقیقا کاری است که می‌خواستید انجام دهید، اما متاسفانه در اکثر مواقع این مسئله را فراموش می‌کنید.

همیشه استایل‌ها را در یک ماژول جداگانه قرار دهید. این کار شما را از استایل‌های خطی امن نگه خواهد داشت.

8. اعتبارسنجی فرم‌ها با استفاده از Redux

این کار، یک اشتباه در پروژه من بود. شاید در پروژه شما پرکاربرد باشد.

برای اعتبارسنجی فرم‌ها با کمک Redux، من مجبور بودم که یک action و action type بسازم، و این کار واقعا آزاردهنده بود.

پس تصمیم گرفتم که این کار را با کمک state انجام دهم. بدون هیچ‌گونه reducer، type و... فقط یک تابع خالص در سطح محفظه. این کار کمک زیادی به من کرد.

9. تکیه کردن بیش از حد به z-Index

افراد زیادی از وب به توسعه React Native‌ می‌آیند، و در وب یک CSS وجود دارد یک ویژگی‌ای به نام z-index دارد. این ویژگی به شما در نمایش لایه‌ای که می‌خواهید، و در سطحی که می‌خواهید کمک می‌کند. در وب، این ابزار بسیار جالب است.

در React Native، چنین امکانی از اول وجود نداشت، اما بعدا به آن اضافه شد. پس من هم شروع به استفاده از آن کردم. در ابتدا بسیار آسان بود. لایه‌ها را در هر ترتیبی که می‌خواهید رندر کنید، و سپس هم ویژگی zIndex را به عنوان یک استایل تنظیم کنید. اما پس از این که آن را بر روی شبیه‌ساز آزمایش کردم، تصمیم گرفتم که لایه‌ها را به ترتیبی که باید نمایش داده شوند، بچینم.

10. نخواندن کد ماژول‌های خارجی

وقتی که می‌خواهید از هدر رفتن زمان خود جلوگیری کنید، از ماژول‌های خارجی استفاده می‌کنید. اما گاهی اوقات این ماژول‌ها ممکن است با شکست مواجه شوند، یا به شکلی که تعریف شده بودند کار نکنند. شاید ماژول مربوطه بد است، یا شاید فقط به روش اشتباهی از آن استفاده می‌کنید. به علاوه، اگر کد ماژول‌های دیگر را بخوانید، نحوه ساخت ماژول خود را نیز یاد خواهید گرفت.

11. آگاهی از PanResponer و APIهای پویانمایی شده

React Native این قابلیت را برای ما فراهم کرده است، که بتوانیم برنامه‌هایی کاملا بومی بسازیم؛ و چه چیزی باعث می‌شود که یک برنامه حس بومی بودن را القا کند؟ طرح‌ها، حرکات و انیمیشن‌ها.

و اگر طرح‌ها به صورت پیشفرض وجود دارند، وقتی که از View، Text، TextInput و دیگر ماژول‌های React Native استفاده می‌کنید، حرکات و انیمیشن‌ها توسط PanResponder و APIهای پویانمایی شده مدیریت می‌شوند.

اگر با پیش‌زمینه‌ای از وب می‌آیید، این مسئله (دریافت حرکات کاربر) می‌تواند کمی برای شما ترسناک باشد. مثلا این که چه زمانی شروع شد، چه زمانی تمام شد، لمس طولانی، لمس کوتاه و... همچنین نحوه پویانمایی اشیا‌ء در React Native اصلا واضح نیست.

در اینجا یک کامپوننت دکمه را مشاهده می‌کنید که با کمک PanResponder و Animated ساخته شده است. این دکمه برای دریافت حرکات کاربر ساخته شده است. برای مثال، کاربر بر روی یک آیتم کلیک می‌کند و انگشت خود را به کنار می‌کشد.

'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 را راه‌اندازی می‌کنیم. در اینجا، handlerهای مختلفی را تنظیم می‌کنیم. چیزی که ما به دنبالش هستیم، onPanResponderGrand (این مورد وقتی فراخوانی می‌شود که کاربر بر روی دکمه لمس می‌کند) و onPanResponderRelease (این مورد وقتی فراخوانی می‌شود که کاربر انگشتش را از روی صفحه بر می‌دارد) است.

ما همچنین آبجکت Animated را راه‌اندازی کردیم که به ما در کار با انیمیشن‌ها کمک می‌کند. در ابتدا مقدار آن را برابر را صفر قرار می‌دهیم، و سپس متد _setOpacity‌را تعریف می‌کنیم، که مقدار this.opacityAnimated را تغییر می‌دهد. قبل از رندر کردن، this.opacityAnimated را برابر با مقدار عادی آن قرار می‌دهیم. ما نه از ماژول View، بلکه از ماژول Aimated.View در جهت استفاده از یک مقدار تغییر یافته به صورت دینامیک استفاده می‌کنیم.

همانطور که می‌بینید، درک آن زیاد سخت نیست. امیدوارم این مثال شروع خوبی برای شما بوده باشد.

منبع

چه امتیازی برای این مقاله میدهید؟

خیلی بد
بد
متوسط
خوب
عالی
در انتظار ثبت رای

/@er79ka

دیدگاه و پرسش

برای ارسال دیدگاه لازم است وارد شده یا ثبت‌نام کنید ورود یا ثبت‌نام

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

عرفان کاکایی

مقالات برگزیده

مقالات برگزیده را از این قسمت میتوانید ببینید

مشاهده همه مقالات