یکی از چالش هایی که من باهاش مواجه بودم، این بود که گهگاه پیش میومد برای یه پروژه ی یکسان مجبور بشم چندتا view متفاوت بزنم، در حالی که همه شون تا حد زیادی، Functionality های یکسان داشتن. اما خب این اتفاق برای خیلی ها نمی افته که مجبور باشن این کار رو بکنن، بنابر این به یه شکل دیگه ای مساله رو مطرح میکنم.
خیلی مواقع پیش اومده که شما باید چیزی که پیاده سازی می کنید، به نسبت display view های مختلف، ریسپانسیو بوده و دیزاین مناسب رو نشون میده. اما خیلی از زمان ها، دیزاین هایی که طراح برای Mobile view و Desktop View میده جوریه که خب پدر developer در میاد تا بخواد پیاده سازیش کنه. خب بزارین ببینیم چه alternative هایی داریم.
یه حالت خیلی ساده ش اینه که Mobile View و Desktop View شما تا حد بسیار خوبی شبیه به هم هستن و با استفاده از یه سری CSS Tricks and Tools می تونید اون رو پیاده کنید، این جور موقع ها برای آرامش جان و مال و ناموستون، برید سراغ Flexbox یا CSS Grid و هرچی بیشتر تلاش کنید از یه چیزی مثل bootstrap دوری کنید. ( نظر شخصیمه، کلا اصراری بهش نیست). اما خب این موضوع بحث ما نیست.
موضوع اساسی و چالش بر انگیز زمانیه که شما تا یه حد خوبی تفاوت UI و Functionality بین دو تا Layout خودتون دارید (خیلی وقتا اینا میشن سه تا، تبلت هم ساز جدایی خودش رو میزنه). خب بزارید ببینیم من چی کار میکنم. یکی از کارایی که همون اول میرم سراغش جدا کردن view ها هست. اصلا هم لازم نیست دنبال آدرس دهی متفاوت باشید و تفاوت رو از اینجا شروع کنید، از خیلی جاهای راحت تری میشه جدایی رو به وجود آورد. بزارید یه مثال بزنم:
من یه آدرس دارم برای هوم پیج مثل localhost:3000/home، خب توی آدرس دهی در router، یه فایل ایندکس رو بهش اساین میکنم که برام سرو بشه به شکل زیر:
{ component: loadable(() => import('components/pages/Home')), path: '/home', exact: true, fetching: import('components/pages/Home/fetching'), },
خب این نوع آدرس دهی، میره و از پوشه ی Home برام فایل index رو load میکنه. حالا میرسیم به مرحله ای که باید محتوای فایل index.js رو تعیین کنیم. فایل های index در پروژه هایی که من میزنم به شکل زیر هست:
import React from 'react'; import { useSelector, useDispatch } from 'react-redux' import { createStructuredSelector } from 'reselect'; import { useInjectReducer } from 'utils/injectReducer'; import { useInjectSaga } from 'utils/injectSaga'; import reducer from 'redux/Vendors/reducer'; import saga from 'redux/Vendors/saga'; import {loadUser} from 'redux/Global/actions' import {getVendors} from 'redux/Vendors/actions' import {makeSelectVendorsList} from 'redux/Vendors/selectors' import View from './view' const key = 'vendors'; export default function VendorsPageIndex(props){ useInjectReducer({ key, reducer }); useInjectSaga({ key, saga }); const mapStateToProps = useSelector(createStructuredSelector({ vendors: makeSelectVendorsList() })); const dispatch = useDispatch(); const mapDispatchToProps = { getVendors: data => dispatch(getVendors(data)), loadUser : () => dispatch(loadUser()), } const finalProps = Object.assign({}, mapStateToProps, mapDispatchToProps, props) return <View {...finalProps}/> }
برای فهم مساله یه توضیح کوتاه بدم اینجا داره چه اتفاقی میوفته. من هر نوع connection ای که با ریداکس نیاز داشته باشم رو اینجا انجام میدم. کلا هر ارتباطی با خارج از این component نیاز باشه، وظیفه ی index هست که اون رو هندل کنه. فایل های Redux من، یعنی Reducer و Saga همه شون lazy load هستن، برای همین وقتی import میشن، باید توسط یک manager ای که اینجا دو هوک useInjectReducer و useInjectSaga هستن، به ساختار store اصلی inject میشن. در مرحله ی بعد، selector هام رو مینویسم و در نهایت هر dispatch ای که نیاز باشه رو اینجا map میکنم و یه funcation به props ام اضافه میکنم. در مرحله ی آخر، تمامی این data رو در یه Object به نام finalProps الصاق میکنم و props های نهاییم رو میسازم.
خب تا اینجای کار ، اتفاق خاصی نمی افته. اما در مرحله ی بعد هست که سعی میکنم کار جدا سازی view ها رو انجام بدم. چطور؟ ببینیم:
فرض میکنیم دو تا فایل برای دو تا view نوشتیم که component های موبایلی و دسکتاپی ما رو هندل میکنن، این دو تا در دوفایل به نام view.mobile.js و view.desktop.js قرار گرفتن. خب این دو تا رو import میکنیم
import MobileView from './view.mobile' import DesktopView from './view.desktop'
بعد از اینکه این دو تا رو import کردیم، باید تصمیم بگیریم از کدوم استفاده کنیم. برای این کار از یه library استفاده میکنیم به نام react-device-detect. شاید بگید میشه از user agent ریکویست ها هم دیتایی که نیازه رو بدست آورد، منم حق رو به شما میدم ولی درد این library خیلی کمتره. به علاوه اینکه کلی دیتای دیگه هم بهتون میده که ممکنه بهش نیاز داشته باشید.
خب مقایسه رو انجام میدیم، به شکل زیر:
import MobileView from './view.mobile' import DesktopView from './view.desktop' import { isMobile } from 'react-device-detect' const View = isMobile ? MobileView : DesktopView
این کار باعث میشه ویو ها توی هر حالتی که بودید، جدا بشه. اما به نظرتون این کار، درسته؟
جواب مشخصه، خیر. چرا، چون وقتی دارید دو تا ویو رو import میکنید، عملا دارید هر دو رو به باندلتون اضافه می کنید، خب چرا وقتی قراره با browser موبایلم بیام، باید js هایی که برای دسکتاپ نوشتم رو ببینم؟
پس بریم سراغ یه راه حل منطقی تر که چیزی نیست جز Dynamic Import. شما میتونید از فانکشن import استفاده کنید ولی من توصیه نمی کنم، یه مقدار دردسر داره، همون روش قدیمی و سنتی CommonJS رو پیش بگیریم بهتره. پس از require استفاده میکنیم. بریم ببینیم چجوری میشه:
یکی از کارایی که میتونید بکنید اینه که موقع import کردن هرچیزی به وسیله require بهش متغیر بدید، همین خاصیت باعث میشه کلی جلو بیفتین، یعنی اینکه یه جوری موقع بیلد شدن پروژه بهش بگید که میخوام این آدرس رو برای لود کنی:
const View = require(`./views/view.${process.env.WEB_ENV}.js`)
این کار میاد هر متغیری که شما به WEB_ENV موقع بیلد شدن پاس میدین رو جایگزین میکنه. با فرض اینکه فایل های view شما توی فولدر sibling فایل index.js به نام views هستن، میتونید از سینتکس بالا استفاده کنید. موقع بیلد شدن هم باید توجه داشته باشید که این مقدار رو به دستورتون پاس بدین، به شکل زیر:
cross-env NODE_ENV=production cross-env WEB_ENV=desktop
وقتی مقدار desktop رو به WEB_ENV پاس بدین، اون وقت در تمامی import های داینامیکی که نوشین این مقدار جایگزین process.env.WEB_ENV میشه.
اما نکته ی اساسی اینه که در صورتی میتونید اینکار رو بکنید که بخواید دو تا domain متفاوت داشته باشید. مثلا
example.com برای دسکتاپ
و m.example.com برای موبایل. دلیلش هم اینه که باید هر کدوم از این ها به صورت جداگانه فایل هایی که بیلد کردین رو براتون سرو کنن. اگر داکر داشته باشید هم میتونید دو تا image متفاوت بسازین و هرکدوم رو روی یه پورت خاص run کنید که به اون دامنه ها وصل شدن. نمونه دستوری build داکرش هم اینجوری میشه:
docker build --rm --build-arg NODE_ENV=production --build-arg WEB_ENV=mobile -t production .
خب این از جداسازی فایل ها. حالا بریم سراغ لاجیک. یه مساله ای که ممکنه پیش بیاد، قسمت های مشترکه. مطمئنا تا الان این سوال براتون پیش اومده که خیلی از کارایی که من میخوام توی دو تا layout انجام بدم، مشترکه، اما اگه این کاری که شما میگی رو بکنم، Duplication دارم. جونم برات بگه که شاید اگه با وجود HOOKS در دنیای React هنوز یه جا باشه که HOC به درد میخوره همینجاست.
من برای اینکه کدهای مشابه رو حذف کنم، یه لایه دیگه بین index و view هام اضافه کردم به نام view.hoc
این لایه کارش اینه که تمامی functionality های مشترک، توی این قسمت پیاده سازی میشن و به صورت props در اختیار view های نهایی قرار میگیرن. برای همین شما در index.js ابتدا view.hoc رو import میکنید و بعد از اون داخل view.hoc کدهای dynamic import رو مینویسید:
import React from 'react';
...
import View from './view.hoc'; .... return <View {...finalProps}/>
و فایل view.hoc:
import React from 'react'; const View = require(`./views/view.${process.env.WEB_ENV}.js`) export default function HomePageHOC(props){ exampleFunction = (a, b) => { return a * b } return <View {...props} exampleFunction={exampleFunction} /> }