از React Native حرفه ای تر استفاده کنیم - قسمت اول - استایل دهی تمیز و مدیریت شده


قسمت قبل در مورد طرز تفکر و یه سری توصیه قبل از شروع کار در React Native صحبت کردیم توی این قسمت در مورد استایل نویسی تمیز و مدیریت شده قرار صحبت کنیم.

وقتی از استایل نویسی توی React Native صحبت میشه توجه همه به ‍StyleSheet.create جلب میشه و همه میگن که کاری نداره که! اما وقتی برنامه بزرگ میشه نوشتن و مدیریت کردن استایل های برنامه خیلی کار سختی میشه مشکلات زیر رو ممکن خیلی ها رو آزار بده:

۱. عدم Reusable بودن استایل ها به دلیل اینکه داخل هر Component نوشته میشن
۲. پراکندگی و کثیفی استایل ها برخی در متد render برخی در بیرون از Component و...
۳. اعمال استایل مبتنی بر یک state یا props خاص یا حتی بر اساس یک Event - استایل های Modular
۴. عدم وجود استایل های Global مثلا یک font family برای همه Text ها با وزن های مختلف و...
۵. پیچیدگی پیاده سازی Helper style یا Modifier ها در Component های ‌Base
۶. سختی در تغییر و نبود استایل های یکپارچه و نبود قابلیت Theming
۷. کثیف شدن کد در هنگام نوشتن Style های مختص یک Platform
۸. عدم وجود استایل های تو در تو شما یعنی نمیتونید از Parent به Child دسترسی پیدا کنید
۹. نبود یک UI Framework که قابلیت Extend شدن داشته باشه

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




ایجاد Style های Reusable - حذف کمی از کثیفی ها و ایجاد استایل های Modular و مبتنی بر وضعیت ها (مشکلات ۱ - ۲ - ۳)

در حالت معمول ما Style ها رو به شکل زیر مینویسیم:

import React, { Component } from 'react';
import { StyleSheet, View, Text } from 'react-native';

export default class Header extends Component {
    render() {
        return (
            <View style={styles.header}>
                <Text style={styles.header_text}>Test App</Text>
            </View>
        );
    }
}

const styles = StyleSheet.create({
    header: {
        height: 64,
        justifyContent: 'center',
        backgroundColor: '#f00'
    },
    header_text: {
        fontSize: 20,
        textAlign: 'center'
    }
});

خب این روش مشکلات خاص خودش رو داره اول اینکه شما برای اینکه بخواهید مثلا رنگی رو از یک متغیر بخونید و مقدار اولیه رو Override کنید باید حتما یک Style Object دیگه توی render ایجاد کنید و یک Array به عنوان Style پاس بدید. یه چیزی مثل شکل زیر:

...
    render() {
        const { brandPrimary } = this.props;
        const backStyles = {
            backgroundColor: brandPrimary
        };
        
        return (
            <View style={[styles.header, backStyles]}>
                ...
            </View>
        );
    }
...


خب میبینم که متد render کثیف شد این مسئله اینجا خیلی کوچیکه ولی اگه Component بزرگ بشه خیلی کثیف تر ازین حرفا میشه. راه حل صریح اینه که Style ها رو تو فایل های جدا بنویسیم اما نه به شکل یک Object بلکه به شکل یه تابعی که یک متغیر رو به عنوان پارامتر دریافت میکنه مثل زیر:

export default (configs) => ({ 
    header: {
        height: 64,
        justifyContent: 'center',
        alignItems: 'center',
        backgroundColor: configs.brandPrimary || '#f00'
     }, 
    header_text: {
        fontSize: 20
    }
});


شاید از خودتون بپرسید چرا Style رو به شکل یک تابع نوشتیم و اون تابع داره Style که ما میخواهیم رو return میکنه. این قضیه دقیقا برای حل همون مشکل Override کردن و پاس دادن یه سری Data به Style هست.

برای استفاده از این استایل کافیه توی پوشه Component بذاریدش و بعد هم import اش کنید و به شکل زیر ازش استفاده کنید.

import generateStyles from './styles';
...
    render() {
        const { brandPrimary } = this.props;
        const styles = generateStyles({
            brandPrimary
        });
        
        return (
            <View style={styles.header}>
                ...
            </View>
        );
    }
...

البته من پیشنهاد میکنم که فایل های StyleSheet رو کنار هر Component نذارید و توی پوشه src یا همون root پروژه تون یک پوشه style یا theme یا ... ایجاد و توی اون به تفکیک component و screen و... فایل هارو جدا کنید و از اونجا import کنید. پوشه theme یا همون استایل شما به شکل زیر در میاد که پوشه theme/component شامل فایل های style برای component ها است و پوشه theme/screens حاوی فایل های style برای screen ها یا همون صفحات برنامه است. یک پوشه utils هم داریم که توابع کمکی و فایل های متغیر و... رو داخل اون قرار میدیم. البته شما میتونید ریز بشید و استایل هاتون ساختار مند تر بشکونید مثلا بخش هایی مثل UI Kit یا... هم ایجاد کنید.

ساختار پیشنهادی برای پوشه theme
ساختار پیشنهادی برای پوشه theme


حالا از شر اون استایل های داخل Component خلاص شدیم که این روش چند تا مزیت داره:

  • دیگه استایل ها مختص یک Component نیستن
  • برای ایجاد استایل های مبتنی بر متغیر ها یا Prop یا State نیست متد Render رو شلوغ کنیم
  • میتونیم ایجاد Theme برای component ها رو به راحتی انجام بدیم مثلا یک دکمه با حالت های Danger, Primary, Success و...
  • میتونیم utils های style هامون رو کنار خودشون داشته باشه مثلا تابعی برای merge کردن استایل ها بنویسیم

این روش میتونه علاوه بر حل مشکل شماره ۱ میتونه مشکل شماره ۲ و ۳ رو هم حل کنه به راحتی میتونید متغیری که به تابع Style Generator مون پاس داده میشه رو بر اساس state یا props یا event ها تغییر بدید و Style ها طبیعتا آپدیت میشه.




ایجاد Style های Global - ایجاد قابلیت Helper و Modifier - (مشکلات ۴ و ۵)

توی React Native نمیشه بگیم که همه Text ها فلان اتفاق براشون بیوفته یا بگیم همه اون View هایی که بهشون یه خصوصیت خاص رو دادیم Style شون تغییر کنه یا مثلا backgroundColor رو بتونیم از طریق props کنترل کنیم. برای حل این مشکل یه راه حل ساده وجود داره و اونم اینه که شما بیایید Component های Core رو شخصی سازی کنید و یک نسخه برای خودتون بسازید و از اون به بعد از اون استفاده کنید.

خب برای مثال ما میخواهیم یک Text بسازیم که از فونت Font Family اصلی برنامه ما استفاده کنه و همچنین یک Helper براش مینویسیم که اگه بهش Bold را به عنوان Prop پاس دادیم (این Prop از نوع ‌Bold هست) متن اش رو Bold کنه و یک Modifier مینویسیم که مستقیم بتونیم رنگ رو از طریق یک prop به نام color تغییر بدیم

import { Text as RNText } from 'react-native';
import generateStyles from './styles';

export default class Text extends Component {
    render() {
        const { bold, color, ...props } = this.props;
        const styles = generateStyles({ bold, color });

        return <RNText style={styles.text} {...props}>{this.props.children}</RNText>
    }
}

خب می بینید که این فایل Text رو import کرده و style های خودمون بهش دادیم و الباقی prop هایی که داریم حالا فایل Style اش به شکل زیر هست:

export default (configs) => ({ 
    text: {
        fontFamily: 'IRANSansMobile',
        fontWeight: configs.bold ? '500' : 'normal'
        color: vars.color || 'black'
     }
});

حالا برای استفاده ازش به جای import کردن Text از React Native از پوشه ای که این فایل رو توش گذاشتید استفاده کنید.

به همین راحتی :) حالا شما میتونید به این روش View یا هر Component دیگه ی مختص به خودتون رو داشته باشید که با helper ها و modifier های اختصاصی خودتون کار میکنه برای مثال توی پروژه های خودم یک View اختصاصی ساختم که یک helper داره به اسم full که فقط اعمال میشه View کل فضای موجود رو میگیره.




ایجاد استایل های یکپارچه و مدیریت شده و تغییر آسان ایجاد قابلیت Theming (مشکل ۶)

اگر بخواهیم رنگ ‌Brand مون رو تغییر بدیم چیکار کنیم؟ اگر بخواهیم فونت اصلی سایت رو تغییر بدیم چیکار کنیم؟ خب توی پروژه های وب وقتی SASS اومد این مشکل حل شد در اصل SASS یک سری امکانات Programaticly توی CSS بهمون میداد خب الان ما توی یه زبان برنامه نویسی هستیم و طبیعتا این قابلیت هارو داریم فقط باید به یه نحوی ازشون استفاده کنیم.

اولین قدم اینه توی پوشه utils که بالاتر گفتیم یه فایل بسازید به اسم variables.js و توش به شکل زیر رنگ ها و Font Family یا چیز های دیگه رو export کنید. به شکل زیر:

import Color from 'color';
import { Platform, Dimensions, PixelRatio } from 'react-native';

// Device Sizes
const deviceHeight = Dimensions.get('window').height;
const deviceWidth = Dimensions.get('window').width;

// OS Info
const platform = Platform.OS;
const isAndroid = Platform.OS === 'android';
const isIos = Platfrom.OS === 'ios';
const isIphoneX = platform === 'ios' && deviceHeight === 812 && deviceWidth === 375;

// Fonts
export const baseFontFamily = 'IRANSansMobile';

// Theme colors
export const brandPrimary = '#63265D';
export const brandPrimaryDark = Color(brandPrimary).darken(0.2);

export const brandInfo = '#03A9F4';
export const brandSuccess = '#4caf58';
export const brandDanger = '#f44336';
export const brandWarning = '#f0ad4e';

export const brandDark = '#000';
export const brandLight = '#f4f4f4';

چیزایی که توی قطعه کد بالا ممکنه براتون سوال باشه یکی پکیج Color هست که یک Package کمکی هستش که توابع که توی SASS واسه کار با رنگ هارو داشت رو توی خودش داره مثلا darken, lighten, mix, rgba و... که به راحتی میتونید ازش استفاده کنید راهنماش هم از ( اینجا ) قابل دسترس هست.

متغیر های گروه Device Info, OS Info متغیر هایی هستند که محاسباتی هستند و برای اینکه هر بار در هر StyleSheet اونارو محاسبه نکنیم. متغیر های بعدی متغیر هایی هستند که مربوط به تم و رنگ بندی و ظاهر هستند.

برای استفاده ازشون هم کافیه توی فایل های استایل import کنید و ازشون استفاده کنید حالا یا میتونید یکی یا چند تا import کنید مثلا به شکل زیر:

import { isAndroid, brandDark } from '../variables';

یا اینکه همه رو تحت یه اسمی import کنید مثلا:

import * as vars from '../variables';

به راحتی با این کار مشکل Themeing حل میشه شما به راحتی میتونید با تغییر متغیر ها کل اپلیکیشن رو به یکباره عوض کنید و نیز به تغییر جای جای برنامه ندارید. البته ما فقط رنگ ها و فونت هارو مثال زدیم شما میتونید فاصله ها و اندازه فونت ها و حتی گردی گوشه و... ها رو هم در قابلیت متغیر داشته باشد که خیلی Modular تر کار کنید.

حل مشکل کوتاه تر کردن کد های مختص یک Platform (مشکل ۷)

خیلی وقت ها پیش میاد که ما میخواهیم که یک سری Style ها رو در هر پلتفرم به صورت متفاوت داشته باشیم مثلا اگر توجه کنید در اکثر مواقع Header توی اندروید رنگی هست و توی iOS به صورت سفید هست. برای حل این مشکل روش های متعددی هست.

روش اول:

به کمک همین متغیر هایی که ساختیم میتونیم استایل هارو جدا کنیم مثلا به شکل زیر:

import * as vars from '../variables';

export default (configs) => ({ 
    header: {
        height: vars.isAndroid ? 64 : 56,
        justifyContent: 'center',
        paddingTop: vars.isIphoneX ? 20 : 0,
        alignItems: 'center',
        backgroundColor: vars.isIphone ? '#f00' : vars.brandPrimary
     }, 
    header_text: {
        fontSize: 20
    }
});

این روش برای مواقعی که تغییرات کوچیک هست خوب جواب میده از طرفی کمتر کدمون کثیف میشه :)

روش دوم:

حالا که فایل های استایل رو جدا کردیم میتونیم از قابلیت Platform-specific extensions که توی React استفاده کنیم برای نوشتن کد های مخصوص هر پلتفرم اینه که کلا فایل های StyleSheet جدا بنویسیم منتها با پسوند های اون مرورگر یعنی اگر توی پوشه src/theme/component یک فایل برای header میسازید باید یا فایلتون رو به شکل header.android.js و header.ios.js نام گذاری کنید یا برای استایل اون component یک پوشه به همون نام بسازید مثلا src/theme/components/header و داخلش index.android.js و index.ios.js بسازید که من دومی رو پیشنهاد میکنم چون مرتب تر هستش. ‌Bundler داخلی React Native در هر پلتفرم فایل با پسوند اون پلتفرم رو import میکنه.


روش سوم:
روشی که اصلا پیشنهاد نمیکنم اینه که از Platfrom.select استفاده کنید.

import { Platform } from 'react-native';

export default configs = configs => ({
    container: {
        ...Platform.select({
            ios: {
                paddingTop: 15,
                borderTopWidth: 1,
                borderTopColor: '#f00'
            },
            android: {
                paddingTop: 10,
                elevation: 3
            }
        })
    }
});

همون جور که میبینید این روش به ازای هر platfrom از شما یک Object میگیره و هر موقع نیاز باشه spread میکندش.

دلیلی که من خیلی دوست ندارم از این روش استفاده کنم شلوغی بیش از حد کد هست ولی در مواقعی که Style ها خیلی باهم فرق میکنند بهتره از این روش استفاده بشه اما برای کار های کوچیک قابلیت اول بهتر جواب میده و اگر خیلی خیلی زیاد تر از حد متوسط استایل ها فرق میکنن بهتره از روش دوم استفاده کنید ولی اگر اندازه تغییرات متوسط هست این روش بهتر هستش.


حل مشکل Nested Style یا Style های تو در تو

برای حل مشکل استایل های تو در تو از Shoutem Theme استفاده کنید که یک Library است برای اینکه شما بتونید تو در تو style بدید و config کردنش هم راحته کافیه به لینک زیر برید و اونجا توضیح داده چجوری باید راه اندازی بشه.

فریم ورک قابل Extend شدن که بتونه مشکل Style های تو در تو رو حل کنه

فریم ورک ها یا UI Kit زیادی واسه React تولید شده که هر کدوم معایب و حسن هایی دارن احتمالا موقع جستجو به اسم های زیر بر میخورید:

بین همه گزینه های بالا Native Base بهترین گزینه است چون همه قابلیت هایی و راه حل هایی که گفتیم رو داخل خودش داره ? و خیلی خوب هم Extend میشه و هم کاملا هم قابل Customize شدن هست و همچنین قابلیت Theme و Variable و استایل های سفارش داره.


فریم ورک Native Base

طبق این راهنما Native Base رو نصب کنید تا بتونید از Component هاش استفاده کنید. برای اینکه بدونید چجوری Native Base رو Customize کنید این لینک رو دنبال کنید. به صورت کلی Native Base در هسته اصلی خودش برای حل مشکل style های تو در تو از Shoutem Theme استفاده میکنه و برای حل مشکلات دیگه از روش هایی که بالاتر توضیح دادیم استفاده میکنه. در اصل Native Base یک Package راحت و آماده است که تمام چیز هایی که بالاتر توضیح دادیم رو یکجا بهمون میده


پس چرا تک تک مسائل بالا رو توضیح دادیم؟

خیلی ها علاقه به استفاده از UI Kit ندارن و ترجیح میدن خودشون UI Kit خودشون رو داشته باشن یا به هر دلیل دیگه ای نمیخوان از Native Base استفاده کنند که شرح مسائل به صورت تک تک به اون دسته از برنامه نویس ها کمک میکنه تا اونا هم از این قابلیت ها استفاده کنند و همچنین دسته دیگه ای هستن که ممکنه برنامه هایی رو تا الان نوشته باشن و در هنگام توسعه و گسترش دادنش ممکنه به مشکل خورده باشن و با یک سری Refactor ساده میتونن از این راه حل ها استفاده کنند و به Native Base مهاجرت نکنند.




همه موارد بالا به صورت تجربی و از جاهای مختلف برداشته شدن یا در هنگام مشکل ساخته شدن امیدوارم بهتون کمک کند و تمیز تر و راحت تر بتونید برنامه هاتون رو توسعه بدید.

قسمت بعد در مورد اشکال یابی یا همون Debugging قسمت های مختلف برنامه مثل Component ها یا State و Redux و ... توضیح میدیم.