نکست‌جی‌اس و ریداکس‌تانک

نگاهی به نکست‌جی‌اس، ریداکس‌تانک و کانفیگ‌ها

این متن رو هفت ماه پیش نوشتم.

دانلود نسخه‌ی چاپی (PDF)

از روی این مطلب، یه نسخه‌ی چاپی هم میتونید داشته باشید!

فهرست مطالب

  1. مقدمه
  2. جاوا‌اسکریپت و اکما‌اسکریپت (ECMAScript)
  3. ای‌اس (ES)
    1. چرا ورژن‌های مختلف از ای‌اس منتشر شدن؟
    2. چرا ای‌اس۶؟
    3. Arrow Functions
    4. ترنس‌پایلر (transpiler)
    5. بابِل، بِیبِل...؟ (Babel)
  4. فرانت‌اند و مفاهیم اس‌پی‌اِی (SPA)
  5. ری‌اکت (React)
    1. جی‌اس‌اکس (JSX)
    2. کمی بیشتر در مورد ری‌اکت
    3. نصب و شروع کار با ری‌اکت
      1. نصاب ری‌اکت
      2. ساختار پوشه‌ها
      3. ساخت کامپوننت
      4. گریزی به state
    4. درک مفاهیم ری‌اکت
      1. Props
      2. State
      3. چرخه زندگی کامپوننت‌ها (Component Lifecycle)
      4. جمع‌بندی
  6. مفهوم Isomorphic یا Universal
  7. Promise و درک ناهم‌گام‌سازی (Asynchronous)
    1. استفاده از fetch
  8. ریداکس (Redux)
    1. تفاوت ریداکس با MVC و Flux
    2. Pure Functions
    3. اجزاء ریداکس
      1. Actions
      2. Reducers
      3. Store
    4. ریداکس تانک (Redux Thunk)
  9. نوشتن یک اپلیکیشن کامل، برای تمرین
    1. راه‌اندازی
    2. ریداکس
    3. ساخت کامپوننت‌ها و استایل‌ها
      1. filter.js
      2. cards.js
      3. card.js
  10. NextJS
    1. نصب و راه‌اندازی پروژه
    2. کانفیگ‌ها
      1. config.js
      2. API
    3. ریداکس تانک
      1. store.js
      2. reducer.js
      3. highlights.js
    4. طراحی UI
      1. ساخت Base Layout
      2. index.js

مقدمه 

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

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

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

سپاس.

جاوا‌اسکریپت و اکما‌اسکریپت (ECMAScript)

اولین چیزی که لازم هست بدونید، اینه که جاوا‌اسکریپت توسط شرکت نِت‌اِسکیِپ (شرکت قدیمی موزیلا فایرفاکس) ساخته و به سازمان اِکما داده شد تا اون رو استانداردسازی کنن. اِکما سازمانیه که وظیفش استانداردسازی اطلاعاته.

اونچه که در نهایت از جاوا‌اسکریپت توسط سازمان اِکما ارائه شد، اِکما‌اِسکریپت بود. ساده‌تر بگم، اِکما‌اِسکریپت یک استاندارد هست، در حالی که جاوا‌اسکریپت محبوب‌ترین پیاده‌سازی از اون استاندارد به حساب میاد. جاوا‌اِسکریپت، اِکما‌اِسکریپت رو پیاده‌سازی میکنه و بر پایه اون ساخته میشه.

حالا سوالی که مطرح میشه، اینه که "ES" چیه؟

ای‌اِس، مخفف اِکما‌اِسکریپته (ECMAScript)، هرجایی که ای‌اِس رو در کنار یک عدد دیدید، مثل ای‌اس۶، یادتون باشه که داره به یک نسخه از اِکما‌اِسکریپت یا در واقع یک نسخه از استاندارد اشاره میشه.

ای‌اِس (ES)

ای‌اِس تا به اینجا ۸ نسخه مختلف رو ارائه کرده، ای‌اِس۱، ۲، ۳ و ۴ بین سال‌های ۱۹۹۷ تا ۱۹۹۹ ارائه شدن و دیگه ازشون پشتیبانی نمیشه. (ما هم کاری بهشون نداریم).

ای‌اِس۵ تقریبا ۱۰ سال بعد از اس‌اس۴ یعنی اواخر سال ۲۰۰۹ (تاریخ دقیق رو نمیدونم) ارائه داده شد.

ای‌اِس۶ در سال ۲۰۱۵ منتشر شد که برای راحتی کار، بهش ای‌اس۲۰۱۵ هم میگن. در واقع چون ای‌اس۶ در سال ۲۰۱۵ ارائه شده، بهش ای‌اِس۲۰۱۵ میگن!

ای‌اِس۷/ای‌اس۲۰۱۶ که مطمئنا میتونید پیشبینی کنید در سال ۲۰۱۶ منتشر شد.

ای‌اس۸/ای‌اس۲۰۱۷ هم، در سال ۲۰۱۷ منتشر شد.

چرا ورژن‌های مختلف از ای‌اس منتشر شدن؟

دلیلش اینه که هر وِرژِن، ویژگی‌های جدیدی رو ارائه کرده که با زمان خودش هم‌خوانی داشته باشه. فقط باید چند نکته رو به خاطر داشته‌باشید:

  1. پیشبینی میشه که هر سال یک نسخه جدید از اِکما‌اِسکریپت ارائه بشه،
  2. اولین نسخه‌های اِکما‌اِسکریپت با عدد نسخه‌بندی میشدن، مثل ای‌اِس۱، ای‌اِس۲ و...
  3. نسخه‌های جدید که از سال ۲۰۱۵ شروع شدن، به شکل ای‌اس[سال انتشار] نام‌گذاری میشن.
  4. اکما یک استاندارده، جاوا‌اسکریپت محبوب‌ترین پیاده‌سازی از اون استاندارد هست.

چرا ای‌اس۶؟

نسخه ۶‌ام از اِکما‌اِسکریپت، ویژگی‌های خیلی خوبی رو به زبان جاوا‌اسکریپت اضافه کرد، و همین، یکی از دلایلی شد که ری‌اَکت به طور پیش‌فرض ازش پشتیبانی میکنه. مثلا مفاهیمی مثل «کلاس‌ها» و «ماژول‌ها»، که برای زبان‌های شئ‌گرا اجباری هستند، بهش اضافه شدن. از جمله ویژگی‌های دیگش، اضافه شدن دستورات for، جِنِراتور‌های شبیه به زبان پایتون، توابع فِلِشی (Arrow Function)، کالِکشِن‌ها، پرامِس‌ها (Promise) و غیره بوده.

متاسفانه هنوز مرورگرها پشتیبانی از ای‌اِس۶ رو کامل نکردن و ای‌اِس۶ به خودی خود تو مرورگرها پشتیبانی نمیشه. اینجاست که مفاهیم تِرَنس‌پایلِرها (transpiler) خودشون رو نشون میدن که بعد از توضیح توابع فِلِشی (برای راحتی کار میگم اَرو فانکشِن) بهشون میپردازم.

Arrow Functions

یکی از ویژگی‌های خیلی خوبی که توی ای‌اِس۶ وجود داره، استفاده از اَرو فانکشن‌هاست. به اسمش دقت نکنید، مفهومش خیلی سادست. ارو فانکشن‌ها، در واقع همون توابع قدیمی جاوااسکریپت هستند (با ویژگی‌های جدید‌تر که اینجا بهشون کاری نداریم) که تو ای‌اس۶ به شکل دیگه‌ای تعریف میشن و موقع تِرَنس‌پایل (یکمی پایینتر توضیح میدم در این مورد) به شکل توابع جاوااسکریپت درمیان، مثلا:

x => {
  return x * x;
}

(m, n) => {
  return m+n
}

data => {
  data.json()
  .then(result => {
    return result;
  })
}

بعد از تِرَنس‌پایل تبدیل میشن به:

(function (x) {
  return x * x;
});

(function (m, n) {
  return m + n;
});

(function (data) {
  data.json().then(function (result) {
    return result;
  });
});

 

ترنس‌پایلر (transpiler)

حتما تا به حال واژه‌های «کامپایلِر» و «مفسر» به گوشتون خورده. تِرَنس‌پایلِرها در واقع نوعی کامپایلر هستند با یک تفاوت اصلی:

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

این درحالیه که ترنس‌پایلرها، یک سورس‌کد رو به یک سورس‌کد دیگه تبدیل می‌کنن (یا مثلا به یک زبان دیگه که مستقیم برای ماشین قابل درک نیست و باید دوباره کامپایل، اینتِرپرِت یا همون تفسیر بشه). مثلا کافی‌اسکریپت (CoffeeScript) که از خودش جاوا‌اسکریپت تولید میکنه، یا بابِل (Babel) که ای‌اس۶ رو به ای‌اس۵ (قابل پشتیبانی برای مرورگرها) تبدیل می‌کنه.

بابِل یا بِیبِل (Babel) یک ترنس‌پایلر برای ای‌اس۶ هست که اون رو به ای‌اس۵ تبدیل میکنه. ای‌اس۵ توسط مرورگرها خیلی خوب پشتیبانی میشه و در حقیقت، ای‌اس۵ همون جاوا‌اسکریپتیه که عموما باهاش آشنایی دارن.

بابِل، بِیبِل...؟ (Babel)

بابل یک تِرَنس‌پایلِر برای جاوا‌اسکریپته. بابل رو اکثرا بخاطر توانایی خوبش تو تبدیل ای‌اس۶ به ای‌اس۵ میشناسن.

به عنوان مثال این کد که با ای‌اس۶ نوشته شده:

let input = [1, 2, 3];
console.log(input.map(item => item + 1)); // [2, 3, 4]

توسط بابل تبدیل میشه به کد ای‌اس۵:

var input = [1, 2, 3];
console.log(input.map(function (item) {
  return item + 1;
})); // [2, 3, 4]

که تقریبا همه‌جا قابل اجراست. بابل خیلی خوب از پُلی‌فیل‌های جاوا‌اسکریپت پشتیبانی میکنه و باعث میشه که کد جاوا‌اسکریپت ساخته شده برای مرورگرهای قدیمی هم قابل پشتیبانی باشه. به همین خاطر، بابل باعث میشه شما از تمام ویژگی‌های ای‌اس۶ استفاده کنید بدون اینکه پشتیبانی از نسخه‌های قدیمی مرورگرها رو از دست بدید (مفهوم پُلی‌فیل اینجا معلوم میشه).

فرانت‌اند و مفاهیم اِس‌پی‌اِی (SPA)

اطلاعاتی که ما در اینترنت میبینیم، مجموعه‌ای از کدهای HTML، JavaScript و CSS هستن. قدیم‌ها، زمانی که خیلی از برنامه‌نویس‌ها از PHP (هنوز هم استفاده میکنن) برای نوشتن صفحات وب استفاده می‌کردن و جِی‌اِس مثل امروز محبوب نبود، هر صفحه از سایت باید بصورت جداگانه نوشته می‌شد. و زمانی که کاربر روی یک لینک کلیک میکرد، کل صفحه از اول رِندِر (Render) می‌شد. این فرایند برای کاربر، خسته‌کننده و طاقت‌فرسا بود و حتی گاهی بخاطر زمان زیادی که باید برای بارگذاری صفحه صرف میکرد، از ادامه کارش پشیمون میشد.

کم‌کم تکنیک‌هایی مثل اِی‌جَکس (AJAX) استفاده شدن، که مثلا وقتی صفحه در حال بارگذاری بود، شروع میکرد یک نماد کوچک لودینگ و بارگذاری (بهش معمولا میگن اِسپینِر یا spinner) رو نشون دادن و کاربر رو متوجه میکرد که اطلاعات در حال ارسال و دریافتن.

این ایده، که اطلاعات تو یک صفحه بارگذاری بشن و کاربر مدام مجبور به عوض کردن صفحه‌ها نشه (مگر در مواقع لازم) باعث ایجاد تعریف جدیدی از وب‌اَپلیکِیشِن‌ها شد، اِس‌پی‌اِی (SPA) یا Single Page Application راهش رو به لغت‌نامه‌ی برنامه‌نویس‌ها باز کرد!

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

اما داستان سمت برنامه‌نویس کمی فرق می‌کنه. کاربر زمانی میتونه اطلاعات رو تو مرورگرش ببینه، که کد‌های HTML ساخته شده باشن، و این یعنی برنامه‌نویس باید با هر درخواست کاربر، یا کلا صفحه رو از اول بارگذاری کنه (که دیگه SPA نیست) یا اینکه اون قسمتی از سایت رو که مرتبط با کاربر هست یا نیاز به تغییر داره رو مجددا با داده‌های جدید که از سمت سرور اومدن بارگذاری کنه.

اینجاست که سایت شما، تا حدود زیادی از سمت سرور خودش جدا میشه و مفهوم جدیدی به عنوان فرانت‌اِند (Front End) رو تعریف می‌کنه. فرانت‌اند و بَک‌اِند، در زمان‌های دور وجود نداشتن یا خیلی به هم نزدیک بودن. شما یک سایت رو کامل می‌نوشتید و مثلا تو یک صفحه PHP، همزمان از HTML و JavaScript استفاده می‌کردید. هر زمان که کاربر درخواست صفحه‌ی جدیدی رو میداد، شما یک اسکریپت جدید رو بارگذاری و صفحه‌های مربوطه رو از نو بازنویسی می‌کردید.

زمانی که تعاریف فرانت‌اند و بَک‌اند ایجاد شدند، طراحی سایت شکل جدیدی به خودش گرفت. فرانت‌اند یجورایی بیشتر نماد طراحی سایت شد و بَک‌اند بیشتر نماد منطق کاری (Business Logic). کدها سمت بک‌اند نوشته می‌شن و اطلاعات رو موقع نیاز به فرانت‌اند ارسال می‌کنن. از اینجا به بعد، برنامه‌نویس فرانت نیازی نداره که نگران SQL و دستورات مربوط به ارتباط با پایگاه داده و دریافت محصولات از اون باشه، یا حتی نگران فرایند عضویت و ورود به سایت. فرانت‌اند خودش رو بیشتر با بهبود تجربه کاربری درگیر کرد. لازم هست که بگم، این یک مفهوم کلی و جداسازی کلی بک‌اند و فرانت‌اند هست. اینکه وظیفه‌ی برنامه‌نویس چی باشه، نسبت به هر پروژه قابل تغییر هست و امرو، خیلی از برنامه‌نویس‌ها به هرد شاخه بَک و فرانت تسلط دارن.

اینجا بود که مارس ۲۰۱۳، فیسبوک اولین نسخه از کتاب‌خونه ری‌اَکت رو ارائه داد...

ری‌اکت (React)

ری‌اَکت یا ری‌اَکت‌جِی‌اِس، یک کتابخونست برای ساختن روابط کاربری. ری‌اکت به برنامه‌نویس این اجازه رو میده، که بتونه وب‌اپلیکیشن‌های بزرگی رو بسازه که از اطلاعات مختلفی استفاده می‌کنن و میتونن تو یک صفحه تغییر کنن، بدون اینکه صفحه رو مجدد لود کنن (مفهوم SPA). هدف اصلی سازندگان ری‌اکت، سرعت، سادگی و مقیاس‌پذیری بوده.

ری‌اکت صرفا تلاش میکنه تا رابط کاربری رو از راه Virtual-DOM (بعد از اینکه این مطلب رو تموم کردید، حتما این لینک رو باز کنید و در مورد ویرچوال‌دام بخونید) تغییر بده. اینکه این ساز.کار به چه صورتی هست رو فعلا تشریح نمیکنم اما، اگر با معماری MVC آشنا باشید، ری‌اکت اون قسمت "V" رو به خودش اختصاص میده و میتونه با بقیه کتابخونه‌های جی‌اس خودش رو وفق بده.

یکی دیگه از اَشکال ری‌اکت، ری‌اَکت‌نِیتیو هست که کمک میکنه، نرم‌افزارهای (اکثرا موبایل) نِیتیو یا سازگار با سیستم‌عامل توسط ری‌اکت ساخته بشن. ضمنا ری‌اکت از جی‌اس‌اکس (JSX) برای نمایش و ساخت المان‌ها استفاده میکنه که توضیح میدم در این مورد.

جی‌اس‌اکس (JSX)

جِی‌اِس‌اِکس، یک زبان مشابه با HTML یا XML که کمک میکنه، تیکه‌های سایت (Component) جدای از هم ساخته بشن و به شکل یک شئ جاوا‌اسکریپت در بیان.

ساده‌تر بگم، با JSX میشه ساختارهای مشابه به HTML رو ساخت. مثال:

var nav = (
    <ul id="nav">
      <li><a href="#">Home</a></li>
      <li><a href="#">About</a></li>
      <li><a href="#">Clients</a></li>
      <li><a href="#">Contact Us</a></li>
    </ul>
);

اینجا، nav به عنوان یک کامپونِنت (Component) شناخته میشه. و شما میتونید جاهای مختلف برنامه، ازش استفاده کنید (یکبار بنویسید و چندبار استفاده کنید).

این استفاده از JSX یکی دیگه از ویژگی‌های ری‌اکت به حساب میاد! اینکه شما برنامتون رو به تیکه‌های کوچکتری تقسیم می‌کنید و میتونید در جاهای مختلفی ازش استفاده کنید.

کمی بیشتر در مورد ری‌اکت

دنیای ری‌اکت جالبه. فرض کنید که یک کامپوننت رو ساختیم، مثلا یه لیست از محصولات. بعد کاربر روی یکی از محصولات کلیک میکنه، صفحه محصول باز میشه و کاربر محصول رو میبینه. کاری که ری‌اکت انجام داده چیه؟ ری‌اکت متوجه میشه که کدوم قسمت‌های سایت باید تغییر کنن و فقط اونها رو تغییر میده! پس اول شناسایی میکنه که چه چیزی تغییر کرده، و بعد تغییرات رو اعمال میکنه. ری‌اکت، به جای اینکه کل صفحه رو از اول بازسازی کنه، فقط قسمت‌هایی که تغییر کردند یا به نوعی وضعیتشون تغییر کرده رو با داده‌های جدید پر می‌کنه و اونها رو از نو میسازه و خیلی هم سریع این کار رو انجام میده!

نصب و شروع کار با ری‌اکت

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

نصاب ری‌اکت

نصاب ری‌اکت، که توسط فیسبوک ساخته شده، تو مخازن npm موجوده و میتونید اون رو به صورت عمومی توی سیستم‌عاملتو بریزید:

$ npm install -g create-react-app

# ساخت نرم‌افزار
$ create-react-app my-app-name

# نصب وابسته‌ها (Dependencies)
$ cd my-app-name
$ npm install

مطمئن باشید که حتما  npm رو روی کامپیوترتون نصب کردید. یکی از مهم‌ترین ویژگی‌های  create-react-app اینه که تمام ابزارهای لازم رو از قبل برای شما فراهم کرده. مثلا از قبل Babel برای شما نصب شده و نیازی نیست که خودتون رو درگیر نصب و راه‌اندازیش بکنید. حالا میریم سراغ یک توضیح در مورد ساختار این کتابخونه.

ساختار پوشه‌ها

اگر داخل پوشه  public رو ببینید، متوجه حضور فایل index.html میشید. این فایل در حقیقت نقطه شروع برنامست و حتما باید وجود داشته باشه، این یکی از بایدهای برنامه‌های ساخته شده با  create-react-app هست. یه نگاهی به داخل این فایل میندازیم:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="theme-color" content="#000000">
    <!--
      manifest.json provides metadata used when your web app is added to the
      homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>React App</title>
  </head>
  <body>
    <noscript>
      You need to enable JavaScript to run this app.
    </noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>

همینطور که میبینید، این فایل چیز خاصی رو داخل خودش نداره. به جز خط ۲۸ام. که اِلِمانی تعریف شده با آی‌دی  root، این رو تا اینجا توی ذهنتون داشته باشید. اتفاقی که از اینجا به بعد رخ میده، توی فایل  src/index.js قرار داره. بذارید یه نگاهی هم به این فایل بندازیم:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';

ReactDOM.render(<App />, document.getElementById('root'));
registerServiceWorker();

این فایل هم یکی از فایل‌هایی هست که حتما باید وجود داشته باشه. دلیلش اینه که تو تتظیمات  create-react-app این دو فایل به عنوان نقاط شروع برنامه تعریف شدند. خود create-react-app این دو فایل رو به هم متصل میکنه و زمانی رو که شما لازم هست خودتون بذارید تا تنظیمات رو انجام بدید، براتون ذخیره می‌کنه. میریم سراغ بررسی این فایل:

  1. اول از همه، این کد با استاندارد ای‌اس۶ نوشته شده. اگر قرار بود از استاندارد ای‌اس۵ (یا به اصطلاح Common JS) پیروی کنیم، باید مینوشتیم:  var React = require("react"); تا کلاس ری‌اکت رو به پروژه اضافه کنیم. بعدا به این خواهیم پرداخت که این خط چه کارهایی انجام میده. فعلا بریم سراغ خط بعد.
  2. تو خط دوم، شئیی به نام  ReactDOM فراخوانی شده. برای اطلاعاتون، قبل از نسخه ۰.۱۴ ری‌اکت، کتابخونه‌های  react و react-dom یکی بودن. تنها وظیفه‌ای که ReactDOM به عهده داره، اینه که با اِی‌پی‌آی‌های render یا ReactDOM.render یک اِلِمان جِی‌اِس‌اِکس رو، داخل یک اِلِمان دیگه (اینجا document.getElementById('root')) نمایش بده.
  3. خط سوم، خیلی ساده اِستایل‌های موجود در فایل index.css رو، برای تمام اِلِمان‌هایی که اینجا قرار هست رِندِر بشن، اعمال میکنه.
  4. خط چهارم یک کامپوننت رو، اینجا به اسم app، از یک کلاس ری‌اکت فراخوانی کرد و بعد تو خط هفتم، به ReactDOM گفت که این کامپوننت رو داخل اِلِمان root رِندِر کنه. اما بریم سراغ کلاس ری‌اکت و ببینیم اصلا حرفش چی هست.
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

class App extends Component {
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Welcome to React</h1>
        </header>
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
      </div>
    );
  }
}

export default App;

خب، طبق معمول خط اول، کلاس‌های ری‌اکت (React) و کامپوننت (Component) رو از کتابخونه ری‌اکت فراخوانی کرده. اما تفاوت براکت‌ها {} برای کامپوننت و ری‌اکت که براکت نداره چیه؟ این یکی از ویژگی‌های ای‌اس۶ هست که به این صورته:

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

class YeClassJadid1 {/*...*/}

export class YeClassJadid2 {/*...*/}

export default class YeClassJadid3 {/*...*/}

export default class YeClassJadid4 extends YeClassDige {/*...*/}

اشیائی که تعریف شدن همشون ویژگی کلاس بودن رو دارن، اما تفاوتشون به این صورته که، کلاس اول (YeClassJadid1)، فقط تو همون فایلی که تعریف شده، یا اسکوپی (Scope) که تعریف شده قابل خونده شدنه و جاهای دیگه قابل استفاده نیست. کلاس دوم (YeClassJadid2) رو شما میتونید جاهای دیگه، با استفاده از روش import { YeClassJadid2 } from "path/to/the/file" فراخوانی کنید. دلیلش هم اینه که کلاس دوم، به طور پیش‌فرض کلاس اصلی این ماژول یا فایل نیست. اما، کلاس سوم YeClassJadid3 به طور پیش‌فرض، کلاس اصلی تعریف شده (default) و زمانی که بخواد فراخوانی بشه، میتونه به صورت import YeClassJadid3 from "path/to/the/file" یا حتی import YeEsmeDige from "path/to/the/file" فراخوانی بشه. و در نهایت هم، کلاس چهارم YeClassJadid4 مثل کلاس‌های قبلی، اما با ارث‌بری از کلاس YeClassDige خارج یا اِکسپورت میشه.

نکته‌ای که گفتنش حائز اهمیته، اینه که هر ماژول، یا هر فایل، فقط یک کلاس رو میتونه به صورت  default خارج کنه! تو مثال بالا، یکی از کلاس‌های YeClassJadid3 یا YeClassJadid4 میتونن خاصیت default رو داشته باشن!

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

ساخت کامپوننت

برای اینکه بهتر متوجه بشید کامپوننت‌ها چی هستند، صفحات وب رو به تیکه‌های کوچیک تقسیم کنید. مثلا نوار بالای صفحه یه کامپوننت جدا، لیست محصولات یه کامپوننت جدا و همینطور ادامه بدید...

اول، کل صفحه یک کامپوننت هست، بعد هر تیکه‌ای از صفحه تقسیم به کامپوننت‌های کوچک‌تر میشه و پیش میره. این موضوع رو با ساخت یک کامپوننت راحتت درک میکنید. برای شروع، داخل پوشه  src یک پوشه دیگه به اسم components یا هر اسم دیگه‌ای که دوست دارید ایجاد کنید. اینکه پوشه‌ها چطور باشن، دست خودتونه، اما یادتون باشه که یک‌سری استاندارد یا کانوِنشِن (Convention) برای اینکار هست که مدیریت کد رو راحتتر میکنه. در نهایت داخل این پوشه، یک فایل به اسم MyComponent.js بسازید و داخلش این کد رو قرار بدید:

import React from "react";

export default class ThisIsAComponent extends React.Component {
    render() {
        return (
            <ul>
                <li>Item 1</li>
                <li>Item 2</li>
                <li>Item 3</li>
                <li>Item 4</li>
                <li>Item 5</li>
            </ul>
        );
    }
}

کامپوننتی که ساختیم خیلی سادست، یه لیست با ۵تا آیتم. که باید، حتما داخل تابع render قرار بگیرن، و این تابع هم، فقط باید یک اِلِمان رو، یا چند اِلِمانی که داخل یک المان والد جمع شدند رو برگردونه. حالا باید این کامپوننت رو داخل  App.js فراخوانی کنیم. کار خیلی ساده‌ایه. App.js رو باز کنید و اون رو به شکل زیر تغییر بدید:

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import ThisIsAComponent from "./components/MyComponent"

class App extends Component {
  render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Welcome to React</h1>
        </header>
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
        
        <ThisIsAComponent/>
      </div>
    );
  }
}

export default App;

خب، کامپوننت رو ساختیم و اون رو به کامپوننت اصلی اضافه کردیم. حالا دستور  npm start رو اجرا کنید و بعد داخل مرورگر به آدرس localhost:3000 برید و نتیجه رو ببینید. بعد از اینکه تموم شد، میتونید سرور رو ببندید و برید سراغ مرحله بعد.

گریزی به state

فرض کنید که قرار بود دکمه‌ای وجود داشته باشه، تا با کلیک کردن روش، کامپوننت ما یا حتی یک بخشی از کامپوننت نمایش داده بشه و یا مخفی بشه، یکی از این دو حالت ساده! اینجا لازم هست که کمی در مورد مفهوم استِیت (state) توضیح بدم. کامپوننت‌های شما، همینطور که تا اینجا دیدید، کار خاصی رو به خودی خودشون انجام نمیدن، چون در حقیقت اِلِمانهای HTML هستند، یا حتی شاید لازم باشه در طول زمان تغییراتی رو پیدا کنن. ملموس‌ترین نوع تغییر، زمانی رخ میده که شما اطلاعاتی رو از جایی (از یک سرور) دریافت میکنید و می‌خواید به کاربر نشون بدید، اما تو مدت زمانی که اطلاعات در حال دریافت هستند، علاقه دارید تا یک اسپینر (spinner) رو نمایش بدید، تا به کاربر بگید که اطلاعات در حال بارگزاری هستن. یه سناریوی دیگه اینکه، با کلیک روی یک چِک‌باکس (checkbox) یک فیلد مخفی شده رو نمایش بدید و مثال‌های دیگه. اینجاست که استیت به کمک شما میاد.

استیت در حقیقت محل ذخیره‌ي آخرین تغییرات و حالات کامپوننته. مثلا شما بهش میگید که کامپوننت من در حالت عادی، نمایش داده میشه، اما من میخوام، زمانی که کاربر روی یک دکمه کلیک میکنه، کامپوننتم رو مخفی کنم. اینجا، نمایش داده شدن یا مخفی شدن، جز حالات یا استیت کامپوننت به حساب میاد. ری‌اکت، زمانی که استیت تغییر میکنه، کامپوننت رو مجدد بارگزاری یا رِندِر میکنه.

برای اینکه با استیت هم کار کنیم و بهتر درکش کنیم، به تریتیب زیر، به کامپوننت ThisIsAComponent استیت میدیم،

  1. حالت دیفالت یا اولیه رو برای کامپوننت تعریف میکنیم
  2. توسط یک ایونت (Event) حالت یا همون استیت رو تغییر میدیم.

کامپوننت رو به شکل زیر بازنویسی میکنیم:

import React from "react";

export default class ThisIsAComponent extends React.Component {
    constructor() {
        super()

        this.state = {
            isHidden: false
        }
    }

    toggleState() {
        this.setState({
            isHidden: !this.state.isHidden
        })
    }

    render() {
        return (
            <div>
                <button onClick={this.toggleState.bind(this)}>Change</button>
                <ul hidden={this.state.isHidden}>
                    <li>Item 1</li>
                    <li>Item 2</li>
                    <li>Item 3</li>
                    <li>Item 4</li>
                    <li>Item 5</li>
                </ul>
            </div>
        );
    }
}

کارهایی که انجام شده، به همراه توضیحاتشون به ترتیب زیر هستن:

  1. constructor به کامپوننت اضافه شده. این تابع که در حقیقت از تعاریف کلاس در جاوا‌اسکریپت برگرفته شده، کارهای متفاوتی رو میتونه انجام بده. اما یادتون باشه، هر موقع که میدونستید کامپوننتتون قرار هست حالات مختلفی رو برای نمایش داشته باشه، حالت اولیش (Initial State) رو اینجا تعریف کنید. مهمترین نکته اینه که به محض تعریف کردن  constructor، تابع super() فراخوانی بشه. اگر فراموش کنید که  super() رو بلافاصله فراخوانی کنید، this که در حقیقت همون کامپوننت شماست و اطلاعات کامپوننت رو توی خودش داره، خالی خواهد موند و در نتیجه امکان استفاده از اِستِیت و بقیه ویژگی کامپوننت‌ها رو نخواهید داشت.
  2. قدم بعدی، داخل  construct و زیر super()، استیت اولیه یا همون Default رو تعریف کردم. شکل تعریفش هم یه شئ ساده بوده که داخلش فقط از isHidden: false استفاده کردم تا بگم، در حالت عادی نمایش داده نمیشه (اینکه چطور و چه چیزی از این حالت استفاده میکنه و تغییر میکنه رو پایین‌تر توضیح میدم)
  3. یک تابعی رو تعریف کردم با اسم دلخواه  toggleState که قرار هست موقع کلیک کردن دکمه، فراخوانی بشه. کاری هم که انجام میده، خیلی ساده، استیت رو تغییر میده. کد نوشته شده شاید شما رو یکم سردرگم کنه. تعریف کد به این شکله: isHidden: !this.state.isHidden و یعنی، مقدار جدید isHidden برابر خواهد بود با هر آنچه که this.state.isHidden بوده، اما چون یک ! هم اولش آوردم، یعنی اون مقدار رو بر عکسش کن. پس اگر this.state.isHidden برابر با false بود، مقدار جدیدش برابر با true میشه و برعکس.
  4. چون یک دکمه هم به کامپوننت اضافه کردم، باید کل اِلِمان‌ها رو داخل یک اِلِمان اصلی و والد جا بدم. برای همین هم، تمام المان‌ها رو داخل یک div گذاشتم.
  5. زمانی که کاربر روی دکمه کلیک میکنه، رویداد (Event)  onClick اتفاق میوفته. اینجا بهش گفتم، زمانی که این رویداد اتفاق افتاد، تابع toggleState رو صدا بزنه. ضمنا از bind هم استفاده کردم، چون دکمه‌ها در حالت عادی، رویدادها رو انجام نمیدن و بایند (Bind) موظف هست تا حالت اصلی شئی که بهش پاس داده میشه رو حفظ و برای تابع مربوطه ارسالش کنه. در این مورد بعدا بیشتر توضیح میدم.
  6. و در نهایت، به المان ul گفتم، تا مخفی بودن یا نبودنش رو از this.state.isHidden بگیره. حالا، هر موقع که isHidden تغییر کنه، المان ul مجددا رِندِر میشه.

برنامه رو تست کنید و ببینید که کارکردش چطوره. کارتون که تموم شد، میریم تا یکم دیگه با تئوری دست و پنجه نرم کنیم.

درک مفاهیم ری‌اکت

مهم‌ترین تفاوت کتاب‌خونه ری‌اکت با فرِیم‌وُرک‌هایی مثل انگولار، اینه که ری‌اکت فقط برای فرانت‌اند ساخته شده. در مورد ری‌اکت، چیزهای خیلی زیادی برای گفتن وجود داره. اما دوتا از ویژگی‌های اصلی ری‌اکت، داشتن مفاهیمی مثل Properties یا به طور خلاصه Prop و همچنین State هست که قبل‌تر کمی با مفهوم State آشنا شدیم. اینجا تصمیم دارم در مورد این دو و همچنین چرخه زندگی کامپوننت‌ها بیشتر توضیح بدم.

Props

پراپ‌ها در واقع راهی برای ارتباط بین کامپوننت‌ها و جز ویژگی‌های اونها هستند (هرجا که شما کلاس React.Component رو استفاده یا همون extend کنید، میتونید ازشون بهره‌ ببرید). مهم‌ترین ویژگی پراپ‌ها، اینه که از سمت کامپوننت والد به فرزند منتقل میشن و اصطلاحا uni-directional (یک‌طرفه) هستن. زمانی هم وجود داره که شما مثل استِیت، پراپ‌های اولیه رو تعریف میکنید تا کامپوننت شما با اطلاعات اولیه (و نه حالات اولیه)، شروع به کار کنه. پراپ‌ها به شما کمک میکنن تا اطلاعات رو بین کامپوننت‌ها جابجا کنید، و زمانی که کاربر بخواد مستقیما روی خود کامپوننت تغییری اعمال کنه، باید از استیت استفاده بشه. بذارید با یک مثال ساده از پراپ استفاده کنیم:

فرض کنید که توی کامپوننت ThisIsAComponent از یک تگ هدر h1 استفاده میکردیم و قرار بود مقدار داخلش رو توسط کامپوننت والد تغییر بدیم. کد جدیدمون به این شکل میشد (فقط تابع رِندِر رو گذاشتم و مابقی کد دست‌نخورده باقی مونده):

render() {
        return (
            <div>
                <h1>{this.props.title}</h1>
                <button onClick={this.toggleState.bind(this)}>Change</button>
                <ul hidden={this.state.isHidden}>
                    <li>Item 1</li>
                    <li>Item 2</li>
                    <li>Item 3</li>
                    <li>Item 4</li>
                    <li>Item 5</li>
                </ul>
            </div>
        );
    }

حالا باید مقداری رو برای title از کامپوننت والد، یعنی App در نظر بگیرم. کار آسونیه و به شکل زیر تغییری رو تو App.js ایجاد میکنم:

...
<ThisIsAComponent title={"This is a new title"}/>
...

State

پراپ‌ها نباید تغییر کنن (به اصطلاح باید Immutable باشن)، برای همین از استیت استفاده میشه. در حالت عادی، کامپوننت‌ها استیت ندارن و از این جهت اصطلاحا بهشون Stateless گفته میشه. کامپوننت‌هایی که استیت پیدا میکنن، بهشون Stateful میگن.

کاربرد استیت برای اینه که کامپوننت بتونه اطلاعاتی که در هر بازسازی دریافت میکنه رو حفظ کنه. زمانی که شما از this.setState() استفاده میکنید، وضعیت کامپوننت بروزرسانی و مجددا بازسازی میشه. تمام این فرایند بازسازی توسط ری‌اکت اتفاق می‌افته و خیلی هم سریعه.

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

چرخه زندگی کامپوننت‌ها (Component Lifecycle)

کامپوننت‌ها به شما کمک میکنن تا یو‌آی (UI) رو به تیکه‌های کوچکتر تقسیم کنید. در حالت کلی، شما کامپوننت‌ها رو به شکل کلاس‌های جاوااسکریپت تعریف میکنید:

class Greeting extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

اما کامپوننت‌ها صرفا جهت نمایش ساخته نمیشن و کارهای بیشتری میشه باهاشون انجام داد. اصطلاحا هر کامپوننتی برای خودش یک چرخه‌زندگی بخصوص یا Lifecycle داره که چندتا از پر کاربردترین‌ها رو توضیح میدم:

  • constructor() که دقیقا قبل از بارگذاری کامپوننت توسط ری‌اکت خونده میشه. بهترین استفادش، تعریف state اولیه کامپوننت هست. اگر کامپوننت stateless باشه، نیازی به تعریف این تابع نیست.
  • componentWillMount() دقیقا قبل از بارگذاری خونده میشه. و قبل از تابع render() اتفاق میوفته. به همین خاطر تعریف استیت تو این تابع پیشنهاد نمیشه. این تابع سمت سرور کارهاش رو انجام میده و اصطلاحا server-side هست. (در این مورد تو بخش Isomorphism توضیح میدم)
  • componentDidMount() بعد از اینکه کامپوننت بارگذاری شد، خونده میشه. این تابع بهترین جا برای ارسال درخواست‌ها به سرور شماست و اگر استیت رو تو این تابع با استفاده از تابع this.setState() تغییر بدید، باعث میشید که باز دوباره تابع render() فراخوانی بشه
  • componentWillReceiveProps(nextProps) زمانی فراخوانی میشه، که شما از طریق کامپوننت والد، پراپ‌های کامپوننت فرزند رو تغییر بدید و بخواید استیت جدید رو بر اساس پراپ‌های جدید تنظیم کنید. برای اینکار میتونید آرگومان‌های nextProps و this.props رو با هم مقایسه کرده و تغییرات رو ایجاد کنید. (مقایسه رو حتما انجام بدید!)
  • componentWillUpdate(nextProps, nextState) قبل از بارگذاری مجدد رخ میده، اگر پراپ‌ها و استیت‌های کامپوننت تغییری کرده باشن.
  • componentDidUpdate(prevProps, prevState) بلافاصله بعد از بارگذاری مجدد اتفاق میوفته و به شما این امکان رو میده تا المان‌ها رو دستکاری کنید.
  • componentDidCatch(error, info) برای مدیریت اِرورها و خطاها تو UI استفاده میشه.
  • مطالعه بیشتر...

جمع‌بندی

پراپها استفاده میشن تا اطلاعات از کامپوننت والد به کامپوننت فرزند یا حتی داخل کامپوننت فرزند منتقل بشن. پراپ‌ها تغییرناپذیر یا به اصطلاح Immutable هستند و نباید در هر رندر تغییر کنند.

استیت برای این استفاده میشه تا اطلاعات رو تغییر بدیم، یا اطلاعات تغییر یافته رو نمایش بدیم و اصطلاحا تغییر پذیر یا Mutable هستند. مثلا کاربر چیزی رو در سایت سرچ میکنه و بلافاصله زیرش تعدادی از نزدیک‌ترین نتایج جست‌وجو نمایش داده میشه.

مفهوم Isomorphic یا Universal

زمانهای قدیم، قبل از موقعی که NodeJS بین برنامه‌نویس‌ها محبوب بشه، جاوااسکریپت زبانی بود که باهاش تغییراتی رو تو صفحه‌های وب ایجاد میکردن تا صفحه‌ها از حالت مرده و استاتیک خارج بشن. تا اون موقع زبان جاوااسکریپت، زبانی بود که تو مرورگر کاربر اجرا میشد و به اصطلاح Client Side یا سمت کاربر بود. بعد از ظهور NodeJS جاوااسکریپت این قابلیت رو پیدا کرد که تو محیط سمت سرور هم اجرا بشه و بعد از اون جاوااسکریپت به یک زبان Server Side هم تبدیل شد.

ایزومورفیزم (Isomorphism) از ریاضیات گرفته شده و «هم‌سان» معنی میشه. چون واژه ایزومورفیک برای برنامه‌نویس‌ها کمی مشکل‌ساز میتونه باشه، به جاش از واژه یونیورسال (Universal) هم استفاده میکنن.

ایزوموفیک در شاخه وب، به معنی ساخته شدن صفحه سمت سرور یا سمت کاربر هست و در حالت کلی به NodeJS و خود JavaScript اشاره میکنه. برای اینکه تعریف رو ساده‌تر کنم، اینطور در نظر بگیرید که، کدی که سمت سرور ساخته شده، به صورت HTML به کاربر ارسال میشه و کاربر میتونه تو کد صفحهش اون رو ببینه. اما کدی که سمت کاربر ساخته میشه، تو کدهای صفحه قابل مشاهده نیست. اینجا لازم هست که به این نکته اشاره کنم، که منظور از سرور، الزاما بک‌اند نیست، و اونچه که مد نظر هست، ابزاریه که وظیفه تبادل ارتباطات رو به عهده داره.

دلایل مختلفی وجود دارن که برنامه‌نویس‌ها به اپ‌های ایزومورف علاقه‌مندند:

  • بهبود سئو،
  • پرفورمنس بهتر،
  • و نگهداری راحت‌تر.

یکی از مضوعاتی که خیلی در این مورد مهم هست، اینه که کد‌های ایزومورفیک که سمت کاربر ساخته شدن، توسط موتورهای جست‌وجو به خوبی خونده نمیشن، برای همین هم اپ‌های SPA معمولا با این ساختار دچار مشکل میشن و لازم هست تا کدشون سمت سرور ساخته بشه. (سرور رو الزاما با کامپیوتر سرور اشتباه نگیرید!)

Promise و درک ناهم‌گام‌سازی (Asynchronous)

بیاید با هم یه دنیای جالبی رو تجسم کنیم، تو این دنیای ما، هنوز گوگل وجود نداره و شما رئیس یک شرکت «پاسخ به سوالات» هستید. نحوه کار به این شکله که کاربر سوال خودش رو بسته‌بندی میکنه (Data Package) و اون رو به یک پست‌چی میده، پست‌چی این بسته رو میاره برای شرکت شما (Request) و شما بسته رو باز میکنید، به سوال جواب میدید و اون رو به پست‌چی میدید و ایشون هم برای کاربر جواب رو میبره (Response). پروسه تا اینجا واضحه فقط چند شرط وجود داره:

از اونجایی که شرکت ما خیلی خاصه :دی،

  1. کاربر زمانی که بسته رو به پست‌چی میده، باید دم در خونه صبر کنه تا پست‌چی برگرده (و از کار و زندگیش هم می‌افته)
  2. پستچی تا زمانی که شما پاسخ رو بسته‌بندی نکردید و بهش تحویل ندادید پیش شما میمونه.

این روش ارتباط، روش هم‌گام یا اصطلاحا Synchronous نام داره و همون روش قدیمیه کار با اینترنته، کد PHP مینوشتیم و با هربار کلیک رو دکمه، کاربر باید منتظر صفحه جدید میموند و نمیتونست کارهای دیگه توی سایت رو انجام بده.

حالا شرکتمون رو کمی پیشرفته‌تر میکنیم، فرایند همون شکل قبلی رو داره اما:

  1. زمانی که کاربر درخواستش رو بسته‌بندی کرد و به پست‌چی داد، برمی‌گرده خونش و کارهاش رو انجام میده
  2. شما فقط یک پست‌چی ندارید و پست‌چی‌ها میتونن از کاربر درخواست‌های مختلفی رو بگیرن و برای شما بیارن

این نوع ارتباط، ارتباط نا‌هم‌گام یا Asynchronous نام داره و خیلی تو وب‌اپ‌های SPA پر کاربرده و AJAX یکی از معروف‌ترین تکنیک‌ها برای برقراری این نوع ارتباطهاست. تو نسخه‌های جدید جی‌اس، برای اینکه کاربر رو منتظر نذاریم بعد از اینکه درخواستش رو ارسال کرد، بهش قول میدیم که در آینده جوابی رو برای درخواستش ارسال میکنیم، و کاربر میتونه به کارش ادامه بده و ماهم پردازشمون رو انجام میدیم. اینجاست که پرامِس (پرامیس؟) یا همون Promiseها خودشون رو نشون میدن.

قبل از معرفی شدن پرامس‌ها، تو جاوااسکریپت از Callbackها استفاده میشد، کال‌بک‌ها توابعی بودن که مثلا به عنوان آرگومان یک تابع دیگه تعریف میشدن، تا هنگام بُروز یک رویداد خاص (Event) کار خاصی رو هم انجام بدن. مشکل اینجا بود که ایده‌ی کال‌بک‌ها، هرچقدر هم که قشنگ بوده، تو پردازش‌های پیچیده مدیریت کد رو خیلی سخت میکرده و به اصطلاح، برنامه‌نویس رو وارد جهنم کالبَک‌ها یا همون Callback-Hell میکرده. برای همین پرامس‌ها ساخته شدند تا جایگزینی برای کال‌بک‌ها باشن.

پرامس‌ها یا جوابی رو برمیگردونن یا اینکه دلیلی رو برای عدم انجام موفقیت‌آمیز اون فرایند ارائه میدن. از اینجا میشه گفت که پرامس‌ها ساختار مشابهی مثل try/catch دارن و از همه مهمتر، سه وضعیت کلی رو شامل میشن:

  1. Pending: که یعنی در حال انجام کاری هستند،
  2. fulfilled: که یعنی کارشون رو به خوبی انجام دادن و
  3. rejected: که یعنی از پس کاری که بهشون داده شده بر نیومدن.

یه مثال ساده برای اینکه با نمونه‌ای از یک پرامس آشنا بشید به این شکله:

fetch(url)
  .then(process)
  .then(save)
  .catch(handleErrors)
;

اینجا، تابع  process صبر میکنه تا کار تابع fetch تموم بشه، بعد تابع save منتظر process میمونه و اگر هرکدوم از این توابع جایی به مشکل خوردند، تابه handleErrors وظیفش رو انجام میده.

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

استفاده از fetch

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

قدم اول، نصب کتابخونه isomorphic-fetch از مخازن npm هست. fetch یکی از کتابخونه‌های مورد علاقه‌ی من که در کنارش کتابخونه axios وجود داره. تفاوت عمده این دو کتابخونه، تو پردازش اطلاعات هست و اکسیوس برای ای‌اس۶ آمادگی بیشتری داره، منتهی من طبق عادت پیش میرم و از فِچ (fetch) استفاده میکنم.

$ npm install --save isomorphic-fetch

کتابخونه رو نصب کنید، اینکه واژه ایزومورفیک اولش استفاده شده، نشون میده که این کتابخونه رو، هم میشه سمت سرور و هم سمت کاربر استفاده کرد. حالا، کامپوننت ThisIsAComponent رو یکمی تغییر میدیم. در نهایت کدمون باید به شکل زیر بشه:

import React from "react";
import fetch from "isomorphic-fetch";

export default class ThisIsAComponent extends React.Component {
    constructor() {
        super()

        this.state = {
            done: true,
            items: []
        }
    }

    fetchData() {
        this.setState({
            done: false
        });

        fetch('http://jsonplaceholder.typicode.com/posts')
        .then(data => {
            data.json()
            .then(res => {
                this.setState({
                    done: true,
                    items: res
                })
            })
        })
        .catch(error => {
            console.log(error)
        })
    }

    render() {
        return (
            <div>
                <h1>{this.props.title}</h1>
                <button onClick={this.fetchData.bind(this)}>Get Data</button>
                <p hidden={this.state.done}>Loading</p>
                <div>
                    {
                        this.state.items.map(item => {
                            return (
                                <p>{item.title}</p>
                            )
                        })
                    }
                </div>
            </div>
        );
    }
}

خط به خط بریم جلو ببینیم چه اتفاقی افتاده:

  1. اول، از کتابخونه isomorphic-fetch شئ fetch رو فراخوانی کردم.
  2. تو تابع constructor، استیت اولیه رو تغییر دادم و به جای isHidden که اول داشتیم، done رو، که وظیفه نگهداری از وضعیت بارگذاری آیتم‌ها رو به عهده داره و items که نگهدارنده آیتم‌های دریافت شده از سرور هستند رو ساختم.
  3. تابع toggleState رو پاک کردم و به جاش از fetchData استفاده کردم. اسمش رو هم خودم انتخاب کردم. داخل این تابع اتفاقات جالبی میوفته.
    زمانی که این تابع خونده میشه (یا در واقع رو دکمه‌ای کلیک میشه که باید این تابع رو اجرا کنه) وضعیت done به false تغییر پیدا میکنه. چون در حقیقت آیتمی دریافت و کار ما هم تموم نشده.
    قدم بعدی، از fetch استفاده کردم تا از یک آدرس پیش‌فرض، یک‌سری اطلاعات الکی رو دریافت کنم. نکته مهم اینه که fetch یک پرامس هست و وضعیتش رو میشه کنترل کرد. برای همین، تو خط پایینش گفتم، هر موقع که دریافت اطلاعات تموم شد، از اطلاعات دریافت شده که اسمشون رو data گذاشتم استفاده کن و...
  4. یکی از مهم‌ترین تفاوت‌های fetch و axios تو این مرحلست، پاسخی که fetch به شما برمیگردونه، یک پاسخ خام هست و باید تبدیل به دیتای قابل خوندن بشه. fetch این کار رو با برگردوندن یک پرامس انجام میده (برای نوشتن توابع از Arrow Functionها استفاده کردم). پس گفتم، زمانی که جواب رو از سرور گرفتی، اون رو به json تبدیل کن (که خود تابع json یک پرامس برمیگردونه) و بعد از اینکه عمل تبدیل به json درست انجام شد، از حاصلش استفاده کن تا دوباره استیت رو تغییر بدی.
    done رو هم برابر با true کردم تا نشون بدم عمل دریافت اطلاعات کامل شده. در نهایت نتیجه حاصل از دریافت اطلاعات، یا همون آیتم‌های مورد نظرم رو، که حالا تبدیل به json شدند، داخل items استیت ذخیره کردم.
    اینکار باعث میشه که ری‌اکت، یکبار دیگه کامپوننت‌هایی که از این استیت استفاده میکنن رو بارگذاری کنه. در نهایت هم بررسی میکنه که آیا اشکالی وجود داشته تو کل این فرایند یا نه.
  5. آخر سر، تو تابع  render دکمه Get Data رو به تابع fetchData وصل کردم. یک تگ p هم اضافه کردم که بهش گفتم، وضعیت hidden بودنش رو از done موجود تو استیت بگیره. (هر موقع که بارگذاری تموم شده بود، این المان محو میشه و هرموقع بارگذاری در حال انجام بود، این المان نمایش داده میشه)
  6. داخل المان div که به جای ul نشسته، با استفاده از تابع map() که شکل جدیدی از forEach هست، آیتم‌ها رو نمایش دادم. (پایین‌تر درمورد map توضیحات بیشتری میدم)

ریداکس (Redux)

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

ریداکس به طور کلی، یک مخزن مدیریت استیت هست، و برای ساده‌تر شدن، یک معماری برای مدیریت داده در برنامه‌هاست. ریداکس در مجموع برای مدیریت استیت برنامه‌ها استفاده میشه، به این صورت که کل استیت برنامه رو داخل یک استیت بزرگ (بهش میگیم Store) نگه‌داری میکنه که این استیت بزرگ یا استور (Store) تغییر‌ناپذیر (Immutable) هست و ساختار درختی داره.

تفاوت ریداکس با MVC و Flux

قبلا گفتم که ریداکس یک معماری جهت مدیریت اطلاعات هست. برای اینکه به یک نقطه مشترک برسیم، بذارید معماری معروف MVC رو در نظر بگیریم. تو این معماری، تفکیک خوبی بین مدل‌ها (Model)، نمایش و ارائه (View) و منطق کار (Controller) وجود داره. یک مشکلی با این معماری به وجود میاد، زمانیه که به یک برنامه با مقیاس بزرگ میرسیم (Large Scale) که چرخش اطلاعات تو این برنامه از تمامی جهات یا به اصطلاح Bi-Directional هست، یعنی یک تغییر مثلا از سمت کاربر یا پاسخ توسط API میتونه حالت و استیت برنامه رو در خیلی جاها تغییر بده خصوصا زمانی که با «اتصال دوطرفه اطلاعات» یا Two-way data binding طرف هستیم (کاری بهش نداریم و به مفهومش هم اینجا نیازی نیست) که در نهایت اشکال‌یابی و نگه‌داری کدها رو سخت‌تر می‌کنه.

فلاکس (Flux) شباهت خیلی زیادی به ریداکس (Redux) داره. تفاوت اصلیشون در اینه که فلاکس، چندید مخزن (Store) برای تغییرات حالت (State) برنامه داره و ریداکس فقط یکی! مهم‌ترین چیزی که در مورد ریداکس لازم هست بدونید، اینه که بر پایه برنامه‌نویسی تابعی (Functional Programming) ساخته شده که دونستن چند نکته در مورد برنامه‌نویسی تابعی به کارتون خواهد اومد:

  • توابع مثل اشیاء درجه اول هستند،
  • میتونید توابع رو به شکل آرگومان‌ها پاس بدید،
  • از طریق توابع، چرخش داده‌ها، بازگشت‌ها (Recursions) و خود توابع مدیریت میشن،
  • قابلیت استفاده از توابع کمکی مثل Map، Filter و Reduce وجود داره،
  • میشه توابع رو زنجیروار به هم متصل کرد،
  • حالت‌ها تغییر نمی‌کنن (Immutable) و
  • ترتیب اجرا شدن کدها دیگه مهم نیست (Hoisting).

Pure Functions

برنامه‌نویسی تابعی به ما کمک میکنه تا از راه نوشتن توابع کوچیک که از لحاظ منطق جداسازی شدن (Isolated) کدهای تمیزتر و ماژولارتری بنویسیم. با اینکار، کدهایی نوشته میشن که تست‌پذیری، نگهداری و اشکال‌یابی (Debugging) بهتری دارن.

توابع کوچک، که به کدهای قابل استفاده مجدد (Reusable Code) تبدیل شدن، به ما کمک میکنن که کمتر بنویسیم و بیشتر فکر کنیم. توابع خیلی راحت میتونن کپی و پیست و هرجایی استفاده بشن. توابعی که ایزوله شدن به کار خاصی و اسکوپ خاصی (منظور از ایزوله شدن، اینه که میتونید از برنامه جداشون کنید و جای دیگه هم استفاده کنید) وابسته نیستند و خیلی راحت باعث میشن که برنامه‌نویسی تابعی لذت‌بخش باشه.

توابع انواع مختلف و زیادی دارن مثل Pure Functions، Anonymous Functions، Closures، Higher-Order Functions و زنجیره متد‌ها (Method Chains) و خیلی چیزهای دیگه که از بحث ما خارجن. اما ریداکس بین همه اینها خیلی زیاد از توابع خالص یا Pure Functions استفاده میکنه که اینجا توضیح میدم.

توابع خالص (Pure Functions) مقدار جدیدی رو به نسبت آرگومان‌ها و ورودی‌هاشون بر‌میگردونن. در واقع اون‌ها اشیاء رو دستکاری نمیکنن و هر‌بار، یک شئ جدید ازشون برگردونده میشه، فاقد از اینکه ورودی‌ها چی باشن. این توابع به استیتی که ازش خونده شدن وابسته نیستن و فقط یک پاسخ رو برای هر ورودی بر‌میگردونن. این باعث میشه که این توابع خیلی قابل پیش‌بینی باشن (Predictable).

چون توابع خالص مقادیر رو تغییر نمیدن، هیچ تاثیری روی محدوده‌ی کاریشون یا همون اسکوپ ندارن یا هیچ اثرات جانبی (Side Effects) به جا نمیذارن که باعث میشه، برنامه‌نویس فقط روی مقداری که از تابع برگردونده میشه تمرکز کنه.

اجزاء ریداکس

مفاهیمی که تو ریداکس وجو دارن، ممکنه پیچیده بنظر برسن، خصوصا که فقط به شکل تئوری باشن و هنوز لمسشون نکرده باشیم. اما یادتون باشه که ریداکس یک کتابخونه کوچیکه (۲ کیلوبایت حجمشه!) و از سه جز اصلی ساخته شده: اکشن‌ها (Actions)، استور (Store) و ریدوسرها (Reducers).

Actions

به طور خلاصه، اکشن‌ها همون رویداد‌ها (Events) هستند. اکشن‌ها اطلاعات رو از سمت اپلیکیشن به اِستور ارسال میکنن (استور، محل ذخیره استیت‌هاست). استور، اطلاعات رو فقط از اکشن‌ها دریافت میکنه. اکشن‌ها، کلا از یک شئ ساخته شدن که یک نوع (type) داره و یک ظرفیت ترابری!! (Payload) که اطلاعات رو تو خودش نگه میداره.

{
    type: DATA_LOADED,
    payload: {title: ‘Title baraye yek post’, text: ‘Ye matne kheyli tulani’}
}

اکشن‌ها توسط سازنده‌های اکشن (Action creators) ساخته میشن که در واقع توابعی هستند که وظیفشون برگردوندن اکشن مربوطست:

function dataLoaded(result) {
    return {
        type: DATA_LOADED,
        payload: result
    }
}

برای اینکه اکشن‌ها رو اجرا کنید کافیه که اونها رو صدا بزنید (Dispatch)، مثلا:


dispatch(dataLoaded(result_from_api));

Reducers

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

function reducer (state = {}, action) {
  switch (action.type) {
    case DATA_LOADING:
      return {
        ...state,
        posts_are_loading: true;
        posts: [];
      };
    case DATA_LOADED:
      return {
        ...state,
        posts_are_loading: false;
        posts: action.payload;
      };
    default:
      return state;
  }
};

زیاد نگران نشید، بریم جلوتر بیشتر توضیح میدم، این مثال (و شاید مثال‌های دیگه) هنوز غیر قابل لمس هستن، تمرین میکنیم روشون.

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

Store

استور، شئیی هست که که تمام استیت‌های اپلیکیشن رو داخل خودش نگه‌داری می‌کنه و چندتا تابع کمکی هم به شما میده تا بتونید به استیت‌ها دسترسی داشته باشید، اونها رو صدا بزنید و کارهای دیگه‌ای روشون انجام بدید. تمام استیت‌های اپلیکیشن توسط یک استور نمایش داده میشن. هر اکشنی هم یک استیت جدید رو توسط ریدوسر برمیگردونه. این باعث میشه که ریداکس خیلی ساده باشه.

import { createStore } from ‘redux’;
let store = createStore(rootReducer);
let filterInfo = {posts: 10;, category: "Tech"};
store.dispatch(fetchPosts(filterInfo));

ریداکس تانک (Redux Thunk)

ریداکس‌تانک به اصطلاح یک ابزار میانی یا middleware برای ریداکس هست. میدِل‌وِیرها (middleware) در واقع توابعی هستند، که به درخواست و پاسخ‌های بین سرور و کاربر دسترسی دارن و میتونن اونارو دستکاری کنن. برای ساده‌تر کردن موضوع، فرض کنید که سایت شما مسیرها یا آدرس‌های مختلفی داره. مثل صفحه اصلی، صفحه ورود، صفحه محصولات و سبد خرید. کاربر شما میتونه صفحه‌های اصلی، محصولات و ورود رو، بدون لاگین‌کردن به سایت ببینه. اما اگر بخواد به پروفایلش و سبد خرید دست‌رسی داشته باشه، باید حتما توی سایت لاگین کنه.

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

کاری که ریداکس‌تانک انجام میده، اینه که به شما اجازه میده، تو اکشن‌کریتورها، به جای یک اکشن (یک شئ)، یک تابع رو برگردونید!

نوشتن یک اپلیکیشن کامل، برای تمرین

حالا که با خیلی از اصول و مفاهیم آشنا شدید، وقتش رسیده که خیلی جدی‌تر دست به کار بشیم و یه اپلیکیشن با هم بنویسیم. سعی میکنم که تا حد امکان از همه‌ی چیزهایی که تا اینجا گفته شده استفاده کنم و خط به خط (تا حد امکان) همه‌چیز رو توضیح بدم. قرار هست یکمی بریم سراغ ایلان ماسک و با ای‌پی‌آی‌های SpaceX بازی کنیم، برناممون باید اطلاعات آخرین پرتاپ موشک‌های اِسپِیس‌اِکس رو بهمون گزارش بده و این امکان رو داشته باشه تا بتونیم اونارو بر اساس سال، فیلتر کنیم. اپلیکیشنی که می‌نویسیم از ری‌اکت، ریداکس و ای‌اس۶ استفاده خواهد کرد. هدف از نوشتن این برنامه، اینه که شما هم کمی تایپ کنید و دستتون رو گرم نگه‌دارید، اگر نخواستید دستتون رو به کد آلوده کنید (ಠ_ಠ) این پروژه رو توی گیت‌هابم هم میذارم و میتونید هر استیج رو از شاخه‌های (Branch) مربوط به هر استیج دانلود کنید.

راه‌اندازی

این مرحله ساده‌ترین مرحله ماست و فقط کافیه پروژه رو نصب و راه‌اندازی کنید (لینک گیت‌هاب):

$ create-react-app our-awesome-app
$ cd our-awesome-app
$ npm install
$ npm install redux react-redux isomorphic-fetch

اولین گام بعد از نصب، اینه که محتوای فایل‌های App.css و Index.css رو پاک کنیم. بعد فایل‌های App.test.js و logo.svg رو حذف می‌کنیم. در نهایت هم محتوای فایل  App.js رو تغییر میدیم:

import React, { Component } from 'react';
import './App.css';

class App extends Component {
  render() {
    return (
      <div>Starting Point</div>
    );
  }
}

export default App;

ریداکس

تو این مرحله، قرار هست ریداکس رو تنظیم کنیم (لینک گیت‌هاب). کافیه فقط فایل src/redux.js رو بسازید. از اونجایی که ساختار پروژمون خیلی سادست، نیازی نیست پوشه‌ها و فایل‌ها رو تو در تو بسازیم. سعی میکنیم همه‌چیز رو تا حد امکان ساده نگه‌داریم. گرچه، تو بخش نکست‌جی‌اس قرار هست که ساختار پیچیده‌تری رو بسازیم.

بعد از اینکه فایل src/redux.js رو ساختید، این‌ها رو بهش اضافه کنید:

import {
    combineReducers,
    createStore,
} from 'redux';

// Constants
const DATA_LOADING = "DATA_IS_LOADING"
const DATA_RESULT = "DATA_THE_RESULT"

// Initial State
const initialState = {
    loading: false,
    items: []
}

// Actions
export function fetchData(){
    return {
        type: DATA_LOADING,
    }
};

export function fetchDataDone (result) {
    return {
        type: DATA_RESULT,
        payload: result
    }
};

// Reducer
export function theReducer(state = initialState, action= {}) {
    switch (action.type) {
        case DATA_LOADING:
            return {
                ...state,
                loading: true,
                items: []
            };
        case DATA_RESULT:
            return {
                ...state,
                loading: false,
                items: action.payload
            };
        default:
            return state;
    }
};

export const reducers = combineReducers({
    theReducer,
});

// Store
export function configureStore(initialState) {
    const store = createStore(reducers, initialState);
    return store;
};

export const store = configureStore();

بریم سراغ توضیحات:

  1. اول از همه، دو تا از مهم‌ترین توابع ریداکس رو فراخوانی کردیم: combineReducers و createStore
    1. combineReducers: هرچی بیشتر اپلیکیشن ما بزرگ‌تر بشه، به مدیریت دقیق‌تر و کامل‌تر ریدوسرهامون بیشتر احتیاج پیدا میکنیم. برای همین اونها رو تو فایل‌های مختلف تقسیم میکنیم. و در نهایت همه رو با کمک این تابع به هم بچسبونیم.
    2. createStore: که برای ما کل استور رو میسازه
  2. برای مدیریت بهتر اکشن‌ها و خصوصا انواعشون، معمولا مقادیر ثابتی رو تعریف میکنیم، که مجبور نباشیم هر بار، اونها رو از اول بنویسیم، مزیت دیگه‌ی اینکار، این هست که اگر زمانی بخوایم تایپ رو عوض کنیم، فقط یکبار مقدارش رو تغییر میدیم و برنامه همچنان به کارش ادامه میده.
    اینجا، دو ثابت رو تعریف کردم به نام‌های DATA_LOADING و DATA_RESULT که اولی، قرار هست وضعیت بارگذاری رو و دومی پاسخ از سمت سرور رو مدیریت کنن.
  3. initialState که حالت اولیه اپلیکیشن رو در خودش نگه می‌داره. مشخصا، در حالت عادی بارگذاری انجام نمیشه (loading: false) و چیزی هم برای نمایش نداریم (items: []). از این مقدار بعدا قرار هست در چندجا استفاده بشه
  4. اکشن‌‌کریتورها رو ساختم، این اکشن‌کریتورها (Action Creators) قرار هست بعدا تو اپلیکیشن صدا زده بشن (Dispatch):
    1. fetchData که موظف به بازگردوندن اکشن مرتبط با حالت بارگذاریه. تو این حالت، ما چیزی برای نمایش به کاربر نداریم، برای همین هم payload رو تعریف نکردیم.
    2. fetchDataDone(result) که وضعیت مربوط به اتمام بارگذاری رو مدیریت میکنه. تو این حالت، اپلیکیشن اطلاعات رو از سرور دریافت کرده، و باید اون رو توی استیت نگه‌داره. payload: result همین کار رو انجام میده.
  5. ریدوسر رو ساختم، اسمش رو هم theReducer انتخاب کردم. این ساختار، ساختار پیش‌فرض هر ریدوسری هست که یک حالت اولیه initialState (که بالاتر ساخته بودم) و یک اکشن رو به عنوان پارامتر دریافت میکنه. نکته مهم اینه که گاهی ممکن هست به هر دلیلی، اکشنی خونده نشه، برای همین هم مقدار پیش‌فرض آرگومان action رو خالی در نظر گرفتم. action = {}
    1. این ریدوسر، باید بین حالت‌های مختلف، حالتی که صدا زده شده رو انتخاب و استیت مربوط بهش رو تغییر بده. (بهترین مکان برای استفاده از مکانیزم کنترل switch)
    2. حالت اول، زمانی هست که اطلاعات میخوان بارگذاری بشن، در واقع زمانی هست که اپلیکیشن با سرور ارتباط برقرار میکنه و ما میخوایم به کاربر Loading نشون بدیم. از طرفی، چون ریدوسر‌ها باید استیت جدیدی برگردونن، و ما همزمان باید استیت قبلی رو حفظ کنیم (تا همه‌ی حالت‌ها از بین نرن)، یک نمونه از استیت قبلی رو میسازیم ...initialState و بعد، چیزهایی که باید توش تغییر کنند رو عوض میکنیم. loading: true چون در حال بارگذاری هستیم و items: [] که اطلاعات از پیش روی کاربر برداریم تا اطلاعات جدید رو بذاریم.
    3. زمانی که اطلاعات دریافت شد، و بارگذاری به اتمام رسید، با حفظ استیت قبلی، استیت جدید رو ساختیم.
    4. اگر اکشن یا تایپش تعریف نشده بودند، حالت اولیه رو برمیگردونیم.
  6. با اینکه اینجا یکدونه ریدوسر بیشتر ندارم، باز هم از combineReducers استفاده کردم و تمام ریدوسر‌ها رو بهش دادم، تا همه رو یکی کنه و یک ریدوسر کلی بهم بده.
  7. تابعی رو ساختم به اسم configureStore تا داخلش، استور رو بسازم و اگر کاری لازم بود همونجا انجامش بدم. این تابع، یک استیت اولیه میگیره که اسمش رو initialState گذاشتم که نباید با initialState خطوط بالاتر اشتباه گرفته بشه. این یک پارامتر جداگانست
    تابع createStore دو تا پارامتر رو دریافت میکنه. اولی، یک ریدوسر (که بالتر همه رو یکی کردیم) و دومی استیت اولیه (که باز هم بالاتر تعریف کرده بودیم.)
    استورمون، که حالا مجموعه‌ای از ریدوسرها و یک استیت اولیست ساخته شد.
  8. استور ساخته شده رو از ماژولمون خارح میکنم تا بعدا ازش استفاده بشه.

بعد از اینکه ریداکس رو ساختیم، حالا باید کل برنامه‌رو داخل یک کامپوننت والد قرار بدیم. وظیفه‌ی این کامپوننت والد، اینه که استور رو بین تمام کامپوننت‌های فرزند منتقل کنه. برای اینکار، خود ریداکس کامپوننتی رو به اسم <Provider/> ساخته که به عنوان یکی از پراپ‌هاش (Prop) استوری که ساخته شده رو دریافت میکنه. این استور بین تمام کامپوننت‌های فرزند منتقل میشه و ما میتونیم ازش استفاده کنیم. برای اینکار، کافیه تو فایل src/index.js تغییرات زیر رو ایجاد کنید:

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';

import { Provider } from 'react-redux';
import { store } from './redux';

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('root')
);
registerServiceWorker();

اتفاقی که افتاده، اینه کا ما کامپوننت Provider رو از کتابخونه react-redux فراخوانی کردیم. بعد استوری که ساخته بودیم رو (فایل src/redux.js خط ۶۰) رو از فایل src/redux.js فراخوانی کردیم و در نهایت کامپوننت Provider رو دور کامپوننت App پیچیدیم (Wrapping) و پراپ استور رو بهش پاس دادیم.

ساخت کامپوننت‌ها و استایل‌ها

این مرحله یکمی طولانی‌تر هست و پیشنهاد میکنم که کد رو از گیتهاب بگیرید. اول از همه، یک فریم‌ورک برای استایل‌های CSS اضافه میکنم.، getbootstrap.com انتخاب خوبی خواهد بود. تگ <link/> مربوط بهش رو به public/index.html اضافه می‌کنم و عنوان اپلیکیشن رو هم عوض میکنم.

...
<title>SpaceX for us!</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous"/>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
...

حالا، داخل src/index.css استایل زیر رو اضافه میکنم:

body {
    padding: 1em;
}

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

 filter.js

خب، فایل src/components/filter.js رو بسازید و کدهای زیر رو بهش اضافه کنید:

import React from 'react';
import { connect } from 'react-redux';
import { fetchData, fetchDataDone } from "../redux";

class Filter extends React.Component {

    constructor(props) {
        super(props);

        this.state = {
            text: ""
        }
    }

    handleFilterChange(e) {
        clearTimeout(this.timer);
        const text = e.target.value;

        this.timer = setTimeout(() => {
            const { fetchData, fetchDataDone } = this.props;
            this.setState({
                text
            })

            fetchData();

            fetch(`https://api.spacexdata.com/v2/launches?launch_year=${text}`)
                .then(res => {
                    res.json()
                        .then(data => fetchDataDone(data))
                })
                .catch(err => {
                    console.log(err)
                })
        }, 800)
    }

    render() {
        return (
            <div className="jumbotron">
                <h1 className="display-4">Hello, world!</h1>
                <p className="lead">This is a simple application, which fetches data from SpaceX APIs.</p>
                <hr className="my-4" />
                <p>SpaceX has gained worldwide attention for a series of historic milestones. It is the only private company ever to return a spacecraft from low-Earth orbit, which it first accomplished in December 2010. The company made history again in May 2012 when its Dragon spacecraft delivered cargo to and from the International Space Station — a challenging feat previously accomplished only by governments. Since then Dragon has delivered cargo to and from the space station multiple times, providing regular cargo resupply missions for NASA.</p>
                <div class="form-group">
                    <label for="filter">Filter per year:</label>
                    <input classId="filter" className="form-control" placeholder="Enter a year like 2018" type="text" onChange={this.handleFilterChange.bind(this)} />
                    <small hidden={!this.props.loading} id="filterHelp" class="form-text text-muted">Fetching all {this.state.text} launches...</small>
                </div>
            </div>
        );
    }
}

const mapStateToProps = (state) => ({
    loading: state.theReducer.loading
})

const mapDispatchToProps = {
    fetchData,
    fetchDataDone
}

const FilterContainer = connect(
    mapStateToProps,
    mapDispatchToProps
)(Filter)

export default FilterContainer;
  1. خط اول مثل همیشه از کلاس ری‌اکت استفاده کردم، خط دوم، connect() رو از react-redux فراخوانی کردم. تابع connect(mapStateToProps, mapDispatchToProps) کامپوننت ری‌اکت رو به استور ریداکس متصل میکنه. این تابع تغییر خاصی رو روی کامپوننت اعمال نمی‌کنه و صرفا یه کامپوننت متصل به استور رو برمیگردونه.
    تابع connect() چهارتا پارامتر رو دریافت میکنه که من اینجا دوتا از مهمترین‌ها رو توضیح میدم:
    1. آرگومان اول mapStateToProps(state, [ownProps]): stateProps که خودش یک تابع هست و به طور خلاصه، تمام استیت‌ها و حالت‌هایی که قرار هست به عنوان پراپ (Prop) توی کامپوننت استفاده بشن رو به کامپوننت اضافه میکنه. این تابع حتما باید یک شئ برگردونه و اگر قرار نبود کامپوننت از استیتی داخل استور استفاده کنه، کافیه به جاش null تعریف کنید.
    2. آرگومان دوم mapDispatchToProps(dispatch, [ownProps]): dispatchProps که یک شئ یا تابع رو برمیگردونه. اگر اون رو به شکل یک شئ تعریف کنیم، تمام توابعی که داخل این شئ وجود دارن، به عنوان سازنده اکشن‌ها (Action Creators) به پراپ‌های کامپوننت اضافه میشن که دور تابع dispatch پیچیده شدن (Wrapped) که میشه مستقیم اجراشون کرد و نیازی به استفاده از dispatch ندارن.
  2. اکشن کریتورهای fetchData و fetchDataDone رو که تو فایل ریداکس ساخته بودیم، اینجا فراخوانی کردم. این دو تابع باید بعدا از طریق connect() (پارامتر دوم) به کامپوننت متصل بشن.
  3. زمانی که کلاس برای اولین‌بار ساخته میشه، حالت اولیه برای فیلتر این کامپوننت خالیه چون در واقع کاربر چیزی رو وارد نکرده هنوز. به همین خاطر موقع تعریف استیت این کامپوننت (فقط این کامپوننت و به استور کاری نداره) رو به شکل this.state = { text: "" } که text همون فیلتریه که کاربر وارد میکنه، تعریف کردم.
  4. handleFilterChange(e) قرار هست تغییرات فیلتر رو مدیریت کنه (زمانی که کاربر چیزی رو وارد میکنه که پایین‌تر بهش میرسیم). آرگومان اولی که دریافت کرده، همون شئی هست که که تغییرات روش انجام میشه (یک <input /> ساختم براش)
    1. تو طرحی که من تو ذهنم داشتم، قرار هست کاربر فیلتر رو داخل یک <input /> وارد کنه و زمانی که از نوشتنش تموم شد، من درخواست رو برای سرور بفرستم. کاری که انجام دادم، این بوده که یک تایمر ساختم با تابع setTimeout و کد رو داخلش نوشتم و ۸۰۰ میلی‌ثانیه تاخیر بهش دادم. یعنی ۸۰۰میلی‌ثانیه بعد از ورود هر کاراکتر با کیبورد.
    2. اگر تایمر رو همینطور نگه میداشتم، هربار که کاربر دکمه‌ای رو روی کیبور میزد، با اختلاف ۸۰۰‌میلی‌ثانیه‌ای درخواست به سرور ارسال میشد. اگر کاربر فقط ۴تا کاراکتر میزد، ۴تا درخواست به سرور ارسال میشد و این سرور رو شلوغ میکرد. برای همین هم، هر زمان که کاربر کاراکتری رو وارد میکنه، من تایمر قبلی رو با استفاده از clearTimeout(this.timer); از بین میبرم و هربار یک تایمر جدید میسازم.
    3. const text = e.target.value; همون نوشته‌ای هست که کاربر داخل <input /> وارد کرده.
    4. داخل تایمر، اکشن‌های fetchData و fetchDataDone رو از داخل پراپ‌های کامپوننت درآوردم. اینکار کمک می‌کنه، به جای نوشتن this.props.fetchData فقط fetchData رو بنویسم.
    5. استیت جدید رو به شکل this.setState({text}) تعریف کردم. میتونستم بجاش this.setState({text: text}) رو بنویسم، اما این کوتاه نوشتن، از ویژگی‌های ای‌اس۶ هست که ازش استفاده کردم.
    6. اکشن fetchData() رو صدا زدم. اینجا، با توجه به فایل src/redux.js:33 (یعنی فایل خط ۳۳) loading انجام میشه و items خالی. و تمام کامپوننت‌های به این استیت دسترسی خواهند داشت.
    7. آدرس API رو به فچ (fetch) دادم که در حالت عادی، درخواست GET ارسال میکنه، و همینطور تو آدرس، بهش فیلتر رو هم اضافه کردم. بعد از دریافت پاسخ با پرامس، پاسخ رو به json تبدیل کردم و در نهایت پاسخ تبدیل شده رو تو آرگومان‌های fetchData() صدا زدم. میتونید فایل src/redux.js:23 و src/redux.js:39 رو ببینید. تو خط ۳۹، دیگه فرایند بارگذاری loading انجام نمیشه، و items با دیتای دریافت شده پر میشه. حالا تمام کامپوننت‌‌ها به items دسترسی دارن.
    8. در نهایت ارورها رو چک کردم و ۸۰۰میلی‌ثانیه به تایمر زمان دادم (۸۰۰‌میلی‌ثانیه بعد از اینکه کاربر آخرین کاراکتر رو وارد کرد)
  5. موقع رندر، یه کامپوننت ساختم که یه المان معمولی از Bootstrap به حساب میاد. مهم‌ترین اتفاقات تو خط ۴۷ و ۴۸ رخ میدن:
    1. المان input موقع تغییر کردن یا وارد کردن چیزی توسط کاربر onChange تابع handleFilterChange رو صدا میزنه. اما چون صرف صدا زدن کافی نیست و باید اطلاعات المانی که این تابع رو صدا زده هم ارسال کنم (تا بتونم ورودی رو بگیرم)، this رو به تابع متصل یا بایند (Bind) کردم.
    2. یک نوشته کوچیک رو زیر input گذاشتم که موقع بارگذاری (hidden={!this.props.loading}) نمایش داده میشه. همون loading که توی استور هست.
  6. تو خط ۵۵، تابعی رو ساختم به اسم mapStateToProps که بالاتر توضیح داده بودم. داخل این تابع، استیت‌هایی که لازم داشتم این کامپوننت ازشون استفاده کنه رو قرار دادم. بهش گفتم، این تابع باید به loading دسترسی داشته باشه که این متغیر رو میشه از state.theReducer.loading پیدا کرد. این استیت همونیه که توی استور وجود داره و با this.state فرق داره.
  7. به همین شکل، تو خط ۵۹ شئی رو ساختم به اسم mapDispatchToProps که قرار هست تمام اکشن کریتورهایی که لازم دارم رو به این کامپوننت وصل کنه.
  8. در نهایت یه کامپوننت جدید رو ساختم با کمک connect() به اسم FilterContainer که در حقیقت قرار هست همون کامپوننت Filterای باشه که به استور متصل شده. اینجا یک نکته هست که باید بگم
    connect()() دوتا پرانتز داره، تو پرانتز اول، آرگومان‌های خودش رو دادم کا بالاتر گفته شده. اما ماجرای پرانتز دوم اینه: تابع connect() خودش یک تابع رو برمیگردونه، و من بهش گفتم، به عنوان آرگومان اول این تابع جدید، از کامپوننت Filter استفاده کنه. این یک مدل نوشتن در ای‌اس۶ هست و کم کم باهاش آشنا خواهید شد.

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

cards.js

کامپوننت بعدی، قرار هست به شکل یک کامپوننت والد عمل کنه. به این شکل که آیتم‌ها رو از استور میخونه و هرکدوم رو جداگانه تو کامپوننت فرزند رندر میکنه. فایل src/components/cards.js رو بسازید . کد زیر رو بهش اضافه کنید

import React from 'react';
import { connect } from 'react-redux';
import Card from './card';

class Cards extends React.Component {

    render() {
        const { items } = this.props;

        return (
            <div className="card-columns">
                {
                    items.map((item) => {
                        return (
                            <Card item={item} />
                        )
                    })
                }
            </div>
        );
    }
}

const mapStateToProps = (state) => ({
    items: state.theReducer.items
})

const CardsContainer = connect(
    mapStateToProps,
    null
)(Cards)

export default CardsContainer;
  1. کلاس‌های لازم رو فراخوانی کردم. و کامپوننت فرزند رو (که قدم بعدیمون هست) بهش اضافه کردم.
  2. تو تابع رندر، از const { items } = this.props; استفاده کردم تا هربار مجبور نباشم بنویسم this.props.items
  3. یک div ساختم که کلاسش رو از بوت‌استرپ برداشتم، و داخلش گفتم، با استفاده از تابع map() هر کدوم از آیتم‌ها رو بگیره، اون رو داخل پراپِ کامپوننت Card قرار بده، و کامپوننت Card رو به Cards اضافه کنه.
  4. تابع mapStateToProps هم که برای این کامپوننت مشخصه. اما دیگه شئ mapDispatchToProps رو لازم ندارم، چون این کامپوننت قرار نیست چیزی رو از استور صدا بزنه. برای همین، تو خط بعدی، زمانی که آرگومان دوم رو به connect() پاس دادم، به جای mapDispatchToProps، null تعریف کردم.

card.js

حالا نوبت میرسه به آخرین کامپوننت، که همون کامپوننت فرزند هست، فایل src/componenets/card.js رو بسازید و کد زیر رو توش قرار بدید

import React from 'react';

class Card extends React.Component {

    render() {
        const {item} = this.props;
        return (
            <div className="card">
                <div className="card-body">
                    <h5 className="card-title">{item.rocket.rocket_name}</h5>
                    <h6 className="card-subtitle mb-2 text-muted">{item.rocket.rocket_type}</h6>
                    <p className="card-text">{item.details}</p>
                    <a target="_blank" href={item.links.article_link} className="card-link">Article</a>
                    <a target="_blank" href={item.links.video_link} className="card-link">Watch Video</a>
                </div>
            </div>
        );
    }
}

export default Card;
  1. این کامپوننت خیلی سادست! آیتم رو از پراپ‌ها گرفتم، و مقدارش رو (که از api میاد) داخل هر المان گذاشتم.
  2. این المان، توی بوت استرپ تعریف شده و من صرفا از بوت‌استرپ استفاده کردم.
  3. این المان، هیچ نیازی به دسترسی به استور نداره، خودش هم هیچ استیتی نداره. کارش فقط نمایش دادن هست.

خب کارمون تموم شد. میتونید یه تست از پروژتون بگیرید و ببینید که چطور شده.

NextJS

تا اینجا اومدیم و با ری‌اکت آشنا شدیم. ریداکس رو دیدیم و کمی هم باهاش کار کردم و فهمیدیم که یک وب‌سایت SPA چطور کار میکنه. اما کار ما به اینجا ختم نمیشه و هنوز یک قدم بزرگ دیگه برای برداشتن مونده! قرار هست با NextJS آشنا بشیم. اول یکمی تئوری راجع بهش داشته باشیم:

تکست‌جی‌اس (NextJS) یک فریم‌ورک مبتنی بر ری‌اکته، که مهمترین ویژگیش اضافه کردن اس‌اس‌آر (SSR) با همون Server Side Rendering به وب‌اپلیکیشن‌های ماست. یکی دیگه از کارهای نکست‌جی‌اس، اجازه صفحه‌بندی، فقط با ساخت فایل‌های جاوا‌اسکریپته، شما فایلتون رو میسازید و اون رو تو پوشه page قرار میدید و نکست اون رو اتوماتیک به صفحه تبدیل میکنه.

نکست یک کامپوننت <Link /> هم داره که کمک میکنه تا صفحه‌ها رو لود کنید و بین صفحه ارتباط برقرار کنید. 

برای شروع به کار با نکست، کافیه نصاب نکست رو نصب کنید:

$ npm install -g create-next-app

$ create-next-app my-app
$ cd my-app/
$ npm run dev

نکست بر پایه ری‌اکت ساخته شده، برای همین دقیقا چیزهایی که تو ری‌اکت وجود دارن، تو نکست هم هستن. تمام قواعد ری‌اکت شامل نکست هم میشن اما با کمی تفاوت.

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

نصب و راه‌اندازی پروژه

پروژه قرار هست که از express, isomorphic-fetch, moment-jalaali, next-redux-wrapper, react-redux, react-router, react-router-redux, redux, redux-thunk استفاده کنه. (دریافت کد این مرحله از گیت‌هاب)

$ npm install express isomorphic-fetch  moment-jalaali  next-redux-wrapper react-redux react-router react-router-redux redux redux-thunk

بعد، محتوای پوشه‌های components و pages رو خالی کنید. حالا آماده هستیم تا قدم بعدی رو شروع کنیم.

کانفیگ‌ها

اپلیکیشن ما قرار هست که از منابع مختلفی برای کار استفاده کنه. مثلا یکی از کارهایی که قرار هست انحام بشه، اینه که می‌خوایم وقتی از تانک (thunk) استفاده میکنیم، بتونیم ای‌پی‌آی‌ها رو صدا بزنیم و اون‌ها رو یک‌جا پردازش کنیم. بدون اینکه مجبور باشیم تو هر فایل، isomorphic-fetch رو فراخوانی کنیم. لازم هست که بگم، ممکنه شما تو هر پروژه، به کانفیگ‌های مختلفی برخورد کنید. چیزی که اینجا میسازم، کانفیگ‌هایی هستند که من به صورت عمومی استفاده میکنم و نیازهای من رو پاسخ میده. این کانفیگ‌ها الزاما بهترین روش یا (Best Practice) نیستن!

اول از همه، پوشه resources رو بسازید. تمام کانفیگ‌ها و ماژول‌ها رو اینجا قرار میدیم. (دریافت کد این مرحله از گیت‌هاب)

config.js

فایل resources/config.js قرار هست اطلاعات کلی و کانفیگ‌های کلی اپلیکیشن رو تو خودش نگه‌داره. این فایل، یک شئ جاوا‌اسکریپت سادست:

const config = {
    "api": {
        "path": "https://api.github.com"
    }
};

export default config;

API

کلاسی رو اینجا میسازیم، که در واقع قرار هست دور isomorphic-fetch پیچیده یا wrapp و بعدا، جزئی از پارامترهای ریداکس تانک بشه. فایل resources/apiClient.js رو با محتوای زیر بسازید:

import config from "./config";
import fetch from "isomorphic-fetch";

const methods = ["get", "post", "put", "patch", "del"];

function formatUrl(path) {
	const adjustedPath = path[0] !== "/" ? "/" + path : path;
	return config.api.path + adjustedPath;
}

function checkStatus(response) {
	if (response.status >= 200 && response.status < 300) {
		return response;
	}
	
	return response.json().then(json => Promise.reject(json));
}

function parseJSON(response) {
	return response.json();
}

function fetchCreator(method) {
	return (url, {data, ...options} = {}) => {
		const fetchOptions          = options;
		fetchOptions.headers        = fetchOptions.headers || {};
		fetchOptions.headers.Accept = "application/json";
		
		if (data) {
			fetchOptions.body                    = JSON.stringify(data);
			fetchOptions.headers["Content-Type"] = "application/json";
		}
		
		fetchOptions.method = method;
		
		return fetch(formatUrl(url), fetchOptions)
			.then(checkStatus)
			.then(parseJSON);
	};
}

export default class ApiClient {
	constructor() {
		methods.forEach((method) => {
			this[method] = fetchCreator(method);
		});
	}
	
	empty() {
	}
}

توضیح در مورد این فایل:

  1. این فایل از کانفیگ‌ها، و همچنین isomorphic-fetch برای تبادل اطلاعات استفاده میکنه.
  2. تو خط ۴، متد‌هایی که فکر میکنم لازم هست برنامم ازشون استفاده کنه رو تعریف کردم. توجه کنید که برانامه‌ما اینجا فقط به get و احتمالا post نیاز داره و میشه مابقیشون رو حذف کرد.
  3. تابع formatUrl که وظیفش ساختن یک آدرس قابل فهم، با استفاده از آدرس اصلی که تو کانفیگ تعریف کردیم و یک پارامتر، برای ارسال به سرور هست.
  4. تابع checkStatus که موظف به کنترل کردن وضعیت پاسخ هست. به این شکل که کنتارل میکنه آیا وضعیت پاسخ تو بازه 200 قرار داره یا نه. بازه ۲۰۰ تو استانداردهای http به عنوان پاسخ درست درنظر گرفته میشه. اگر وضعیت پاسخ عددی غیر از بازه ۲۰۰ بود، پرامس رو رد و یک پرامس برگشت خورده رو برمی‌گردونه.
  5. parseJson، هر آرگومانی که بهش داده بشه رو، تبدیل به json میکنه که اون رو به شکل یک پرامس برمی‌گردونه.
  6. تابع fetchCreator، باید یک نوعی از درخواست رو، بر اساس متدی که به عنوان پارامتر بهش میدیم بسازه. مثلا headerهایی که لازم هست رو تعریف کنه و زمانی که درخواست رو ارسال کرد، پردازش‌های لازم رو روی پاسخ انجام بده.
  7. به حالت پیشفرض هم، کلاس ApiClient رو خارج کردم و بهش گفتم که این کلاس، متدهایی، به ازای متدهای درخواست من خواهد داشت.

ریداکس تانک

تا اینجا، کانفیگ اصلی پروژه رو انجام دادیم. تو این بخش، قرار هست که بریم سراغ تنظیمات ریداکس‌تانک و استور و تمام زیر مجموعه‌هاش رو بسازیم تا استور برنامه و استیت‌های لازم رو به طور کامل داشته باشیم. این دو بخش بعدی، قرار هست که تو پوشه redux باشن. (دریافت کد این مرحله از گیت‌هاب)

store.js

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

فایل redux/store.js رو با محتویات زیر بسازید:

import { applyMiddleware, createStore } from "redux";
import thunk from "redux-thunk";
import reducers from "./reducers";
import apiClient from "../resources/apiClient";

export default function create(client, data) {
	let middleware;
	const thunkMiddleware = thunk.withExtraArgument(client);
	middleware = applyMiddleware(thunkMiddleware);
	return createStore(reducers, data, middleware);
}

export const initStore = (initialState) => {
	const api = new apiClient();
	return create(api, initialState)
};

بریم سراغ توضیح:

  1. بین خطوط یک تا چهار، تمام ماژول‌ها و کتاب‌خونه‌های مورد نیاز رو فراخوانی کردم. خط یک و دو، در واقع توابعی هستند که خود ریداکس بهشون نیاز داره. اما خط سوم، که هنوز بهش نرسیدیم و تو مرحله بعدی خواهیم ساختش، تمام ریدوسرهامون رو توی خودش نگه میداره. ما داخل این فایل، آدرس تمام ریدوسهامون رو میدیم و فایل store.js همه‌رو برای ما نگه‌داری میکنه. تو خط چهارم هم، کلاس ای‌پی‌آیمون رو گرفتم. واضحع که میخوام ازش توی استو استفاده کنم.
  2. بین خط ۶ تا ۱۱، یک تابع دلخواه به اسم create رو ساختم. این تابع قرار هست استور رو برای من بسازه، به همراه میدل‌ویرهایی که لازم دارم.
    مهمترین نکته تو خط ۸ هست، که ثابتی رو به اسم thunkMiddleware تعریف کردم، که خود تانک رو، به همراه یک آرگومان اضافی به عنوان میدل‌ویر به من برمیگردونه.
  3. ثابت initStore هم برای من استور رو میسازه. یعنی هرجایی که صداش بزنم، برای من استور رو با حالت اولیش میسازه.

reducer.js

حالا که استور رو ساختیم، باید ریدوسر‌هامون رو هم بهش بدیم.  فایل redux/reducers.js رو با محتوای زیر بسازید:

import { combineReducers } from "redux";
import { routerReducer } from "react-router-redux";
import { reducer as formReducer } from "redux-form";

import highlights from "./modules/highlights";

export default combineReducers({
    highlights,
    form: formReducer,
    routing: routerReducer,
});
  1. یکی از ویژگی‌های ریداکس، اینه که شما میتونید چندین ریدوسر رو به هم چسبونده یه درواقع combine کنید و در نهایت همه رو داخل یک استور قرار بدید. خط ۱ تا ۳، تمامی توابعی که ریداکس بهشون نیاز داره رو فراخوانی کردم.

  2. تو خط ۵، یک ماژول ریداکس رو، که تو مرحله بعد قراره بسازیم، فراخوانی کردم و در نهایت، بین خط ۷ تا ۱۱، تنظیمات رو انجام دادم.
  3. نکته‌ی مهم اینه که، تمامی ماژول‌ها رو، باید داخل یک شی (اینجا combineReducers) قرار بدید و اون رو به صورت دیفالت، خارج کنید.

highlights.js

این فایل میتونه براتون به شکل یک استاندارد عمل کنه. بعدها تو پروژه‌های خودتون میتونید چندین بار از این ساختار استفاده کنید و استیت رو مدیریت کنید. من شخصا، تمامی ماژول‌های ریداکسم رو، داخل پوشه redux/modules قرار میدم. داخل این پوشه میتونید هرطور که راحت هستید فایل‌ها رو مدیریت کنید، چون در نهایت باید اون‌ها رو تو redux/reducers.js قرار بدید و آدرسشون خیلی حائز اهمیت نمیشن. برای شروع، فایل redux/modules/highlights.js رو با این محتوا بسازید:

// Action Types
export const REPOS_LOAD    = "app/repos/load";
export const REPOS_FAILED  = "app/repos/failed";
export const REPOS_SUCCESS = "app/repos/success";

// Initial state
const initialState = {
	repos_loaded : false,
	repos_loading: false,
	repos        : null,
};

// Reducer
export default function reducer(state = initialState, action = {}) {
	switch (action.type) {
		case REPOS_LOAD:
			return {
				...state,
				repos_loaded : false,
				repos_loading: true,
				repos        : []
			};
		case REPOS_SUCCESS:
			return {
				...state,
				repos_loaded : true,
				repos_loading: false,
				repos        : action.data
			};
		case REPOS_FAILED:
			return {
				...state,
				repos_loaded : false,
				repos_loading: false,
				repos        : action.error
			};
		default:
			return state;
	}
}

// Action creators
export function ReposLoadFailed(error) {
	return {
		type: REPOS_FAILED,
		error
	};
}

export function ReposLoadSuccess(data) {
	return {
		type: REPOS_SUCCESS,
		data
	};
}

export function ReposLoad(query) {
	return (dispatch, getState, client) => {
		
		dispatch({
			type: REPOS_LOAD
		});
		
		return client
			.get(`search/repositories?q=language:${query}&sort=stars&order=desc`)
			.then(data => {
				dispatch(ReposLoadSuccess(data));
			})
			.catch(error => {
				dispatch(ReposLoadFailed(error));
			});
	};
}

لازم هست که اینجا چند نکته رو بگم:

  • من اصولا هرچه که مطعلق به یک ماژول ریداکس هست، یعنی هرچیزی (استیت، ریدوسر، اکشن کریتور و...) که قرار هست، یک استیت مشخصی رو مدیریت کنه، توی یک فایل قرار میدم. یک کانونشن دیگه اینه که، اکشن‌تایپ‌ها، ریدوسر‌ها، اکشن کریتور‌ها و غیره رو تو فایل‌های جداگانه نگه‌داری کنیم.
  • در نظر میگیرم که هر استیتی، سه حالت اصلی داره:
    • LOAD: که به من نشون میده، آیا این استیت درحال بارگذاری هست یا نه،
    • FAILED: که میگه آیا بارگذاری استیت ناموفق بوده یا نه،
    • SUCCESS: به وضوح، میگه که آیا استیت کامل و درست بارگذاری شده یا نه.
  • استیت اولیه یا initialState همیشه به این صورته که:
    • استیت بارگذاری نشده (loaded = false)
    • استیت در حال بارگذاری نیست (loading = false)
    • استیت خالیه (state = [])

میرم سراغ توضیحات:

  1. بین خطوط ۱ تا ۳، نوع اتفاقات رو معلوم میکنم یا در واقع، اکشن‌تایپ‌هارو مشخص میکنم. یادتون باشه که بین تمام ماژول‌ها، اکشن‌تایپ‌ها باید یکتا یا unique باشن!
  2. بین خط ۷ تا ۱۱، استیت اولیه رو تعریف کردم که بالاتر راجع‌بهش توضیح داده بودم
  3. بین خط ۱۴ تا ۴۰، ریدوسر اصلی این ماژول رو، که باید به صورت دیفالت یا پیش‌فرض خارج بشه، تعریف کردم. اینجا بهش گفتم که بین اکشن‌تایپ‌هایی که براش به عنوان آرگومان میفرستم، بگرده (switch کنه) و اکشنی که مرتبط با این ماژول هست رو انتخاب، و استیتش رو بر اون اساس بروز کنه.
    مثلا زمانی که استیت درحال بارگذاری هست (اینجا، ریپازیتوری‌های گیت میخوان لود بشن):
    1. استیت بارگذاری نشده: repos_loaded: false
    2. استیت در حال بارگذاری هست: repos_loading: true
    3. نتیجه‌ای دریافت نشده و چیزی قابل نمایش نیست: repos: []
  4. اکشن‌کریتور‌ها، معمولا سه تا بیشتر نیستن ولی میشه بیشتر‌هم باشن. در مورد اکشن‌کریتور‌ها قبلا توضیح دادم که پیشنهاد میکنم اگر یادتون نیست، یه سری بهش بزنید. با این حال، بین خط ۵۷ تا ۷۳، یک اکشن‌کریتوری دارم، به اسم ReposLoad که وظیفش، بارگذاری ریپازیتوری‌هاست. اگر با دقت بهش نگاه کنید، میبینید که آرگومان سوم تابع فلشی (Arrow function) یک client تعریف شده. این کلاینت، در واقع همون کلاس apiClient هست که قبلا تعریف کرده‌بودیم. این اکشن‌کریتور، اول از همه، به این تابع یک query رو پاس میدم تا ازش برای ارسال درخواست استفاده کنم. بعد تابع اکشن REPOS_LOAD رو صدا میزنه (dispatch میکنه)، بعد، از کلاس کلاینت استفاده میکنه و یک درخواست از نوع get رو به آدرس search/repositories?sort=stars&order=desc میفرسته. اگر به نحوه نوشتن دقت کنید، متوجه میشید که از ${query} توی آدرس استفاده شده. به این روش، اصطلاحا String Templating میگن. اگر فایل resources/config.js رو نگاه کنید، میبینید که من آدرس پایه ای‌پی‌آی رو اونجا تعریف کردم. حالا، کلاس apiClient از طریق تابع formatUrl که داخلش تعریف کردم، آدرسی رو که به متد get دادم رو به این آدرس پایه میچسبونه و ازش یک آدرس کامل میسازه. در نتیجه من آدرس https://api.github.com/search/repositories?q=language:javascriptsort=stars&order=desc رو خواهم داشت. این متد get یک پرامس رو برمیگردونه، که این پرامس ۲ حالت بیشتر نداره، یا اطلاعات از منبع کامل و درست اومدن (وضعیت تو بازه 200) و یا به مشکل برخوردن (هر وضعیتی غیر از بازه 200). این وضعیت تو کلاس apiClient بین خطوط ۱۱ تا ۱۷ مدیریت میشه.
    حالا اگر اطلاعات کامل و درست دریافت بشن، اکشن ReposLoadSuccess صدا زده میشه و اگر ایرادی پیش‌اومده باشه، اکشن ReposLoadFailed.

طراحی UI

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

ساخت Base Layout

یکی از چیزهایی که تو هر سایتی لازم هست، یه لایه‌ی اصلی برای چینش المان‌هاست. در حقیقت شما ساختار اصلی سایتتون رو تعریف و در نهایت المان‌ها رو توش جاگذاری میکنید. برای اینکار، فرض میکنیم که لایه اصلی، یک کامپوننت بزرگه. در نتیجه فایل components/page.js رو با این محتوا میسازیم:

import React, { Component } from "react";
import Head from "next/head";
import Link from "next/link";

class Page extends Component {
	render() {
		return (
			<div className="container">
				<Head>
					<meta charSet='utf-8'/>
					<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
					<title>Our awesome next app</title>
					<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css"/>
				</Head>
				
                <div className="row">
                    <div className="twelve columns">
                        <h1>Our awesome next.js app</h1>
                        <h3>With redux!</h3>
                    </div>
                </div>

                { this.props.children }
			</div>
		);
	}
}

export default Page;

برای این طراحی، از Skeleton.css استفاده کردم. بریم ببینیم چه اتفاقی افتاده اینجا:

  1. کلاس Component رو از کتابخونه react به همراه دوتا کامپوننت دیگه از next به این کلاس اضافه کردم.
  2. کلاس Page رو، که خودش از کلاس Component استفاده میکنه (ارث برده؟) تعریف کردم. چینشی رو که توی ذهنم داشتم ساختم، ضمن اینکه، از کامپوننت Head داخل next استفاده کردم، تا المان‌هایی که قرار هست تو تگ head رندر بشن رو توش بذارم. همونطور که میبینید، محتوای این کامپوننت، همون‌هایی هستند که ما همیشه توی تگ head استفاده میکنیم.
  3. تو خط ۲۳، گفتم که هرچیزی که به عنوان فرزند این کامپوننت (Page) هست رو، اینجا رندر کن. یعنی، هر چیزی که بعدن، تو صفحات دیگه، بین <Page></Page> قرار بگیره، تو خط ۲۳ این فایل رندر میشه.
  4. در نهایت این کلاس رو خارج کردم (راستش هنوز به فعل صادر کردن به عنوان معنی extract عادت نکردم!)

index.js

خب، کامپوننت اصلی و نگهدارنده صفخات رو ساختیم. این همون شیوه‌ای هست که سازندگان NextJS اون رو پیشنهاد میدن برای ساختن یک لایه‌ی اصلی. در کل، یادتون باشه که ما تمام کامپوننت‌ها رو تو پوشه components نگهداری میکنیم.

حالا میریم سراغ ساخت صفحه اصلی سایتمون و دریافت اطلاعات و غیره. برای شروع فایل pages/index.js رو اگر موجود نیست بسازید و بعد، محتوای زیر رو بهش بدید:

import React, { Component } from "react";
import Page from "../components/page";
import Link from 'next/link';

import { bindActionCreators } from "redux"
import withRedux from "next-redux-wrapper";
import { initStore } from "../redux/store";


import { ReposLoad } from "../redux/modules/highlights";

class Index extends Component {

  constructor(props) {
    super(props);

    this.state = {
      query : "javascript"
    }
  }

  componentWillMount() {
    const { ReposLoad } = this.props;

    ReposLoad(this.state.query);
  }

  handleQueryChange(e) {
    this.setState({
      query: e.target.value
    })
  }

  handleOnClick() {
    const { ReposLoad } = this.props;

    ReposLoad(this.state.query);
  }

  render() {

  const { repos_loaded, repos_loading, repos } = this.props;
  const { query } = this.state;

    return (
      <Page>
        <div className="row">
          <div className="twelve columns">
            {
              repos_loading ?
                <p>Fetching data from Github...</p>
                :
                null
            }
            {
              repos_loaded ?
                <p>Data fetched from Github!</p>
                :
                null
            }
          </div>
        </div>

        <div className="row">
          <div className="twelve columns">
            <div>
              <span>I want to look for </span>

              <input style={{
                height: '38px',
                padding: '6px 10px',
                backgroundColor: '#fff',
                border: '1px solid #D1D1D1',
                borderRadius: '4px',
                boxShadow: 'none',
                boxSizing: 'border-box',
                marginRight: '5px'}} 
                value={query} 
                onChange={this.handleQueryChange.bind(this)}/>

              <button className="button" onClick={this.handleOnClick.bind(this)}>Search!</button>
            </div>
          </div>
        </div>

        <div className="row">
          <div className="twelve columns">
            <table className="u-full-width">
              <thead>
                <tr>
                  <th>Repository Name</th>
                  <th>Description</th>
                  <th>Owner</th>
                  <th>URL</th>
                </tr>
              </thead>
              <tbody>
                {
                  repos_loaded && repos.items.length > 0 ?
                    repos.items.map(item => {
                      return (
                        <tr key={item.id}>
                          <td><a href={item.homepage}>{item.name}</a></td>
                          <td>{item.description}</td>
                          <td>
                            <img  style={{verticalAlign: 'middle', marginRight: '5px'}} width="32px" src={item.owner.avatar_url}/>
                            <span>{item.owner.login}</span>
                          </td>
                          <td><a href={item.html_url}>Click here!</a></td>
                        </tr>
                      )
                    }) : null
                }
              </tbody>
            </table>
          </div>
        </div>
      </Page>
    );
  }
}

const mapStateToProps = (state) => {
  return {
    repos: state.highlights.repos,
    repos_loading: state.highlights.repos_loading,
    repos_loaded: state.highlights.repos_loaded,
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    ReposLoad: bindActionCreators(ReposLoad, dispatch)
  }
}

export default withRedux(initStore, mapStateToProps, mapDispatchToProps)(Index);

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

بریم سراغ توضیحات:

  1. بین خط ۱ تا ۳، کلاس‌های مربوطه رو فراخوانی کردم. لایه‌ی اصلی سایت رو، که همون کامپوننت Page بود اینجا وارد کردم.
  2. بین خط ۵ تا ۷، توابع مربوط به ریداکس رو فراخواندم:
    1. bindActionCreators: این تابع، وظیفش اینه که یک شئ رو، که مقادیرش اکشن‌کریتور‌ها هستند، با کلید‌هایی به همون اسم میسازه. با این تفاوت که اکشن‌کریتور‌ها اینجا به صورت اتوماتیک از dispatch استفاده میکنن. بهترین نوع استفاده از این تابع، زمانیه که شما کامپوننتی دارید، که از ریداکس خبری نداره و نمیدونه تو ریداکس چه اتفاقاتی میفته. (میتونید یه نگاهی به خط ۱۳۳ بندازید.)
    2. withRedux: یه تابع ساده یا درواقع یه wrapper مخصوص ریداکس که برای nextjs نوشته شده. (خط ۱۳۷)
    3. initStore: که کارش ساختن استور اولیست. یادتون باشه که شما در عمل باید این سه خط رو تو تمام فایل‌هاتون داشته باشید.
  3. خط ۱۰، ReposLoad رو که اکشن‌کریتور هست، از فایل redux/modules/highlights.js گرفتم. این اکشن‌کریتور تا اینجا به خودی خودش کاری رو انجام نمیده، چون باید صدا زده بشه (dispatch  بشه). دوباره یه نگاهی به خط ۱۳۳ بندازید.
  4. بین خط ۱۴ تا ۲۰، استیت اولیه رو تعریف کردم. یه query دارم، که میخوام مقداری که کاربر وارد میکنه رو توش نگه دارم. این کار بیشتر جنبه‌ی آموزشی داره و شیوه‌های بهتری هم برای انجامش هستن.
  5. بین خط ۲۲ تا ۲۶، یه Life Cycle ری‌اکت دارم که قبلا در این مورد توضیح دادم، چون پایین‌تر، استیت‌‌های ریداکس و اکشن‌کریتور‌ها رو به این کامپوننت متصل کرده بودم، حالا میتونم به همشون از راه this.props دسترسی داشته باشم. ReposLoad رو با کوئری که توی استیتم دارم صدا میزنم. در واقع، زمانی که کاربر این صفحه رو باز میکنه، ریپازیتوری‌های گیت‌هاب اتوماتیک بارگذاری میشن.
  6. بین خط ۲۸ تا ۳۲، زمانی که کاربر توی <input> چیزی مینویسه، من استیت کامپوننت، و نه استیت کل برنامه، رو تغییر میدم.
  7. بین خط ۳۴ تا ۳۸، مشابه componentWillMount وقتی کاربر روی دکمه جست‌وجو کلیک کرد، میرم و دوباره استیت رو پر میکنم.
  8. بین خط ۴۰ تا ۱۲۱، تابع render رو دارم که المان‌ها رو نمایش میده.
    1. خط ۴۲، استیت‌هایی که توی redux/modules/highlights.js تعریف شده بودند رو که بین خطوط ۱۲۳ تا ۱۲۹ به کامپوننت چسبوندم (مپ کردم!)، گرفتم تا پایین‌تر ازشون استفاده کنم.
    2. خط ۴۳ هم کوئری که توی استیت کامپوننت هست رو گرفتم.
  9. بین خطوط ۴۹ تا ۵۴ و ۵۵ تا ۶۰، چک کردم که آیا ریپو‌ها در حال بارگذاری هستند، یا بارگذاری شدند؟ و اگر آره، یک المان رو متابق با هر وضعیت نمایش دادم.
  10. تو خط ۷۹، وقتی کاربر نوشته‌ی داخل <input> رو عوض میکنه، با هربار تغییر تابع handleQueryChange صدا زده میشه که توضیحش رو تو مورد ۶ گفتم.
  11. خط ۸۱، کاربر روی دکمه کلیک میکنه و تابع handleOnClick صدا زده میشه. این تابع، کوئری کاربر رو به عنوان آرگومان به اکشن‌کریتور ReposLoad پاس میده.
  12. بین خط ۹۹ تا ۱۱۲، چک کردم که آیا ریپوها بارگذاری شدند یا نه، و آیا آیتم‌های داخل ریپوها (که از طریق ریداکس پر شدند) بیشتر از صفر هستند یا نه. اگر آره،
    هرکدوم از آیتم‌ها رو به یک ردیف جدول مپ کردم. پیشنهادم اینه که در مورد تابع مپ بیشتر بخونید. به ازای هر آیتمی که داخل پاسخ وجود داره، یک ردیف جدید ساخته میشه که محتوای هر ستونش، اطلاعاتی هستند که داخل آیتمن.
  13. بین خطوط ۱۲۳ تا ۱۲۹، استیت‌هایی که لازم داشتم اینجا توی این کامپوننت استفاده کنم رو از استور خوندم و به یک شئ به نام mapStateToProps پاس دادم.
    اینکه چرا نحوه خوندن استیت‌ها به شکل repos: state.highlights.repos شده رو، میتونید از فایل redux/reducers.js خط ۵ و ۸ پیدا کنید.
  14. بین خطوط ۱۳۱ و ۱۳۵، مشابه مورد ۱۳، اکشن‌کریتور‌هایی که لازم بوده اینجه، توی این کامپوننت ازشون استفاده بشه رو، با پیچیدن دور dispatch به mapDispatchToProps پاس دادم.
  15. در نهایت تو خط ۱۳۷، کلاس Index رو که حالا، از استور ریداکس استفاده میکنه، به نکست اکسپورت کردم! (دیگه واقعا هیچ کلمه‌ای پیدا نکردم)

 

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

ضمنا، دوست خوبم @arastuq پادکست‌های مفیدی رو برای آموزش جاوا‌اسکریپت به صورت ویدئویی ارائه میده که پیشنهاد میکنم حتما اون رو دنبال کنید.

در کنار همه‌ی اینها، حتما در مورد Virtual DOM هم بخونید. این بلاگ‌پست خیلی خوب تشریح میکنه این موضوع رو.

اگر هم سوالی داشتید میتونید به آدرس ایمیل من aien[at]saidi27[dot]com سوالتون رو ارسال کنید، و من هم در اولین فرصت سعی میکنم پاسختون رو بدم.

موفق و پیروز باشید.

ویرایش: دوم

برگردیم به بالای صفحه