آموزش ری اکت (React) — کامل و رایگان | از صفر تا صد
مقالهای که پیش رو دارید مروری جامع در مورد React است که گرچه ادعا ندارد همه موارد مربوط به این فریمورک را در خود گنجانده، اما مبانی مقدماتی مورد نیاز برای این که به یک توسعهدهنده خبره React تبدیل شوید را در اختیار شما قرار خواهد داد. این مجموعه مطلب در 8 فصل ارائه شده است که فصل نخست آن شامل مباحث زیر میشود:
- متغیرها
- تابعهای Arrow
- Rest و spread
- تخریب شیء و آرایه
- الفاظ قالبی (template literals)
- کلاسها
- Callback-ها
- promise-ها
- Async/Await
- ماژولهای ES
توجه داشته باشید که آنچه در ادامه آمده است تنها فصل اول این مجموعه مطلب را شامل میشود و برای مطالعه فصول بعدی باید به بخشهای آتی این سری مقالات مراجعه کنید.
مقدمهای بر یک کتابخانه View به نام React
در این بخش React و ماهیت آن را توضیح میدهیم.
React چیست؟
React یک کتابخانه جاوا اسکریپت است که هدف آن سادهسازی مراحل توسعه رابطهای بصری است. React از سوی فیسبوک توسعه یافته است و در سال 2013 انتشاریافته است. در واقع React زیرساخت تشکیل دهنده بسیاری اپلیکیشنهای پر استفاده از قبیل فیسبوک و اینستاگرام را تشکیل میدهد.
هدف اولیه React این بوده است ساخت یک رابط و حالت آن را در هر زمان با تقسیم کردن رابط و حالت آن در هر زمان به کاری آسان تبدیل کند.
پیش از ادامه این مبحث لازم است یادآور شویم که میتوانید ری اکت را با استفاده از مجموعه آموزش ری اکت، مقدماتی تا پیشرفته فرادرس یاد بگیرید.
چرا React چنین محبوب است؟
React به طوری طوفانی دنیای توسعه وب فرانتاند را تسخیر کرده است. برای این وضعیت چند دلیل میتوان برشمرد.
پیچیدگی آن کمتر از جایگزینهای دیگر است
در هنگامی که React انتشار یافت، Ember.js و Angular 1.x گزینههای غالب فریمورکها محسوب میشوند. هر دو این موارد چنان دستکاریهایی در کد میکنند که پورت کردن یک اپلیکیشن موجود به کار دشواری تبدیل میشود.
در مقابل React طوری طراحی شده که ادغام آن در یک پروژه از قبل موجود آسان باشد، چون از ابتدا بدین صورت طراحی شده بود که بتواند در کد از قبل موجود فیسبوک ادغام شود. ضمناً دو فریمورک دیگر بسیار سنگین بودند، در حالی که React به جای مجموعه کامل MVC صرفاً یک لایه View را عرضه میکرد.
زمانبندی عالی
در برههای از زمان نسخه دوم انگولار (Angular 2.x) از سوی گوگل معرفی شد که با نسخههای قبل تطبیق نمییافت و تغییرات زیادی در آن رخ داده بود. حرکت از انگولار 1 به 2 مانند رفتن به یک فریمورک جدید بود. همچنین بهبود سرعت اجرایی که React نوید میداد، موجب شد که اغلب توسعهدهندگان React را انتخاب کنند.
پشتیبانی از سوی فیسبوک
این که یک فریمورک از سوی شرکت بزرگی مانند فیسبوک پشتیبانی شود به معنی پایدار بودن و موفقیت بیشتر آن است. فیسبوک در حال حاضر علاقه زیادی به React دارد، ارزش آن را در اوپنسورس بودن میداند و این به علاوه انرژی همه توسعهدهندگانی که از آن در پروژههایشان استفاده میکنند، موجب موفقیت React شده است.
آیا یادگیری React آسان است؟
گرچه گفتیم که React نسبت به جایگزینهای خود سادهتر است؛ اما استفاده عملی از آن همچنان پیچیده است؛ با این حال، اغلب پیچیدگی React در فناوریهای جانبی که در آن ادغام میشوند مانند Redux و GraphQL است.
React خودش یک API بسیار کوچک دارد. اساساً برای آغاز کار با آن به درک چهار مفهوم زیر نیاز دارید:
- کامپوننتها
- JSX
- حالت
- Props
همه این مفاهیم و موارد بیشتر در این مجموعه مطلب توضیح داده شدهاند.
چگونه React را روی رایانه محیط توسعه خود نصب کنیم؟
React یک کتابخانه است و از این رو شاید استفاده از واژه نصب در مورد آن کمی عجیب باشد. شاید راهاندازی کلمه بهتری باشد؛ در هر حال ما با مفاهیم کار داریم و نه عبارتها. روشهای مختلفی برای راهاندازی React برای استفاده روی اپلیکیشن یا وبسایت وجود دارد.
بارگذاری مستقیم React در صفحه وب
سادهترین روش افزودن فایل جاوا اسکریپت React به صفحه به صورت مستقیم است. این روش در مواردی که اپلیکیشن React با عناصر موجود روی یک صفحه منفرد تعامل خواهد داشت و به صورت مستقیم کل جنبه ناوبری را کنترل نمیکند بهترین گزینه محسوب میشود. در این حالت 2 تگ اسکریپت به تگ body اضافه میکنیم:
1<html>
2 ...
3 <body>
4 ...
5 <script
6 src="https://cdnjs.cloudflare.com/ajax/libs/react/16.7.0-alpha.2/umd/react.development.js"
7 crossorigin
8 ></script>
9 <script
10 src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.7.0-alpha.2/umd/react-dom.production.min.js"
11 crossorigin
12 ></script>
13 </body>
14</html>
نسخه 16.7.0-alpha.2 در لینکها به جدیدترین نسخه ریاکت در زمان نگارش این مجموعه مطلب یعنی 16.7 آلفا اشاره میکند که در آن قابلیت Hook ارائه شده است. در صورتی که نسخه جدیدتری وجود دارد باید از آن استفاده کنید.
بدین ترتیب React و React DOM را بارگذاری میکنیم. اما شاید از خود بپرسید چرا 2 کتابخانه بارگذاری شده است؟ چون React 100% مستقل از مرورگر عمل میکند و میتواند خارج از آن (برای مثال روی دستگاههای موبایل با استفاده از React Native) نیز فعال باشد. از این رو باید React Dom نیز بارگذاری شود تا پوششهای مناسب مرورگر را اضافه کند.
پس از این که آن تگها اشاره شد میتوانید فایلهای جاوا اسکریپت خود را که از React استفاده میکنند بارگذاری کنید و یا این که کد جاوا اسکریپت را در یک تگ Script به صورت inline درج کنید:
1<script src="app.js"></script>
2
3<!-- or -->
4
5<script>
6 //my app
7</script>
برای استفاده از JSX باید یک مرحله دیگر نیز طی کنید که مرحله بارگذاری Babel است:
1 <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
بدین ترتیب اسکریپتها با نوع MIME خاص text/babel بارگذاری میشوند:
<script src="app.js" type="text/babel"></script>
اینک میتوانید JSX را به فایل app.js خود اضافه کنید:
1const Button = () => {
2 return <button>Click me!</button>
3}
4
5ReactDOM.render(<Button />, document.getElementById('root'))
راه اندازی ریاکت به این روش برای ساخت پروتوتایپ مناسب است، زیرا امکان آغاز سریع بدون نیاز به راهاندازی گردش کار کامل را فراهم میسازد.
چگونه از create-react-app استفاده کنیم؟
create-react-app یک پروژه است که هدف آن ایجاد امکان راهاندازی پروژههای ریاکت در کمترین زمان ممکن است و هر اپلیکیشن create-react-app که بیشتر از یک صفحه داشته باشد، create-react-app این نیاز را رفع میکند.
در ابتدا از npx استفاده میکنیم که یک روش آسان برای دانلود و اجرای دستورهای Node.js بدون نصب آنها است. npx به همراه npm عرضه میشود و اگر npm را روی سیستم نصب ندارید، باید همین الان آن را از آدرس (+) نصب کنید.
اگر مطمئن نیستید که کدام نسخه از npm را نصب کنید، باید دستور npm –v را اجرا کنید تا نیاز بهروزرسانی را بررسی کنید.
زمانی که دستور <npx create-react-app <app-name را اجرا بکنید، npx شروع به دانلود جدیدترین نسخه create-react-app کرده، آن را اجرا و سپس از روی سیستم حذف میکند. این وضعیت عالی است، زیرا شما هرگز نسخه قدیمی از آن روی سیستم خود نخواهید داشت و هر بار که آن را اجرا کنید، جدیدترین و بهترین کد ممکن را دریافت میکنید. با استفاده از دستور زیر آن را اجرا کنید:
npx create-react-app todolist
زمانی که اجرای آن پایان یابد با تصویر زیر مواجه میشوید:
create-react-app یک ساختار فایل در پوشهای که تعیین کردهاید (در این مثال totolist) ایجاد میکند و یک ریپازیتوری Git نیز مقداردهی میکند.
همچنین چند دستور را در فایل اضافه میکند به طوری که میتوانید بیدرنگ اپلیکیشن خود را با رفتن به پوشه مربوطه و اجرای دستور npm start آغاز کنید:
create-react-app علاوه بر npm start، چند دستور دیگر نیز اضافه میکند:
- npm run build – برای ساختن فایلهای اپلیکیشن React در پوشه build، که آماده انتشار روی سرور است.
- npm test – برای اجرای مجموعه تست با استفاده از Jest
- Npm eject – برای خارج شدن از create-react-app
خارج شدن یا eject به حالتی گفته میشود که تشخیص میدهید create-react-app وظیفه خود را انجام داده است، اما میخواهید کارهای بیشتری از آنچه create-react-app مجاز است انجام دهید. از آنجا که create-react-app مجموعهای از قراردادهای مشترک و مقدار کمی از گزینههای اختیاری است، این احتمال وجود دارد که در مواردی به چیز خاصی نیاز داشته باشید که از ظرفیتهای create-react-app فراتر میرود.
بدین ترتیب زمانی که eject میکنید، امکان بهروزرسانی خودکار را از دست میدهید؛ اما میتوانید با استفاده از پیکربندی Balel و Webpack انعطافپذیری بیشتری به دست آورید.
باید بدانید که عمل eject برگشتناپذیر است. شما 2 پوشه در دایرکتوری اپلیکیشن خود به نامهای config و scripts به دست میآورید. این پوشهها شامل پیکربندیها هستند و اینک میتوانید شروع به ویرایش آنها بکنید.
اگر از قبل یک اپلیکیشن ریاکت داشته باشید که با استفاده از نسخههای قدیمی ریاکت ساخته شده است، ابتدا باید نسخه آن را با استفاده از (console.log(React.version در اپلیکیشن خود تست کنید و سپس میتوانید با اجرای دستور yarn add react@16.7 آن را بهروزرسانی کنید. بدین ترتیب yarn به شما هشدار میدهد که باید بهروزرسانی کنید. دقت کنید که عدد نسخه مورد نظرتان باید جدیدترین نسخه باشد.
CodeSandbox
یک روش آسان برای این که ساختار create-react-app را بدون نصب کردن آن به دست بیاورید این است که به آدرس https://codesandbox.io/s بروید و گزینه React را انتخاب کنید.
CodeSandbox روشی عالی برای آغاز یک پروژه ریاکت بدون نیاز به نصب محلی است.
Codepen
Codepen نیز یک گزینه عالی محسوب میشود. شما میتوانید از پروژه استارتر Codepen که از قبل با React پیکربندی شده است و از hook ها نیز پشتیبانی میکند استفاده کنید.
Pen-های Codepen برای پروژههای سریع با یک فایل جاوا اسکریپت عالی هستند؛ در حالی که project-ها برای پروژههای با چندین فایل مانند آنهایی که در اغلب موارد هنگام ساخت اپلیکیشنهای ریاکت استفاده میکنیم مناسب هستند.
نکتهای که باید اشاره کنیم این است که Codepen به دلیل طرز کار درونی خود نیازی به استفاده ساختار import برای ماژولهای معمولی ES ندارد، بلکه میتوان برای مثال useState را با استفاده از دستور زیر ایمپورت کرد:
const { useState } = React
و نه دستور زیر:
import { useState } from 'react'
بخش اول: مفاهیم اساسی جاوا اسکریپت مدرن برای استفاده از React
اینک به فصل اول راهنمای React رسیدهایم. با ما همراه باشید.
آیا قبل از یادگیری عملی React باید چیزی را بیاموزیم؟
اگر قصد دارید ریاکت را بیاموزید ابتدا باید چند چیز را یاد گرفته باشید. برخی فناوریهای پیشنیاز وجود دارند که باید با آنها آشنا باشید و از آن جمله برخی از موارد مرتبط با ویژگیهای جدید جاوا اسکریپت که بارها در React مشاهده خواهید کرد.
برخی اوقات فکر میکنند که یک ویژگی خاص از سوی React ارائه شده است؛ در حالی که آن ویژگی بخشی از ساختار جاوا اسکریپت مدرن است. البته لازم نیست در حال حاضر در این زمینه یک خبره باشید؛ اما هر چه در React جلوتر برویم، به یادگیری این موارد بیشتر نیاز خواهید داشت. در ادامه فهرستی از مواردی که به یادگیری سریعتر شما کمک میکند را ارائه کردهایم.
متغیرها
منظور از متغیر، عبارتی است که به یک شناسه انتساب یافته است و از این رو میتوان در موارد آتی در برنامه آن را مورد ارجاع قرار داده و استفاده کرد. متغیرها در جاوا اسکریپت هیچ نوع مشخصی ندارند. میتوان یک نوع به متغیر نسبت داد و سپس میتوان نوع دیگری برای آن تعیین کرد و هیچ خطا یا مشکلی نیز پیش نمیآید.
به همین دلیل است که جاوا اسکریپت در مواردی یک زبان برنامهنویسی «فاقد نوع» (untyped) نامیده میشود. متغیر باید پیش از استفاده، اعلان شود. 2 روش برای انجام این کار وجود دارد که شامل استفاده از کلیدواژههای var ،let یا const است. تفاوت این سه روش در نوع تعامل بعدی با متغیر است.
استفاده از var
تا ES2015 کلیدواژه var تنها سازه موجود برای تعریف کردن متغیرها بود.
1Var a = 0
اگر فراموش کنید از var استفاده کنید، مقدار مورد نظر خود را به یک متغیر اعلان نشده انتساب میدهید و نتایج این کار بسته به موقعیت متفاوت خواهد بود.
در محیطهای مدرن که در آنها حالت strict فعالشده است، در این مورد با خطا مواجه میشوید. در محیطهای قدیمیتر یا وقتی که حالت strict غیرفعال است، این مورد باعث میشود که متغیر مقداردهی شود و به یک شیء سراسری انتساب مییابد.
اگر متغیری را هنگام اعلان کردن، مقداردهی نکنید، دارای مقدار undefined خواهد بود تا این که مقداری به آن انتساب داده شود.
1var a //typeof a === 'undefined'
شما میتوانید بارها و بارها مقدار جدیدی به متغیر انتساب دهید و مقدار قبلی را باطل کنید:
1var a = 1
2var a = 2
همچنین میتوانید متغیرهای چندگانهای را به یکباره در گزاره واحد اعلان کنید:
1var a = 1، b = 2
دامنه (scope) متغیر بخشی از کد است که متغیر در آن قابل مشاهده است.
متغیری که با var خارج از هر تابع مقداردهی شود، به یک شیء سراسری انتساب مییابد و دارای دامنه سراسری است یعنی در همه جای کد قابل مشاهده است. متغیری که با یک var درون یک تابع مقداردهی شود، به آن تابع انتساب مییابد و به صورت محلی در آن و تنها از درون آن قابل مشاهده است و از این حیث مانند یک پارامتر تابع است.
هر متغیری که در یک تابع با همان نام متغیر سراسری تعریف شود، نسبت به متغیر سراسری تقدم مییابد و به سایه آن تبدیل میشود.
باید دقت کنید که یک بلوک (که به وسیله آکولادها مشخص میشود) دامنه جدیدی تعریف نمیکند. دامنه جدید تنها زمانی که یک تابع ایجاد شود تعریف میشود، زیرا var دارای دامنه بلوکی نیست بلکه دامنه تابعی دارد.
درون یک تابع هر متغیری که در آن تعریف شود، در تمام کد درون تابع قابل مشاهده خواهد بود حتی اگر متغیر در انتهای تابع اعلان شود همچنان میتوان در آغاز تابع به آن ارجاع داد، چون جاوا اسکریپت پیش از اجرای تابع همه متغیرها را به ابتدای آن میبرد (این کار hoisting نامیده میشود.) برای جلوگیری از سردرگمی همواره تابعها را در ابتدای یک تابع اعلان کنید.
استفاده از let
Let ویژگی جدیدی است که در ES2015 معرفی شده است. در واقع let یک نسخه دارای دامنه بلوکی از var محسوب میشود. دامنه آن به بلوک، گزاره یا عبارتی که در آن تعریف میشود و همه بلوکهای درونی که داخل آن بلوک قرار دارند، مربوط میشود.
توسعهدهندگان جاوا اسکریپت مدرن، در غالب موارد صرفاً از let استفاده میکند و استفاده از var را کنار گذاردهاند. تعریف کردن let در خارج از هر تابع، برخلاف var باعث ایجاد یک متغیر سراسری نمیشود.
استفاده از const
متغیرهایی که با var و let اعلان شوند، میتوانند در ادامه در برنامه تغییر یابند و مجدداً مقادیری به آنها انتساب یابد. زمانی که یک const مقداردهی بشود، مقدار آن نمیتواند دیگر تغییر یابد و نمیتوان مقدار دیگری به آن انتساب داد.
1const a = 'test'
بدین ترتیب دیگر نمیتوان مقادیر دیگری به const با نام a انتساب داد. با این حال میتوان a را در صورتی که شیئی باشد که متدهایی برای تغییر محتوای خود داشته باشد، تغییر داد.
Const تغییرناپذیری را تضمین نمیکند و صرفاً این اطمینان را میدهد که ارجاع را نمیتوان تغییر داد. Const مانند let دارای دامنه بلوکی است.
توسعهدهندگان جاوا اسکریپت مدرن برای متغیرهایی که نیازی به انتساب مقادیر مجدد در برنامه نخواهند داشت، همواره از const استفاده میکنند. دلیل این مسئله آن است که همواره باید سعی کنیم از ساختارهایی استفاده کنیم که امکان بروز خطا در ادامه را کاهش دهند.
تابعهای Arrow
تابعهای Arrow در ES6 / ECMAScript 2015 معرفی شدهاند و از زمان معرفی خود، روش نمایش و کارکرد کدهای جاوا اسکریپت را برای همیشه تغییر دادهاند. این تغییر چنان مثبت بوده است که دیگر به ندرت استفاده از کلیدواژه function را در کدهای مدرن میبینید.
این ساختار از نظر بصری ساده و خوشایند است و امکان نوشتن تابعهایی با ساختار کوتاهتر را فراهم ساخته است. بدین ترتیب ساختار زیر:
1const myFunction = function() {
2 //...
3}
به حالت زیر تغییر یافته است:
1const myFunction = () => {
2 //...
3}
اگر بدنه تابع تنها شامل یک گزاره باشد، میتوان پرانتزها را حذف کرد و همه آن را در یک خط نوشت:
1const myFunction = () => doSomething()
پارامترها درون پرانتزها ارسال میشوند:
1const myFunction = (param1، param2) => doSomething(param1، param2)
اگر یک پارامتر داشته باشید و این تنها پارامتر شما باشد، میتوانید پرانتزها را به طور کامل حذف کنید:
1const myFunction = param => doSomething(param)
به لطف این ساختار کوتاه، تابعهای Arrow استفاده از تابعهای کوچک را ترویج دادهاند.
بازگشت ضمنی (Implicit Return)
تابعهای Arrow امکان داشتن مقادیر بازگشتی ضمنی را فراهم ساختهاند. این مقادیر بدون استفاده از کلیدواژه return بازگشت مییابند. طرز کار این بازگشت به این صورت است که گزارههای تکخطی در بدنه تابع به صورت زیر تعریف میشوند:
1const myFunction = () => 'test'
2myFunction() //'test'
مثال دیگر زمانی است که یک شیء بازگشت میدهیم. دقت کنید که باید آکولادها را درون پرانتزها قرار دهید تا با پرانتزهای تشکیل دهنده بدنه تابع اشتباه گرفته نشوند:
1const myFunction = () => ({ value: 'test' })
2myFunction() //{value: 'test'}
طرز کار this در تابعهای Arrow
This مفهومی است که شاید درک آن دشوار باشد، چون بسته به زمینه و همچنین بر اساس حالت جاوا اسکریپت (strict یا غیر آن) متفاوت است.
میبایست این نکته را روشن کنیم، زیرا تابعهای Arrow به طرزی کاملاً متفاوت از تابعهای معمولی عمل میکنند. زمانی که یک متد برای یک شیء تعریف میشود، در تابع معمولی this اشاره به شیء دارد و از این رو میتوان کد زیر را داشت:
1const car = {
2 model: 'Fiesta',
3 manufacturer: 'Ford',
4 fullName: function() {
5 return `${this.manufacturer} ${this.model}`
6 }
7}
بدین ترتیب فراخوانی ()car.fullName مقدار "Ford Fiesta" را بازگشت میدهد.
دامنه this در تابعهای Arrow از زمینه اجرایی به ارث میرسید. یک تابع Arrow به هیچ وجه به this اتصال نمییابد و از این رو مقدار آن در پشته فراخوانی جستجو میشود. بدین ترتیب کدی مانند ()car.fullName کار نخواهد کرد و رشته "undefined undefined" را بازگشت میدهد:
1const car = {
2 model: 'Fiesta',
3 manufacturer: 'Ford',
4 fullName: () => {
5 return `${this.manufacturer} ${this.model}`
6 }
7}
به همین دلیل، تابعهای Arrow به عنوان متدهای شیء مناسب نیستند. همچنین تابعهای Arrow نمیتوانند به عنوان سازنده (constructor) استفاده شوند، زیرا در هنگام مقداردهی اولیه یک خطای TypeError ایجاد میشود.
در این موارد باید از تابعهای معمولی استفاده کرد که زمینه دینامیک مورد نیاز نیست. همچنین این مورد در هنگام مدیرت رویدادها، موجب ایجاد مشکل میشود. شنوندههای رویداد DOM سعی میکنند this را روی عنصر هدف تنظیم کنند و اگر در handler رویداد روی this تکیه کنیم، به یک تابع معمولی نیاز خواهیم داشت:
1const link = document.querySelector('#link')
2link.addEventListener('click', () => {
3 // this === window
4})
5
6const link = document.querySelector('#link')
7link.addEventListener('click', function() {
8 // this === link
9})
Rest و Spread
میتوان یک آرایه، یک شیء یا یک رشته را با استفاده از عملگر spread بسط داد. توضیح خود را با مثالی از آرایه آغاز میکنیم. فرض کنید:
1const a = [1، 2، 3]
شما میتوانید یک آرایه جدید با کد زیر بسازید:
1const c = [...a]
این کد برای شیء نیز به خوبی کار میکند. شیء را با دستور زیر کلون کنید:
1const newObj = { ...oldObj }
با استفاده از رشتهها، عملگر spread یک آرایه با هر یک از کاراکترهای رشته ایجاد میکند:
1const hey = 'hey'
2const arrayized = [...hey] // ['h'، 'e'، 'y']
این عملگر برخی کاربردهای بسیار مفید دارد. مهمترین کاربر توانایی استفاده از یک آرایه به عنوان آرگومان تابع به روشی بسیار ساده است:
1const f = (foo، bar) => {}
2const a = [1، 2]
3f(...a)
در گذشته این کار را میتوانستید با استفاده از (f.apply(null، a انجام دهید اما این روش زیبا و خوانا نیست. عنصر rest زمانی مفید است که میخواهیم با تخریب آرایه (array destructuring):
1const numbers = [1، 2، 3، 4، 5]
2[first، second، ...others] = numbers
و spread عناصر کار کنیم:
1const numbers = [1، 2، 3، 4، 5]
2const sum = (a، b، c، d، e) => a + b + c + d + e
3const sum = sum(...numbers)
ES2018 مشخصات rest را معرفی کرده است که همین مفهوم اما در مورد شیء را داراست.
مشخصات Rest:
1const { first, second, ...others } = {
2 first: 1,
3 second: 2,
4 third: 3,
5 fourth: 4,
6 fifth: 5
7}
8
9first // 1
10second // 2
11others // { third: 3, fourth: 4, fifth: 5 }
مشخصات spread امکان ایجاد یک شیء جدید با ترکیب کردن مشخصات شیء ارسالی پس از عملگر spread را میدهد:
1const items = { first, second, ...others }
2items //{ first: 1, second: 2, third: 3, fourth: 4, fifth: 5 }
تخریب شیء و آرایه
با فرض وجود یک شیء میتوان با استفاده از ساختار تخریب، تنها برخی از مقادیر را استخراج کرد و آنها را در «متغیرهای با نام» قرار داد:
1const person = {
2 firstName: 'Tom',
3 lastName: 'Cruise',
4 actor: true,
5 age: 54 //made up
6}
7
8const { firstName: name, age } = person //name: Tom, age: 54
name و age شامل مقادیر مطلوب ما هستند. همین ساختار در مورد آرایهها نیز کار میکند:
1const a = [1، 2، 3، 4، 5]
2const [first، second] = a
این گزاره با دریافت آیتمها از اندیسهای 0، 1 و 4، سه متغیر جدید از آرایه a ایجاد میکند:
1const [first، second،،، fifth] = a
الفاظ قالبی (Template Literals)
الفاظ قالبی ویژگی جدید ES2015 / ES6 است که امکان کار با رشتهها به روشی کاملاً جدید نسبت به ES5 و قبلتر را فراهم ساخته است.
ساختار آن در نگاه اول بسیار ساده است و کافی است به جای گیومههای تکی یا جفتی از علامت backtick (`) استفاده کرد:
1const a_string = `something`
دقت کنید که این الفاظ کاملاً منحصر به فرد هستند، زیرا ویژگیهای زیادی ارائه میکنند که رشتههای معمولی با گیومه ندارند. از آن جمله:
- ساختاری عالی دارند که امکان تعریف رشتههای چندخطی را میدهد.
- روش آسانی برای میانیابی متغیرها و عبارتها در داخل رشتهها در اختیار ما قرار میدهند.
- امکان ایجاد DSL با استفاده از تگهای قالبی را فراهم میسازند. DSL به معنی «زبان خاص یک حوزه» است و برای مثال در React در کامپوننتهای دارای سبک برای تعریف CSS کامپوننتها استفاده میشوند.)
هر کدام از جزییات فوق را در ادامه بررسی میکنیم.
رشتههای چندخطی
تا قبل از ES6 برای ایجاد یک رشته که در دو یا چند خط گسترش مییابد از کاراکتر \ در انتهای هر خط استفاده میکردیم:
1const string =
2 'first part \
3second part'
بدین ترتیب امکان ایجاد یک رشته 2 خطی پدید میآمد؛ اما تنها در یک خط رندر میشد:
first part second part
برای رندر کردن رشته در چند خط باید به صورت زیر صریحاً از \n در انتهای هر خط استفاده میکردیم:
1const string =
2 'first line\n \
3second line'
یا
1const string = 'first line\n' + 'second line'
الفاظ قالبی امکان تعریف رشتههای چندخطی را سادهتر ساختهاند.
زمانی که یک لفظ قالبی با backtick باز شود، میتوانید برای ایجاد یک خط جدید از اینتر استفاده کنید و دیگر نیازی به کاراکترهای خاص نیست و همچنان که دیده میشود رندر خواهد شد:
1const string = `Hey
2this
3
4string
5is awesome!`
به خاطر داشته باشید که در این حالت فاصله معنیدار است. از این رو در کد زیر:
1const string = `First
2 Second`
قصد داریم یک رشته مانند زیر ایجاد کنیم:
1First
2 Second
یک روش ساده برای اصلاح این مشکل آن است که یک خط خالی داشته باشیم و متد ()trim را درست پس از backtick پایانی قرار دهیم تا هر فاصلهای را پیش از کاراکتر اول حذف کند:
1const string = `
2First
3Second`.trim()
میانیابی
الفاظ قالبی یک روش آسان برای میانیابی متغیرها و عبارتهای درون رشتهها ارائه کردهاند.
این کار با استفاده از ساختار {...}$ ممکن است:
1const var = 'test'
2const string = `something ${var}` //something test
درون {}$ میتوان هر چیزی حتی عبارتها را اضافه کرد:
1const string = `something ${1 + 2 + 3}`
2const string2 = `something ${foo() ? 'x' : 'y'}`
کلاسها
در سال 2015 و با معرفی استاندارد ECMAScript 6 یا ES6، مفهوم کلاس معرفی شد.
جاوا اسکریپت یک روش کاملاً نامتداول برای پیادهسازی وراثت دارد که وراثت پروتوتایپی نامیده میشود. وراثت پروتوتایپی با این که شاید چنان بد نباشد؛ اما برخلاف اغلب پیادهسازیهای وراثت از سوی زبانهای برنامهنویسی محبوب تعریف میشود که مبتنی بر کلاس هستند.
افرادی که با سابقه یادگیری زبانهای برنامهنویسی جاوا یا پایتون یا دیگر زبانها به سراغ جاوا اسکریپت میآیند، برای درک ماهیت وراثت پروتوتایپی با مشکل مواجه میشوند و از این رو کمیته ECMAScript تصمیم گرفت تا این تغییر ظاهری در ساختار (syntactic sugar) را بر مبنای وراثت پروتوتایپی پیاده کند به طوری که شبیه به طرز کارکرد وراثت مبتنی بر کلاس در زبانهای برنامهنویسی دیگر به نظر بیاید.
نکته مهمی که بدانید این است که جاوا اسکریپت در پسزمینه همچنان به صورت قبل عمل میکند و شما میتوانید به طرز معمول به پروتوتایپ یک شیء دسترسی داشته باشید.
تعریف یک کلاس
تعریف کلاس به شکل زیر انجام میشود:
1class Person {
2 constructor(name) {
3 this.name = name
4 }
5
6 hello() {
7 return 'Hello, I am ' + this.name + '.'
8 }
9}
هر کلاس یک شناسه دارد که میتواند برای ایجاد یک شیء جدید با استفاده از ()new ClassIdentifier مورد استفاده قرار گیرد. زمانی که یک شیء مقداردهی میشود، متد سازنده (constructor) با پارامترهای ارسالی فراخوانی میشود.
همچنین یک کلاس بسته به نیاز، متدهای زیادی دارد. در این مورد hello یک متد است و میتواند روی همه اشیای مشتق شده از کلاس فراخوانی شود:
1const flavio = new Person('Flavio')
2flavio.hello()
وراثت کلاس
یک کلاس میتواند کلاس دیگری را بسط دهد و شیءهایی که با استفاده از آن مقداردهی شوند، همه متدهای هر دو کلاس را به ارث میبرند.
اگر کلاسی که به ارث رسیده متدی با همان نام متد کلاس بالاتر در سلسلهمراتب داشته باشد، نزدیکترین متد، تقدم مییابد:
1class Programmer extends Person {
2 hello() {
3 return super.hello() + ' I am a programmer.'
4 }
5}
6
7const flavio = new Programmer('Flavio')
8flavio.hello()
این کد عبارت «.Hello، I am Flavio. I am a programmer» را نمایش میدهد.
در کلاسها اعلان متغیر کلاس به صورت صریح وجود ندارد؛ اما میبایست هر متغیری که در سازنده قرار دارد را مقداردهی کرد. درون یک کلاس میتوان با فراخوانی ()super به کلاس والد ارجاع داد.
متدهای استاتیک
به طور معمول متدها در وهله و نه در خود کلاس تعریف میشوند؛ اما متدهای استاتیک روی خود کلاس اجرا میشوند:
1class Person {
2 static genericHello() {
3 return 'Hello'
4 }
5}
6
7Person.genericHello() //Hello
متدهای خصوصی
جاوا اسکریپت یک روش داخلی برای تعریف متدهای خصوصی (private) یا حفاظتشده (protected) ندارد. البته برخی راهکارها وجود دارند اما معرفی آنها خارج از حیطه این مقاله است.
Getter-ها و Setter-ها
شما میتوانید متدهایی با پیشوند get و set برای ایجاد getter و setter تعریف کنید. این دو متد بر اساس این که بخواهید به یک متغیر دسترسی داشته باشید، یا مقدار آن را تغییر دهید فراخوانی میشوند:
1class Person {
2 constructor(name) {
3 this.name = name
4 }
5
6 set name(value) {
7 this.name = value
8 }
9
10 get name() {
11 return this.name
12 }
13}
اگر تنها یک getter داشته باشیم، مشخصه نمیتواند تعیین شود و هر تلاشی برای انجام این کار نادیده گرفته میشود:
1class Person {
2 constructor(name) {
3 this.name = name
4 }
5
6 get name() {
7 return this.name
8 }
9}
اگر یک setter داشته باشیم، میتوانیم مقدار را تغییر دهیم؛ اما نمیتوانیم از بیرون کلاس به آن دسترسی داشته باشیم:
1class Person {
2 constructor(name) {
3 this.name = name
4 }
5
6 set name(value) {
7 this.name = value
8 }
9}
Callback-ها
رایانهها دارای طراحی ناهمگام (asynchronous) هستند. منظور از ناهمگام این است که کارهای مختلف میتوانند مستقل از گردشکار اصلی برنامه اجرا شوند.
در رایانههای مصرفی امروزی هر برنامه روی بازه زمانی معینی اجرا میشود و سپس اجرای آن متوقف میشود تا برنامههای دیگر بتوانند اجرا شوند. این کار در چرخهای با چنان سرعت صورت میگیرد که امکان این که متوجه آن بشویم وجود ندارد و فکر میکنیم که رایانهها بسیاری از برنامهها را به صورت همزمان اجرا میکنند؛ اما این یک توهم است (البته طرز کار رایانههای چندهستهای متفاوت است).
برنامهها به طور درونی از وقفهها (interrupts) استفاده میکنند. منظور از وقفه سیگنالی است که به سمت پردازنده ارسال میشود و توجه سیستم را جلب میکند.
ما در اینجا قصد نداریم به سازوکار درونی آن اشاره کنیم؛ اما باید به خاطر داشته باشید که ناهمگام بودن برنامهها یک حالت نرمال است و به طور معمول اجرای آنها تا زمانی که نیاز به توجه داشته باشند معلق میشود و رایانه میتواند در این زمان کارهای دیگری انجام دهد. زمانی که یک برنامه منتظر پاسخ از سوی شبکه است، نمیتواند تا زمانی که درخواست پاسخ خود را دریافت میکند، پردازنده را معطل نگه دارد.
به طور معمول زبانهای برنامهنویسی به صورت «همگام» هستند و برخی از آنها در خود زبان یا کتابخانههایش، روشهایی برای مدیریت ناهمگام ارائه میکنند. زبانهای C J،ava ،C# ،PHP ،Go ،Ruby ،Swift و Python همگی به صورت پیشفرض همگام هستند. برخی از آنها از رویدادهای ناهمگام با استفاده از نخها و ایجاد یک پردازش جدید پشتیبانی میکنند.
جاوا اسکریپت نیز به صورت پیشفرض همگام و تک نخی است. این بدان معنی است که کد نمیتواند یک نخ جدید ایجاد کرده و به صورت موازی اجرا شود.
خطوط کد به صورت پشت سر هم یکی پس از دیگری اجرا میشوند. برای نمونه:
1const a = 1
2const b = 2
3const c = a * b
4console.log(c)
5doSomething()
اما جاوا اسکریپت درون مرورگر متولد شده است و وظیفه اصلی آن در ابتدا این بوده است که به اقدامات کاربر مانند onClick ،onMouseOver ،onChange و onSubmit پاسخ دهد. این کار از طریق مدل برنامهنویسی همگام چگونه میسر میشود؟
پاسخ در محیط جاوا اسکریپت است. مرورگر روشی برای انجام این کار ارائه میکند، یعنی مجموعهای از API-ها ارائه کرده است که میتواند این نوع از کارکرد را اجرا کند.
در سالهای اخیر Node.js به معرفی یک محیط بدون انسداد I/O اقدام کرده است که این مفهوم را برای دسترسی به فایلها، فراخوانیهای شبکه و غیره بسط میدهد.
ما نمیتوانیم بدانیم که یک کاربر چه هنگام روی یک دکمه کلیک خواهد کرد، بنابراین تنها میتوانیم یک «دستگیره رویداد» (event handler) برای عمل کلیک تعریف کنیم. این دستگیره رویداد یک تابع میپذیرد که هنگام تحریک شدن رویداد فراخوانی خواهد شد:
1document.getElementById('button').addEventListener('click', () => {
2 //item clicked
3})
این همان callback است.
منظور از callback تابع سادهای است که به صورت یک مقدار به تابع دیگر ارسال میشود و تنها زمانی اجرا خواهد شد که رویداد رخ دهد. ما به این دلیل میتوانیم این کار را انجام دهیم که جاوا اسکریپت «تابعهای درجه اول» (first-class functions) دارد که میتوانند به متغیرها انتساب یابند و به تابعهای دیگر ارسال شوند (تابعهای درجه بالاتر یا higher-order نیز نامیده میشوند).
به طور معمول همه کدهای کلاینت در یک دستگیره رویداد load روی شیء window پوشش مییابد که تابع callback را تنها زمانی که صفحه آماده است فراخوانی میکند:
1window.addEventListener('load', () => {
2 //window loaded
3 //do what you want
4})
Callback-ها همهجا استفاده میشوند و اختصاص به رویدادهای DOM ندارند. یک مثال رایج در استفاده از تایمر چنین است:
1setTimeout(() => {
2 // runs after 2 seconds
3}, 2000)
درخواستهای XHR نیز یک callback میپذیرند. در این مثال یک تابع به یک مشخصه انتساب مییابد که هنگام اتفاق افتادن یک رویداد خاص، فراخوانی میشود:
1const xhr = new XMLHttpRequest()
2xhr.onreadystatechange = () => {
3 if (xhr.readyState === 4) {
4 xhr.status === 200 ? console.log(xhr.responseText) : console.error('error')
5 }
6}
7xhr.open('GET', 'https://yoursite.com')
8xhr.send()
مدیریت خطا در callback-ها
یکی از روشهای رایج برای مدیریت خطا در callback-ها استفاده از چیزی که است Node.js به خدمت گرفته است و آن پارامتر اول در هر تابع callback است که شیء error یا error-first callbacks نامیده میشود.
اگر خطایی وجود نداشته باشد، این شیء nul است. اما اگر خطایی باشد شامل برخی توضیحات در مورد خطا و دیگر اطلاعات خواهد بود:
1fs.readFile('/file.json', (err, data) => {
2 if (err !== null) {
3 //handle error
4 console.log(err)
5 return
6 }
7
8 //no errors, process data
9 console.log(data)
10})
مشکل callback-ها
callback ها برای کاربردهای ساده عالی هستند. با این وجود هر callback یک سطح از تو در تو بودن اضافه میکند و زمانی که تعداد زیادی callback وجود داشته باشد، کد به سرعت پیچیده میشود:
1window.addEventListener('load', () => {
2 document.getElementById('button').addEventListener('click', () => {
3 setTimeout(() => {
4 items.forEach(item => {
5 //your code here
6 })
7 }, 2000)
8 })
9})
این فقط یک کد 4 سطحی است؛ اما سطوح تو در تو بودن بیش از این نیز میتواند باشد که چندان جالب نیست. چگونه میتوان این مشکل را حل کرد؟
جایگزینهای callback
از نسخه ES6 به بعد، جاوا اسکریپت چند ویژگی معرفی کرده است که به ما کمک میکند کدهای ناهمگام بنویسیم و درگیر استفاده از callback نیز نشویم:
- (promises (ES6
- (Async/Await (ES8
promise-ها
promise-ها یکی از روشهای نوشتن کد ناهمگام هستند که در آن لازم نیست نگران وجود callback-های زیاد در کد خود باشیم. با این که این ویژگی سالها است که عرضه شده؛ اما در ES2015 استاندارد و معرفی شدهاند و اینک با معرفی تابعهای async در ES2017 منسوخ شدهاند.
تابعهای Async از API مربوط به promise-ها به عنوان بلوک اصلی سازنده خود استفاده میکنند و از این رو درک آنها حتی در صورتی که در کدهای جدید بخواهید از تابعهای async به جای promise استفاده کنید، ضروری خواهد بود.
توضیح خلاصه طرز کار promise-ها
زمانی که یک promise فراخوانی میشود، کار خود را در حالت معلق (pending) شروع میکند. این بدان معنی است که تابع فراخوانی کننده به اجرای خود ادامه میدهد و در این زمان منتظر promise است تا پردازشش را انجام دهد و بازخوردی به تابع فراخوانی کننده بدهد.
سپس تابع فراخوانی کننده منتظر مقدار بازگشتی از سوی promise در حالت resolved یا در حالت rejected میماند؛ اما میدانیم که جاوا اسکریپت ناهمگام است و از این رو تابع در هنگامی که promise مشغول کار است، به اجرای خود ادامه میدهد.
چرا API جاوا اسکریپت از promise-ها استفاده میکند؟
promise-ها علاوه بر کد اپلیکیشن و کد کتابخانه در API های وب مدرن استاندارد مانند Fetch یا Service Workers نیز استفاده میشوند. این که در جاوا اسکریپت مدرن بخواهید از promise-ها استفاده نکنید نامحتمل است و از این رو باید آنها را کاملاً یاد بگیرید.
ایجاد یک promise
API مربوط به promise یک سازنده promise معرفی کرده است که با استفاده از new promise() مقداردهی میشود:
1let done = true
2
3const isItDoneYet = new Promise((resolve, reject) => {
4 if (done) {
5 const workDone = 'Here is the thing I built'
6 resolve(workDone)
7 } else {
8 const why = 'Still working on something else'
9 reject(why)
10 }
11})
همان طور که میبینید promise ثابت سراسری done را بررسی میکند و اگر true باشد، یک promise به صورت reolved بازگشت مییابد و در غیر این صورت مقدار بازگشتی rejected خواهد بود.
ما با استفاده از resolve و reject میتوانیم یک مقدار را بازگشت دهیم و گرچه در مثال فوق یک رشته بازگشت یافته است؛ اما میتواند یک شیء نیز باشد.
مصرف یک promise
در بخش قبلی شیوه ایجاد یک promise را توضیح دادیم. اینک میخواهیم ببینیم promise چگونه میتواند مصرف شود.
1const isItDoneYet = new Promise()
2//...
3
4const checkIfItsDone = () => {
5 isItDoneYet
6 .then(ok => {
7 console.log(ok)
8 })
9 .catch(err => {
10 console.error(err)
11 })
12}
اجرای تابع ()checkIfItsDone باعث راهاندازی یک promise به نام ()isItDoneYet میشود که با استفاده از یک callback به نام then منتظر resolve شدن میماند و در صورت وجود یک خطا آن را با callback به نام catch مدیریت میکند.
زنجیرهسازی promise-ها
میتوان یک promise را به promise دیگر بازگشت داد و بدین ترتیب زنجیرهای از promise-ها ایجاد کرد. یک مثال عالی از زنجیرهسازی promise-ها در Fetch API ارائه شده است که یک لایه روی API مربوط به XMLHttpRequest ساخته و میتوانیم از آن برای دریافت منابع و صفبندی یک زنجیره از promise-ها برای اجرا در مواد واکشی منابع استفاده کنیم.
Fetch API یک سازوکار مبتنی بر promise است و فراخوانی ()fetch معادل تعریف کردن promise با استفاده از ()new promise است:
مثال
1const status = response => {
2 if (response.status >= 200 && response.status < 300) {
3 return Promise.resolve(response)
4 }
5 return Promise.reject(new Error(response.statusText))
6}
7
8const json = response => response.json()
9
10fetch('/todos.json')
11 .then(status)
12 .then(json)
13 .then(data => {
14 console.log('Request succeeded with JSON response', data)
15 })
16 .catch(error => {
17 console.log('Request failed', error)
18 })
در این مثال ()fetch را برای دریافت لیستی از آیتمهای TODO از فایل todos.json در دامنه root فراخوانی میکنیم و بدین ترتیب زنجیرهای از promise-ها ایجاد میشود.
اجرای ()fetch یک پاسخ بازگشت میدهد که مشخصات زیادی دارد و درون آن موارد زیر را ارجاع میدهیم:
- Status – یک مقدار عددی است که کد وضعیت HTTP را نشان میدهد.
- statusText – یک پیام وضعیت است که در صورت موفق بودن درخواست، به صورت OK خواهد بود.
Response نیز یک متد Jason است که یک promise بازگشت میدهد. این promise با محتوای بدنه پردازش شده و انتقال یافته به JSON به صورت resolve درمیآید.
بنابراین با توجه به این promise-ها اتفاقات زیر رخ میدهد:
promise نخست در زنجیره یک تابع است که به نام ()status تعریف کردهایم و وضعیت پاسخ را بررسی کرده و در صورت عدم موفقیت (کدهای بین 200 تا 299) promise را رد میکند.
این عملیات موجب میشود که زنجیره promise-ها قطع شود و مستقیماً به گزاره ()catch در انتها میرود و متن Request failed به همراه پیام خطا، log میشود.
اما اگر موفق باشد تابع ()json را که تعریف کردیم فرا میخواند. از آنجا که وقتی promise قبلی موفق بود، شیء response را بازگشت داده است، ما آن را به عنوان ورودی promise دوم میگیریم.
در این حالت دادههای JSON که پردازش شده را بازگشت میدهیم به طوری که promise سوم مستقیماً JSON را بازیابی کند:
1.then((data) => {
2 console.log('Request succeeded with JSON response', data)
3})
و آن را در کنسول log میکنیم.
مدیریت خطاها
در مثال فوق در بخش قبلی یک catch داشتیم که به زنجیره promise-ها الحاق میشد. زمانی که هر کدام از بخشهای زنجیره promise-ها از کار میافتد و یک خطا رخ میدهد و یا پاسخ reject دریافت میشود، کنترل به نزدیکترین گزاره ()catch در سمت پایین زنجیره میرسد.
1new Promise((resolve, reject) => {
2 throw new Error('Error')
3}).catch(err => {
4 console.error(err)
5})
6
7// or
8
9new Promise((resolve, reject) => {
10 reject('Error')
11}).catch(err => {
12 console.error(err)
13})
آبشارسازی خطاها
اگر درون ()catch خطایی رخ دهد، میتوانید یک متد ثانویه catch() برای مدیریت آن تعریف کنید و همین طور تا آخر.
1new Promise((resolve, reject) => {
2 throw new Error('Error')
3})
4 .catch(err => {
5 throw new Error('Error')
6 })
7 .catch(err => {
8 console.error(err)
9 })
هماهنگسازی promise-ها با استفاده از ()promise.all
اگر لازم باشد که promise-های مختلف را با هم هماهنگ کنیم، میتوانیم از ()promise.all کمک بگیریم و با تعریف کردن لیستی از همه promise-ها، وقتی همه آنها resolve شدند، چیزی را اجرا کنیم.
مثال:
1const f1 = fetch('/something.json')
2const f2 = fetch('/something2.json')
3
4Promise.all([f1, f2])
5 .then(res => {
6 console.log('Array of results', res)
7 })
8 .catch(err => {
9 console.error(err)
10 })
ساختار انتساب تخریب ES2015 امکان انجام این کار را در اختیار ما قرار میدهد:
1Promise.all([f1, f2]).then(([res1, res2]) => {
2 console.log('Results', res1, res2)
3})
البته شما محدود به استفاده از fetch نیستید و هر promise برای این کار مناسب است.
هماهنگسازی promise-ها با استفاده از ()promise.race
()promise.race به محض این که یکی از promise-ها به صورت resolve شده به آن ارسال شود اجرا میشود و callback الحاقی خود را تنها یک بار با نتیجه promise نخست resolve شده اجرا میکند. مثال:
1const promiseOne = new Promise((resolve, reject) => {
2 setTimeout(resolve, 500, 'one')
3})
4const promiseTwo = new Promise((resolve, reject) => {
5 setTimeout(resolve, 100, 'two')
6})
7
8Promise.race([promiseOne, promiseTwo]).then(result => {
9 console.log(result) // 'two'
10})
Async/Await
جاوا اسکریپت در طی زمان بسیار کوتاهی از callbacks به (promises (ES2015 تکامل یافت و سپس از ES2017 جاوا اسکریپت ناهمگام با معرفی ساختار Async/Await باز هم سادهتر شد.
تابعهای Async ترکیبی از promise-ها و generator-ها هستند و در واقع سطح بالاتری از انتزاع را نسبت به promise-ها ارائه میکنند. یک بار دیگر باید تکرار کنیم که async/await بر مبنای promise ساخته شده است.
چرا async/await معرفی شد؟
این ساختار باعث میشود که سازههای تکراری promise-ها کاهش یابند و محدودیت عدم قطع کردن زنجیره در promise-های زنجیرهای نیز رفع شود.
زمانی که promise-ها در ES2015 معرفی شدند، به منظور رفع مشکل کد ناهمگام عرضه شده بودند و در این کار نیز موفق بودند؛ اما در طی 2 سال که ES2015 و ES2017 را از هم جدا میساخت، مشخص شد که promise-ها نمیتوانند راهحل نهایی باشند.
promise-ها برای رفع مشکل مشهور callbak عرضه شدند؛ اما آنها نیز مشکلات خاص خود را داشتند و باعث ایجاد پیچیدگی بیشتر در ساختار میشدند.
در ادامه مشخص شد که میتوان سازههای بهتری با ساختارهای مناسبتر در اختیار توسعهدهندهها قرار داد و هنگامی که زمان مناسب فرارسید، تابعهای async عرضه شدند.
این تابعها باعث شدند کد طوری به نظر بیاید که گویا همگام است؛ اما ناهمگام بود و در پشتصحنه نیز مسدودسازی نداشت.
طرز کار async/await
یک تابع async در عمل مانند مثال زیر یک promise بازگشت میدهد:
1const doSomethingAsync = () => {
2 return new Promise(resolve => {
3 setTimeout(() => resolve('I did something'), 3000)
4 })
5}
زمانی که بخواهیم این تابع را فراخوانی کنیم یک await در اول آن میگذاریم و کد فراخوانی کننده تا زمانی که promise به صورت resolve شده یا reject شده بازگشت نیافته است متوقف میشود. تنها محدودیت این است که تابع کلاینت باید به صورت async تعریف شده باشد. به مثال زیر توجه کنید:
1const doSomething = async () => {
2 console.log(await doSomethingAsync())
3}
یک مثال ساده
در ادامه مثال سادهای از کاربرد async/await برای اجرای ناهمگام یک تابع را میبینید:
1const doSomethingAsync = () => {
2 return new Promise(resolve => {
3 setTimeout(() => resolve('I did something'), 3000)
4 })
5}
6
7const doSomething = async () => {
8 console.log(await doSomethingAsync())
9}
10
11console.log('Before')
12doSomething()
13console.log('After')
کد فوق مواد زیر را در کنسول مرورگر نمایش میدهد:
Before After I did something //after 3s
Pomise کردن همه چیز
افزودن کلیدواژه async در ابتدای هر تابعی به این معنی است که تابع یک promise بازگشت خواهد داد. حتی اگر این کار به روش صریحی صورت نگیرد در ساز و کار درونی خودش یک promise بازگشت میدهد. به همین دلیل است که کد زیر معتبر است:
1const aFunction = async () => {
2 return 'test'
3}
4
5aFunction().then(alert) // This will alert 'test'
و همانند کد زیر عمل میکند:
1const aFunction = async () => {
2 return Promise.resolve('test')
3}
4
5aFunction().then(alert) // This will alert 'test'
خوانایی کد بسیار افزایش مییابد
همان طور که در مثال فوق شاهد هستید، کد ما بسیار ساده به نظر میرسد. آن را با کدهایی که از promise-های ساده با زنجیرهسازی و تابعهای callback استفاده میکنند مقایسه کنید.
این مثال بسیار سادهای است و مزیتهای اصلی زمانی مشخص میشوند که از کدهای بسیار پیچیده استفاده میکنید. برای نمونه در ادامه یک منبع JSON ارائه شده که آن را با استفاده از promise-ها تجزیه میکنیم:
1const getFirstUserData = () => {
2 return fetch('/users.json') // get users list
3 .then(response => response.json()) // parse JSON
4 .then(users => users[0]) // pick first user
5 .then(user => fetch(`/users/${user.name}`)) // get user data
6 .then(userResponse => response.json()) // parse JSON
7}
8
9getFirstUserData()
و در ادامه همین کارکرد با استفاده از await/async ارائه شده است:
1const getFirstUserData = async () => {
2 const response = await fetch('/users.json') // get users list
3 const users = await response.json() // parse JSON
4 const user = users[0] // pick first user
5 const userResponse = await fetch(`/users/${user.name}`) // get user data
6 const userData = await user.json() // parse JSON
7 return userData
8}
9
10getFirstUserData()
سری کردن تابعهای چندگانه async
تابعهای async میتوانند به سادگی به حالت زنجیری درآیند و ساختار آن بسیار خواناتر از promise-های ساده خواهد بود:
1const promiseToDoSomething = () => {
2 return new Promise(resolve => {
3 setTimeout(() => resolve('I did something'), 10000)
4 })
5}
6
7const watchOverSomeoneDoingSomething = async () => {
8 const something = await promiseToDoSomething()
9 return something + ' and I watched'
10}
11
12const watchOverSomeoneWatchingSomeoneDoingSomething = async () => {
13 const something = await watchOverSomeoneDoingSomething()
14 return something + ' and I watched as well'
15}
16
17watchOverSomeoneWatchingSomeoneDoingSomething().then(res => {
18 console.log(res)
19})
کد فوق عبارت زیر را نمایش میدهد:
I did something and I watched and I watched as well
دیباگ کردن سادهتر
دیباگ کردن promise-ها دشوار است، زیرا دیباگر روی کدهای ناهمگام متوقف نمیشود. Async/await این کار را بسیار سادهتر ساخته است، زیرا از نظر کامپایلر این کد مانند کدهای همگام است.
ماژولهای ES
در حالی که Node.js سالها است که از استاندارد CommonJS استفاده میکند، اما مرورگرها هرگز یک سیستم module نداشتهاند، چون هر تصمیم بزرگی مانند سیستم ماژول باید ابتدا از سوی ECMAScript استانداردسازی و سپس از سوی مرورگر پیادهسازی شود.
این فرایند استانداردسازی در ES6 تکمیل شد و مرورگرها شروع به پیادهسازی این نوعبندی استاندارد کرده و تلاش نمودند همه چیز همراستا بماند و به همان روش سابق کار کند. اینک ماژولهای ES در کروم، سافاری، Edge و فایرفاکس (از نسخه 60) پشتیبانی میشوند.
ماژولها بسیار جالب هستند، زیرا امکان کپسولهسازی همه انواع کارکردها را در اختیار ما قرار میدهند و این کارکرد را به فایلهای دیگر جاوا اسکریپت مانند کتابخانهها نیز اکسپورت میکنند.
ساختار ماژولهای ES
ساختار ایمپورت کردن یک ماژول به صورت زیر است:
import package from 'module-name'
در حالی که CommonJS از ساختار زیر استفاده میکند:
const package = require('module-name')
یک ماژول در واقع یک فایل جاوا اسکریپت قرار دارد که یک یا چند مقدار (شیء، تابع یا متغیر) را با استفاده از کلیدواژه export، اکسپورت میکند. برای نمونه ماژول زیر یک تابع اکسپورت میکند که یک رشته با حروف بزرگ بازگشت میدهد:
uppercase.js
export default str => str.toUpperCase()
در مثال فوق، این ماژول یک اکسپورت منفرد پیشفرض تعریف میکند، به طوری که میتواند یک تابع ناهمگام باشد. در غیر این صورت باید یک نام داشته باشد تا از دیگر اکسپورتها متمایز شود.
اکنون هر ماژول دیگر جاوا اسکریپت میتواند این کارکرد را که از سوی uppercase.js ارائه شده با ایمپورت کردن آن به دست آورد.
یک صفحه HTML میتواند یک ماژول را با استفاده از تگ <script> با خصوصیت ویژه "type="module اضافه کند:
1<script type="module" src="index.js"></script>
لازم به ذکر است که هر اسکریپتی که با "type="module بارگذاری شود در حالت strict خواهد بود. در این مثال ماژول uppercase.js یک اکسپورت پیشفرض تعریف میکند و وقتی آن را ایمپورت کردیم میتوانیم از نام ترجیحی خودمان استفاده کنیم:
1import toUpperCase from './uppercase.js'
و میتوانیم آن را استفاده کنیم:
1toUpperCase('test') //'TEST'
همچنین میتوانیم از یک مسیر مطلق برای ایمپورت کردن ماژول استفاده کنیم تا ماژولهای تعریف شده در دامنه دیگری را نیز مورد ارجاع قرار دهیم:
1import toUpperCase from 'https://flavio-es-modules-example.glitch.me/uppercase.js'
این نیز یک ساختار ایمپورت معتبر است:
1import { foo } from '/uppercase.js'
2import { foo } from '../uppercase.js'
اما این ساختار معتبر نیست:
1import { foo } from 'uppercase.js'
2import { foo } from 'utils/uppercase.js'
این ساختار یا مطلق است و یا یک./ یا / در ابتدای نام خود دارد.
گزینههای ایمپورت/اکسپورت دیگر
ما این مثال را قبلاً دیدیم:
1export default str => str.toUpperCase()
این مثال یک اکسپورت پیشفرض ایجاد میکند. میتوانیم با استفاده از ساختار زیر بیش از یک چیز را اکسپورت کنیم:
1const a = 1
2const b = 2
3const c = 3
4
5export { a، b، c }
ماژول دیگر میتواند همه این موارد را با استفاده از دستور زیر ایمپورت کند:
1import * from 'module'
میتوانیم تنها چند مورد از این اکسپورتها را با بهرهگیری از انتساب تخریبی ایمپورت کنیم:
1import { a } from 'module'
2import { a، b } from 'module'
میتوانیم نام هر ایمپورت را برای سهولت با استفاده از as تغییر دهیم:
1import { a, b as two } from 'module'
میتوانیم ابتدا اکسپورت پیشفرض را ایمپورت کنیم و سپس همه اکسپورتهای غیر پیشفرض را به صورت «با نام» ایمپورت کنیم. برای مثال این کار در ایمپورت react زیر صورت گرفته است:
1import React، { Component } from 'react'
CORS
ماژولها با استفاده از CORS واکشی میشوند. این بدان معنی است که اگر اسکریپتها از یک دامنه دیگر مورد ارجاع قرار گیرند، باید یک هدر CORS معتبر داشته باشند که امکان بارگذاری از سایت دیگر را فراهم سازد.
در مورد مرورگرهایی که از ماژولها پشتیبانی نمیکنند چه میتوان کرد؟ در این موارد میتوان از ترکیبی از "type="module و nomodule استفاده کرد:
1<script type="module" src="module.js"></script>
2<script nomodule src="fallback.js"></script>
ماژولهای ES یکی از بزرگترین ویژگیهای معرفی شده در مرورگرهای مدرن هستند. ماژولها بخشی از ES6 هستند؛ اما مسیری که برای پیادهسازی آنها طی شده بسیار طولانی است. اینک میتوانیم از آنها استفاده کنیم؛ اما باید به خاطر داشته باشیم که داشتن بیش از چند ماژول ممکن است بر روی عملکرد صفحه تأثیر منفی بگذارد، چون یک گام دیگر به مراحلی که مرورگر باید در زمان اجرا به آن بپردازد میافزاید.
با این که ماژولهای ES وارد مرورگرها شدهاند؛ Webpack احتمالاً همچنان یکی از بزرگترین بازیگرها خواهد ماند؛ اما داشتن یک چنین ویژگی که به طور مستقیم در یک زبان ساخته شده است برای یکنواخت سای روش کار ماژولها در سمت کلاینت و Node.js بسیار مفید است.
بخش دوم: مفاهیم پایه ری اکت (React)
در ادامه و در این بخش به بررسی مفاهیم پایه خود React میپردازیم.
- اپلیکیشنهای تکصفحهای
- رویکرد اعلانی ریاکت
- تغییرناپذیری
- محض بودن یا purity
- ترکیببندی
- DOM مجازی
- گردش داده غیر جهتدار
اپلیکیشنهای تکصفحهای
اپلیکیشنهای React به نام اپلیکیشنها با صفحه منفرد نیز نامیده میشوند، اما شاید بپرسید معنی این اصطلاح چیست؟
در گذشته، زمانی که مرورگرها قابلیتهای کمتری نسبت به امروز داشتند و عملکرد جاوا اسکریپت نیز ضعیفتر بود، هر صفحه از یک سرور میآمد. هر بار که شما روی چیزی کلیک میکردید، یک درخواست جدید به سرور ارسال میشد و متعاقباً مرورگر یک صفحه جدید را بارگذاری میکرد. در آن زمان تنها محصولات بسیار نوآورانه به طرز متفاوتی عمل کرده و رویکردهای جدیدی را آزمایش میکردند.
امروزه با رواج فریمورکهای فرانتاند مدرن جاوا اسکریپت مانند React، اپلیکیشن معمولاً به صورت یک اپلیکیشن تکصفحهای ساخته میشود یعنی شما تنها یک کد (HTML، CSS و جاوا اسکریپت)، اپلیکیشن را بارگذاری میکنید و هنگامی که با اپلیکیشن تعامل پیدا میکنید؛ جاوا اسکریپت به طور تدریجی رویدادهای مرورگر را تفسیر میکند و به جای ارسال درخواستهای جدید به سرور و بازگشت یک سند جدید از آن، دادههایی را به صورت JSON از سرور درخواست میکند و یا عملی روی سرور اجرا میشود؛ اما خود صفحه هرگز به طور کامل بارگذاری مجدد نمیشود و در فرایندی شبیه به اپلیکیشن دسکتاپ عمل میکند.
اپلیکیشنهای با صفحه منفرد در جاوا اسکریپت ساخته میشوند، یا بهتر است بگوییم در جاوا اسکریپت کامپایل و در مرورگر اجرا میشوند. فناوری مورد استفاده همواره یکسان بوده است، اما فلسفه و برخی اجزای کلیدی طرز کار اپلیکیشنها فرق کرده است.
نمونههایی از اپلیکیشنهای با صفحه منفرد
برخی نمونههای اپلیکیشنهای تکصفحهای که میتوان مورد اشاره قرار داد، به شرح زیر هستند:
- جیمیل
- گوگل مپ
- فیسبوک
- توییتر
- گوگل درایو
مزایا و معایب اپلیکیشنهای تکصفحهای
عملکرد یک اپلیکیشن تکصفحهای از نظر کاربر بسیار سریعتر به نظر میرسد، چون به جای این که منتظر ارتباط بین سرور-کلاینت بماند و همچنین صبر کند تا مرورگر صفحه را مجدداً رندر کند، میتواند بازخورد آنی داشته باشد. این طراحی در حوزه مسئولیت سازنده اپلیکیشن است؛ اما میتوان از انیمیشنهای گذار و چرخشی یا هر نوع بهبود تجربه کاربری (UX) که قطعاً بهتر از گردش کار سنتی است نیز بهره گرفت.
علاوه بر اینکه این رویکرد موجب افزایش سرعت اپلیکیشنها در نظر کاربر میشود، سرور نیز منابع کمتری مصرف میکند، زیرا میتوان به جای ساخت طرحهای سمت سرور روی ارائه یک API بهینه متمرکز شد. بدین ترتیب رویکرد اپلیکیشن تکصفحهای برای ساخت اپلیکیشنهای موبایل روی API مناسب است چون میتوان به طور کامل از کد سمت سرور موجود به طور مجدد استفاده کرد.
اپلیکیشنهای تکصفحهای به راحتی قابل تبدیل به اپلیکیشنهای وب پیشرونده (progressive) هستند که به نوبه خود امکان کش کردن به صورت محلی و پشتیبانی از تجربه آفلاین سرویسها را فراهم میسازند. همچنین در صورتی که نیاز باشد کاربر آنلاین شود خطایی که ارائه میکنند به صورت بهتری ارائه میشود.
اپلیکیشنهای تکصفحهای در مواردی استفاده میشوند که نیازی به سئو (بهینهسازی موتور جستجو) نباشد. برای نمونه در مورد اپلیکیشنهایی که در پشت فرم login قرار میگیرند از این حالت استفاده میشود.
موتورهای جستجو با این که امور روزمره را بهبود میبخشد؛ اما همچنان در ایندکس کردن سایتهایی که با رویکرد اپلیکیشن تکصفحهای ساخته شدهاند در برابر صفحاتی که به طور سنتی در سمت سرور رندر میشوند با مشکل مواجه هستند. این واقعیت در مورد بلاگها مصداق دارد. اگر قصد دارید در محصولی که میسازید از موتورهای جستجو استفاده کنید هرگز بدون ساخت بخشهای رندر شده در سمت سرور، اقدام به ساخت اپلیکیشنهای تکصفحهای نکنید.
زمانی که به کدنویسی اپلیکیشنهای تکصفحهای میپردازید، باید مقادیر زیادی کدهای جاوا اسکریپت بنویسید. از آنجا که اپلیکیشن میتواند مدتهای زیادی اجرا شود، باید به مشکلات ناشی از نشت حافظه کاملاً دقت داشته باشید. اگر در گذشته صفحههای یک وبسایت عمری به اندازه چند دقیقه داشتند، اپلیکیشنهای تکصفحهای ممکن است برای ساعتها در مرورگر باز بمانند و اگر مشکل نشت حافظه وجود داشته باشد، در طی زمان باعث افزایش مصرف حافظه مرورگر میشود و در صورتی که این مشکل رفع نشود، با تجربه ناخوشایند کند شدن اپلیکیشن مواجه میشوید.
اپلیکیشنهای تکصفحهای در مورد کارهای تیمی بسیار خوب عمل میکنند. توسعهدهندگان بکاند میتوانند روی API متمرکز شوند و توسعهدهندگان فرانتاند نیز میتوانند روی ایجاد بهترین تجربه کاربری با بهرهگیری از API ساخته شده در بکاند تمرکز داشته باشند.
یکی دیگر از مشکلات اپلیکیشنهای تکصفحهای این است که تکیه زیادی روی جاوا اسکریپت دارند. این امر میتواند منجر به کند شدن تجربه کاربری در دستگاههای با توان پایین میشود. همچنین برخی از بازدیدکنندگان ممکن است جاوا اسکریپت را غیرفعال کرده باشند و باید در این حالت نیز قابلیت دسترسی برای امکانات اپلیکیشن را در نظر بگیرید.
مدیریت ناوبری مرورگر
از آنجا که رفتار ناوبری پیشفرض مرورگر در اپلیکیشنهای تکصفحهای بازنویسی میشود، URL-ها باید به صورت دستی مدیریت شوند. بخشی از این اپلیکیشنها، مسیریاب یا روتر (Router) نامیده میشود. برخی فریمورکها مانند Ember این وظیفه را بر عهده دارند و برخی دیگر مانند ریاکت به کتابخانههایی مثل React router برای انجام این کار نیازمند هستند.
مشکل این است که در آغاز توسعه اپلیکیشنهای تکصفحهای، به این وضعیت که دکمههای ناوبری مرورگر چه تأثیری روی عملکرد اپلیکیشن دارند اندیشیده نشده بود. این امر موجب خطای مشهور «از کار افتادن دکمه بازگشت» میشد و در زمان ناوبری درون اپلیکیشن، URL تغییر نمییافت (چون رفتار پیشفرض مرورگر استفاده میشد) و زدن دکمه بازگشت موجب میشد که کاربران به صفحه قبلی که بازدید کرده بودند بروند و این مورد میتوانست وبسایتی باشد که مدتها قبل مورد بازدید قرار گرفته بود.
این مشکل هم اینک با استفاده از History API که از سوی مرورگرها ارائه شده رفع شده است؛ اما در اغلب موارد باید از کتابخانهای مانند React Router استفاده کنید که به طور داخلی از این API بهره میگیرد.
رویکرد اعلانی (Declarative)
زمانی که به شما گفته میشود ریاکت از رویکرد اعلانی استفاده میکند، معنی این گفته برای شما چیست؟ در مقالات زیادی اشاره شده است که ریاکت از یک رویکرد اعلانی برای ساخت رابط کاربری استفاده میکند.
ریاکت باعث شده است که «رویکرد اعلانی» بسیار رایج و مطرح شود و از این رو دنیای فرانتاند به وسیله آن همراه با ریاکت گسترش یافته است. رویکرد اعلانی عملاً مفهوم جدیدی محسوب نمیشود؛ اما روشی که ریاکت برای ساخت رابط کاربری استفاده میکند بسیار اعلانیتر از قالبهای HTML است. به عنوان نمونه:
- با استفاده از ریاکت میتوان رابطهای وب را بدون حتی تماس مستقیم با DOM ساخت.
- میتوان یک سیستم رویداد بدون نیاز به تعامل با رویدادهای DOM واقعی طراحی کرد.
متضاد رویکرد اعلانی یک رویکرد «تکراری» (Iterative) است. مثال رایج برای رویکرد تکراری به گشتن در میان عناصر DOM با استفاده از jQuery یا رویدادهای DOM میتوان اشاره کرد.
رویکرد اعلانی ریاکت این وضعیت را برای ما به حالت انتزاعی درمیآورد. ما صرفاً به ریاکت اعلام میکنیم که چه کامپوننتی باید به چه روشی رندر شود و دیگر هرگز لازم نیست با DOM برای ارجاعات بعدی تعامل داشته باشیم.
تغییرناپذیری (Immutability)
یکی از مفاهیمی که احتمالاً در زمان برنامهنویسی در React با آن مواجه خواهید شد بحث تغییرناپذیری و متضاد آن تغییرپذیری (mutability) است. البته این یک موضوع پر حرف و حدیث است؛ اما هر دیدگاهی که در مورد مفهوم تغییرناپذیری داشته باشید؛ در هر حال ریاکت و غالب بخشهای اکوسیستم آن به نوعی استفاده از این مفهوم را اجبار کردهاند و از این رو باید دستکم درکی از این مفهوم داشته باشید. به همین جهت این مفهوم و نتایج ضمنی آن مهم است.
در برنامهنویسی یک متغیر زمانی تغییرناپذیر نامیده میشود که مقدار آن پس از ایجاد متغیر نتواند عوض شود.
شما تاکنون هنگام دستکاری رشتهها از متغیرهای تغییرناپذیر، احتمالاً بدون این که حتی بدانید، استفاده کردهاید. رشتهها به صورت پیشفرض تغییرناپذیر هستند و هنگامی که آنها را در واقعیت تغییر میدهید یک رشته جدید ایجاد میکنید و آن را به متغیری با همان نام انتساب میدهید.
یک متغیر تغییرناپذیر هرگز نمیتواند تغییر یابد. برای بهروزرسانی مقدار آن میتوانید یک متغیر جدید بسازید. همین مفاهیم در مورد اشیا و آرایهها نیز کاربرد دارند. به جای تغییر دادن آرایهها برای افزودن یک آیتم جدید ما همواره یک آرایه جدید با الحاق آرایه قبلی به علاوه آیتم جدید میسازیم. یک شیء هرگز نمیتواند بهروزرسانی شود؛ بلکه پیش از آن که تغییر یابد کپی میشود.
این مفهوم در بخشهای مختلف ریاکت نیز مصداق دارد. برای نمونه، شما هرگز نباید خصوصیت state یک کامپوننت را به صورت مستقیم تغییر دهید؛ بلکه این کار باید صرفاً از طریق متد ()setState صورت بگیرد. در Redux شما هرگز نباید state را به صورت مستقیم تغییر دهید؛ بلکه این کار از طریق reducer-ها که تابعهای تعریف شده ما هستند تغییر مییابند.
اگر بخواهیم دلایل این مسئله را توضیح دهیم مهمترین موارد به صورت زیر هستند:
- تغییرپذیری میتواند به صورت متمرکز مانند حالت redux انجام یابد که موجب بهبود قابلیتهای دیباگ و کاهش منابع خطا میشود.
- در این حالت کد تمیزتر به نظر میرسد و درک آن آسانتر است. شما هرگز انتظار ندارید که یک تابع مقداری را بدون اطلاع شما تغییر دهد، چون میخواهید «قابلیت پیشبینی» (predictability) داشته باشید. هنگامی که یک تابع شیءها را تغییر ندهد؛ بلکه یک شیء جدید بازگشت دهد، این تابع یک «تابع محض» (pure function) یا در مواردی تابع خالص نیز نامیده میشود.
- کتابخانه میتواند موجب بهینهسازی کد شود، زیرا برای مثال جاوا اسکریپت زمانی که به جای تغییر شیء موجود، ارجاع یک شیء قدیمی را با شیئی کاملاً جدید عوض میکند، بسیار سریعتر میشود. بدین ترتیب بهبود عملکرد رخ میدهد.
محض بودن
در جاوا اسکریپت وقتی یک تابع شیءها را تغییر نمیدهد، بلکه صرفاً یک شیء جدید برگشت میدهد به این تابع، تابع محض گفته میشود. یک تابع یا یک متد برای این که محض نامیده شود، نباید موجب ایجاد عوارض جانبی شود و باید هر چند بار که با ورودیهای یکسان فراخوانی میشود، خروجیهای یکسانی را ارائه دهد. در واقع یک تابع محض یک ورودی میگیرد و یک خروجی بدون تغییر دادن ورودی و یا هیچ چیز دیگر بازگشت میدهد.
خروجی یک تابع محض صرفاً بر اساس آرگومانهایش مشخص میشود. اگر این تابع را 1 میلیون بار نیز فراخوانی کنیم و مجموعه آرگومانهای یکسانی به آن بدهیم، خروجی همواره همان خواهد بود. React از این مفهوم در مورد کامپوننتها استفاده میکند. یک کامپوننت ریاکت زمانی یک کامپوننت محض است که خروجی آن صرفاً به props کامپوننت وابسته باشد. همه کامپوننتهای تابعی کامپوننتهای محض هستند:
1const Button = props => {
2 return <button>{props.message}</button>
3}
کامپوننتهای کلاس در صورتی که خروجیشان تنها به props وابسته باشد، میتوانند کامپوننتهای محض باشند:
1class Button extends React.Component {
2 render() {
3 return <button>{this.props.message}</button>
4 }
5}
ترکیببندی
در برنامهنویسی، ترکیببندی امکان ساخت کارکردهای پیچیدهتر را از طریق ترکیببندی تابعهای کوچک و متمرکز فراهم میکند. برای نمونه میتوان از ()map برای ساخت یک آرایه جدید از مجموعه اولیهای استفاده کرد و سپس نتایج را با استفاده از ()filter، فیلتر کرد:
1const list = ['Apple', 'Orange', 'Egg']
2list.map(item => item[0]).filter(item => item === 'A') //'A'
در React، ترکیببندی امکان داشتن مزیتهای کاملاً جالبی را فراهم کرده است. بدین ترتیب میتوان از کامپوننتهای کوچک و روان استفاده کرده و از آنها برای ترکیببندی کارکردهای پیچیدهتر بهره گرفت.
ایجاد نسخه سفارشی از یک کامپوننت
با گسترش و تخصصی ساختن یک کامپوننت خاص، میتوان یک کامپوننت عمومیتر ساخت:
1const Button = props => {
2 return <button>{props.text}</button>
3}
4
5const SubmitButton = () => {
6 return <Button text="Submit" />
7}
8
9const LoginButton = () => {
10 return <Button text="Login" />
11}
ارسال متدها به عنوان props
برای نمونه یک کامپوننت میتواند روی ردگیری یک رویداد کلیک متمرکز شود و آنچه در عمل رخ میدهد این باشد که وقتی رویداد کلیک رخ میدهد عمل متقابل آن بر عهده کامپوننت کانتینر گذاشته شود:
1const Button = props => {
2 return <button onClick={props.onClickHandler}>{props.text}</button>
3}
4
5const LoginButton = props => {
6 return <Button text="Login" onClickHandler={props.onClickHandler} />
7}
8
9const Container = () => {
10 const onClickHandler = () => {
11 alert('clicked')
12 }
13
14 return <LoginButton onClickHandler={onClickHandler} />
15}
استفاده از فرزندان
خصوصیت props.children امکان تزریق کامپوننتها به درون کامپوننتهای دیگر را فراهم میسازد. این کامپوننت باید props.children را در JSX خود به صورت خروجی ارائه کند:
1const Sidebar = props => {
2 return <aside>{props.children}</aside>
3}
شما میتوانید کامپوننتهای بیشتری را به این ترتیب به روشی کاملاً شفاف جاسازی کنید:
1<Sidebar>
2 <Link title="First link" />
3 <Link title="Second link" />
4</Sidebar>
کامپوننتهای درجه بالا
زمانی که یک کامپوننت، کامپوننت دیگری را به عنوان prop دریافت میکند و یک کامپوننت بازگشت میدهد، این کامپوننت بازگشتی به نام کامپوننت درجه بالا نامیده میشود. این نوع کامپوننتها را در ادامه بیشتر بررسی خواهیم کرد.
DOM مجازی
بسیاری از فریمورکهای موجود که پیش از React به صحنه آمدهاند، به طور مستقیم DOM را در هر تغییر دستکاری میکنند. اگر نمیدانید DOM چیست، باید بگوییم که DOM یا «مدل شیء سند» (Document Object Model) یک بازنمایی درختی از صفحه است که از تگ <html> آغاز میشود و به سمت پایین و همه فرزندان که گره نامیده میشوند، حرکت میکند.
این DOM در حافظه مرورگر نگهداری میشود و مستقیماً با آنچه روی صفحه دیده میشود ارتباط دارد. DOM یک API دارد که میتوان آن را پیمایش کرد و به تک تک گرهها دسترسی داشت و آنها را فیلتر کرد یا تغییر داد.
این API ساختار آشنایی دارد و اگر قبلاً از API ارائه شده از سوی jQuery استفاده نکرده باشید نیز احتمالاً این ساختار را بارها دیدهاید:
document.getElementById(id) document.getElementsByTagName(name) document.createElement(name) parentNode.appendChild(node) element.innerHTML element.style.left element.setAttribute() element.getAttribute() element.addEventListener() window.content window.onload window.dump() window.scrollTo()
React یک کپی از بازنمایی DOM برای آنچه «رندرینگ ریاکت» نامیده میشود نگهداری میکند.
توضیح DOM مجازی
هر بار که DOM تغییر مییابد، مرورگر باید دو عملیات وسیع را اجرا کند: یک عملیات «رسم مجدد» (repaint) که در طی آن محتوا یا عناصر بصری روی عنصری تغییر مییابند به طوری که روی طرحبندی و ترکیببندی آن با عناصر دیگر تأثیر نمیگذارد و عملیات دیگر نیز reflow است که در طی آن طرحبندی بخشی از صفحه تغییر مییابد و یا این که طرح کلی صفحه عوض میشود.
React از DOM مجازی برای کمک به مرورگر جهت استفاده کمتر از منابع در زمان نیاز به اجرای تغییرات روی صفحه استفاده میکند. هنگامی که ()setState روی یک کامپوننت فراخوانی میشود، یک حالت متفاوت از حالت قبلی تعیین میشود و React کامپوننت را به صورت dirty معرفی میکند. دقت کنید که ریاکت تنها زمانی بهروزرسانی میشود که یک کامپوننت، حالت (state) آن را صراحتاً تغییر دهد.
سپس اتفاقهای زیر رخ میدهند:
- ریاکت، DOM مجازی را در رابطه با کامپوننتهایی که به صورت dirty علامتگذاری شدهاند به همراه برخی بررسیهای اضافی مانند ردگیری ()shouldComponentUpdate بهروزرسانی میکند.
- الگوریتم diffing اجرا میشود تا تغییرات هماهنگ شوند.
- DOM واقعی بهروزرسانی میشود.
DOM مجازی با استفاده از تجمیع تغییرات چگونه مفید واقع میشود؟
نکته کلیدی در مورد DOM مجازی این است که اغلب تغییرات تجمیع میشوند و یک بهروزرسانی یکتا روی DOM واقعی اعمال میشود و همه عناصری که باید عوض شوند به یکباره تغییر مییابند. بنابراین مرورگر باید عملیات repaint و reflow را اجرا کند تا تغییرات اجرا شده را به یک باره رندر کند.
گردش داده غیر جهتدار
هنگام کار کردن با ریاکت با اصطلاح گردش داده غیر جهتدار مواجه میشویم. اگر میخواهید معنی دقیق آن را بدانید باید بگوییم که گردش داده غیر جهتدار مفهومی منحصر به فرد در React محسوب نمیشود؛ اما توسعهدهندگان جاوا اسکریپت ممکن است نخستین باری باشد که آن را میشنوند.
این مفهوم به طور کلی بدان معنی است که دادهها، یک روش و واقعاً تنها یک روش برای انتقال به بخشهای دیگر اپلیکیشن دارند. ترجمه تعریف فوق در ریاکت به صورت زیر است:
- حالت (State) به view و سپس کامپوننتهای فرزند ارسال میشود.
- اکشنها به وسیله view تحریک میشوند.
- اکشنها میتوانند state را بهروزرسانی کنند.
- تغییرات state به view و کامپوننتهای فرزند آن ارسال میشوند.
View نتیجه state یک اپلیکیشن است. state تنها هنگامی میتواند تغییر یابد که اکشنی رخ داده باشد. زمانی که اکشن رخ دهد، state بهروزرسانی میشود. به لطف اتصالهای یکطرفه دادهها نمیتوانند در مسیر معکوس گردش یابند، چون اتصال دوطرفه وجود ندارد و این وضعیت چندین مزیت کلیدی دارد:
- امکان بروز خطا کاهش مییابد، چون کنترل بیشتری روی دادههای خود دارید.
- دیباگ کردن آسانتر است زیرا میدانید که چه چیز و از کجا میآید.
- کارایی بالاتری دارد زیرا کتابخانه از قبل میداند که کرانهای هر بخش از سیستم چه هستند.
یک state تحت مالکیت کامپوننت است. هر دادهای که تحت تأثیر این state قرار گیرد، میتواند تنها کامپوننتهای زیرش یعنی فرزندان آن را تحت تأثیر قرار دهد. تغییر دادن state روی کامپوننت هرگز والد آن یا همنیاها و یا دیگر کامپوننتهای اپلیکیشن را تحت تأثیر قرار نمیدهد. فقط فرزندان هستند که تأثیر میپذیرند.
به همین جهت است که state در اغلب موارد در درخت کامپوننتها به سمت بالا حرکت میکند، چون بدین ترتیب میتواند بین کامپوننتهایی که به آن دسترسی دارند به اشتراک گذارده شود.
بخش سوم: مفاهیم عمیق ری اکت
در ادامه و در بخش سوم به بررسی مفاهیم عمیق React میپردازیم که فهرست آنها به شرح زیر است:
- جیاسایکس (JSX)
- کامپوننتها
- حالت (State)
- Props
- کامپوننتهای ارائهای در برابر کانتینری
- State در برابر Props
- انواع Prop
- فرگمان ریاکت (React Fragment)
- رویدادها (Events)
- رویدادهای چرخه عمر (Lifecycle Events)
JSX
JSX یک فناوری معرفی شده از سوی React است.
با این که React بدون استفاده از JSX نیز کاملاً به خوبی عمل میکند؛ اما JSX یک فناوری ایدهآل برای کار با کامپوننتها محسوب میشود و از این رو در ریاکت استفاده زیادی از آن میکنیم. شاید در ابتدا به نظر بیاید که JSX مانند ترکیب کردن HTML و جاوا اسکریپت است؛ اما این حرف صحیح نیست. چون وقتی از JSX استفاده میکنیم، در واقع از یک ساختار اعلانی برای نوشتن UI یک کامپوننت بهره میگیریم. بدین ترتیب مشخص میشود که UI از رشتهها استفاده نمیکند؛ بلکه از جاوا اسکریپت استفاده میکند که کارهای جالب زیادی با آن میتوان انجام داد.
مقدمهای بر JSX
در این بخش یک تگ h1 تعریف میکنیم که شامل یک رشته است:
1const element = <h1>Hello، world!</h1>
این تگ شبیه به ترکیبی از جاوا اسکریپت و HTML است؛ اما در واقع کلاً در جاوا اسکریپت نوشته شده است. دلیل این که بخشی از آن شبیه به HTML به نظر میآید، آن است که دارای «فرم دگرگون یافته» (syntactic sugar) برای تعریف کردن کامپوننتها و قرار دادن آنها درون بخش markup است. درون یک عبارت JSX میتوان به سادگی «خصوصیات» (attributes) را درج کرد:
1const myId = 'test'
2const element = <h1 id={myId}>Hello، world!</h1>
کافی است توجه کنید هر جایی که در نام یک خصوصیت از «خط تیره» (-) استفاده شده است، باید به ساختار «حالت شتری» (camelCase) تبدیل شود و همچنین به دو نکته زیر نیز توجه داشته باشید:
- class به className تبدیل میشود.
- for به htmlFor تبدیل میشود.
چون اینها واژههای رزرو شده در جاوا اسکریپت هستند. در کد JSX زیر، دو کامپوننت درون تگ div پیچیده شدهاند:
1<div>
2 <BlogPostsList />
3 <Sidebar />
4</div>
در JSX تگها همواره باید بسته شوند، چون ساختار آنها در قیاس با HTML شباهت بیشتری به XML دارد. اگر با XHTML آشنا باشید، احتمالاً این ساختار برای شما آشنا خواهد بود؛ اما از زمان معرفی HTML5 این محدودیت چندان رعایت نمیشود. در این مورد از یک تگ self-closing استفاده شده است. اگر دقت کنید، متوجه میشوید که ما دو کامپوننت را درون یک div قرار دادهایم. دلیل این مسئله آن است که تابع ()render تنها یک گره منفرد را میتواند بازگشت دهد و از این رو در این مورد میخواهیم دو همنیا را بازگشت دهد و تنها یک والد داشته باشیم. دقت کنید که میتوان از هر تگ دیگری نیز به جز div استفاده کرد.
Transpile کردن JSX
یک مرورگر نمیتواند فایلهای جاوا اسکریپتی را اجرا کند که شامل کد JSX باشند. این کدها ابتدا باید به کد جاوا اسکریپت معمولی تبدیل شوند. به این کار Transpile کردن گفته میشود. قبلاً گفتیم که استفاده از JSX اختیاری است، زیرا هر خط JSX یک جایگزین متناظر ساده جاوا اسکریپت دارد و هنگام Transpile کردن کد به جاوا اسکریپت از این خصوصیت استفاده میکنیم. برای نمونه در ادامه دو ساختار ارائه شدهاند که معادل هم هستند:
جاوا اسکریپت ساده
1ReactDOM.render(
2 React.DOM.div(
3 { id: 'test' },
4 React.DOM.h1(null, 'A title'),
5 React.DOM.p(null, 'A paragraph')
6 ),
7 document.getElementById('myapp')
8)
کد JSX
1ReactDOM.render(
2 <div id="test">
3 <h1>A title</h1>
4 <p>A paragraph</p>
5 </div>,
6 document.getElementById('myapp')
7)
این مثال کاملاً ساده تنها یک نقطه شروع محسوب میشود؛ اما میتوانید از روی همین نمونه نیز متوجه شوید که کد جاوا اسکریپت تا چه حد پیچیدهتر از JSX است.
در حال حاضر متداولترین روش برای Transpile کردن کدهای JSX استفاده از Babel است که اگر اپلیکیشن خود را از مسیر create-react-app ساخته باشید، گزینه پیشفرض محسوب میشود. از این رو اگر از این گزینه برای ایجاد اپلیکیشن ریاکت استفاده کردهاید، جای هیچ نگرانی نیست، چون همه چیز به طور خودکار اجرا خواهد شد؛ اما اگر از create-react-app استفاده نکردهاید، باید Babel را به صوت دستی نصب و راهاندازی کنید.
جاوا اسکریپت در JSX
JSX هر نوع کد ترکیبی جاوا اسکریپت که برایش ارسال شود را میپذیرد. هر زمان که لازم باشد کدهای جاوا اسکریپت دیگری اضافه کنیم، کافی است آن را درون آکولاد قرار دهیم. برای نمونه در کد زیر شیوه استفاده از یک مقدار ثابت را میبینید که در جای دیگری تعریف شده است:
1const paragraph = 'A paragraph'
2ReactDOM.render(
3 <div id="test">
4 <h1>A title</h1>
5 <p>{paragraph}</p>
6 </div>,
7 document.getElementById('myapp')
8)
این یک مثال ساده است. آکولادها هر نوع کد جاوا اسکریپت را میپذیرند:
1const paragraph = 'A paragraph'
2ReactDOM.render(
3 <table>
4 {rows.map((row, i) => {
5 return <tr>{row.text}</tr>
6 })}
7 </div>,
8 document.getElementById('myapp')
9)
همان طور که میبینید ما کد جاوا اسکریپت را درون JSX قرار دادهایم و خود JSX نیز داخل کد جاوا اسکریپتِ تعریف شده درون JSX قرار دارد.
HTML در JSX
با این که ساختار JSX تا حدود زیادی به HTML شباهت دارد؛ اما در واقع ساختار آن شبیه به XML است. ما در نهایت کد HTML را رندر خواهیم کرد و از این رو لازم است با برخی تفاوتهای روش تعریف چیزها در HTML در برابر تعریف کردن آنها در JSX آشنا باشید.
همه تگها باید بسته شوند
همانند XHTML همه تگها در JSX باید بسته شوند. برای نمونه در صورت استفاده از <br> باید در انتها از تگ بستن <br /> استفاده شود.
از استاندارد جدید camelCase استفاده میشود
در HTML خصوصیات دارای حالت حروف کوچک هستند. برای مثال خصوصیتی مانند onchange داریم. در JSX این تگها به صورت معادل «حالت شتری» نوشته میشوند:
- onchange => onChange
- onclick => onClick
- onsubmit => onSubmit
class به className تبدیل میشود
به دلیل این واقعیت که JSX همان جاوا اسکریپت است و class یک کلمه رزرو شده محسوب میشود، نمیتوان کدی مانند زیر نوشت:
1<p class="description">
بلکه باید از کد زیر استفاده کنید:
1<p className="description">
همین موضوع در مورد for نیز صدق میکند که به صورت htmlFor ترجمه میشود.
CSS در React
JSX روشی جالب برای تعریف CSS عرضه کرده است. اگر تجربه اندکی با استایلهای درونخطی در HTML داشته باشید، در نگاه اول حس میکنید که به 10 تا 15 سال قبل پرتاب شدهاید. در آن زمان استفاده از CSS-های درونخطی امری کاملاً معمول بود. این وضعیت امروز دیگر متداول نیست و صرفاً به عنوان یک راهحل «اصلاح سریع» (Quick Fix) نگریسته میشود.
استایل JSX چنین نیست، قبل از هر چیز به جای پذیرش یک رشته شامل مشخصات CSS، خصوصیت style در JSX، یک شیء را میپذیرد. این بدان معنی است که باید مشخصات را در یک شیء تعریف کنیم:
1var divStyle = {
2 color: 'white'
3}
4ReactDOM.render(<div style={divStyle}>Hello World!</div>, mountNode)
یا به صورت زیر عمل کنیم:
1ReactDOM.render(<div style={{ color: 'white' }}>Hello World!</div>, mountNode)
مقادیر CSS که در JSX مینویسیم، اندکی متفاوت از CSS ساده هستند:
- نام مشخصات کلیدها به صورت «حالت شتری» (camelCased) هستند.
- مقادیر صرفاً به صورت رشته هستند.
- چندتاییها با کاما از هم جدا میشوند.
چرا این وضعیت به CSS / SASS / LESS ساده ترجیح داده میشود؟
CSS یک مسئله حلنشده است. از زمان ظهور آن، دهها ابزار پیرامون آن شکل گرفته و سپس نابود شدهاند. مشکل اصلی در مورد جاوا اسکریپت این است که هیچ نوع دامنهبندی ندارد و به سادگی ممکن است کد CSS بنویسیم که به هیچ ترتیبی پیادهسازی نمیشود. در این موارد، میتوان یک «اصلاح سریع» برای عناصری که تأثیر نپذیرفتهاند، نوشت.
JSX به کامپوننتها اجازه میدهد که استایل خودشان را به طور کامل کپسولهسازی کنند.
آیا این راهحل موقت است؟
استایلهای «درونخطی» (inline) در JSX در موارد زیر مفید هستند:
- نوشتن کوئریهای رسانه
- انیمیشنهای استایل
- ارجاع به شبه کلاسها (مانند hover:)
- ارجاع به شبه عنصرها (مانند first-letter::)
به طور خلاصه موارد ابتدایی را مدیریت میکنند؛ اما یک راهحل نهایی محسوب نمیشوند.
فرمها در JSX
JSX تغییراتی در طرز کار HTML ایجاد میکند و هدف آن این است که کارها برای توسعهدهنده آسانتر شود.
خصوصیتهای value و defaultValue
- خصوصیت value مقدار کنونی فیلد را نگهداری میکند.
- خصوصیت defaultValue مقدار پیشفرضی که هنگام ایجاد فیلد تعیین میشود را نگهداری میکند.
این وضعیت به حل برخی از رفتارهای عجیب در زمان کار با DOM معمولی و بررسی ('input.value و input.getAttribute('value کمک میکند و بین دو مقدار کنونی و مقدار پیشفرض فیلد تمایز قائل میشود. این وضعیت در مورد فیلد textarea نیز صدق میکند:
1<textarea>Some text</textarea>
اما به جای آن از کد زیر استفاده میکنیم:
1<textarea defaultValue={'Some text'} />
در مورد فیلدهای select به جای استفاده از کد زیر:
1<select>
2 <option value="x" selected>
3 ...
4 </option>
5</select>
از این کد استفاده میکنیم:
1<select defaultValue="x">
2 <option value="x">...</option>
3</select>
خصوصیت منسجمتر onChange
ارسال یک تابع به خصوصیت onChange باعث میشود که رویدادهایی روی فیلدهای فرم ثبت کنیم. این وضعیت به طور سازگاری روی فیلدها و حتی فیلدهای ورودی radio ،select و checkbox که یک رویداد onChange را اجرا میکنند، عمل میکند. onChange همچنین در مواردی که یک کاراکتر در فیلد input یا textarea وارد میکنید، فعال میشود.
Auto Escapes در JSX
JSX جهت کاهش ریسکهای همیشه موجود XSS، الزام میکند که از خصوصیت auto escapes در عبارتها استفاده کنیم. این به آن معنی است که وقتی گزارههای HTML را در یک عبارت رشتهای مورد استفاده قرار میدهید، با مشکلاتی مواجه خواهید شد. برای نمونه ما میخواهیم با کد زیر عبارت 2017 © را نمایش دهیم:
1<p>{'© 2017'}</p>
اما مشاهده میکنیم که عبارت copy; 2017& نمایش مییابد، زیرا رشته ما escape شده است. جهت اصلاح این وضعیت یا باید گزارههای HTML را به خارج از عبارت ببریم:
1<p>© 2017</p>
یا این که از یک ثابت استفاده کنیم که بازنمایی یونیکد متناظر با گزاره HTML را نمایش میدهد:
1<p>{'\u00A9 2017'}</p>
فاصله خالی در JSX
برای افزودن فاصله خالی در JSX دو روش وجود دارد:
قاعده 1: فاصله خالی افقی به اندازه 1 فاصله کاهش مییابد
اگر بین عناصر در یک خط، فواصل خالی وجود داشته باشد، همه آنها به یک فاصله خالی کاهش مییابند. بدین ترتیب کد زیر:
1<p>Something becomes this</p>
به صورت زیر درمیآید:
1<p>Something becomes this</p>
قاعده 2: فواصل خالی عمودی حذف میشوند
بدین ترتیب کد زیر:
1<p>
2 Something
3 becomes
4 this
5</p>
به صورت زیر درمیآید:
1<p>Somethingbecomesthis</p>
برای حل این مشکل، باید فاصلههای خالی را صریحاً با افزودن یک عبارت فاصله مانند زیر اضافه کنید:
1<p>
2 Something
3 {' '}becomes
4 {' '}this
5</p>
همچنین میتوانید رشتهای را در یک عبارت فاصله (Space) وارد کنید:
1<p>
2 Something
3 {' becomes '}
4 this
5</p>
افزودن کامنت در JSX
با استفاده از کامنتهای معمول جاوا اسکریپت درون یک عبارت میتوان کامنتهایی به کد JSX اضافه کرد:
1<p>
2 {/* a comment */}
3 {
4 //another comment
5 }
6</p>
توزیع خصوصیتها
یک عملیات متداول در JSX، انتساب مقادیر به خصوصیتها است. بدین ترتیب به جای این که این کار را به صورت دستی انجام دهیم:
1<div>
2 <BlogPost title={data.title} date={data.date} />
3</div>
میتوانیم از کد زیر استفاده کنیم:
1<div>
2 <BlogPost {...data} />
3</div>
مشخصات شیء data به لطف عملگر spread در ES6 به صورت خودکار به عنوان خصوصیتها استفاده میشود.
تعریف حلقه در JSX
اگر مجموعهای از عناصر داشته باشید و بخواهید روی آنها حلقهای اجرا کنید تا بخشی از JSX را تولید کنید، میتوانید یک loop ایجاد کنید و سپس JSX را به آرایه اضافه کنید:
1const elements = [] //..some array
2const items = []
3for (const [index, value] of elements.entries() {
4 items.push(<Element key={index} />)
5}
اینک زمانی که JSX را رندر کنید، میتوانید آرایه items را به سادگی با قرار دادن داخل آکولاد، جاسازی کنید:
1const elements = ['one', 'two', 'three'];
2const items = []
3for (const [index, value] of elements.entries() {
4 items.push(<li key={index}>{value}</li>)
5}
6return (
7 <div>
8 {items}
9 </div>
10)
همین کار را میتوان مستقیماً در JSX با استفاده از map به جای حلقه for اجرا کرد:
1const elements = ['one', 'two', 'three'];
2return (
3 <ul>
4 {elements.map((value, index) => {
5 return <li key={index}>{value}</li>
6 })}
7 </ul>
8)
کامپوننتها
یک کامپوننت در واقع بخش مجزایی از اینترفیس است. برای نمونه در صفحه اصلی یک وبلاگ معمولی میتوان کامپوننت سایدبار را مشاهده کرد و همچنین احتمالاً دارای یک کامپوننت فهرست مطالب بلاگ نیز هست. این کامپوننتها به نوبه خود از کامپوننتهای دیگری تشکیل یافتهاند و از این رو میتوان کامپوننتهایی برای فهرست مطالب وبلاگ داشت که هر مطلب خود یک کامپوننت بوده و هر یک دارای مشخصات خاصی است.
- ریاکت همه چیز را ساده ساخته است، چون در ریاکت همه چیز یک کامپوننت محسوب میشود.
- حتی تگهای ساده HTML نیز خود یک کامپوننت هستند و به صورت پیشفرض اضافه میشوند.
دو خط بعدی کد زیر معادل هم هستند و کار یکسانی انجام میدهد. یکی از آنها با JSX و دیگری بدون آن نوشته شده و <h1>Hello World!</h1> درون یک عنصر با id به نام app تزریق شده است.
1import React from 'react'
2import ReactDOM from 'react-dom'
3ReactDOM.render(<h1>Hello World!</h1>, document.getElementById('app'))
4ReactDOM.render(
5 React.DOM.h1(null, 'Hello World!'),
6 document.getElementById('app')
7)
میبینید که React.DOM یک کامپوننت h1 در اختیار ما قرار داده است. همه تگهای دیگر HTML نیز موجود هستند. آنها را میتوانید با وارد کردن React.DOM در کنسول مرورگر مشاهده کنید. کامپوننتهای داخلی جالب هستند؛ اما چندان به کار نمیآیند. نقطه قوت ریاکت در این است که به ما اجازه میدهد یک UI را با ترکیب کردن کامپوننتهای سفارشی بسازیم.
کامپوننتهای سفارشی
دو روش برای تعریف کردن یک کامپوننت در ریاکت وجود دارد:
کامپوننت تابع
1const BlogPostExcerpt = () => {
2 return (
3 <div>
4 <h1>Title</h1>
5 <p>Description</p>
6 </div>
7 )
8}
کامپوننت کلاس
1import React, { Component } from 'react'
2class BlogPostExcerpt extends Component {
3 render() {
4 return (
5 <div>
6 <h1>Title</h1>
7 <p>Description</p>
8 </div>
9 )
10 }
11}
تا همین اواخر کامپوننتهای کلاس تنها روش ما برای تعریف کردن یک کامپوننت بودند که دارای State باشد و بتواند به متدهای چرخه عمر دسترسی داشته باشد و کارهایی را در زمان رندر اولیه، بهروزرسانی یا حذف کامپوننت انجام دهد.
«قلابها» (Hooks) در ریاکت این وضعیت را تغییر دادهاند و بنابراین کامپوننتهای تابع ما اینک میتوانند بسیار قدرتمندتر از همیشه باشند و در آینده احتمالاً شاهد کامپوننتهای کلاس چندانی نخواهیم بود؛ هر چند کامپوننتهای کلاس هنوز هم روشی معتبر برای ایجاد کامپوننت خواهند بود. ساختار سومی نیز وجود دارد که از دستورهای ES6 بدون کلاس استفاده میکند:
1import React from 'react'
2React.createClass({
3 render() {
4 return (
5 <div>
6 <h1>Title</h1>
7 <p>Description</p>
8 </div>
9 )
10 }
11})
امروزه در کدهای مدرن ES6 دیگر شاهد چنین ساختارهایی نیستیم.
State
در این بخش در مورد مفهوم State در ریاکت توضیح خواهیم داد.
تعیین State پیشفرض برای یک کامپوننت
this.state را در سازنده کامپوننت مقداردهی اولیه میکنیم. برای نمونه کامپوننت BlogPostExcerpt میتواند یک State به صورت clicked داشته باشد:
1class BlogPostExcerpt extends Component {
2 constructor(props) {
3 super(props)
4 this.state = { clicked: false }
5 }
6 render() {
7 return (
8 <div>
9 <h1>Title</h1>
10 <p>Description</p>
11 </div>
12 )
13 }
14}
دسترسی به State
State به نام clicked میتواند با ارجاع به this.state.clicked مورد دسترسی قرار گیرد:
1class BlogPostExcerpt extends Component {
2 constructor(props) {
3 super(props)
4 this.state = { clicked: false }
5 }
6 render() {
7 return (
8 <div>
9 <h1>Title</h1>
10 <p>Description</p>
11 <p>Clicked: {this.state.clicked}</p>
12 </div>
13 )
14 }
15}
تغییرپذیر ساختن State
یک State را هرگز نباید با کد زیر تغییر داد:
1this.state.clicked = true
به جای آن؛ همواره باید از ()setState استفاده کرد و آن را به یک شیء ارسال کرد:
1this.setState({ clicked: true })
این شیء میتواند شامل یک زیرمجموعه یا یک ابرمجموعه (superset) از State باشد. تنها مشخصاتی که ارسال میکنید، تغییر خواهند یافت و آنهایی که نادیده گرفته شوند، در State کنونی باقی میمانند.
چرا همواره باید از ()setState استفاده کنیم؟
دلیل این مسئله آن است که در این متد، ریاکت میداند که State تغییر یافته است. سپس شروع به یک سری از رویدادها میکند که منجر به رندر گیری مجدد از کامپوننت و همچنین بهروزرسانی DOM میشود.
گردش داده غیر جهتدار
یک State همواره تحت مالکیت یک کامپوننت است. هر دادهای که از این State تأثیر بپذیرد، تنها کامپوننتهای زیرش یعنی فرزندانش را تحت تأثیر قرار میدهد.
تغییر دادن State یک کامپوننت هرگز بر والدین آن یا همنیاهایش و یا هر کامپوننت دیگر در اپلیکیشن تأثیر نمیگذارد و صرفاً فرزندانش را تحت تأثیر قرار میدهد.
به همین دلیل است که State در اغلب موارد به درخت کامپوننت منتقل میشود.
انتقال دادن State به درخت
به دلیل وجود قاعده گردش غیر جهتدار داده، اگر دو کامپوننت نیاز به اشتراک State داشته باشند، این State باید به جد مشترک آنها انتقال یابد. در اغلب موارد نزدیکترین جد، بهترین مکان برای مدیریت State است؛ اما این یک قاعده اجباری نیست. State به کامپوننتهایی که به آن نیاز دارند از طریق props ارسال میشود:
1class Converter extends React.Component {
2 constructor(props) {
3 super(props)
4 this.state = { currency: '€' }
5 }
6 render() {
7 return (
8 <div>
9 <Display currency={this.state.currency} />
10 <CurrencySwitcher currency={this.state.currency} />
11 </div>
12 )
13 }
14}
در این مرحله State میتواند به وسیله کامپوننت فرزند از طریق ارسال یک تابع تغییردهنده (mutating) به صورت یک prop تغییر یابد:
1class Converter extends React.Component {
2 constructor(props) {
3 super(props)
4 this.state = { currency: '€' }
5 }
6 handleChangeCurrency = event => {
7 this.setState({ currency: this.state.currency === '€' ? '$' : '€' })
8 }
9 render() {
10 return (
11 <div>
12 <Display currency={this.state.currency} />
13 <CurrencySwitcher
14 currency={this.state.currency}
15 handleChangeCurrency={this.handleChangeCurrency}
16 />
17 </div>
18 )
19 }
20}
21const CurrencySwitcher = props => {
22 return (
23 <button onClick={props.handleChangeCurrency}>
24 Current currency is {props.currency}. Change it!
25 </button>
26 )
27}
28const Display = props => {
29 return <p>Current currency is {props.currency}.</p>
30}
Props
Props به شیوه دریافت مشخصات از سوی کامپوننتها گفته میشود. اگر از کامپوننت فوقانی شروع کنیم، هر کامپوننت فرزند، prop-های خود را از والد دریافت میکند. در یک کامپوننت تابع، props همه آن چیزی است که ارسال میشود و با افزودن props به عنوان آرگومان تابع قابل دسترسی هستند:
1const BlogPostExcerpt = props => {
2 return (
3 <div>
4 <h1>{props.title}</h1>
5 <p>{props.description}</p>
6 </div>
7 )
8}
در یک کامپوننت کلاس، props به صورت پیشفرض ارسال میشوند. نیازی به افزودن هیچ چیز خاصی نیست و از طریق this.props در یک وهله از کامپوننت در دسترس هستند:
1import React, { Component } from 'react'
2class BlogPostExcerpt extends Component {
3 render() {
4 return (
5 <div>
6 <h1>{this.props.title}</h1>
7 <p>{this.props.description}</p>
8 </div>
9 )
10 }
11}
ارسال props به کامپوننتهای فرزند، روشی عالی برای ارسال مقادیر درون یک اپلیکیشن محسوب میشود. یک کامپوننت یا دادهها را نگهداری میکند، یعنی دارای State است و یا دادهها را از طریق props خود دریافت میکند.
در موارد زیر این وضعیت پیچیده میشود:
- اگر بخواهیم به State یک کامپوننت از یک فرزند دسترسی داشته باشیم که چند سطح پایینتر است. چون همه کامپوننتهای میانی باید پست سر گذاشته شوند، هر چند به دانستن State نیازی نداشته باشند و این موجب پیچیدهتر شدن امور میشود.
- در مواردی که نیاز باشد State یک کامپوننت از یک کامپوننت کاملاً نامرتبط گرفته شود.
مقادیر پیشفرض برای Props
اگر هر مقداری مورد نیاز نباشد، باید در صورتی که در زمان مقداردهی اولیه مقداری تعیین نشده باشد، یک مقدار پیشفرض برای آن تعیین کنیم:
1BlogPostExcerpt.propTypes = {
2 title: PropTypes.string,
3 description: PropTypes.string
4}
5BlogPostExcerpt.defaultProps = {
6 title: '',
7 description: ''
8}
برخی کیت ابزارها مانند ESLint (+) توانایی الزام تعریف کردن defaultProps برای یک کامپوننت با برخی propTypes که صریحاً مورد نیاز نیستند را فراهم ساختهاند.
Props چگونه ارسال میشوند؟
هنگامی که یک کامپوننت مقداردهی اولیه میشود، props به روشی مشابه خصوصیتهای HTML ارسال میشود:
1const desc = 'A description'
2//...
3<BlogPostExcerpt title="A blog post" description={desc} />
ما عنوان را به صورت یک رشته ساده و توضیح را به صورت یک متغیر ارسال میکنیم.
فرزندان (Children)
یک prop خاص وجود دارد که نام آن Children است. این prop شامل مقدار هر چیزی است که در body کامپوننت ارسال میشود. برای نمونه:
1<BlogPostExcerpt title="A blog post" description="{desc}">
2 Something
3</BlogPostExcerpt>
در این حالت، درون BlogPostExcerpt میتوانیم با بررسی this.props.children به Something دسترسی داشته باشیم.
با این که props به کامپوننت امکان میدهد که مشخصاتش را از والدینش دریافت کند و برای نمونه جهت نمایش برخی دادهها دستوراتی دریافت کند؛ اما State به کامپوننت اجازه میدهد که خودش حیات پیدا کند و مستقل از محیط پیرامون خودش باشد.
کامپوننتهای ارائهای در برابر کامپوننتهای کانتینر
در ریاکت، کامپوننتها غالباً به دو دسته بزرگ تقسیم میشوند که شامل «کامپوننتهای ارائهای» (Presentational) و «کامپوننتهای کانتینری» (container) هستند. هر کدام از این انواع دارای مشخصات منحصر به خود هستند.
کامپوننتهای ارائهای غالباً در مورد ایجاد برخی کدهای «نشانهگذاری» (markup) که در خروجی ارائه میشوند مورد استفاده قرار میگیرند. این کامپوننتها هیچ نوع State مگر آنها که برای ارائه استفاده میشوند را مدیریت نمیکنند.
کامپوننتهای کانتینر غالباً به عملیات «بکاند» (Back-end) مرتبط هستند. این کامپوننتها State کامپوننتهای فرعی مختلف را مدیریت میکنند. آنها میتوانند چندین کامپوننت ارائهای را در خود جای دهند. این وضعیت با استفاده از Redux قابل پیادهسازی به صورت اینترفیس است.
برای این که این تمایز را به بیانی سادهتر مطرح کنیم، میتوانیم بگوییم که کامپوننتهای ارائهای به نمای اپلیکیشن مرتبط هستند و کامپوننتهای کانتینر به طرز کار بخشهای مختلف ارتباط دارند.
برای نمونه، کامپوننت زیر از نوع ارائهای است و دادهها را از props خود میگیرد و صرفاً روی نمایش یک عنصر متمرکز است:
1const Users = props => (
2 <ul>
3 {props.users.map(user => (
4 <li>{user}</li>
5 ))}
6 </ul>
7)
از سوی دیگر کامپوننت زیر از نوع کانتینر است و به مدیریت و ذخیرهسازی دادههای خود پرداخته و از کامپوننت ارائهای برای نمایش آن استفاده میکند:
1class UsersContainer extends React.Component {
2 constructor() {
3 this.state = {
4 users: []
5 }
6 }
7 componentDidMount() {
8 axios.get('/users').then(users =>
9 this.setState({ users: users }))
10 )
11 }
12 render() {
13 return <Users users={this.state.users} />
14 }
15}
State در برابر Props
در یک کامپوننت React، منظور از props متغیرهایی هستند که از سوی کامپوننت والد ارسال میشوند. در سوی دیگر state نیز متغیر است، اما مستقیماً از سوی کامپوننت مقداردهی شده و مدیریت میشود.دقت کنید که state میتواند به وسیله props مقداردهی شود.
برای نمونه یک کامپوننت والد ممکن است کامپوننت فرزندش را با فراخوانی کد زیر شامل شود:
1<ChildComponent />
در این وضعیت، والد یک prop را با استفاده از ساختار زیر ارسال میکند:
1<ChildComponent color=green />
درون سازنده ChildComponent میتوانیم به صورت زیر به prop دسترسی داشته باشیم:
1class ChildComponent extends React.Component {
2 constructor(props) {
3 super(props)
4 console.log(props.color)
5 }
6}
و هر متد دیگر در این کلاس میتواند با استفاده از this.props به props اشاره کند. props میتوانند به صورت زیر برای تعیین state درونی بر اساس یک مقدار prop در سازنده مورد استفاده قرار گیرند:
1class ChildComponent extends React.Component {
2 constructor(props) {
3 super(props)
4 this.state.colorName = props.color
5 }
6}
البته یک کامپوننت میتواند state را بدون بررسی props آن نیز مقداردهی کند. این وضعیت فایده چندانی ندارد؛ اما تصور کنید قرار است بر اساس مقدار prop کارهای متفاوتی انجام دهید، احتمالاً تعیین مقدار یک state ، بهترین کار است. prop-ها هرگز نباید در یک کامپوننت فرزند تغییر یابند، بنابراین اگر چیزی در آن باشد که بخواهد متغیری را تغییر دهد، آن متغیر باید به state کامپوننت تعلق داشته باشد.
prop-ها میتوانند برای ایجاد امکان دسترسی کامپوننتهای فرزند به متدهای تعریف شده در کامپوننت والد نیز مورد استفاده قرار گیرند. این یک روش مناسب برای مدیریت متمرکز state در کامپوننت والد است. در این وضعیت دیگر لازم نیست که کامپوننتهای فرزند state خاصی برای خود داشته باشند.
اغلب کامپوننتها صرفاً نوعی اطلاعات را بر مبنای prop-ی که دریافت کردهاند نمایش میدهند و از این رو «بیحالت» (stateless) هستند.
انواع Prop
از آنجا که جاوا اسکریپت زبانی با نوعبندی دینامیک است، در عمل نیازی به داشتن روشی برای الزام تعیین نوع یک متغیر در زمان کامپایل نداریم و اگر از انواع نادرستی استفاده کنیم، صرفاً در زمان اجرا با مشکل مواجه میشویم. در صورتی که انواع ارسالی سازگار اما غیر مورد انتظار باشند، نتایج عجیبی در زمان اجرا به دست خواهد آمد.
در این زمینه Flow و TypeScrip کمک زیادی میکنند؛ اما ریاکت روشی برای کمک مستقیم در مورد انواع props دارد و این کار را حتی پیش از اجرای کد انجام میدهد. ابزارهای ما (ویرایشگرها و linter-ها میتوانند زمانی که مقادیر نادرستی ارسال میکنیم، آن را تشخیص دهند:
1import PropTypes from 'prop-types'
2import React from 'react'
3class BlogPostExcerpt extends Component {
4 render() {
5 return (
6 <div>
7 <h1>{this.props.title}</h1>
8 <p>{this.props.description}</p>
9 </div>
10 )
11 }
12}
13BlogPostExcerpt.propTypes = {
14 title: PropTypes.string,
15 description: PropTypes.string
16}
17export default BlogPostExcerpt
از چه نوعهایی میتوانیم استفاده کنیم؟
در ادامه انواع بنیادینی که میتوانیم بپذیریم را مشاهده میکنید:
- PropTypes.array
- PropTypes.bool
- PropTypes.func
- PropTypes.number
- PropTypes.object
- PropTypes.string
- PropTypes.symbol
ما میتوانیم یکی از دو نوع زیر را بپذیریم:
1PropTypes.oneOfType([
2 PropTypes.string,
3 PropTypes.number
4]),
ما میتوانیم یکی از مقادیر متعدد را بپذیریم:
1PropTypes.oneOf(['Test1'، 'Test2'])،
ما میتوانیم یک وهله از کلاس را بپذیریم:
1PropTypes.instanceOf(Something)
ما میتوانیم هر گره React را بپذیریم:
1PropTypes.node
یا حتی به طور کلی هر نوعی را بپذیریم:
1PropTypes.any
آرایهها ساختار خاصی دارند که میتوانیم برای پذیرش یک آرایه از نوع خاص استفاده کنیم:
1PropTypes.arrayOf(PropTypes.string)
ما میتوانیم مشخصات شیء را با استفاده از کد زیر ترکیب کنیم:
1PropTypes.shape({
2 color: PropTypes.string,
3 fontSize: PropTypes.number
4})
الزام مشخصات
الصاق به هر گزینه PropTypes موجب خواهد شد که ریاکت در موارد فقدان آن مشخصه، یک خطا بازگشت دهد:
1PropTypes.arrayOf(PropTypes.string).isRequired,
2PropTypes.string.isRequired,
فرگمان React
دقت کنید که چگونه مقادیر بازگشتی را در یک div قرار میدهیم. دلیل این امر آن است که یک کامپوننت میتواند صرفاً یک عنصر منفرد بازگشت دهد و اگر بخواهید بیش از یکی داشته باشید، باید آن را درون یک تگ کانتینر دیگر قرار دهید. با این وجود، این وضعیت منجر به وجود یک div غیر ضروری در خروجی میشود. شما میتوانید با استفاده از React.Fragment از این وضعیت اجتناب کنید:
1import React, { Component } from 'react'
2class BlogPostExcerpt extends Component {
3 render() {
4 return (
5 <React.Fragment>
6 <h1>{this.props.title}</h1>
7 <p>{this.props.description}</p>
8 </React.Fragment>
9 )
10 }
11}
ضمناً این وضعیت ساختار خلاصه بسیار زیبایی به صورت </><> دارد که صرفاً در نسخههای اخیر و Babel 7 به بعد پشتیبانی میشود:
1import React, { Component } from 'react'
2class BlogPostExcerpt extends Component {
3 render() {
4 return (
5 <>
6 <h1>{this.props.title}</h1>
7 <p>{this.props.description}</p>
8 </>
9 )
10 }
11}
رویدادها (Events)
ریاکت یک روش آسان برای مدیریت رویدادها ارائه کرده است. بدین ترتیب دیگر باید با addEventListener خداحافظی کنیم. در بخش قبلی در مورد state، مثال زیر را مشاهده کردیم:
1const CurrencySwitcher = props => {
2 return (
3 <button onClick={props.handleChangeCurrency}>
4 Current currency is {props.currency}. Change it!
5 </button>
6 )
7}
اگر تاکنون از جاوا اسکریپت استفاده کرده باشید، متوجه میشوید که این کد تا حدود زیادی شبیه به «دستگیرههای رویداد» (event handlers) در جاوا اسکریپت است، به جز این که اقدام به تعریف کردن همه چیز در جاوا اسکریپت میکنیم و با HTML کاری نداریم و یک تابع؛ و نه یک رشته را ارسال میکنیم. نامهای رویداد واقعی کمی متفاوت هستند، چون در ریاکت از حالت شتری برای همه چیز استفاده میکنیم و از این رو onclick به onClick و onsubmit به onSubmit تبدیل میشوند.
صرفاً جهت اطلاع، باید بگوییم که این یک HTML به سبک قدیم است که با رویدادهای جاوا اسکریپت همراه شده است:
1<button onclick="handleChangeCurrency()">...</button>
Event Handler
این یک قرارداد مرسوم است که دستگیرههای رویداد را به صورت متدهایی روی کلاس کامپوننت تعریف کنیم:
1class Converter extends React.Component {
2 handleChangeCurrency = event => {
3 this.setState({ currency: this.state.currency === '€' ? '$' : '€' })
4 }
5}
همه دستگیرهها یک شیء رویداد را دریافت میکنند که به آن الصاق شوند و این وضعیت بر اساس دستورالعمل «W3C UI Events» (+) یک خصوصیت بین مرورگری است.
اتصال this در متدها
اگر از کامپوننتهای کلاس استفاده کنید، نباید متدهای bind را فراموش کنید. متدهای کلاسهای ES6 به صورت پیشفرض bind نشدهاند. این بدان معنی است که this تعریف نشده است؛ مگر این که متدهایی به صورت تابعهای Arrow تعریف کنید:
1class Converter extends React.Component {
2 handleClick = e => {
3 /* ... */
4 }
5 //...
6}
این وضعیت زمانی که از ساختار مقداردهی مشخصه به همراه Babel استفاده میکنیم مورد نیاز است. در موارد دیگر باید به صورت دستی در سازنده آن را «متصل» (bind) کرد:
1class Converter extends React.Component {
2 constructor(props) {
3 super(props)
4 this.handleClick = this.handleClick.bind(this)
5 }
6 handleClick(e) {}
7}
مرجع رویدادها
رویدادهای زیادی در ریاکت پشتیبانی میشوند و در ادامه یک فهرست خلاصه از آنها ارائه میکنیم:
کلیپ بورد
- onCopy
- onCut
- onPaste
ترکیببندی
- onCompositionEnd
- onCompositionStart
- onCompositionUpdate
صفحهکلید
- onKeyDown
- onKeyPress
- onKeyUp
فوکوس
- onFocus
- onBlur
فرم
- onChange
- onInput
- onSubmit
ماوس
- onClick
- onContextMenu
- onDoubleClick
- onDrag
- onDragEnd
- onDragEnter
- onDragExit
- onDragLeave
- onDragOver
- onDragStart
- onDrop
- onMouseDown
- onMouseEnter
- onMouseLeave
- onMouseMove
- onMouseOut
- onMouseOver
- onMouseUp
انتخاب
- onSelect
لمس
- onTouchCancel
- onTouchEnd
- onTouchMove
- onTouchStart
رابط کاربری
- onScroll
چرخ ماوس
- onWheel
رسانه
- onAbort
- onCanPlay
- onCanPlayThrough
- onDurationChange
- onEmptied
- onEncrypted
- onEnded
- onError
- onLoadedData
- onLoadedMetadata
- onLoadStart
- onPause
- onPlay
- onPlaying
- onProgress
- onRateChange
- onSeeked
- onSeeking
- onStalled
- onSuspend
- onTimeUpdate
- onVolumeChange
- onWaiting
تصویر
- onLoad
- onError
انیمیشن
- onAnimationStart
- onAnimationEnd
- onAnimationIteration
گذار (Transition)
- onTransitionEnd
رویدادهای چرخه عمر
کامپوننتهای کلاس ریاکت میتوانند برای چندین رویداد چرخه عمر قلاب داشته باشند.
قلابها به کامپوننتهای تابع امکان دسترسی به روشی متفاوت را میدهند. در طی چرخه عمر یک کامپوننت یک سری از رویدادها هستند که فراخوانی میشوند و برای هر رویداد میتوانید قلابی داشته باشید که کارکردهای سفارشی خاصی را ارائه کند. در ادامه بررسی میکنیم که کدام قلاب برای کدام کارکرد مناسبتر است. ابتدا باید اشاره کنیم که در چرخه عمر کامپوننتهای ریاکت سه مرحله داریم:
- فعال شدن (Mounting)
- بهروزرسانی (Updating)
- غیر فعال شدن (Unmounting)
Mounting
در زمان فعال شدن کامپوننت با 4 متد چرخه عمر مواجه هستیم که پیش از این که کامپوننت روی DOM سوار شود فعال میشوند: constructor ،getDerivedStateFromProps ،render و componentDidMount.
- سازنده (Constructor)
این نخستین متدی است که هنگام فعال شدن کامپوننت فراخوانی میشود.
ما معمولاً از سازنده برای راهاندازی State اولیه با استفاده از ...= this.state بهره میگیریم.
- ()getDerivedStateFromProp
هنگامی که State به props وابسته باشد، getDerivedStateFromProps میتواند برای بهروزرسانی State بر مبنای مقدار props مورد استفاده قرار گیرد. این متد در نسخه 16.3 ریاکت و باهدف جایگزینی برای متد منسوخ componentWillReceiveProps عرضه شده است.
در این متد ما به this دسترسی نداریم، زیرا یک متد استاتیک است. از سوی دیگر این یک متد «محض» (pure) است و از این رو نباید موجب هیچ گونه عارضه جانبی شود و همچنین نباید هنگامی که چندین بار با ورودیهای یکسان فراخوانی میشود، خروجیهای متفاوتی ارائه دهد. این متد یک شیء با عناصر بهروزرسانی شده State بازگشت میدهد. در حالتی نیز که حالت تغییر نیافته باشد، تهی خواهد بود.
- ()render
این متد JSX را بازگشت میدهد که اینترفیس کامپوننت را میسازد. این متد نیز یک متد محض است و از این رو موجب هیچ عوارض جانبی نشده و در موارد فراخوانی مکرر با ورودیهای یکسان، خروجیهای یکسانی ارائه خواهد کرد.
- ()componentDidMount
این متد برای اجرای فراخوانیهای API مورد استفاده قرار میگیرد یا عملیات روی DOM را مورد پردازش قرار میدهد.
مرحله به روز رسانی (Updating)
زمانی که مشغول بهروزرسانی کامپوننت هستیم 5 متد چرخه عمر وجود دارد که پیش از فعال شدن کامپوننت در DOM تعریف شدهاند: getDerivedStateFromProps، shouldComponentUpdate، render، getSnapshotBeforeUpdate و componentDidUpdate.
- ()getDerivedStateFromProps
در بخش قبل در این مورد توضیح داده شد.
- ()shouldComponentUpdate
این متد یک مقدار بولی به صورت true یا false بازگشت میدهد. از این متد در مواردی استفاده میکنیم که میخواهیم به ریاکت بگوییم باید اقدام به رندرگیری بکند یا نه و مقدار پیشفرض آن true است. هنگامی که رندرگیری پرهزینه باشد و یا کنترل بیشتری در مورد زمان وقوع آن مورد نیاز باشد از مقدار false استفاده میشود.
- ()render
در بخش قبل در این مورد توضیح داده شد.
- ()getSnapshotBeforeUpdate
در این متد ما به props و State رندر قبلی و همچنین رندر کنونی دسترسی پیدا میکنیم. موارد استفاده از این متد معدود هستند و جزو متدهایی هستند که احتمالاً سر و کار شما بسیار کم به آن خواهد افتاد.
- ()componentDidUpdate
این متد زمانی فراخوانی میشود که کامپوننت در DOM بهروزرسانی شده باشد. از این متد برای اجرای API شخص ثالث DOM که باید در هنگام تغییرات DOM بهروزرسانی شود استفاده میکنیم. این متد متناظر با متد ()componentDidMount در مرحله فعالسازی کامپوننت است.
غیر فعالسازی کامپوننت (Unmounting)
در این مرحله ما تنها یک متد داریم که componentWillUnmount است و در ادامه توضیح دادهایم:
- ()componentWillUnmount
این متد زمانی فراخوانی میشود که کامپوننت از DOM حذف شود. از این متد برای اجرای هر نوع پاکسازی مورد نیاز، استفاده میشود.
دقت کنید که اگر از هر یک از متدهای componentWillMount ،componentWillReceiveProps یا componentWillUpdate استفاده میکنید، این موارد در نسخه 16.3 منسوخ شدهاند و باید به متدهای جدیدتر چرخه عمر مراجعه کنید.
فرمها در ریاکت
فرمها یکی از معدود عناصر HTML هستند که به صورت پیشفرض تعاملپذیر هستند. فرمها به این منظور طراحی شدهاند که به کاربر امکان تعامل با صفحه را بدهند. در ادامه موارد استفاده رایج از فرمها را ارائه کردهایم:
- جستجو
- فرم تماس
- فرم سبد خرید
- فرم ورود و ثبتنام
- و موارد دیگر
ما با استفاده از ریاکت میتوانیم فرمهای خود را تعاملپذیرتر کنیم و از جنبههای استاتیک آنها بکاهیم. دو روش عمده برای مدیریت فرمها در ریاکت وجود دارد که دارای تفاوتهای بنیادی در زمینه شیوه مدیریت دادهها هستند.
- اگر دادهها از سوی DOM مدیریت شوند، ما آنها را کامپوننتهای «کنترل نشده» مینامیم.
- اگر دادههای از سوی کامپوننتها مدیریت شوند، آنها را کامپوننتهای «کنترل شده» مینامیم.
همان طور که احتمالاً حدس میزنید، کامپوننتهای کنترل شده آن چیزی هستند که در اغلب اوقات مورد استفاده قرار میگیرند. در این وضعیت State کامپوننت به جای DOM، منبع منفرد «حقیقت» و مبنای عمل ما است. برخی فیلدهای فرم مانند فیلد <"input type="file> به دلیل نوع رفتارشان دارای ماهیت کنترل نشده هستند.
زمانی که تغییرات State یک عنصر در فیلد فرم از سوی کامپوننت مدیریت میشود، این تغییرات را با استفاده از خصوصیت onChane ردگیری میکنیم.
1class Form extends React.Component {
2 constructor(props) {
3 super(props)
4 this.state = { username: '' }
5 }
6 handleChange(event) {}
7 render() {
8 return (
9 <form>
10 Username:
11 <input
12 type="text"
13 value={this.state.username}
14 onChange={this.handleChange}
15 />
16 </form>
17 )
18 }
19}
BIND
جهت تعیین State جدید، باید this را به متد handleChange متصل (bind) کنیم. در غیر این صورت this از درون متد قابل دسترسی نخواهد بود:
1class Form extends React.Component {
2 constructor(props) {
3 super(props)
4 this.state = { username: '' }
5 this.handleChange = this.handleChange.bind(this)
6 }
7 handleChange(event) {
8 this.setState({ value: event.target.value })
9 }
10 render() {
11 return (
12 <form>
13 <input
14 type="text"
15 value={this.state.username}
16 onChange={this.handleChange}
17 />
18 </form>
19 )
20 }
21}
به طور مشابه باید از خصوصیت onSubmit برای فراخوانی متد handleSubmit در موارد تحویل فرم استفاده کنیم:
1class Form extends React.Component {
2 constructor(props) {
3 super(props)
4 this.state = { username: '' }
5 this.handleChange = this.handleChange.bind(this)
6 this.handleSubmit = this.handleSubmit.bind(this)
7 }
8 handleChange(event) {
9 this.setState({ value: event.target.value })
10 }
11 handleSubmit(event) {
12 alert(this.state.username)
13 event.preventDefault()
14 }
15 render() {
16 return (
17 <form onSubmit={this.handleSubmit}>
18 <input
19 type="text"
20 value={this.state.username}
21 onChange={this.handleChange}
22 />
23 <input type="submit" value="Submit" />
24 </form>
25 )
26 }
27}
اعتبارسنجی
«اعتبارسنجی» (Validation) یک فرم از طریق متد handleChange ممکن است. در این مورد به مقدار قبلی و همچنین مقدار فعلی State دسترسی داریم. میتوان State جدید را بررسی کرد و در صورتی که معتبر نباشد مقدار بهروزرسانی شده را رد کرد. این موضوع باید به طریقی به اطلاع کاربر برسد.
فرمهای HTML ناسازگار هستند. آنها تاریخچهای طولانی دارند. با این وجود ریاکت آنها را برای ما سازگارتر ساخته است و میتوان فیلدها را با استفاده از خصوصیت value دریافت و بهروزرسانی کرد. در ادامه یک فیلد textarea را برای نمونه مشاهده میکنید:
1<textarea value={this.state.address} onChange={this.handleChange} />
وضعیت مشابهی در مورد تگ select وجود دارد:
1<select value="{this.state.age}" onChange="{this.handleChange}">
2 <option value="teen">Less than 18</option>
3 <option value="adult">18+</option>
4</select>
ما قبلاً به فیلد <"input type="file> اشاره کردیم. طرز کار آن اندکی متفاوت است.
در این حالت باید با انتساب خصوصیت ref به یک مشخصه تعریف شده در سازنده با ()React.createRef یک ارجاع به فیلد داشته باشیم و از آن برای دریافت مقدار آن در دستگیره رویداد استفاده کنیم:
1class FileInput extends React.Component {
2 constructor(props) {
3 super(props)
4 this.curriculum = React.createRef()
5 this.handleSubmit = this.handleSubmit.bind(this)
6 }
7 handleSubmit(event) {
8 alert(this.curriculum.current.files[0].name)
9 event.preventDefault()
10 }
11 render() {
12 return (
13 <form onSubmit={this.handleSubmit}>
14 <input type="file" ref={this.curriculum} />
15 <input type="submit" value="Submit" />
16 </form>
17 )
18 }
19}
این همان طرز کار کامپوننتهای کنترل نشده است. در این وضعیت، State به جای خود کامپوننت در DOM ذخیره میشود. دقت کنید که ما از this.curriculum برای دسترسی به فایل آپلود شده استفاده کردهایم و به state دست نزدهایم.
شاید با خود فکر کنید که به جز این مبانی مطرح شده باید کتابخانهای وجود داشته باشد که مدیریت این موارد و خودکارسازی اعتبارسنجی، مدیریت خطا و موارد دیگر را بر عهده بگیرد. البته چنین کتابخانهای وجود دارد و Formik (+) نام دارد.
ارجاع به یک عنصر DOM
ریاکت از این جهت که هنگام ساخت اپلیکیشن موجب انتزاع شما از DOM میشود، بسیار عالی است. اما اگر بخواهیم به عنصر DOM-ی که یک کامپوننت ریاکت نمایش میدهد دسترسی داشته باشیم چطور؟
در این موارد احتمالاً باید یک کتابخانه اضافه کنیم که به صورت مستقیم با DOM مانند یک کتابخانه chart تعامل داشته باشد و شاید برخی API-های DOM را فراخوانی کرده و روی یک عنصر فوکوس اضافه کند. دلیل شما هر چه که باشد، باید مطمئن شوید که هیچ راه دیگری برای این کار به جز دسترسی مستقیم وجود ندارد. در بخش JSX هر کامپوننت، میتوان با استفاده از خصوصیت زیر، ارجاعی به عنصر DOM روی مشخصه کامپوننت اضافه کرد:
1ref={el => this.someProperty = el}
برای نمونه این کد را میتوان روی یک عنصر button اضافه کرد:
1<button ref={el => (this.button = el)} />
در این جا button به یک مشخصه کامپوننت اشاره میکند که میتواند متعاقباً از سوی متدهای چرخه عمر کامپوننت یا متدهای دیگر برای تعامل با DOM مورد استفاده قرار گیرد:
1class SomeComponent extends Component {
2 render() {
3 return <button ref={el => (this.button = el)} />
4 }
5}
در یک کامپوننت تابع نیز سازوکار مشابهی وجود دارد. کافی است از استفاده از this اجتناب کنید، چون به وهلهای از کامپوننت اشاره نمیکند و به جای آن از یک «مشخصه» (property) استفاده کنید:
1function SomeComponent() {
2 let button
3 return <button ref={el => (button = el)} />
4}
رندرگیری سمت سرور
رندرینگ سمت سرور که به نام SSR نیز شناخته میشود، به قابلیت یک اپلیکیشن جاوا اسکریپت برای رندر کردن در سمت سرور به جای انجام این کار در سمت مرورگر گفته میشود.
چرا باید بخواهیم از رندر سمت سرور استفاده کنیم؟
- رندرگیری سمت سرور امکان افزایش سرعت بارگذاری صفحه که یکی از کلیدهای بهبود تجربه کاربری است را فراهم میسازد.
- این امکان برای سئو بسیار مهم است، چون موتورهای جستجو نمیتوانند اپلیکیشنهایی را که به صورت انحصاری در سمت کلاینت رندر میشوند به طرز مؤثری اندیسگذاری بکنند. علیرغم بهبودهای اخیری که در فرایند اندیسگذاری موتور جستجوی گوگل ایجاد شده است؛ اما موتورهای جستجوی دیگری نیز وجود دارند و گوگل نیز در انجام این کار چندان عالی عمل نمیکند. ضمناً گوگل به وبسایتهایی که سریعتر بارگذاری میشوند اهمیت بیشتری میدهد و رندر شدن در سمت کاربر موجب کندتر شدن سرعت بارگذاری صفحه میشود.
- رندرگیری سمت سرور در مواردی که افراد صفحهای از وبسایت را روی رسانههای اجتماعی به اشتراک میگذارند مناسب خواهد بود، زیرا میتوان فراداده مورد نیاز شامل تصاویر، عنوان، توضیح و موارد دیگر که برای اشتراک مناسبتر صفحه لازم هستند را از سرور گرفت.
بدون وجود رندرگیری سمت سرور، تنها چیزی که سرور به کاربر ارسال میکند، یک صفحه HTML بدون هیچ متنی است و صرفاً شامل تگهای اسکریپت هستند که متعاقباً از سوی مرورگر کاربر برای رندر کردن اپلیکیشن مورد استفاده قرار میگیرند.
اپلیکیشنهایی که در سمت کلاینت رندر میشوند، پس از بارگذاری اولیه، در موارد استفاده بعدی عملکردی بسیار عالی دارند. رندرینگ سمت سرور به ما امکان میدهد که بتوانیم نقطه تعادل مناسبی بین اپلیکیشنهای با رندر سمت سرور و اپلیکیشنهای دارای رندر سمت کلاینت داشته باشیم. بدین ترتیب یک صفحه در سمت سرور تولید میشود؛ اما زمانی که در سمت کاربر بارگذاری شد، همه تعاملها با صفحه در آن سمت مدیریت میشوند.
با این وجود رندرگیری سمت سرور معایبی نیز دارد:
- تثبیت مفهوم رندرگیری سمت سرور کار نسبتاً آسانی است؛ اما پیچیدگی رندرگیری سمت سرور با افزایش پیچیدگی اپلیکیشن بالاتر میرود.
- رندرگیری یک اپلیکیشن بزرگ در سمت سرور میتواند به منابع زیادی نیاز داشته باشد و در موارد وجود بار زیاد روی سرور، تجربه کُندی نسبت به رندرگیری سمت کاربر ارائه کند، چون با یک نقطه «تنگنای منفرد» (single bottleneck) مواجه هستیم.
مثالی ساده از رندرگیری سمت سرور یک اپلیکیشن ریاکت
تنظیمات رندرگیری سمت سرور ممکن است کاملاً پیچیده باشند و در اغلب راهنماها از همان ابتدا از Redux ،React Router و احتمالاً مفاهیم دیگر استفاده میشود. برای درک طرز کار رندرگیری سمت سرور، توضیح خود را از مبانی مقدماتی این مفهوم آغاز میکنیم.
برای پیادهسازی «SSR» (رندر گیری سمت سرور) در مراحل مقدماتی از Express استفاده میکنیم. دقت کنید که پیچیدگی SSR با افزایش پیچیدگی اپلیکیشن بالاتر میرود. در ادامه یک تنظیمات کمینه برای رندر کردن یک اپلیکیشن ساده ریاکت ارائه شده است. در مورد نیازهای پیچیدهتر باید کار بیشتری انجام دهید و به کتابخانههای بیشتری برای ریاکت هم نیاز خواهید داشت. ما فرض میکنیم که شما یک اپلیکیشن ریاکت را با استفاده از create-react-app آغاز کردهاید. اگر تازه مطالعه این راهنما را آغاز کردهاید، میتوانید یک اپلیکیشن جدید را با استفاده از npx create-react-app ssr نصب کنید.
در ترمینال به پوشه اصلی اپلیکیشن بروید و سپس دستور زیر را اجرا کنید:
npm install express
اکنون در دایرکتوری app خود مجموعهای از پوشهها دارید. یک پوشه جدید به نام server ایجاد کنید و سپس به درون آن رفته و فایلی به نام server.js بسازید.
با پیروی از قراردادهای مرسوم create-react-app، این اپلیکیشن در فایل src/App.js قرار دارد. ما قصد داریم این کامپوننت را بارگیری بکنیم و آن را با استفاده از ()ReactDOMServer.renderToString که از سوی react-dom عرضه شده است، در یک رشته رندر کنیم.
در این وضعیت، محتوای فایل build/index.html/. را دریافت میکنید و <div id="root"></div> را جایگزین کنید. این یک تگ است که اپلیکیشن در آن به صورت پیشفرض به صورت زیر قلاب میشود:
1 `<div id="root">\${ReactDOMServer.renderToString(<App />)}</div>
Express
همه محتوای درون پوشه build به صورت موجود و استاتیک از سوی Express عرضه میشود.
1import path from 'path'
2import fs from 'fs'
3import express from 'express'
4import React from 'react'
5import ReactDOMServer from 'react-dom/server'
6import App from '../src/App'
7const PORT = 8080
8const app = express()
9const router = express.Router()
10const serverRenderer = (req, res, next) => {
11 fs.readFile(path.resolve('./build/index.html'), 'utf8', (err, data) => {
12 if (err) {
13 console.error(err)
14 return res.status(500).send('An error occurred')
15 }
16 return res.send(
17 data.replace(
18 '<div id="root"></div>',
19 `<div id="root">${ReactDOMServer.renderToString(<App />)}</div>`
20 )
21 )
22 })
23}
24router.use('^/$', serverRenderer)
25router.use(
26 express.static(path.resolve(__dirname, '..', 'build'), { maxAge: '30d' })
27)
28// tell the app to use the above rules
29app.use(router)
30// app.use(express.static('./build'))
31app.listen(PORT, () => {
32 console.log(`SSR running on port ${PORT}`)
33})
اکنون در اپلیکیشن کلاینت و در فایل src/index.js به جای فراخوانی ()ReactDOM.render به صورت زیر:
1ReactDOM.render(<App />، document.getElementById('root'))
متد ()ReactDOM.hydrate را فراخوانی کنید که مشابه همان است، اما این قابلیت اضافی را دارد که در زمان بارگیری React به «شنوندههای رویداد» (event listeners) موجود در markup اتصال یابد:
1ReactDOM.hydrate(<App />، document.getElementById('root'))
Transpile کردن JSX
همه کد Node.js باید از سوی Babel، ترجمه شود، چون کد Node.js سمت سرور هیچ چیز در مورد JSX و همچنین ماژولهای ES نمیداند. سه بسته زیر را نصب کنید:
1npm install @babel/register @babel/preset-env @babel/preset-react ignore-styles express
ignore-styles یک ابزار Babel است که به این منظور استفاده میشود تا بگوییم باید فایلهای CSS ایمپورت شده با استفاده از ساختار import نادیده گرفته شوند. در ادامه یک نقطه ورودی در فایل server/index.js ایجاد میکنیم:
1require('ignore-styles')
2require('@babel/register')({
3 ignore: [/(node_modules)/],
4 presets: ['@babel/preset-env', '@babel/preset-react']
5})
6require('./server')
اپلیکیشن React را Build میکنیم به طوری که پوشه build/ ایجاد شود:
npm run build
سپس دستور زیر را اجرا میکنیم:
node server/index.js
قبلاً گفتیم که این رویکرد ساده خواهد بود و واقعاً هم چنین است:
- در این رویکرد تصاویر رندر شده در زمان استفاده از import-ها به درستی مدیریت نمیشوند، چون برای این منظور به Webpack نیاز داریم که به میزان زیادی بر پیچیدگی فرایند میافزاید.
- این رویکرد به مدیریت فراداده هدر صفحه نمیپردازد و این مسئله برای سئو و مقاصد اشتراک روی رسانهها ضروری است.
بنابراین گرچه مثال فوق برای توضیح استفاده از ()ReactDOMServer.renderToString و ReactDOM.hydrate برای اجرای رندرینگ سمت سرور مناسب است؛ اما برای کاربردهای واقعی چندان مناسب نیست.
رندرگیری سمت سرور با استفاده از کتابخانه
اجرای صحیح SSR کار دشواری است و React روش پیشفرضی برای پیادهسازی آن ندارد. هنوز در مورد این که آیا این همه دردسر، پیچیدگی و هزینه بالاسری در برابر مزیتهایش میارزد یا نه بحث زیادی وجود دارد و برخی نیز استفاده از فناوریهای جایگزین برای ارائه صفحه را پیشنهاد میکنند. با این که رندرگیری سمت سرور موضوع مهمی است؛ اما پیشنهاد ما این است که شما بیشتر روی کتابخانههای پیشساخته و ابزارهایی که از ابتدا این موضوع را در ذهن خود دارند تکیه کنید. به طور خاص دو پروژه Next.js و Gatsby توصیه میشود که در ادامه این سری مقالات در مورد آنها بیشتر خواهیم خواند.
API زمینه
API زمینه روشی عالی برای ارسال State درون اپلیکیشن بدون نیاز به استفاده از props است. این امکان به این منظور معرفی شده است که بتوان State را ارسال کرد و امکان بهروزرسانی State را درون اپلیکیشن بدون نیاز به استفاده از props ایجاد کرد.
تیم ریاکت پیشنهاد میکنند که اگر قرار است چیزی صرفاً در چند سطح معدود درون اپلیکیشن ارسال شود، بهتر است همچنان از props استفاده کنیم، زیرا نسبت به API زمینه پیچیدگی کمتری دارد. در موارد زیادی API زمینه به ما امکان میدهد که از Redux اجتناب کنیم و موجب سادهسازی زیادی در اپلیکیشن میشود. همچنین باعث میشود در مورد طرز استفاده از React اطلاعات زیادی کسب کنیم.
طرز کار API زمینه چگونه است؟
یک زمینه (context) با استفاده از ()React.createContext ایجاد میکنیم که یک شیء زمینه را بازگشت میدهد:
1const { Provider، Consumer } = React.createContext()
سپس یک کامپوننت پوششی (wrapper component) میسازیم که یک کامپوننت Provider بازمیگرداند و همه کامپوننتهایی که قصد دارید از آنها به زمینه دسترسی داشته باشید را به عنوان فرزند آن اضافه میکنید:
1class Container extends React.Component {
2 constructor(props) {
3 super(props)
4 this.state = {
5 something: 'hey'
6 }
7 }
8 render() {
9 return (
10 <Provider value={{ state: this.state }}>{this.props.children}</Provider>
11 )
12 }
13}
14class HelloWorld extends React.Component {
15 render() {
16 return (
17 <Container>
18 <Button />
19 </Container>
20 )
21 }
22}
ما برای نام این کانتینر از Container استفاده کردیم، زیرا یک Provider سراسری است و میتوانید با آن زمینههای کوچکتر نیز بسازید. درون یک کامپوننت که در یک Provider پیچیده شده است، از یک کامپوننت Consumer استفاده کنید تا بتوانید از یک زمینه بهرهمند شوید:
1class Button extends React.Component {
2 render() {
3 return (
4 <Consumer>
5 {context => <button>{context.state.something}</button>}
6 </Consumer>
7 )
8 }
9}
همچنین میتوانید تابعها را به یک مقدار Provider ارسال کنید و این تابعها از سوی مصرفکننده برای بهروزرسانی State زمینه استفاده خواهند شد:
1<Provider value={{
2 state: this.state,
3 updateSomething: () => this.setState({something: 'ho!'})
4 {this.props.children}
5</Provider>
6/* ... */
7<Consumer>
8 {(context) => (
9 <button onClick={context.updateSomething}>{context.state.something}</button>
10 )}
11</Consumer>
استفاده عملی از این state را میتوانید در این Glitch (+) مشاهده کنید. میتوان context–های متعددی را ایجاد کرد تا state روی کامپوننتهای مختلف توزیع یابد. در این وضعیت، state از سوی هر کدام از کامپوننتها قابل دسترسی است. هنگامی که از فایلهای چندگانه استفاده میکنید، محتوا در یک فایل ایجاد میشود و در همه مکانهایی که مورد نیاز است مورد استفاده قرار میگیرد:
1//context.js
2import React from 'react'
3export default React.createContext()
4//component1.js
5import Context from './context'
6//... use Context.Provider
7//component2.js
8import Context from './context'
9//... use Context.Consumer
- برای دیدن فیلم آموزش مقدماتی کتابخانه ReactJS در جاوا اسکریپت + اینجا کلیک کنید.
کامپوننتهای درجه بالا
احتمالاً با مفهوم تابعهای درجه بالا در جاوا اسکریپت آشنایی دارید. اینها تابعهایی هستند که تابعهای دیگر را به عنوان آرگومان میپذیرند و/یا تابعهایی را بازگشت میدهند. دو نمونه از این تابعها ()Array.map و ()Array.filter هستند. در React، ما این مفهوم را به کامپوننتها بسط دادهایم و از این رو کامپوننتهای درجه بالا (HOC) را داریم که در آن یک کامپوننت به عنوان ورودی، کامپوننت دیگری را میپذیرد یا یک کامپوننت را در خروجی ارائه میکند.
به طور کلی کامپوننتهای درجه بالا امکان ایجاد کدی را میدهند که قابل ترکیب شدن و استفاده مجدد است و کپسولهسازی آن نیز بیشتر است. ما میتوانیم از کامپوننتهای درجه بالا برای افزودن متدها یا مشخصهها به State یک کامپوننت و یا برای نمونه یک Redux استفاده کنیم. احتمالاً ممکن است بخواهید از کامپوننتهای درجه بالا هنگام نیاز به بهینهسازی یک کامپوننت موجود، کار روی state یا props و یا رندر کردن markup استفاده کنید.
یک روش مرسوم برای نامگذاری کامپوننت درجه بالا استفاده از رشته with در ابتدای نام است، بنابراین اگر یک کامپوننت Button داشته باشید، کامپوننت درجه بالای آن باید به صورت withButton نامگذاری شود. در ادامه یک چنین کامپوننتی را ایجاد میکنیم. سادهترین مثال از یک کامپوننت درجه بالا نمونهای است که به سادگی کامپوننت را بدون هیچ تغییری بازگشت میدهد:
1const withButton = Button => () => <Button />
اگر بخواهیم این کامپوننت را کمی مفیدتر کنیم، میتوانیم یک مشخصه به این دکمه اضافه کنیم تا علاوه بر همه مشخصههایی که دارد، یک مشخصه color نیز پیدا بکند:
1const withButton = Element => props => <Button {...props} color="red" />
از این کامپوننت درجه بالا در یک JSX کامپوننت به صورت زیر میتوان استفاده کرد:
1const Button = () => {
2 return <button>test</button>
3}
4const WrappedButton = withButton(Button)
در نهایت میتوانیم کامپوننت WrappedButton را در JSX اپلیکیشن خود مورد استفاده قرار دهیم:
1function App() {
2 return (
3 <div className="App">
4 <h1>Hello</h1>
5 <WrappedButton />
6 </div>
7 )
8}
این یک مثال بسیار ساده است؛ اما امیدواریم شما را با مفهوم کامپوننتهای درجه بالا پیش از بهکارگیری آنها در موارد پیچیدهتر آشنا کرده باشد.
رندر کردن Props
یک الگوی رایج برای اشتراک state بین کامپوننتهای مختلف استفاده از یک prop به نام children است. درون JSX یک کامپوننت میتوان {this.props.children} را رندر کرد که به طور خودکار هر JSX ارسالی در کامپوننت والد را به صورت یکی از children تزریق میکند:
1class Parent extends React.Component {
2 constructor(props) {
3 super(props)
4 this.state = {
5 /*...*/
6 }
7 }
8 render() {
9 return <div>{this.props.children}</div>
10 }
11}
12const Children1 = () => {}
13const Children2 = () => {}
14const App = () => (
15 <Parent>
16 <Children1 />
17 <Children2 />
18 </Parent>
19)
با این وجود، مشکلی در این کد وجود دارد، چون state کامپوننت والد نمیتواند از درون فرزند مورد دسترسی قرار گیرد. برای این که بتوان state را به اشتراک گذاشت، باید از یک کامپوننت رندر prop استفاده کنید و به جای ارسال کامپوننتها به صورت فرزندان کامپوننت والد؛ یک تابع ارسال کنید که متعاقباً در {()this.props.children} اجرا شود. این تابع میتواند آرگومانهایی بپذیرد:
1class Parent extends React.Component {
2 constructor(props) {
3 super(props)
4 this.state = { name: 'Flavio' }
5 }
6 render() {
7 return <div>{this.props.children(this.state.name)}</div>
8 }
9}
10const Children1 = props => {
11 return <p>{props.name}</p>
12}
13const App = () => <Parent>{name => <Children1 name={name} />}</Parent>
به جای استفاده از prop-ی با نام children که معنای کاملاً خاصی دارد، میتوانیم از هر prop دیگری استفاده کنیم و از این رو میتوانیم از این الگو در موارد متعدد روی کامپوننت یکسانی بهره بگیریم:
1class Parent extends React.Component {
2 constructor(props) {
3 super(props)
4 this.state = { name: 'Flavio', age: 35 }
5 }
6 render() {
7 return (
8 <div>
9 <p>Test</p>
10 {this.props.someprop1(this.state.name)}
11 {this.props.someprop2(this.state.age)}
12 </div>
13 )
14 }
15}
16const Children1 = props => {
17 return <p>{props.name}</p>
18}
19const Children2 = props => {
20 return <p>{props.age}</p>
21}
22const App = () => (
23 <Parent
24 someprop1={name => <Children1 name={name} />}
25 someprop2={age => <Children2 age={age} />}
26 />
27)
28ReactDOM.render(<App />, document.getElementById('app'))
قلابها (Hooks)
قلاب یکی از ویژگیهایی است که در نسخه 16.7 ریاکت معرفی شده است و قرار است شیوه نوشتن اپلیکیشنهای ریاکت را در آینده متحول کند.
پیش از معرفی مفهوم قلاب، برخی موارد کلیدی در کامپوننتها تنها با استفاده از کامپوننتهای کلاس قابل دسترسی بودند و از این رو باید State خاص خود را داشتند و از رویدادهای چرخه عمر استفاده میشد. کامپوننتهای تابع سبکتر هستند و انعطافپذیری بیشتری دارند؛ اما کارکردهای آنها در این زمینه محدودتر بود.
مفهوم قلاب به کامپوننتهای تابع کمک میکند که آنها نیز State داشته باشند و به رویدادهای چرخه عمر واکنش نشان دهند. بدین ترتیب میتوان گفت که کامپوننتهای کلاس منسوخ شدهاند. قلاب همچنین موجب میشود که کامپوننت کلاس روش مناسبی برای مدیریت رویدادها داشته باشد.
دسترسی به State
با استفاده از API مربوط به ()useState، میتوان یک متغیر state جدید ایجاد کرد و روشی برای تغییر دادن آن به دست آورد. ()useState مقدار اولیه آیتم state را پذیرفته و یک آرایه شامل متغیر state بازگشت میدهد و تابعی را برای تغییر دادن State فراخوانی میکند. از آنجا که این API یک آرایه بازگشت میدهد میتوانیم از «تجزیه آرایه» (array destructuring) برای دسترسی به هر یک از آیتمهای منفرد مانند زیر استفاده کنیم:
1const [count، setCount] = useState(0)
مثال عملی این کار چنین است:
1import { useState } from 'react'
2const Counter = () => {
3 const [count, setCount] = useState(0)
4 return (
5 <div>
6 <p>You clicked {count} times</p>
7 <button onClick={() => setCount(count + 1)}>Click me</button>
8 </div>
9 )
10}
11ReactDOM.render(<Counter />, document.getElementById('app'))
میتوانید هر تعداد فراخوانی ()useState که میخواهید اضافه کنید تا هر تعداد متغیر state که دوست دارید ایجاد کنید. تنها باید اطمینان حاصل کنید که آن را در سطح فوقانی یک کامپوننت؛ و نه در یک بلوک if یا هر چیز دیگر فراخوانی میکنید.
دسترسی به قلابهای چرخه عمر
یکی دیگر از ویژگیهای مهم قلابها این است که به کامپوننتهای تابع امکان دسترسی به قلابهای چرخه عمر را میدهند. با استفاده از کامپوننت کلاس میتوان یک تابع روی رویدادهای componentDidMount ،componentWillUnmount و componentDidUpdate ثبت کرد. این مورد کاربردهای زیادی از مقدار دهلی اولیه متغیرها با فراخوانی API برای پاکسازی دارد.
قلابها یک API به نام ()useEffect ارائه میکنند که یک تابع را به عنوان آرگومان میپذیرد.
این تابع زمانی اجرا میشود که کامپوننت برای نخستین بار رندر شود و در همه رندرهای مجدد یا بهروزرسانیهای متعاقب نیز اجرا خواهد شد. ریاکت ابتدا DOM را بهروزرسانی میکند و سپس هر تابعی که به ()useEffect ارسال شده باشد را فراخوانی میکند. همه این کارها بدون مسدودسازی رندرگیری UI و حتی مسدودسازی اجرای کد صورت میگیرد و از این نظر شباهتی به componentDidMount و componentDidUpdate ندارد و موجب میشود که اپلیکیشن سریعتر شود.
مثال
1const { useEffect, useState } = React
2const CounterWithNameAndSideEffect = () => {
3 const [count, setCount] = useState(0)
4 const [name, setName] = useState('Flavio')
5 useEffect(() => {
6 console.log(`Hi ${name} you clicked ${count} times`)
7 })
8 return (
9 <div>
10 <p>
11 Hi {name} you clicked {count} times
12 </p>
13 <button onClick={() => setCount(count + 1)}>Click me</button>
14 <button onClick={() => setName(name === 'Flavio' ? 'Roger' : 'Flavio')}>
15 Change name
16 </button>
17 </div>
18 )
19}
20ReactDOM.render(
21 <CounterWithNameAndSideEffect />,
22 document.getElementById('app')
23)
استفاده از پارامترهای ()useEffect
همان کار componentWillUnmount را میتوان به صورت اختیاری با بازگشت دادن یک تابع از پارامتر ()useEffect به دست آورد:
1useEffect(() => {
2 console.log(`Hi ${name} you clicked ${count} times`)
3 return () => {
4 console.log(`Unmounted`)
5 }
6})
()useEffect را میتوان چندین بار فراخوانی کرد و این وضعیت برای جداسازی منطق تغییر نیافته مفید است.
از آنجا که تابعهای ()useEffect در همه رندرهای مجدد/بهروزرسانیها اجرا میشوند، میتوان به منظور افزایش عملکرد، به ریاکت گفت که از یک اجرا صرفنظر کند. این کار از طریق افزودن پارامتر دوم به صورت یک آرایه ممکن است که شامل فهرستی از متغیرهای State است که باید مورد مراقبت قرار گیرند. ریاکت تنها در صورتی موارد جانبی را مجدداً اجرا میکند که یکی از آیتمهای موجود در این آرایه تغییر پیدا کنند.
1useEffect(
2 () => {
3 console.log(`Hi ${name} you clicked ${count} times`)
4 },
5 [name, count]
6)
به طور مشابه میتوانیم به ریاکت بگوییم که تنها یک بار (در زمان فعالسازی) موارد جانبی را اجرا کند. این کار از طریق ارسال آرایه خالی زیر ممکن است:
1useEffect(() => {
2 console.log(`Component mounted`)
3}, [])
()useEffect برای افزودن log-ها، دسترسی به API-های شخص ثالث و موارد دیگر بسیار مفید است.
مدیریت رویدادها در کامپوننتهای تابع
پیش از معرفی قلابها شما یا باید از کامپوننتهای کلاس استفاده میکردید و یا یک دستگیره رویداد را با استفاده از props ارسال میکردید. اینک میتوان از API داخلی برای مدیریت رویدادها بهره گرفت:
1const Button = () => {
2 const handleClick = useCallback(() => {
3 //...do something
4 })
5 return <button onClick={handleClick} />
6}
هر پارامتری که درون تابع استفاده میشود باید از طریق یک پارامتر دوم در یک آرایه به ()useCallback ارسال شود:
1const Button = () => {
2 let name = '' //... add logic
3 const handleClick = useCallback(
4 () => {
5 //...do something
6 },
7 [name]
8 )
9 return <button onClick={handleClick} />
10}
فعالسازی ارتباط بین کامپوننتها با استفاده از قلابهای سفارشی
امکان نوشتن قلابهای سفارشی یکی از ویژگیهایی است که موجب ایجاد تغییرات عدیدهای در شیوه ساختن اپلیکیشنهای ریاکت در آینده خواهد شد.
شما با استفاده از قلابهای سفارشی، یک روش دیگر برای اشتراک state و منطق بین کامپوننتها پیدا میکنید و بهبود زیادی در الگوهای رندر گیری props و کامپوننتهای درجه بالا به دست میآید. این وضعیت بسیار عالی است؛ اما اینک معرفی امکان تعریف قلابهای سفارشی موجب شده که این مسئله در اغلب موارد دیگر موضوعیتی نداشته باشد.
چگونه یک قلاب سفارشی بسازیم؟
یک قلاب صرفاً تابعی است که به طور قراردادی با use آغاز میشود. این تابع میتواند تعداد دلخواهی از آرگومانها را بپذیرد و از سوی دیگر هر چیزی را میتواند بازگشت دهد. در ادامه مثالی از یک قلاب سفارشی را میبینید:
1const useGetData() {
2 //...
3 return data
4}
همچنین یک مثال دیگر به صورت زیر است:
1const useGetUser(username) {
2 //...const user = fetch(...)
3 //...const userData = ...
4 return [user, userData]
5}
شما میتوانید در کامپوننتهای خود از قلاب به صورت زیر استفاده کنید:
1const MyComponent = () => {
2 const data = useGetData()
3 const [user, userData] = useGetUser('flavio')
4 //...
5}
این که دقیقاً در چه مواردی باید به جای تابعهای معمولی از قلاب استفاده کرد، بر اساس موارد استفاده تعیین میشود و این موضوعی است که برحسب تجربه میتوان در مورد آن تصمیمگیری کرد.
افراز کد
اپلیکیشنهای مدرن جاوا اسکریپت بر حسب اندازه بسته میتوانند کاملاً بزرگ باشند. بدیهی است که ما نمیخواهیم کاربرانمان صرفاً برای بارگذاری صفحه اول، یک بسته 1 مگابایتی جاوا اسکریپت، یعنی کد اپلیکیشن را به همراه کتابخانههایش دانلود کنند. اما این دقیقاً همان وضعیتی است که هنگام ارسال یک وب اپلیکیشن مدرن ساخته شده با Webpack رخ میدهد.
این بستهها شامل کدی هستند که شاید هرگز اجرا نشوند زیرا کاربر در صفحه ورود متوقف شود و هرگز بقیه اپلیکیشن را نبیند. افراز کد در عمل موجب میشود که کدهای جاوا اسکریپت صرفاً در زمانی بارگذاری شوند که مورد نیاز هستند. این امر موجب بهبود موارد زیر میشود:
- عملکرد اپلیکیشن
- تأثیر اپلیکیشن روی حافظه بهبود مییابد و از این رو مصرف باتری روی دستگاههای موبایل بهینه میشود.
- اندازه پهنای باند مصرفی برای دانلود اپلیکیشن کاهش مییابد.
ریاکت نسخه 16.6.0 در اکتبر 2018 منتشر شده است و در آن روش جدیدی برای اجرای افراز کد به نام React.lazy و Suspense معرفی شده است که میتواند به جای همه ابزارها یا کتابخانههایی که قبلاً استفاده میشد، قرار گیرد. React.lazy و Suspense یک روش عالی برای بارگذاری با تأخیر یک «وابستگی» (dependency)، معرفی میکنند و در این وضعیت وابستگی موصوف، تنها در زمانی بارگذاری میشود که مورد نیاز باشد.
React.lazy
توضیح خود را با React.lazy آغاز میکنیم. شما باید از آن برای ایمپورت کردن هر کامپوننت استفاده کنید:
1import React from 'react'
2const TodoList = React.lazy(() => import('./TodoList'))
3export default () => {
4 return (
5 <div>
6 <TodoList />
7 </div>
8 )
9}
Suspense
کامپوننت ToDoList به صورت دینامیک به محض این که موجود شود به خروجی اضافه میشود. Webpack یک بسته جداگانه برای آن میسازد و در موارد نیاز وظیفه بارگذاری آن را بر عهده میگیرد. Suspense کامپوننتی است که میتوان برای قرار دادن هر کامپوننتی که قرار است با تأخیر بارگذاری شود، مورد استفاده قرار داد:
1import React from 'react'
2const TodoList = React.lazy(() => import('./TodoList'))
3export default () => {
4 return (
5 <div>
6 <React.Suspense>
7 <TodoList />
8 </React.Suspense>
9 </div>
10 )
11}
این کامپوننت وظیفه مدیریت خروجی را در مواردی که کامپوننت به صورت بارگذاری با تأخیر واکشی و رندر میشود بر عهده دارد. از prop آن به نام fallback میتوان برای ارائه برخی کدهای JSX یا یک خروجی کامپوننت استفاده کرد:
1...
2 <React.Suspense fallback={<p>Please wait</p>}>
3 <TodoList />
4 </React.Suspense>
5...
همه این موارد به خوبی به همراه React Router کار میکنند:
1import React from 'react'
2import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
3const TodoList = React.lazy(() => import('./routes/TodoList'))
4const NewTodo = React.lazy(() => import('./routes/NewTodo'))
5const App = () => (
6 <Router>
7 <React.Suspense fallback={<p>Please wait</p>}>
8 <Switch>
9 <Route exact path="/" component={TodoList} />
10 <Route path="/new" component={NewTodo} />
11 </Switch>
12 </React.Suspense>
13 </Router>
14)
بدین ترتیب به پایان این بخش از راهنما میرسیم؛ در ادامه به معرفی نمونههایی عملی از کدهای ریاکت خواهیم پرداخت.
بخش چهارم: تمرینهای عملی ری اکت
در ادامه این راهنمای جامع به کمک 2 اپلیکیشن ساده React به توضیح عملی برخی از مفاهیمی میپردازیم که تا به اینجا یاد گرفتهایم.
ساخت اپلیکیشن ساده React به صورت یک شمارنده
در این مثال خلاصه، قصد داریم یک اپلیکیشن بسیار ساده شمارنده در ریاکت بسازیم و بسیاری از مفاهیم و نظریههایی که در بخشهای قبلی معرفی کردهایم را در آن به کار بگیریم.
در ابتدا یک شمارنده را در یک div نمایش میدهیم و چند دکمه نیز برای افزایش مقدار شمارنده اضافه میکنیم:
1const Button = ({ increment }) => {
2 return <button>+{increment}</button>
3}
4const App = () => {
5 let count = 0
6 return (
7 <div>
8 <Button increment={1} />
9 <Button increment={10} />
10 <Button increment={100} />
11 <Button increment={1000} />
12 <span>{count}</span>
13 </div>
14 )
15}
16ReactDOM.render(<App />, document.getElementById('app'))
در ادامه کارکردی به کد خود اضافه میکنیم که امکان تغییر دادن شمارهها با کلیک کردن روی دکمهها را فراهم سازد و به این منظور یک prop به نام onClickFunction به آن اضافه میکنیم:
1const Button = ({ increment, onClickFunction }) => {
2 const handleClick = () => {
3 onClickFunction(increment)
4 }
5 return <button onClick={handleClick}>+{increment}</button>
6}
7const App = () => {
8 let count = 0
9 const incrementCount = increment => {
10 //TODO
11 }
12 return (
13 <div>
14 <Button increment={1} onClickFunction={incrementCount} />
15 <Button increment={10} onClickFunction={incrementCount} />
16 <Button increment={100} onClickFunction={incrementCount} />
17 <Button increment={1000} onClickFunction={incrementCount} />
18 <span>{count}</span>
19 </div>
20 )
21}
22ReactDOM.render(<App />, document.getElementById('app'))
در این بخش هر عنصر دکمه 2 prop دارد که یکی increment و دیگری onClickFunction است. ما 4 دکمه مختلف ایجاد میکنیم که میزان افزایش هر کدام به ترتیب 1، 10، 100 و 1000 هستند. زمانی که دکمه موجود در کامپوننت Button کلیک شود، تابع incrementCount فراخوانی میشود.
این تابع باید شمارنده محلی را افزایش دهد. به این منظور میتوان از «قلاب» (Hook) استفاده کرد:
1const { useState } = React
2const Button = ({ increment, onClickFunction }) => {
3 const handleClick = () => {
4 onClickFunction(increment)
5 }
6 return <button onClick={handleClick}>+{increment}</button>
7}
8const App = () => {
9 const [count, setCount] = useState(0)
10 const incrementCount = increment => {
11 setCount(count + increment)
12 }
13 return (
14 <div>
15 <Button increment={1} onClickFunction={incrementCount} />
16 <Button increment={10} onClickFunction={incrementCount} />
17 <Button increment={100} onClickFunction={incrementCount} />
18 <Button increment={1000} onClickFunction={incrementCount} />
19 <span>{count}</span>
20 </div>
21 )
22}
23ReactDOM.render(<App />, document.getElementById('app'))
متد ()useState متغیر count را با مقدار 0 مقداردهی میکند و متد ()setCount را برای بهروزرسانی مقدار آن در اختیار ما قرار میدهد.
ما از این دو متد در پیادهسازی متد ()incrementCount استفاده میکنیم که به فراخوانی ()setCount برای بهروزرسانی مقدار موجود count به علاوه افزایش ارسال شده از کامپوننت دکمه کلیک شده اقدام میکند.
کد کامل این اپلیکیشن به صورت زیر است:
1const { useState } = React
2
3const Button = ({ increment, onClickFunction }) => {
4 const handleClick = () => {
5 onClickFunction(increment)
6 }
7 return (
8 <button onClick={handleClick}>
9 +{increment}
10 </button>
11 )
12}
13
14const App = () => {
15 const [count, setCount] = useState(0)
16
17 const incrementCount = (increment) => {
18 setCount(count + increment)
19 }
20
21 return (
22 <div>
23 <Button increment={1} onClickFunction={incrementCount} />
24 <Button increment={10} onClickFunction={incrementCount} />
25 <Button increment={100} onClickFunction={incrementCount} />
26 <Button increment={1000} onClickFunction={incrementCount} />
27 <span>{count}</span>
28 </div>
29 )
30}
31
32ReactDOM.render(<App />, document.getElementById('app'))
مثالی برای واکشی و نمایش اطلاعات کاربران گیتهاب از طریق API
در این مثال کاملاً ساده یک فرم را نمایش میدهیم که اقدام به پذیرش نام کاربری گیتهاب میکند و زمانی که رویداد submit را دریافت کرد، از API گیتهاب تقاضا میکند که اطلاعات کاربر را ارائه و در ادامه آن را نمایش میدهد.
این کد یک کامپوننت Card با قابلیت استفاده مجدد میسازد. زمانی که یک نام را در فیلد input وارد کنیم از سوی کامپوننت Form مدیریت میشود و این name به state آن «متصل» (bind) میشود. هنگامی که دکمه Add Card کلیک شود، فرم ورودی با پاکسازی حالت username در کامپوننت Form پاک میشود. در این کد علاوه بر ریاکت از کتابخانه Axios استفاده میشود. این کتابخانه سبک و مفید به مدیریت درخواستهای شبکه میپردازد. میتوانید با دستور زیر آن را به صورت محلی نصب کنید:
npm install axios
کار خود را با ایجاد کامپوننت Card آغاز میکنیم. این کامپوننت به نمایش تصویر و جزییات دریافتی از گیتهاب میپردازد. این دادهها از طریق props و با استفاده از موارد زیر دریافت میشوند:
- props.avatar_url آواتار کاربر است.
- props.name نام کاربر است.
- props.blog همان URL وبسایت کاربر است.
1const Card = props => {
2 return (
3 <div style={{ margin: '1em' }}>
4 <img alt="avatar" style={{ width: '70px' }} src={props.avatar_url} />
5 <div>
6 <div style={{ fontWeight: 'bold' }}>{props.name}</div>
7 <div>{props.blog}</div>
8 </div>
9 </div>
10 )
11}
ما لیستی از این کامپوننتها ساختهایم که از سوی یک کامپوننت والد در prop-ی به نام cards به CardList ارسال میشوند و به سادگی با استفاده از ()map میتوان روی آن حلقه تکرار تعریف کرده و لیستی از کارتها را استخراج کرد:
1const App = () => {
2 const [cards, setCards] = useState([])
3 return (
4 <div>
5 <CardList cards={cards} />
6 </div>
7 )
8}
کامپوننت والد App است که آرایه cards را در state خود نگهداری میکند و از سوی قلابی به نام ()useState مدیریت میشود:
1const App = () => {
2 const [cards, setCards] = useState([])
3 return (
4 <div>
5 <CardList cards={cards} />
6 </div>
7 )
8}
اینک ما باید روشی برای درخواست جزییات یک نام کاربری منفرد از گیتهاب داشته باشیم. این کار با استفاده از یک کامپوننت Form صورت میگیرد که در آن state خود را که username است مدیریت میکنیم و از گیتهاب اطلاعاتی در مورد یک کاربر با استفاده از API عمومی و از طریق Axios میپرسیم:
1const Form = props => {
2 const [username, setUsername] = useState('')
3 handleSubmit = event => {
4 event.preventDefault()
5 axios.get(`https://api.github.com/users/${username}`).then(resp => {
6 props.onSubmit(resp.data)
7 setUsername('')
8 })
9 }
10 return (
11 <form onSubmit={handleSubmit}>
12 <input
13 type="text"
14 value={username}
15 onChange={event => setUsername(event.target.value)}
16 placeholder="GitHub username"
17 required
18 />
19 <button type="submit">Add card</button>
20 </form>
21 )
22}
تحویل فرم
زمانی که فرم تحویل شد، رویداد handleSubmit را فراخوانی میکنیم و پس از فراخوانی شبکه، اقدام به فراخوانی props.onSubmit کرده و دادههای کامپوننت والد یعنی App را که از گیتهاب دریافت کردهایم به آن ارسال میکنیم.
این متد addNewCard را به App اضافه میکنیم تا یک کارت جدید به لیست کارتها به صورت prop-ی به نام onSubmit اضافه کند:
1const App = () => {
2 const [cards, setCards] = useState([])
3 addNewCard = cardInfo => {
4 setCards(cards.concat(cardInfo))
5 }
6 return (
7 <div>
8 <Form onSubmit={addNewCard} />
9 <CardList cards={cards} />
10 </div>
11 )
12}
در نهایت app را رندر میکنیم:
1ReactDOM.render(<App />, document.getElementById('app'))
بدین ترتیب کد منبع کامل اپلیکیشن کوچک ریاکت ما چنین خواهد بود:
1const { useState } = React
2const Card = props => {
3 return (
4 <div style={{ margin: '1em' }}>
5 <img alt="avatar" style={{ width: '70px' }} src={props.avatar_url} />
6 <div>
7 <div style={{ fontWeight: 'bold' }}>{props.name}</div>
8 <div>{props.blog}</div>
9 </div>
10 </div>
11 )
12}
13const CardList = props => <div>{props.cards.map(card => <Card {...card} />)}</div>
14const Form = props => {
15 const [username, setUsername] = useState('')
16 handleSubmit = event => {
17 event.preventDefault()
18 axios
19 .get(`https://api.github.com/users/${username}`)
20 .then(resp => {
21 props.onSubmit(resp.data)
22 setUsername('')
23 })
24 }
25 return (
26 <form onSubmit={handleSubmit}>
27 <input
28 type="text"
29 value={username}
30 onChange={event => setUsername(event.target.value)}
31 placeholder="GitHub username"
32 required
33 />
34 <button type="submit">Add card</button>
35 </form>
36 )
37}
38const App = () => {
39 const [cards, setCards] = useState([])
40 addNewCard = cardInfo => {
41 setCards(cards.concat(cardInfo))
42 }
43 return (
44 <div>
45 <Form onSubmit={addNewCard} />
46 <CardList cards={cards} />
47 </div>
48 )
49}
50ReactDOM.render(<App />, document.getElementById('app'))
نتیجه نهایی اجرای اپلیکیشن نیز به صورت زیر است:
بدین ترتیب به پایان این بخش از مطلب راهنمای جامع ریاکت با موضوع بررسی اپلیکیشنهای ساده به عنوان مثالهای عملی از آموختههای ریاکت خود رسیدیم. در بخش بعدی به بررسی روشهای استایل دهی یا سبکبندی اپلیکیشنهای ریاکت خواهیم پرداخت.
بخش پنجم: استایلدهی در ری اکت
در بخش قبلی این سری مطالب آموزش React به معرفی دو اپلیکیشن نمونه ساده ریاکت پرداختیم. در این بخش چگونگی استایلدهی و CSS در React را مورد بررسی قرار میدهیم.
CSS در React
زمانی که از React استفاده میکنیم، روشهای زیادی برای افزودن استایل به کامپوننتها وجود دارد که در ادامه هر کدام از آنها را بررسی میکنیم.
استفاده از کلاسها و CSS
نخستین و سادهترین روش برای تعیین استایل کامپوننتها، استفاده از یک فایل CSS معمولی برای هدفگیری برخی کلاسها است:
1const Button = () => {
2 return <button className="button">A button</button>
3}
4.button {
5 background-color: yellow;
6}
شما میتوانید فایل stylesheet را با استفاده از گزاره import به صورت زیر ایمپورت کنید:
1import './style.css'
در ادامه Webpack مسئولیت افزودن خصوصیت CSS به بسته را بر عهده میگیرد.
استفاده از خصوصیت Style
روش دوم برای افزودن استایلهای مختلف به کامپوننتها بهره گرفتن از خصوصیت style در مورد عناصر JSX است. شما با استفاده از این رویکرد دیگر نیازی به یک فایل CSS جداگانه ندارید.
1const Button = () => {
2 return <button style={{ backgroundColor: 'yellow' }}>A button</button>
3}
CSS در اینجا به روشی نسبتاً متفاوت تعریف شده است. ابتدا به آکولادهای دوتایی توجه کنید. دلیل این وضعیت آن است که style یک شیء را قبول میکند. ما یک شیء جاوا اسکریپت را ارسال میکنیم که در داخل آکولادها تعریف شده است. این کار به صورت زیر انجام میگیرد:
1const buttonStyle = { backgroundColor: 'yellow' }
2const Button = () => {
3 return <button style={buttonStyle}>A button</button>
4}
زمانی که از create-react-app استفاده میکنیم، این استایلها به لطف استفاده از Autoprefixer به صورت پیشفرض پیشوندهای خودکاری دریافت میکنند.
ضمناً استایلها اینک به جای استفاده از خط تیره (-) به صورت «حالت شتری» (camelCased) نوشته میشوند. هر بار که یک خصوصیت CSS دارای خط تیره باشد، آن را حذف میکنیم و حرف اول کلمه بعدی با حروف بزرگ نوشته میشود.
استایلها این مزیت را نیز دارند که برای کامپوننت به صورت محلی تعریف میشوند و نمیتوانند به کامپوننتهای دیگر در بخشهای مختلف اپلیکیشن نشت یابند. این وضعیتی است که هنگام استفاده از کلاسها پیش میآید و یک فایل CSS بیرونی نمیتواند این وضعیت را داشته باشد.
استفاده از ماژولهای CSS
ماژولهای CSS نیز گزینه مناسبی محسوب میشوند. در این روش از کلاسها استفاده میکنیم؛ اما این کلاسها در دامنه کامپوننت هستند و این بدان معنی است که هر نوع استایلی که اضافه شود نمیتواند بدون اجازه شما روی کامپوننتهای دیگر اعمال شود. با این وجود استایلهای شما همچنان در فایل CSS جداگانهای تعریف میشوند که نگهداری آنها سادهتر از CSS در جاوا اسکریپت است و میتوانید از نامهای خصوصیتهای CSS قدیمی خود نیز استفاده کنید.
کار خود را با ایجاد فایل CSS آغاز میکنیم که با.module.css خاتمه مییابد. برای نمونه نام فایل به صورت Button.module.css است. یک گزینه عالی این است که نام یکسانی مانند نام کامپوننتی که قرار است استایل دهی شود به آن بدهیم. در این فایل CSS را تعیین میکنیم و سپس آن را درون فایل کامپوننتی که میخواهیم استایل دهی کنیم، ایمپورت میکنیم:
1import style from './Button.module.css'
اینک میتوان از آن در JSX استفاده کرد:
1const Button = () => {
2 return <button className={style.content}>A button</button>
3}
کار ما بدین ترتیب به پایان میرسد. در کد نشانهگذاری شده حاصل، React یک کلاس منحصر به فرد خاص برای هر کامپوننت رندر شده ایجاد میشوند و CSS را به آن کلاس انتساب میدهد بدین ترتیب CSS تحت تأثیر markup-های دیگر قرار نمیگیرد.
SASS در React
هنگامی که یک اپلیکیشن React را با استفاده از create-react-app میسازید، گزینههای زیادی برای استایلدهی در اختیار خود دارید. البته اگر از create-react-app استفاده نکردهاید، هم چنان گزینههای زیادی در اختیار شما هستند؛ اما ما توضیحات خود را به حالتی که اپلیکیشن ریاکت با استفاده از create-react-app ساخته میشود محدود میکنیم.
شما میتوانید کلاسهای ساده و فایلهای CSS را با استفاده از خصوصیت style یا ماژولهای CSS سبکبندی کنید.
SASS/SCSS نیز گزینهای محبوب است که از سوی برخی توسعهدهندگان به شدت دنبال میشود. میتوان از آن بدون نیاز به هیچ گونه پیکربندی استفاده کرد. تنها چیزی که لازم است یک فایل با پسوند sass. یا scss. است که در کامپوننت ایمپورت میشود:
1import './styles.scss'
کامپوننتهای استایلدار
کامپوننتهای استایلدار انواعی از کامپوننت هستند که از CSS در جاوا اسکریپت مدرن استفاده میکنند این بدان معنی است که نسل جدیدی از ماژولهای CSS و در واقع روشی برای نوشتن CSS محسوب میشوند که دامنه آن محدود به یک کامپوننت منفرد است و به کامپوننتهای دیگر در صفحه نشت نمیکند.
سابقه کوتاهی از کامپوننتهای استایلدار
زمانی بود که وب کاملاً ساده بود و CSS اساساً وجود نداشت. بدین ترتیب صفحههای وب با استفاده از جدولها و فریمها طراحی میشد.
سپس CSS متولد شد و پس از مدتی روشن شد که فریمورکهای مختلف میتوانند به ایجاد شبکهها و طرحبندیها در صفحهها کمک زیادی بکنند. بدین ترتیب فریمورکهایی مانند Bootstrap و Foundation شروع به ایفای نقش گسترده کردند.
پیش پردازشگرهایی مانند SASS و موارد دیگر کمک زیادی به کاهش استفاده گسترده از فریمورکها کردند و موجب سازماندهی بهتر کد شدند. در ادامه قراردادهایی مانند BEM و SMACSS به خصوص درون تیمهای کاری گسترش یافتند.
قراردادها راهحلی برای همه چیز محسوب نمیشوند و بهخاطرسپاری آنها دشوار است، از این رو در طی چند سال اخیر با افزایش استفاده از جاوا اسکریپت و ساخت فرایندهایی در همه پروژههای فرانتاند، CSS راه خود را به جاوا اسکریپت باز کرده است.
ابزارهای جدید، روشهای جدیدی برای اجرای CSS در جاوا اسکریپت معرفی کردهاند و تعدادی از آنها نیز موفق بوده و محبوبیت زیادی کسب کردهاند:
- React Style
- jsxstyle
- Radium
- و موارد دیگر
معرفی کامپوننتهای استایل دار
یکی از محبوبترین ابزارهای جدید که در بخش فوق معرفی کردیم کامپوننتهای استایل دار هستند.
این ابزار به عنوان نسل جدیدی از ماژولهای CSS مطرح شده است که روشی برای نوشتن CSS با دامنهبندی کامپوننت منفرد است و موجب میشود سبکبندی یک کامپوننت به موارد دیگر در صفحه نشت نکند.
کامپوننتهای استایل دار امکان نوشتن CSS ساده در کامپوننتها بدون نگرانی در مورد تصادم نام را فراهم ساختهاند.
نصب
برای نصب این ابزار کافی است از npm یا yarn استفاده کنید:
npm install styled-components yarn add styled-components
بسیار ساده است. اینک تنها کاری که باید انجام داد افزودن این ایمپورت است:
import styled from 'styled-components'
نوشتن اولین کامپوننت استایلدار
زمانی که شیء styled ایمپورت شد، میتوانید شروع به ایجاد کامپوننتهای استایلدار بکنید. به مثال زیر توجه کنید:
1const Button = styled.button`
2 font-size: 1.5em;
3 background-color: black;
4 color: white;
5`
Button اینک یک کامپوننت React با تمام خصوصیتهای خود است. ما آن را با استفاده از تابع شیء استایل دار ایجاد کردهایم که در این مورد button نام دارد و برخی مشخصههای CSS را در یک template literal ارسال میکند.
اکنون این کامپوننت میتواند در کانتینر ما با استفاده از ساختار معمول React رندر شود:
render(<Button />)
کامپوننتهای استایلدار تابعهای دیگری را ارائه میکنند که میتوانند برای ایجاد کامپوننتهای دیگر مانند section، h1 ،input و موارد دیگر استفاده شوند و منحصر به buton نیستند.
ساختار مورد استفاده که به وسیله کاراکتر (`) مشخص میشود ممکن است در نگاه نخست عجیب باشد؛ اما قالب تگدار نامیده میشود و با زبان جاوا اسکریپت ساده نوشته شده است. این یک روش برای ارسال آرگومان به تابع است.
استفاده از props برای سفارشیسازی کامپوننتها
زمانی که برخی pro-ها s به یک کامپوننت استایلدار ارسال میشوند، آنها را به گره DOM که روی آن سوار شده میفرستند.
برای نمونه در کد زیر چگونگی ارسال prop-هایی به نام placeholder و type به کامپوننت input را مشاهده میکنید:
1const Input = styled.input`
2 //...
3`
4render(
5 <div>
6 <Input placeholder="..." type="text" />
7 </div>
8)
این کد همان کاری را میکند که میتوان تصور کرد یعنی این prop–ها را به صورت خصوصیتهای HTML درج میکند.
Prop–ها به جای این که به صورت کور به DOM ارسال شوند، میتوانند برای سفارشیسازی یک کامپوننت بر مبنای مقدار prop نیز استفاده شوند. به مثال زیر توجه کنید:
1const Button = styled.button`
2 background: ${props => (props.primary ? 'black' : 'white')};
3 color: ${props => (props.primary ? 'white' : 'black')};
4`
5render(
6 <div>
7 <Button>A normal button</Button>
8 <Button>A normal button</Button>
9 <Button primary>The primary button</Button>
10 </div>
11)
تعیین prop-ی به نام primary موجب تغییر یافتن رنگ دکمه میشود.
بسط دادن یک کامپوننت استایلدار موجود
اگر کامپوننتی دارید و میخواهید نوع مشابهی را ایجاد کنید، میتوانید استایل آن را کمی تغییر دهید. به این منظور باید از extend استفاده کنید:
1const Button = styled.button`
2 color: black;
3 //...
4`
5const WhiteButton = Button.extend`
6 color: white;
7`
8render(
9 <div>
10 <Button>A black button, like all buttons</Button>
11 <WhiteButton>A white button</WhiteButton>
12 </div>
13)
این CSS معمولی است
ما در کامپوننتهای استایلدار از CSS استفاده میکنیم که از قبل با آن آشنا هستیم. این همان CSS ساده است و نه شبه CSS یا CSS خطی که محدودیتهای خاص خود را دارد. بدین ترتیب میتوان از کوئریهای رسانه، «تودرتوسازی» (Nesting) و همه موارد دیگر که در CSS وجود دارند بهره گرفت.
استفاده از پیشوندهای Vendor
کامپوننتهای استایلدار به طور خودکار همه پیشوندهای vendor مورد نیاز را اضافه میکنند و در این خصوص هیچ نگرانی نخواهیم داشت. بدین ترتیب به پایان این بخش از مطلب راهنمای جامع ریاکت میرسیم. در بخش بعدی در مورد ابزارهای مورد نیاز برای کار با فریمورک ریاکت صحبت خواهیم کرد.
بخش ششم: ابزارهای مورد نیاز برای کار با ری اکت
در بخش قبلی از این سری مقالات آموزش جامع ریاکت در مورد روش استایلدهی این اپلیکیشنها صحبت کردیم. در این بخش با Babel و Webpack که به بستهبندی و آمادهسازی کد اپلیکیشنهای ریاکت برای توزیع کمک میکنند آشنا خواهیم شد.
Babel
Babel ابزاری جذاب است که مدتها از زمان معرفی آن میگذرد و امروزه تقریباً همه توسعهدهندگان جاوا اسکریپت از آن بهره میگیرند. این وضعیت همچنان در آینده نیز تداوم خواهد داشت زیرا Babel اینک بخشی جداییناپذیر از جاوا اسکریپت محسوب میشود و مشکل بزرگی را برای افراد زیادی حل کرده است.
مشکل چیست؟
این مشکلی است که هر توسعهدهنده وب حتماً داشته است. برخی از ویژگیهای جاوا اسکریپت در جدیدترین انتشار یک مرورگر اضافه شدهاند؛ اما در نسخههای قبلی وجود ندارند یا مثلاً احتمالاً کروم و فایرفاکس آن را پیاده سای کردهاند؛ اما Safari iOS و Edge هنوز آن را ندارند.
برای نمونه در ES6 مفهوم جدیدی به نام تابع Arrow معرفی شده است:
1[1، 2، 3].map((n) => n + 1)
که اینک از سوی همه مرورگرهای مدرن پشتیبانی میشود. IE11 هنوز از آن پشتیبانی نمیکند و Opera mini نیز وضعیت مشابهی دارد. اینک سؤال این است که چگونه میتوان این وضعیت را حل کرد؟ آیا باید به کار خود ادامه دهیم و آن کاربرانی که مرورگرهای قدیمی/ناسازگار دارند را فراموش کنیم یا باید کد قدیمی جاوا اسکریپت بنویسیم تا همه کاربران از ما راضی باشند؟
پاسخ مشکل در Babel است. Babel یک کامپایلر است که کد نوشته شده به یک استاندارد را میگیرد و آن را به کدی نوشته شده به استاندارد دیگر transpile میکند.
میتوان Babel را طوری پیکربندی کرد که کد جاوا اسکریپت مدرن ES2017 را به ساختار کد جاوا اسکریپت ES5 تبدیل کند:
1[1, 2, 3].map(function(n) {
2 return n + 1
3})
این کار باید در زمان Build رخ دهد و از این رو باید گردش کاری طراحی کنید که این وضعیت را برای شما مدیریت کند. Webpack یک راهحل رایج در این زمینه محسوب میشود.
نصب Babel
Babel به سادگی با استفاده از npm به صورت محلی در یک پروژه نصب میشود:
npm install --save-dev @babel/core @babel/cli
از آنجا که npm اینک به همراه npx عرضه میشود، بستههای CLI که به صوت محلی نصب شدهاند میتوانند با تایپ کردن دستور در پوشه پروژه اجرا شوند. بنابراین میتوانیم Babel را با وارد کردن دستور زیر اجرا کنیم:
npx babel script.js
نمونهای از پیکربندی Babel
Babel به صورت پیشفرض هیچ کار مفیدی انجام نمیدهد و باید آن را پیکربندی کنید و افزونههایی به آن بیفزایید. در این لینک (+) میتوانید فهرستی از افزونههای Babel را مشاهده کنید.
برای حل مشکلی که در بخش قبل اشاره کردیم (یعنی استفاده از تابعهای Arrow در همه مرورگرها) میتوانیم دستور زیر را اجرا کنیم:
npm install --save-dev \ @babel/plugin-transform-es2015-arrow-functions
تا بسته مورد نظر در پوشه node_modules مربوطه به اپلیکیشن دانلود شود و سپس دستور زیر را به فایل babelrc. که در پوشه root اپلیکیشن موجود است، اضافه میکنیم:
{ "plugins": ["transform-es2015-arrow-functions"] }
اگر این فایل را در پوشه خود ندارید، کافی است یک فایل خالی ایجاد کنید و محتوای فوق را در آن قرار دهید.
نکته: اگر تاکنون هیچ فایلی که نام آن با نقطه شروع شود ندیدهاید، ممکن است در نگاه اول برایتان عجیب باشد. این فایلها در نرمافزارهای مدیریت فایل به صورت پیشفرض نمایش نمییابند و از این رو فایلهای مخفی محسوب میشوند.
اینک اگر فایلی با نام script.js و محتوای زیر داشته باشیم:
1var a = () => {};
2var a = (b) => b;
3const double = [1,2,3].map((num) => num * 2);
4console.log(double); // [2,4,6]
5var bob = {
6 _name: "Bob",
7 _friends: ["Sally", "Tom"],
8 printFriends() {
9 this._friends.forEach(f =>
10 console.log(this._name + " knows " + f));
11 }
12};
13console.log(bob.printFriends());
با اجرای دستور زیر:
babel script.js
خروجی زیر به دست میآید:
1var a = function () {};var a = function (b) {
2 return b;
3};
4const double = [1, 2, 3].map(function (num) {
5 return num * 2;
6});console.log(double); // [2,4,6]
7var bob = {
8 _name: "Bob",
9 _friends: ["Sally", "Tom"],
10 printFriends() {
11 var _this = this;
12 this._friends.forEach(function (f) {
13 return console.log(_this._name + " knows " + f);
14 });
15 }
16};
17console.log(bob.printFriends());
همان طور که میبینید تابعهای Arrow همگی به تابعهای ES5 جاوا اسکریپت تبدیل شدهاند.
پیشتنظیمهای Babel
تا به اینجا دیدیم که چگونه میتوانیم Babel را پیکربندی کنیم که ویژگیهای خاص جاوا اسکریپت را trabplie کنید. همچنین میتوانید افزونههای بسیار بیشتری به آن اضافه کنید؛ اما نمیتوانید مواردی را به صورت یک به یک به ویژگیهای پیکربندی اضافه کنید چون این کار عملی نیست.
به همین دلیل است که مفهومی به نام «پیشتنظیم» (preset) در Babel مطرح شده است. محبوبترین پیشتنظیمها، env و react نام دارند.
نکته: در نسخه 7 Babel برخی پیشتنظیمهایی که سالها مطرح بودند مانند preset-es2017 و stage را منسوخ شده است به جای آن میتوانید از babel/preset-env@ استفاده کنید.
پیشتنظیم Env
پیشتنظیم env بسیار جالب است. کافی است به آن اعلام کنیم که میخواهیم از کدام محیط پشتیبانی کنیم تا همه کارهای مورد نظر را برای ما انجام دهد و از همه ویژگیهای مدرن جاوا اسکریپت نیز پشتیبانی میکند.
برای نمونه فرض کنید میخواهیم از آخرین 2 نسخه همه مرورگرها پشتیبانی کنیم؛ اما در مورد Safari از همه نسخهها از شماره 7 به بعد پشتیبانی کنیم. بدین ترتیب باید از پیکربندی زیر استفاده کنیم:
1{
2 "presets": [
3 ["env", {
4 "targets": {
5 "browsers": ["last 2 versions", "safari >= 7"]
6 }
7 }]
8 ]
9}
اگر به پشتیبانی از مرورگر نیاز نداشته باشیم و صرفاً بخواهیم با Node.js نسخه 6.10 کار کنیم میتوانیم از پیکربندی زیر استفاده کنیم:
1{
2 "presets": [
3 ["env", {
4 "targets": {
5 "node": "6.10"
6 }
7 }]
8 ]
9}
پیشتنظیم React
پیشتنظیم react در زمان نوشتن اپلیکیشنهای ریاکت کاملاً راهگشا است. شما میتوانید موارد preset-flow ،syntax-jsx transform-react-jsx ،transform-react-display-name را اضافه کنید. با استفاده از این پیشتنظیم آماده میشویم که اپلیکیشنهای ریاکت را با تبدیل JSX و پشتیبانی از Flow بنویسیم.
برای کسب اطلاعات بیشتر در مورد پیشتنظیم ها میتوانید به این لینک (+) مراجعه کنید.
استفاده از Babel به همراه Webpack
اگر میخواهید کدهای مدرن جاوا اسکریپت را در مرورگر اجرا کنید، Babel به تنهایی کافی نیست و باید بتوانید کد را مدیریت هم بکنید. بهترین ابزار به این منظور Webpack است.
جاوا اسکریپت مدرن به دو مرحله متفاوت نیاز دارد که یکی مرحله کامپایل و دیگری مرحله «زمان اجرا» (runtime) است. دلیل این امر آن است که ویژگیهای ES6 به بعد به یک ابزار کمکی polyfill یا runtime نیاز دارند.
برای نصب کارکرد زمان اجرای polyfill برای Babel میتوان دستور زیر را اجرا کرد:
npm install @babel/polyfill \ @babel/runtime \ @babel/plugin-transform-runtime
اینک در فایل Webpack.config.js کد زیر را اضافه کنید:
1entry: [
2 'babel-polyfill',
3 // your app scripts should be here
4],
5module: {
6 loaders: [
7 // Babel loader compiles ES2015 into ES5 for
8 // complete cross-browser support
9 {
10 loader: 'babel-loader',
11 test: /\.js$/,
12 // only include files present in the `src` subdirectory
13 include: [path.resolve(__dirname, "src")],
14 // exclude node_modules, equivalent to the above line
15 exclude: /node_modules/,
16 query: {
17 // Use the default ES2015 preset
18 // to include all ES2015 features
19 presets: ['es2015'],
20 plugins: ['transform-runtime']
21 }
22 }
23 ]
24}
با حفظ اطلاعات پیشتنظیمها و افزونهها درون فایل Webpack.config.js، دیگر نیازی به یک فایل babelrc. نخواهیم داشت.
Webpack
Webpack ابزاری است که امکان کامپایل کردن ماژولهای جاوا اسکریپت را فراهم میکند و به نام module bundler نیز مشهور است. اگر تعداد بالایی از فایلها داشته باشیم، Webpack میتواند یک فایل منفرد (یا چند فایل معدود) تولید کند که به اجرای اپلیکیشن شما میپردازند.
Webpack عملیات مختلفی را میتواند اجرا کند:
- به بستهبندی منابع کمک میکند.
- تغییرات را برسی کرده و وظایف را مجدداً اجرا میکند.
- میتواند عملیات Babel transpilation را در مورد ES5 اجرا کند و به این ترتیب امکان استفاده از جدیدترین ویژگیهای جاوا اسکریپت بدون نگرانی از پشتیبانی مرورگر را فراهم میکند.
- میتواند CoffeeScript را به جاوا اسکریپت تبدیل کند.
- میتواند تصاویر «درونخطی» (inline) را به URI های داده تبدیل کند.
- امکان استفاده از ()require برای فایلهای CSS را ایجاد میکند.
- میتواند یک وبسرور development را اجرا کند.
- میتواند جایگزین hot module را مدیریت کند.
- میتواند فایلهای خروجی را به چندین فایل تقسیم کند تا از ایجاد یک فایل خیلی بزرگ جاوا اسکریپت که امکان ارسال آن در نخستین بارگذاری صفحه دشوار است خودداری شود.
- میتواند عملیات tree shaking (+) را اجرا کند.
Webpack در بکاند
Webpack تنها محدود به استفاده از فرانتاند نیست و برای توسعه بکاند Node.js نیز مفید است. ابزارهای قبل از Webpack که برخی از آنها را در لیست زیر مشاهده میکنید، همچنان استفاده میشوند:
- Grunt
- Broccoli
- Gulp
مشابهتهای زیادی بین تواناییهای این ابزارها با کارهایی که Webpack انجام میدهد وجود دارد؛ اما تفاوت اصلی در این است که این ابزارها به نام task runners مشهور هستند؛ اما Webpack از همان ابتدا به صورت یک «ابزار بستهبندی ماژول» (module bundler) ارائه شده است.
در واقع Webpack ابزار متمرکزتری است، کافی است یک نقطه ورودی برای اپلیکیشن خود تعیین کنید (این نقطه ورودی میتواند حتی یک فایل HTML با تگهای اسکریپت باشد) تا Webpack فایلها را تحلیل کرده و همه آن چه را که برای اجرای اپلیکیشن نیاز دارید، در یک فایل خروجی منفرد جاوا اسکریپت بستهبندی کند. همچنین اگر به افزار کد نیاز داشته باشید میتواند فایل خروجی را به چند بخش تقسیم کند.
نصب Webpack
Webpack میتواند به صورت سراسری یا به صورت محلی برای پروژه نصب شود.
نصب سراسری
با استفاده از دستور زیر میتوانید Webpack را به صورت سراسری با استفاده از yarn نصب کنید:
yarn global add Webpack Webpack -cli
برای نصب با استفاده از npm از دستور زیر استفاده کنید:
npm i -g Webpack Webpack -cli
زمانی که این دستور اجرا شود، میتوانید دستور زیر را اجرا کنید:
Webpack -cli
نصب محلی
Webpack -cli میتواند به صورت محلی نیز نصب شود. این روش نصب بیشتر توصیه شده است، زیرا Webpack -cli در هر پروژه بهروزرسانی شود. برای نمونه برای استفاده از جدیدترین ویژگیها برای یک پروژه کوچک مقاومت کمتری خواهیم داشت تا این که بخواهیم همه پروژههایی که از Webpack استفادهی کنند را به یکباره بهروزرسانی کنیم.
webpavk با استفاده از yarn به صورت زیر نصب میشود:
yarn add Webpack Webpack -cli –D
با استفاده از npm از دستور زیر استفاده کنید:
npm i Webpack Webpack -cli --save-dev
زمانی که کار نصب پایان یافت، میتوانید کد زیر را به فایل package.json خود اضافه کنید:
1{
2 //...
3 "scripts": {
4 "build": "webpack"
5 }
6}
سپس میتوانید با اجرای دستور زیر در ریشه پروژه Webpack را اجرا کنید:
yarn build
پیکربندی Webpack
Webpack به صورت پیشفرض (از نسخه 4 به بعد) در صورت رعایت موارد زیر نیاز به هیچ پیکربندی خاصی نخواهد داشت:
- «نقطه ورودی» (entry point) اپلیکیشن به صورت src/index.js/. باشد.
- خروجی در dist/main.js/. قرار گیرد.
- Webpack در حالت production کار کند.
البته همه جزییات Webpack را میتوان بسته به نیاز پیکربندی کرد. پیکربندی بندی Webpack در فایل Webpack.config.js در پوشه ریشه پروژه ذخیره میشود.
نقطه ورودی
به صورت پیشفرض نقطه ورودی اپلیکیشن در مسیر src/index.js/. است. در مثال ساده زیر نقطه index.js/. را به عنوان نقطه ورودی پیکربندی میکنیم:
1module.exports = {
2 /*...*/
3 entry: './index.js'
4 /*...*/
5}
خروجی
به صورت پیشفرض خروجی در مسیر dist/main.js/. ارائه میشود. در مثال زیر خروجی Webpack در app.js قرار میگیرد:
1module.exports = {
2 /*...*/
3 output: {
4 path: path.resolve(__dirname, 'dist'),
5 filename: 'app.js'
6 }
7 /*...*/
8}
Loader
با استفاده از Webpack میتوان گزارههایی را در کد جاوا اسکریپت import یا require کرد و این وضعیت محدود به کدهای جاوا اسکریپت نیست؛ بلکه میتوانید هر نوع فایل، برای مثال CSS را ایمپورت کنید.
Webpack میخواهد همه وابستگیهای شما را مدیریت کند و این امر منحصر به کدهای جاوا اسکریپت نیست. یکی از روشهای این کار استفاده از loader-ها است. برای نمونه شما میتوانید در کد خود از دستور زیر استفاده کنید:
import 'style.css'
در این مورد، پیکربندی loader به صورت زیر خواهد بود:
1module.exports = {
2 /*...*/
3 module: {
4 rules: [
5 { test: /\.css$/, use: 'css-loader' },
6 }]
7 }
8 /*...*/
9}
عبارت منظم فوق همه فایلهای CSS را شامل میشود. یک loader میتواند گزینههای مختلفی داشته باشد:
1module.exports = {
2 /*...*/
3 module: {
4 rules: [
5 {
6 test: /\.css$/,
7 use: [
8 {
9 loader: 'css-loader',
10 options: {
11 modules: true
12 }
13 }
14 ]
15 }
16 ]
17 }
18 /*...*/
19}
الزام چندگانه
شما میتواند چندین loader را برای هر قاعده، «الزام» (require) کنید:
1module.exports = {
2 /*...*/
3 module: {
4 rules: [
5 {
6 test: /\.css$/,
7 use:
8 [
9 'style-loader',
10 'css-loader',
11 ]
12 }
13 ]
14 }
15 /*...*/
16}
در این مثال، css-loader به تفسیر دایرکتیو 'import 'style.css در CSS میپردازیم. سپس style-loader مسئول تزریق آن CSS در DOM با استفاده از یک تگ <style> خواهد بود.
ترتیب
ترتیب این کار مهم است و دارای ترتیب معکوس است یعنی آخرین دستور اول اجرا میشود. شاید از خود بپرسید که چه نوع loader-هایی وجود دارند؟ پاسخ این است که تعداد آنها زیاد است و در این لینک (+) میتوانید فهرست کامل آنها را ملاحظه کنید.
یک loader که به طور معمول استفاده میشود Babel است که برای transpile کردن کدهای مدرن جاوا اسکریپت به کد ES5 مورد استفاده قرار میگیرد:
1module.exports = {
2 /*...*/
3 module: {
4 rules: [
5 {
6 test: /\.js$/,
7 exclude: /(node_modules|bower_components)/,
8 use: {
9 loader: 'babel-loader',
10 options: {
11 presets: ['@babel/preset-env']
12 }
13 }
14 }
15 ]
16 }
17 /*...*/
18}
مثال فوق موجب میشود که Babel همه فایلهای React/JSX را پیشپردازش کند:
1module.exports = {
2 /*...*/
3 module: {
4 rules: [
5 {
6 test: /\.(js|jsx)$/,
7 exclude: /node_modules/,
8 use: 'babel-loader'
9 }
10 ]
11 },
12 resolve: {
13 extensions: [
14 '.js',
15 '.jsx'
16 ]
17 }
18 /*...*/
19}
گزینههای موجود برای Babel را میتوانید در این صفحه (+) ملاحظه کنید.
افزونههای Babel
افزونهها نیز شبیه به loader-ها اما بسیار قویتر هستند. افزونهها میتوانند کارهایی انجام دهند که loader-ها از انجام آنها عاجز هستند و در واقع بلوکهای اصلی تشکیلدهنده Webpack محسوب میشوند. مثال زیر را در نظر بگیرید:
1module.exports = {
2 /*...*/
3 plugins: [
4 new HTMLWebpackPlugin()
5 ]
6 /*...*/
7}
افزونه HTMLWebpack Plugin وظیفه ایجاد خودکار یک فایل HTML را بر عهده دارد و مسیر بسته جاوا اسکریپت را به خروجی اضافه میکند به طوری که جاوا اسکریپت آماده عرضه شدن باشد. افزونههای زیادی برای Webpack وجود دارند که فهرست آنها را میتوانید در این لینک (+) مشاهده کنید.
یک افزونه مفید، CleanWebpack Plugin نام دارد که برای پاکسازی پوشه dist/ پیش از ایجاد هر نوع خروجی استفاده میشود. بدین ترتیب فایلها در زمانی که نام فایل خروجی عوض میشود، بر جای نمیمانند:
1module.exports = {
2 /*...*/
3 plugins: [
4 new CleanWebpackPlugin(['dist']),
5 ]
6 /*...*/
7}
حالت Webpack
«حالت» (mode) که در نسخه 4 Webpack معرفی شده است محیطی که Webpack روی آن عمل میکند را تعیین میکند. آن را میتوان به صورت development یا production تنظیم کرد. حالت پیشفرض به صورت production است و از این رو تنها میتوانید آن را به صورت development تغییر دهید.
1module.exports = {
2 entry: './index.js',
3 mode: 'development',
4 output: {
5 path: path.resolve(__dirname, 'dist'),
6 filename: 'app.js'
7 }
8}
حالت Development خصوصیات زیر را دارد:
- Build بسیار سریع است.
- نسبت به حالت production کمتر بهینهسازی شده است.
- کامنت ها حذف نمیشوند.
- پیامهای خطا و پیشنهادهای دقیقتری ارائه میشود.
- تجربه دیباگ بهتری دارد.
در حالت Production شما build کندتری دارید، چون باید یک بسته با بهینهسازی بالاتر تولید کند. فایل جاوا اسکریپت حاصل، اندازه کوچکتری دارد و بسیاری از چیزهایی که در production لازم نیستند را حذف میکند.
به عنوان مثل تصور کنید یک اپلیکیشن منفرد ایجاد میکنیم که صرفاً گزاره console.log را نمایش میدهد. بسته production آن چنین است:
بسته development نیز به صورت زیر است:
اجرای Webpack
در صورتی که Webpack به صورت سراسری نصب شده باشد، میتواند از خط فرمان به صورت درستی اجرا شود؛ اما به طور کلی یک اسکریپت درون یک فایل package.json نوشته میشود که سپس با استفاده از npm یا yarn اجرا میشود. برای نمونه این تعاریف اسکریپت package.json را قبلاً استفاده کردهایم:
1"scripts": {
2 "build": "webpack"
3}
تعریف فوق به ما امکان میدهد که Webpack را با دستور زیر اجرا کنیم:
npm run build
همچنین اجرای آن با دستور زیر نیز ممکن است:
yarn run build
و یا به سادگی میتوانیم دستور زیر را اجرا کنیم:
yarn build
پایش تغییرات
Webpack میتواند به صورت خودکار بسته را در مواردی که تغییراتی در اپلیکیشن رخ میدهد بازسازی کند و سپس منتظر تغییرات بعدی بماند. کافی است اسکریپت زیر را به آن اضافه کنید:
1"scripts": {
2 "watch": "webpack --watch"
3}
و دستور زیر را اجرا کنید:
npm run watch
همچنین میتوانید از دستور زیر استفاده کنید:
yarn run watch
یا صرفاً دستور زیر را اجرا کنید:
yarn watch
یکی از ویژگیهای خوب حالت «پایش» (watch) این است که بسته تنها در صورتی بازسازی میشود که تغییرات توأم با خطا نباشد. اگر خطاهایی وجود داشته باشند watch همچنان منتظر ادامه تغییرات میماند و تلاش میکند زمانی که همه خطاها رفع شد، بسته را بازسازی کند. بدین ترتیب بسته صحیح از این build-های مشکلدار تأثیر نمیپذیرد.
مدیریت تصاویر
Webpack به ما امکان استفاده از تصاویر را به روشی بسیار آسان میدهد. به این منظور باید از loader-ی به نام file-loader (+) استفاده کنیم. به پیکربندی ساده زیر توجه کنید:
1module.exports = {
2 /*...*/
3 module: {
4 rules: [
5 {
6 test: /\.(png|svg|jpg|gif)$/,
7 use: [
8 'file-loader'
9 ]
10 }
11 ]
12 }
13 /*...*/
14}
این پیکربندی به ما امکان میدهد که تصاویر را در کد جاوا اسکریپت خود ایمپورت کنیم:
1import Icon from './icon.png'
2const img = new Image()
3img.src = Icon
4element.appendChild(img)
Img یک عنصر HTMLImageElement است که مستندات آن را میتوانید در این لینک (+) ملاحظه کنید.
file-loader میتواند انواع فایلهای دیگر مانند فونت، فایلهای CSV ،xml و موارد دیگر را نیز مدیریت کند و ابزار جالب دیگر برای کار با تصاویر loader-ی به نام url-loader است. مثال زیر هر فایل PNG کمتر از 8 کیلوبایت را به صورت یک URL داده بارگذاری میکند.
1module.exports = {
2 /*...*/
3 module: {
4 rules: [
5 {
6 test: /\.png$/,
7 use: [
8 {
9 loader: 'url-loader',
10 options: {
11 limit: 8192
12 }
13 }
14 ]
15 }
16 ]
17 }
18 /*...*/
19}
پردازش کد SASS و تبدیل آن به CSS
با استفاده از sass-loader ،css-loader و style-loader میتوان کد SASS را به صورت زیر به CSS تبدیل کرد:
1module.exports = {
2 /*...*/
3 module: {
4 rules: [
5 {
6 test: /\.png$/,
7 use: [
8 {
9 loader: 'url-loader',
10 options: {
11 limit: 8192
12 }
13 }
14 ]
15 }
16 ]
17 }
18 /*...*/
19}
تولید نگاشتهای منبع
از آنجا که Webpack کد را بستهبندی میکند، «نگاشتهای منبع» (Source Maps) در موارد مختلف مثلاً برای داشتن ارجاعی به فایل اصلی که موجب خطایی شده است ضروری هستند.
بدین ترتیب به Webpack اعلام میکنیم که نگاشتهای منبع را با استفاده از مشخصه devtool در پیکربندی تولید کند:
1module.exports = {
2 /*...*/
3 devtool: 'inline-source-map',
4 /*...*/
5}
Devtool مقادیر بسیار مختلفی میتواند بگیرد که در این لینک (+) میتوانید فهرست آنها را ملاحظه کنید. محبوبترین و پرکاربردترین انواع این مقادیر به صورت زیر هستند:
- none – هیچ نگاشت منبعی اضافه نمیکند.
- source-map – برای محیط production ایدهآل است و نگاشت منبع جداگانهای تهیه میکند که میتواند کمینهسازی شود و یک ارجاع به بسته ایجاد میکند تا ابزارهای توسعه بدانند که نگاشت منبع موجود است. البته باید سرور را طوری پیکربندی کنید که از ارسال این نگاشت خودداری کند و صرفاً به منظور دیباگ استفاده شود.
- inline-source-map – برای محیط development ایدهآل است و نگاشت منبع را به صورت یک URL داده و inline درمیآورد.
به این ترتیب به پایان یکی دیگر از بخشهای آموزش جامع ریاکت میرسیم.
بخش هفتم: روشهای تست اپلیکیشنهای ری اکت
در این بخش با روشهای تست اپلیکیشنهایی که در فریمورک React نوشته میشوند و به طور خاص ابزار Jest آشنا خواهیم شد.
Jest
Jest کتابخانهای برای تست کدهای جاوا اسکریپت است. این کتابخانه اوپنسورس از سوی فیسبوک نگهداری میشود و به طور خاص برای تست کردن کدهای React مناسب است. با این حال Jest محدود به React نیست و میتوان از آن برای تست کردن هر نوع کد جاوا اسکریپت استفاده کرد. نقاط قوت آن به شرح زیر هستند:
- سریع است.
- امکان تست snapshot را دارد.
- دارای گزینههای محدودی است و هر چیزی که به دنبالش هستید را بدون نیاز به انتخاب از بین گزینههای زیاد در اختیار شما قرار میدهد.
ابزار دیگر به نام Mocha نیز وجود دارد که کاملاً شبیه به Jest است؛ ولی چند تفاوت دارد:
- Mocha گزینههای بیشتری دارد؛ در حالی که Jest یک سری قواعد خاص خود را ارائه میکند.
- Mocha به پیکربندی بیشتری نیاز دارد؛ در حالی که Jest معمولاً به لطف گزینههای محدود آماده اجرا است.
- Mocha قدیمیتر بوده و ثبات بیشتری دارد و با ابزارهای بیشتری ادغام یافته است.
در واقع بزرگترین مزیت Jest این است که یک راهحل آماده محسوب میشود و بدون نیاز به تعامل با دیگر کتابخانههای تست برای اجرای وظیفه خود آمادگی دارد.
نصب
Jest به طور خودکار در زمان استفاده از create-react-app نصب میشود، بنابراین اگر از آن استفاده میکنید، لازم نیست Jest را نصب کنید.
با استفاده از Yarn میتوان Jest را روی هر پروژه دیگری نیز نصب کرد:
yarn add --dev jest
همچنین از npm نیز میتوان بدین منظور استفاده کرد:
npm install --save-dev jest
دقت کنید که چگونه Jest را در بخش devDependencies از فایل package.json قرار دادیم، بنابراین تنها در محیط توسعه نصب میشود و در محیط production قابل دسترسی نخواهد بود.
خط زیر را به بخش اسکریپتهای فایل package.json اضافه کنید:
1{
2 "scripts": {
3 "test": "jest"
4 }
5}
بدین ترتیب این تستها میتوانند با استفاده از yarn test یا npm run test اجرا شوند. به طور جایگزین میتوانید Jest را به صورت سراسری نیز نصب کنید:
yarn global add jest
در این صورت همه تستها با استفاده از ابزار خط فرمان jest قابل اجرا خواهند بود.
ایجاد نخستین تست Jest
در پروژههایی که با استفاده از create-react-app ایجاد میشوند، Jest به صورت پیشفرض نصب و به طور آماده استفاده پیکربندی شده است؛ اما افزودن Jest به هر پروژهای نیز به راحتی وارد کردن دستور زیر است:
yarn add --dev jest
خط زیر را به فایل package.json خود اضافه کنید:
1{
2 "scripts": {
3 "test": "jest"
4 }
5}
و تستها را با اجرا کردن yarn test در محیط shell خودتان اجرا کنید. در حال حاضر ما هیچ تستی نداریم و از این رو هیچ چیزی اجرا نخواهد شد.
در ادامه نخستین تست خود را ایجاد میکنیم. به این منظور فایل math.js را باز کرده و چند تابع را که در ادامه تست خواهیم کرد وارد نمایید:
const sum = (a, b) => a + b const mul = (a, b) => a * b const sub = (a, b) => a - b const div = (a, b) => a / b export default { sum, mul, sub, div }
اینک یک فایل به نام math.test.js در همان پوشه ایجاد کرده و از آن برای استفاده از Jest جهت تست کردن تابعهای تعریف شده در math.js بهره بگیرید:
const { sum, mul, sub, div } = require('./math') test('Adding 1 + 1 equals 2', () => { expect(sum(1, 1)).toBe(2) }) test('Multiplying 1 * 1 equals 1', () => { expect(mul(1, 1)).toBe(1) }) test('Subtracting 1 - 1 equals 0', () => { expect(sub(1, 1)).toBe(0) }) test('Dividing 1 / 1 equals 1', () => { expect(div(1, 1)).toBe(1) })
اجرای دستور yarn test موجب میشود که Jest روی همه فایلهای تست که پیدا میکند اجرا شود و نتیجه نهایی را بازگشت دهد.
اجرای Jest با VS Code
ویژوال استودیو کد یک ویرایشگر قوی برای توسعه جاوا اسکریپت محسوب میشود. اکستنشن Jest برای این ادیتور (+) یکپارچهسازی مناسبی برای تستهای شما ارائه میکند.
زمانی که این افزونه را نصب کنید به طور خودکار تشخیص میدهد که Jest در devDependencies نصب شده و تستها را اجرا میکند. همچنین میتوانید تستها را به صورت دستی با انتخاب کردن دستور Jest: Start Runner اجرا کنید. این دستور تستها را اجرا کرده و در حالت watch باقی میماند تا هر زمان که تغییری روی یکی از فایلهایی که تست شدهاند صورت گرفت، دوباره تست را اجرا کند:
Matcher
Matcher متدی است که امکان تست کردن مقادیر را فراهم میسازد. برای نمونه ()toBe یک Matcher است:
test('Adding 1 + 1 equals 2', () => { expect(sum(1, 1)).toBe(2) })
Matcher-های پراستفاده برای مقایسه مقدار نتیجه ()expext با مقدار ارسالی به عنوان آرگومان به صورت زیر هستند:
- toBe تساوی صریح را با استفاده از عملگر === مقایسه میکند.
- toEqual مقادیر دو متغیر را مقایسه میکند. اگر یک شیء یا آرایه باشد، تساوی همه مشخصهها یا عناصر را مورد بررسی قرار میدهد.
- toBeNull زمانی true است که یک مقدار تهی ارسال شده باشد.
- toBeDefined زمانی true است که یک مقدار تعریف شده (برخلاف وضعیت فوق) ارسال شده باشد.
- toBeUndefined زمانی true است که یک مقدار تعریف نشده ارسال شده باشد.
- toBeUndefined زمانی استفاده میشود که مقادیر اعشاری مقایسه شوند و از خطای رند کردن اجتناب میشود.
- toBeTruthy زمانی true است که مقدار آن true تلقی شود (شبیه به if عمل میکند).
- toBeFalsy زمانی true است که مقدار آن false تلقی شود (شبیه به if عمل میکند).
- toBeGreaterThan زمانی true است که ()expect بزرگتر از آرگومان باشد.
- toBeGreaterThanOrEqual زمانی true است که ()expect برابر یا بالاتر از آرگومان باشد.
- toBeLessThan زمانی true است که نتیجه ()expect کمتر از آرگومان باشد.
- toBeLessThanOrEqual زمانی true است که ()expect برابر با آرگومان یا کمتر از آرگومان باشد.
- toMatch برای مقایسه رشتهها با تطبیق الگوی «عبارتهای منظم» (regular expression) استفاده میشود.
- toContain در آرایهها استفاده میشود و در صورتی true است که آرایه مورد نظر آرگومانی را در مجموعه عناصر خود داشته باشد.
- (toHaveLength(number طول یک آرایه را بررسی میکند.
- (toHaveProperty(key, value بررسی میکند که آیا یک شیء دارای مشخصهای است و به طور اختیاری مقدار آن را نیز بررسی میکند.
- toThrow بررسی میکند که آیا تابعی که ارسال شده، بک خطای استثنا (به طور کلی) و یا یک خطای استثنای خاص صادر میکند یا نه.
- ()toBeInstanceOf بررسی میکند که آیا یک شیء وهلهای از یک کلاس است یا نه.
همه این matcher-ها میتوانند با استفاده از عملگر .not. درون گزاره به صورت زیر منفی شوند:
test('Adding 1 + 1 does not equal 3', () => { expect(sum(1, 1)).not.toBe(3) })
برای این که از این موارد به همراه promise استفاده کنید، میتوانید از resolves. و rejects. بهره بگیرید:
expect(Promise.resolve('lemon')).resolves.toBe('lemon') expect(Promise.reject(new Error('octopus'))).rejects.toThrow('octopus')
راهاندازی
پیش از اجرای تستها، ممکن است بخواهید برخی آمادهسازیها را داشته باشید. اگر میخواهید کاری را پیش از اجرای همه تستها انجام دهید میتوانید از تابع ()beforeAll استفاده کنید:
beforeAll(() => { //do something })
برای اجرای چیزی پیش از هر تست میتوانید از ()beforeEach استفاده کنید:
beforeEach(() => { //do something })
Teardown
همان طور که پیش از تست میتوان برخی کارها را اجرا کرد، پس از اجرای هر تست نیز میتوان برخی موارد را تنظیم کرد:
afterEach(() => { //do something })
برای اجرای کاری پس از اجرای همه تستها از ساختار زیر استفاده کنید:
afterAll(() => { //do something })
تستهای گروهی با استفاده از ()describe
شما میتوانید گروهی از تستها را در یک فایل منفرد بسازید تا تابعهای راهاندازی و Teardown را به صورت مجرد داشته باشید:
1describe('first set', () => {
2 beforeEach(() => {
3 //do something
4 })
5 afterAll(() => {
6 //do something
7 })
8 test(/*...*/)
9 test(/*...*/)
10})
11describe('second set', () => {
12 beforeEach(() => {
13 //do something
14 })
15 beforeAll(() => {
16 //do something
17 })
18 test(/*...*/)
19 test(/*...*/)
20})
تست کردن کد ناهمگام
کد «ناهمگام» (Asynchronous) در جاوا اسکریپت مدرن اساساً میتواند دو شکل داشته باشد: callback و promise. در مورد promise میتوان از async/await استفاده کرد.
Callback
نمیتوانیم در یک Callback تستی داشته باشیم، زیرا Jest آن را اجرا نمیکند. دلیل این وضعیت آن است که اجرای فایل تست پیش از فراخوانی Callback پایان مییابد. برای رفع این مشکل باید یک پارامتر به تابع تست ارسال کنیم که برای نمونه میتوان آن را done نامید. Jest تا زمانی که شما ()done را فراخوانی کنید صبر میکند تا تست را به پایان برساند:
1//uppercase.js
2function uppercase(str, callback) {
3 callback(str.toUpperCase())
4}
5module.exports = uppercase
6//uppercase.test.js
7const uppercase = require('./src/uppercase')
8test(`uppercase 'test' to equal 'TEST'`, (done) => {
9 uppercase('test', (str) => {
10 expect(str).toBe('TEST')
11 done()
12 }
13})
Promise
در تابعهایی که Promise بازمیگردانند، کافی است در تست نیز از return a promise استفاده کنیم:
1//uppercase.js
2const uppercase = str => {
3 return new Promise((resolve, reject) => {
4 if (!str) {
5 reject('Empty string')
6 return
7 }
8 resolve(str.toUpperCase())
9 })
10}
11module.exports = uppercase
12//uppercase.test.js
13const uppercase = require('./uppercase')
14test(`uppercase 'test' to equal 'TEST'`, () => {
15 return uppercase('test').then(str => {
16 expect(str).toBe('TEST')
17 })
18})
Promise-هایی که رد میشوند را میتوان با استفاده از ()catch. تست کرد:
1//uppercase.js
2const uppercase = str => {
3 return new Promise((resolve, reject) => {
4 if (!str) {
5 reject('Empty string')
6 return
7 }
8 resolve(str.toUpperCase())
9 })
10}
11module.exports = uppercase
12//uppercase.test.js
13const uppercase = require('./uppercase')
14test(`uppercase 'test' to equal 'TEST'`, () => {
15 return uppercase('').catch(e => {
16 expect(e).toMatch('Empty string')
17 })
18})
Async/await
تابعهای تست که promise بازگشت میدهند، میتوانند با استفاده از Async/await نیز تست شوند. بدین ترتیب ساختاری کاملاً سرراست و ساده به دست میآید:
1//uppercase.test.js
2const uppercase = require('./uppercase')
3test(`uppercase 'test' to equal 'TEST'`, async () => {
4 const str = await uppercase('test')
5 expect(str).toBe('TEST')
6})
Mocking
Mocking در زمان تست کردن، امکان تست کارکردهایی را در اختیار ما قرار میدهد که به موارد زیر وابسته هستند:
- پایگاه داده
- درخواستهای شبکه
- دسترسی به فایلها
- هر نوع سیستم خارجی
بدین ترتیب گزارههای زیر را میتوان در مورد آن بیان کرد:
- تستها سریعتر اجرا میشوند و در زمان توسعه اپلیکیشن صرفهجویی زمانی زیادی ایجاد میشود.
- تستها مستقل از شرایط شبکه خواهند بود و وضعیت پایگاه داده نیز در این امر دخیل نخواهد بود.
- تستها هیچ آلودگی دادهای روی دیسک تولید نمیکنند، زیرا پایگاه داده را دستکاری نمیکنند.
- هر تغییری که در طی تست رخ دهد، تأثیری روی وضعیت تستهای بعدی نخواهد داشت و اجرای مجدد تستها را میتوان از نقطه آغازین شناخته شده و با قابلیت تولید مجدد شروع کرد.
- دیگر نیاز نیست نگران محدودیت نرخ فراخوانی API و درخواستهای شبکه باشید.
Mocking زمانی مفید خواهد بود که بخواهید از برخی عوارض جانبی مانند منتظر ماندن برای پایگاه داده اجتناب کنید و یا بخواهید از برخی بخشهای کد مانند دسترسی شبکه صرفنظر نمایید. همچنین با استفاده از mocking میتوانید از تأثیرهای اجرای چندباره تستها جلوگیری کنید. برای نمونه تابعی را تصور کنید که یک ایمیل ارسال میکند یا یک API دارای محدودیت نرخ را فراخوانی میکند.
نکته مهمتر این است که اگر مشغول نوشتن Unit Test هستید، باید کارکردهای یک تابع را به صورت مستقل از موارد دیگر تست کنید و از همه مواردی که به آن وابسته هستند صرفنظر کنید.
با استفاده از mock-ها میتوان بررسی کرد که آیا یک تابع ماژول فراخوانی شده یا نه و این که کدام پارامترها مورد استفاده قرار گرفتهاند. به این منظور میتوان از موارد زیر استفاده کرد:
- ()expect().toHaveBeenCalled بررسی میکند که آیا یک تابع خاص فراخوانی شده است یا نه.
- ()expect().toHaveBeenCalledTimes تعداد دفعاتی که یک تابع خاص فراخوانی شده را میشمارد.
- ()expect().toHaveBeenCalledWith بررسی میکند که آیا تابعی با مجموعه خاصی از پارامترها فراخوانی شده یا نه.
- ()expect().toHaveBeenLastCalledWith به بررسی پارامترهای آخرین بار اجرای تابع میپردازد.
بررسی بستهها بدون تأثیرگذاری بر کد تابع
زمانی که یک بسته را ایمپورت میکنید، میتوانید به Jest اعلام کنید که با استفاده از ()spyOn و بدون تأثیر گذاشتن بر طرز کار آن متد، روی مراحل اجرای یک تابع خاص نظارت کند. مثالی از این وضعیت را در ادامه مشاهده میکنید:
1const mathjs = require('mathjs')
2test(`The mathjs log function`, () => {
3 const spy = jest.spyOn(mathjs, 'log')
4 const result = mathjs.log(10000, 10)
5 expect(mathjs.log).toHaveBeenCalled()
6 expect(mathjs.log).toHaveBeenCalledWith(10000, 10)
7})
Mock کردن کل بسته
Jest یک روش آسان برای mock کردن کل یک بسته ارائه کرده است. به این منظور باید یک پوشه به نام __mocks__ در ریشه پروژه ایجاد کنید و در این پوشه یک فایل جاوا اسکریپت برای هر یک از بستههای خود ایجاد کنید.
فرض کنید بسته mathjs را ایمپورت کردهایم. بنابراین باید فایل mocks__/mathjs.js__ را در ریشه پروژه خود ایجاد کرده و محتوای زیر را به آن اضافه کنید:
1module.exports = {
2 log: jest.fn(() => 'test')
3}
بدین ترتیب تابع ()log بسته را mock میکنید. هر تعداد تابع که میخواهید برای mock کردن اضافه کنید:
1const mathjs = require('mathjs')
2test(`The mathjs log function`, () => {
3 const result = mathjs.log(10000, 10)
4 expect(result).toBe('test')
5 expect(mathjs.log).toHaveBeenCalled()
6 expect(mathjs.log).toHaveBeenCalledWith(10000, 10)
7})
Mock کردن یک تابع منفرد
یک روش سادهتر این است که یک تابع منفرد را با استفاده از ()jest.fn مانند زیر mock کنید:
1const mathjs = require('mathjs')
2mathjs.log = jest.fn(() => 'test')
3test(`The mathjs log function`, () => {
4 const result = mathjs.log(10000, 10)
5 expect(result).toBe('test')
6 expect(mathjs.log).toHaveBeenCalled()
7 expect(mathjs.log).toHaveBeenCalledWith(10000, 10)
8})
همچنین میتوانید از (jest.fn().mockReturnValue('test' برای ایجاد یک mock ساده استفاده کنید که هیچ کاری به جز بازگشت یک مقدار انجام نمیدهد.
Mock-های پیشساخته
میتوانید mock-های پیشساخته برای کتابخانههای محبوب را نیز دانلود کنید. برای نمونه این بسته (+) امکان mock کردن فراخوانیهای ()fetch را فراهم ساخته و مقادیر بازگشتی سادهای بدون تعامل با سرور واقعی در تستها بازگشت میدهد.
تست snapshot
تست snapshot یک ویژگی کاملاً جالب ارائه شده از سوی Jest است. این نوع تست میتواند شیوه رندر شدن کامپوننتهای UI شما را به خاطر بسپارد و آن را با تست کنونی مقایسه کند. در این حالت در صورت عدم مطابقت خطایی را ایجاد میکند.
این یک تست ساده روی کامپوننت App یک اپلیکیشن create-react-app ساده است. دقت کنید که باید حتماً create-react-app را نصب کرده باشید:
1import React from 'react'
2import App from './App'
3import renderer from 'react-test-renderer'
4it('renders correctly', () => {
5 const tree = renderer.create(<App />).toJSON()
6 expect(tree).toMatchSnapshot()
7})
نخستین باری که این تست را اجرا کنید، Jest اقدام به ذخیرهسازی اسنپشات حاصل در پوشه __snapshots__ خواهد کرد. محتوای App.test.js.snap به صورت زیر است:
1// Jest Snapshot v1, https://goo.gl/fbAQLP
2exports[`renders correctly 1`] = `
3<div
4 className="App"
5>
6 <header
7 className="App-header"
8 >
9 <img
10 alt="logo"
11 className="App-logo"
12 src="logo.svg"
13 />
14 <h1
15 className="App-title"
16 >
17 Welcome to React
18 </h1>
19 </header>
20 <p
21 className="App-intro"
22 >
23 To get started, edit
24 <code>
25 src/App.js
26 </code>
27 and save to reload.
28 </p>
29</div>
30`
همانطور که میبینید این کدی است که کامپوننت App رندر میکند و چیزی اضافه بر آن نیست. دفعه بعد که تست اقدام به مقایسه <App /> با این بکند، در صورتی که App تغییر یافته باشد، خطایی دریافت خواهید کرد:
زمانی که از yarn test در create-react-app استفاده میکنید، در حالت watch هستید و در این حالت میتوانید با فشردن w گزینههای بیشتری را مشاهده کنید:
1Watch Usage
2 › Press u to update failing snapshots.
3 › Press p to filter by a filename regex pattern.
4 › Press t to filter by a test name regex pattern.
5 › Press q to quit watch mode.
6 › Press Enter to trigger a test run.
اگر تغییرات شما رندر شود، با فشردن u میتوانید همه snapshot-های ناموفق را بهروزرسانی کرده و تست را با موفقیت به پایان ببرید. همچنین میتوانید snapshot را با استفاده از jest -u (یا updateSnapshot-) در خارج از حالت watch بهروزرسانی کنید.
تست کردن کامپوننتهای React
سادهترین راه برای آغاز تست کردن کامپوننتهای React اجرای تست snapshot است. این همان تکنیک تستی است که امکان تست کامپوننتها را به صورت مستقل از هم فراهم میسازد.
اگر با مراحل تست کردن نرمافزار آشنا باشید؛ این وضعیت همانند تست کردن کلاسها است و در این وضعیت کارکردهای هر کامپوننت تست میشوند.
تصور ما این است که شما یک اپلیکیشن React با استفاده از create-react-app ایجاد کردهاید که Jest به صورت پیشفرض روی آن نصب شده است و به تست کردن پکیجها نیاز دارید.
کار خود را با یک تست ساده آغاز میکنیم. CodeSandbox یک محیط عالی برای امتحان کردن این وضعیت است. کار خود را با React sandbox آغاز میکنیم و یک کامپوننت App.js را در یک پوشه components ایجاد کرده و یک فایل App.test.js اضافه میکنیم.
1import React from 'react'
2export default function App() {
3 return (
4 <div className="App">
5 <h1>Hello CodeSandbox</h1>
6 <h2>Start editing to see some magic happen!</h2>
7 </div>
8 )
9}
نخستین تست ما بدون بازگشتی است:
1test('First test', () => {
2 expect(true).toBeTruthy()
3})
زمانی که CodeSandbox فایلهای تست را تشخیص دهد، به طور خودکار آنها را برای شما اجرا میکند. میتوانید با کلیک روی دکمه Tests در انتهای view، نتایج تست خود را ملاحظه کنید:
یک فایل تست میتواند شامل چندین تست باشد:
در ادامه میخواهیم کار مفیدتری انجام دهیم و یک کامپوننت React را واقعاً تست کنیم. ما در حال حاضر صرفاً App را داریم که در واقع کار چندان مفیدی انجام نمیدهد و از این رو ابتدا باید محیط خود را با طراحی یک اپلیکیشن کوچک که کاراییهای بیشتری دارد بسازیم. اپلیکیشن counter را در سری مقالات قبلی از این سری آموزشی ساختهایم. اگر آن را انجام ندادهاید، میتوانید با مراجعه به بخش پنجم این آموزش آن را بسازید. اما برای مطالعه آسانتر یک بار دیگر آن را در این جا توضیح میدهیم.
این اپلیکیشن صرفاً از دو کامپوننت به نامهای App و Button ساخته شده است. فایل App.js را به صورت زیر بسازید:
1import React, { useState } from 'react'
2import Button from './Button'
3const App = () => {
4 const [count, setCount] = useState(0)
5 const incrementCount = increment => {
6 setCount(count + increment)
7 }
8 return (
9 <div>
10 <Button increment={1} onClickFunction={incrementCount} />
11 <Button increment={10} onClickFunction={incrementCount} />
12 <Button increment={100} onClickFunction={incrementCount} />
13 <Button increment={1000} onClickFunction={incrementCount} />
14 <span>{count}</span>
15 </div>
16 )
17}
18export default App
فایل Button.js نیز به صورت زیر است:
1import React from 'react'
2const Button = ({ increment, onClickFunction }) => {
3 const handleClick = () => {
4 onClickFunction(increment)
5 }
6 return <button onClick={handleClick}>+{increment}</button>
7}
8export default Button
ما قصد داریم از react-testing-library استفاده کنیم که کمک زیادی به ما میکند، زیرا خروجی هر کامپوننت را بازبینی کرده و رویدادهایی را روی آنها اعمال میکند. برای دریافت توضیح بیشتر در این خصوص به این صفحه (+) مراجعه کنید.
در ادامه ابتدا کامپوننت Button را تست میکنیم. کار خود را با پیادهسازی render و fireEvent از react-testing-library آغاز میکنیم که دو رویداد کمکی هستند. مورد اول امکان رندر کردن JSX را ایجاد میکند. مورد دوم نیز امکان صدور رویدادهایی روی یک کامپوننت را فراهم میسازد. یک فایل به نام Button.test.js بسازید و آن را در همان پوشه Button.js قرار دهید:
1import React from 'react'
2import { render, fireEvent } from 'react-testing-library'
3import Button from './Button'
این دکمهها در اپلیکیشن برای پذیرش یک رویداد کلیک استفاده میشوند و سپس تابعی را به prop-ی نام onClickFunction ارسال میکنند. ما یک متغیر count اضافه میکنیم و تابعی میسازیم که آن را افزایش دهد:
1let count
2const incrementCount = increment => {
3 count += increment
4}
اینک نوبت تستهای واقعی رسیده است. ابتدا شماره را برابر با 0 قرار میدهیم و یک کامپوننت +1 Button را با ارسال مقدار 1 به increment و تابع incrementCount به onClickFunction رندر میکنیم.
سپس محتوای فرزند اول کامپوننت را میگیریم و آن را با خروجی 1+ بررسی میکنیم. در ادامه روی دکمه کلیک میکنیم و بررسی میکنیم که آیا شماره از 0 به 1 میرسد یا نه:
1test('+1 Button works', () => {
2 count = 0
3 const { container } = render(
4 <Button increment={1} onClickFunction={incrementCount} />
5 )
6 const button = container.firstChild
7 expect(button.textContent).toBe('+1')
8 expect(count).toBe(0)
9 fireEvent.click(button)
10 expect(count).toBe(1)
11})
به طور مشابه یک دکمه 100+ را نیز تست میکنیم و این بار خروجی را با 100+ بررسی کرده و با کلیک روی دکمه شماره را به 100 افزایش میدهیم.
1test('+100 Button works', () => {
2 count = 0
3 const { container } = render(
4 <Button increment={100} onClickFunction={incrementCount} />
5 )
6 const button = container.firstChild
7 expect(button.textContent).toBe('+100')
8 expect(count).toBe(0)
9 fireEvent.click(button)
10 expect(count).toBe(100)
11})
اینک نوبت به تست کامپوننت App رسیده است. این کامپوننت 4 دکمه را نمایش میدهد و نتیجه در صفحه است. میتوانیم هر دکمه را بازبینی کرده و ببینیم آیا نتیجه کار زمانی که روی آنها کلیک میکنیم افزایش مییابد یا نه. همچنین کلیکهای چندگانه را نیز بررسی میکنیم:
1import React from 'react'
2import { render, fireEvent } from 'react-testing-library'
3import App from './App'
4test('App works', () => {
5 const { container } = render(<App />)
6 console.log(container)
7 const buttons = container.querySelectorAll('button')
8 expect(buttons[0].textContent).toBe('+1')
9 expect(buttons[1].textContent).toBe('+10')
10 expect(buttons[2].textContent).toBe('+100')
11 expect(buttons[3].textContent).toBe('+1000')
12 const result = container.querySelector('span')
13 expect(result.textContent).toBe('0')
14 fireEvent.click(buttons[0])
15 expect(result.textContent).toBe('1')
16 fireEvent.click(buttons[1])
17 expect(result.textContent).toBe('11')
18 fireEvent.click(buttons[2])
19 expect(result.textContent).toBe('111')
20 fireEvent.click(buttons[3])
21 expect(result.textContent).toBe('1111')
22 fireEvent.click(buttons[2])
23 expect(result.textContent).toBe('1211')
24 fireEvent.click(buttons[1])
25 expect(result.textContent).toBe('1221')
26 fireEvent.click(buttons[0])
27 expect(result.textContent).toBe('1222')
28})
کد عملی این تست را میتوانید در این سند باکس (+) مورد بررسی قرار دهید. بدین ترتیب به پایان این بخش از راهنمای جامع ریاکت میرسیم.
بخش هشتم: اکوسیستم ری اکت
اکوسیستم شکلگرفته پیرامون ریاکت بسیار بزرگ است. در این بخش که آخرین بخش از این آموزش محسوب میشود، 4 مورد از محبوبترین پروژهها بر مبنای React یعنی React Router ،Redux ،Next.js و Gatsby را معرفی میکنیم.
React Router
«مسیریاب ریاکت» (React Router) کتابخانه پیشفرض مسیریابی برای ریاکت محسوب میشود و یکی از محبوبترین پروژهها است که بر مبنای ریاکت ساخته شده است. ریاکت در هسته مرکزی خود یک کتابخانه بسیار ساده است و هیچ نکتهای در خصوص مسیریابی را شامل نمیشود.
مسیریابی یک اپلیکیشن تکصفحهای است که روشی برای معرفی برخی ویژگیهای جدید در زمینه ناوبری در اپلیکیشن از طریق لینکها محسوب میشود؛ این روش در زمینه وباپلیکیشنها کاملاً نرمال تلقی میشود:
- زمانی که به صفحههای مختلفی مراجعه میکنید، آدرس نمایش یافته در مرورگر باید تغییر پیدا کند.
- «لینک دهی عمیق» (Deep linking) باید کار کند، یعنی اگر مرورگر به یک لینک هدایت شود، اپلیکیشن باید همان ویو را که در زمان ایجاد URL ایجاد شده بود ارائه کند.
- دکمههای عقب و جلوی مرورگر باید مطابق انتشار عمل کنند.
در واقع مسیریاب ریاکت روشی برای ایجاد ارتباط بین ناوبری اپلیکیشن و ویژگیهای ناوبری ارائه شده از سوی مرورگر میسازد: یعنی ترکیبی از نوار آدرس مرورگر و دکمههای ناوبری اپلیکیشن. مسیریاب ریاکت روشی برای کدنویسی ارائه میکند که به وسیله آن کامپوننتهای خاصی از اپلیکیشن تنها در صورتی نمایش مییابند که مسیر مورد نظر با آن چه تعریف شده مطابقت داشته باشد.
نصب
با استفاده از npm میتوانید مسیریاب را با دستور زیر نصب کنید:
npm install react-router-dom
با استفاده از Yarn با دستور زیر روتر ریاکت را نصب کنید:
yarn add react-router-dom
انواع مسیریابی
مسیریاب ریاکت دو نوع مختلف از مسیریابی ارائه میکند:
- BrowserRouter
- HashRouter
یک روش URL-های کلاسیک میسازد و دیگری URL-هایی با هش ایجاد میکند:
https://application.com/dashboard/* BrowserRouter */ https://application.com/#/dashboard /* HashRouter */
این که باید از کدام یک استفاده کرد عموماً از سوی مرورگرهایی که باید پشتیبانی شوند تعیین میشود. BrowserRouter از History API (+) استفاده میکند که نسبتاً جدید است و در IE9 و نسخههای قدیمیتر از آن پشتیبانی نمیشود. اگر ضرورتی برای نگرانی در مورد مرورگرهای قدمی ندارید، این گزینه پیشنهاد خوبی محسوب میشود.
کامپوننتها
3 کامپوننت وجود دارند که هنگام کار با مسیریاب ریاکت با آنها بیشتر سر و کار خواهید داشت و به شرح زیر هستند:
- BrowserRouter که به صورت معمول Router نوشته میشود.
- Link
- Route
BrowserRouter همه کامپوننتهای Route را دربر میگیرد. کامپوننتهای Link همان طور که میتوان تصور کرد، آنهایی هستند که برای تولید لینک برای مسیرها استفاده میشوند. کامپوننتهای Route مسئول نمایش، یا مخفی سازی کامپوننتهایی که در خود جای دادهاند هستند.
BrowserRouter
در ادامه مثال سادهای از کامپوننت BrowserRouter را میبینید. آن را از react-router-dom ایمپورت کنید و برای پوشش همه اپلیکیشن از آن بهره بگیرید:
1import React from 'react'
2import ReactDOM from 'react-dom'
3import { BrowserRouter as Router } from 'react-router-dom'
4ReactDOM.render(
5 <Router>
6 <div>
7 <!-- -->
8 </div>
9 </Router>,
10 document.getElementById('app')
11)
یک کامپوننت تنها میتواند یک عنصر فرزند داشته باشد و از این رو همه آن چیزهایی که پوشش میدهد در یک عنصر div اضافه میشوند.
Link
کامپوننت Link برای آغاز مسیرهای جدید استفاده میشود. آن را از react-router-dom ایمپورت کنید. میتوانید کامپوننتهای Link را برای اشاره به مسیرهای مختلف با استفاده از خصوصیت to اضافه کنید:
1import React from 'react'
2import ReactDOM from 'react-dom'
3import { BrowserRouter as Router, Link } from 'react-router-dom'
4ReactDOM.render(
5 <Router>
6 <div>
7 <aside>
8 <Link to={`/dashboard`}>Dashboard</Link>
9 <Link to={`/about`}>About</Link>
10 </aside>
11 <!-- -->
12 </div>
13 </Router>,
14 document.getElementById('app')
15)
Route
اینک نوبت اضافه کردن کامپوننت Route در قطعه کد فوق است تا همه چیز عملاً آن چنان که میخواهیم کار کند:
1import React from 'react'
2import ReactDOM from 'react-dom'
3import { BrowserRouter as Router, Link, Route } from 'react-router-dom'
4const Dashboard = () => (
5 <div>
6 <h2>Dashboard</h2>
7 ...
8 </div>
9)
10const About = () => (
11 <div>
12 <h2>About</h2>
13 ...
14 </div>
15)
16ReactDOM.render(
17 <Router>
18 <div>
19 <aside>
20 <Link to={`/`}>Dashboard</Link>
21 <Link to={`/about`}>About</Link>
22 </aside>
23 <main>
24 <Route exact path="/" component={Dashboard} />
25 <Route path="/about" component={About} />
26 </main>
27 </div>
28 </Router>,
29 document.getElementById('app')
30)
برای آشنایی بیشتر میتوانید مثال موجود در این لینک (+) را بررسی کنید. زمانی که مسیری با / مطابقت مییابد، اپلیکیشن کامپوننت Dashboard را نمایش میدهد.
زمانی که مسیری از طریق کلیک کردن روی لینک «About» به about/ تغییر پیدا میکند، کامپوننت Dashboard حذف میشود و کامپوننت About در DOM درج میشود.
به خصوصیت exact دقت کنید. بدون وجود این خصوصیت، ”/”=path میتواند با about. مطابقت یابد و از این رو / در مسیر جای میگیرد.
مطابقت با مسیرهای چندگانه
شما میتوانید مسیری داشته باشید که به سادگی با استفاده از یک regex به چندین مسیر پاسخ دهد، زیرا path میتواند یک رشته «عبارتهای منظم» (Regular Expresions) باشد:
1<Route path="/(about|who)/" component={Dashboard} />
رندرینگ درونخطی
به جای تعیین مشخصه component روی Route میتوان یک prop به نام render را نیز تعیین کرد:
1<Route
2 path="/(about|who)/"
3 render={() => (
4 <div>
5 <h2>About</h2>
6 ...
7 </div>
8 )}
9/>
مطابقت پارامتر مسیر دینامیک
شما قبلاً شیوه استفاده از مسیرهای استاتیک را به صورت زیر مشاهده کردهاید:
1const Posts = () => (
2 <div>
3 <h2>Posts</h2>
4 ...
5 </div>
6)
7//...
8<Route exact path="/posts" component={Posts} />
در ادامه روش مدیریت مسیرهای دینامیک را بررسی میکنیم:
1const Post = ({match}) => (
2 <div>
3 <h2>Post #{match.params.id}</h2>
4 ...
5 </div>
6)
7//...
8<Route exact path="/post/:id" component={Post} />
در کامپوننت Route میتوانید پارامترهای دینامیک را در match.params بررسی کنید.
Match در مسیرهایی که به صورت درونخطی رندر میشوند نیز وجود دارد و در این مورد خاص بسیار مفید هست، زیرا میتوانیم پیش از رندر کردن Post با استفاده از پارامتر id به دنبال دادههای پست در منابع دادهای خود بگردیم:
1const posts = [
2 { id: 1, title: 'First', content: 'Hello world!' },
3 { id: 2, title: 'Second', content: 'Hello again!' }
4]
5const Post = ({post}) => (
6 <div>
7 <h2>{post.title}</h2>
8 {post.content}
9 </div>
10)
11//...
12<Route exact path="/post/:id" render={({match}) => (
13 <Post post={posts.find(p => p.id === match.params.id)} />
14)} />
Redux
Redux یک ابزار مدیریت حالت است که به طور معمول به همراه React استفاده میشود؛ اما ارتباط مستقیمی با این کتابخانه ندارد و میتواند از سوی فناوریهای دیگر نیز استفاده شود. در هر حال Redux همراه با ریاکت شناخته شده است. Redux روشی برای مدیریت حالت یک اپلیکیشن در اختیار ما قرار میدهد و آن را به یک external global store انتقال میدهد.
چندین مفهوم هستند که باید درک کنید و زمانی که با این مفاهیم آشنا شوید، متوجه میشوید که Redux رویکرد بسیار سادهای برای حل مسئله محسوب میشود.
Redux در اپلیکیشنهای ریاکت کاملاً محبوب است؛ اما به هیچ وجه منحصر به ریاکت نیست و برای همه فریمورکهای محبوب اتصالهایی ارائه شده است. ما در این نوشته مثالهایی را با استفاده از React به عنوان متداولترین کاربرد آن ارائه میکنیم.
چرا باید از Redux استفاده کنیم؟
Redux برای اپلیکیشنهای متوسط رو به بالا بسیار مناسب است. شما صرفاً باید زمانی از آن استفاده کنید که در مدیریت حالت با استفاده از گزارههای پیشفرض حالت در React یا هر کتابخانه دیگری که استفاده میکنید، مشکل داشته باشید. اپلیکیشنهای ساده نباید به هیچ وجه از آن استفاده کنند.
درخت حالت تغییر ناپذیر
در Redux کل حالت اپلیکیشن از سوی یک شیء جاوا اسکریپت به نام State یا State Tree ارائه میشود.
ما آن را «درخت حالت تغییرناپذیر» (Immutable State Tree) مینامیم، زیرا فقط-خواندنی است و نمیتواند مستقیماً تغییر پیدا کند. تنها راه تغییر دادن آن از طریق ارسال یک Action است.
Action-ها
منظور از Action، یک شیء جاوا اسکریپت است که یک تغییر را به روشی کمینه (یعنی صرفاً با اطلاعات ضروری) توصیف میکند:
1{
2 type: 'CLICKED_SIDEBAR'
3}
4// e.g. with more data
5{
6 type: 'SELECTED_USER',
7 userId: 232
8}
تنها الزام برای یک شیء اکشن این است که مشخصه type داشته باشد که مقدار آن یک رشته است.
نوع اکشنها باید ثابت باشد
در یک اپلیکیشن ساده نوع اکشن میتواند به صورت یک رشته تعریف شود و ما نیز چنین کاری را در بخش قبلی انجام دادهایم.
زمانی که اپلیکیشن بزرگتر میشود بهتر است که از ثابتها به این منظور استفاده کنیم:
1const ADD_ITEM = 'ADD_ITEM'
2const action = { type: ADD_ITEM, title: 'Third item' }
و بهتر است که اکشنها را در فایلهای مستقل خود جداسازی و آنها را به صورت زیر ایمپورت کنیم:
1import { ADD_ITEM, REMOVE_ITEM } from './actions'
Action Creator
«ایجادکننده اکشن» (Action Creator) تابعی است که اکشنها را ایجاد میکند.
1function addItem(t) {
2 return {
3 type: ADD_ITEM,
4 title: t
5 }
6}
ما به طور معمول ایجادکننده اکشنها را در ترکیب با راهاندازی یک dispatcher اجرا میکنیم:
1dispatch(addItem('Milk'))
همچنین همراه با تعریف کردن یک تابع dispatcher اکشن نیز اجرا میشوند:
1const dispatchAddItem = i => dispatch(addItem(i))
2dispatchAddItem('Milk')
کاهندهها
زمانی که یک اکشن صادر میشود، اتفاقی باید بیفتد و حالت اپلیکیشن باید تغییر یابد. این کار «کاهندهها» (Reducers) است. یک کاهنده در واقع یک «تابع خالص» (Pure Function) است که حالت بعدی درخت حالت را بر مبنای درخت حالت قبلی و اکشن صادر شده، محاسبه میکند.
1; (currentState, action) => newState
یک تابع خالص یک ورودی میگیرد و یک خروجی را بدون تغییر دادن ویو یا هر چیز دیگری عوض میکند. از این رو یک کاهنده موجب بازگشت یک شیء درخت کاملاً جدید میشود که جایگزین درخت قبلی میشود.
کاهنده چه کارهایی نباید بکند؟
یک کاهنده باید تابعی خالص باشد و از این رو موارد زیر مجاز نیست:
- هرگز نباید آرگومانهایش را تغییر دهد.
- هرگز نباید حالت را تغییر دهد؛ بلکه باید یک حالت جدید با استفاده از ({}, ...)Object.assign ایجاد کند.
- هرگز نباید عارضههای جانبی داشته باشد (هیچ فراخوانی API نباید هیچ چیزی را تغییر دهد).
- هرگز نباید تابعهای غیر خالص یعنی تابعهایی را که خروجی خود را بر مبنای عواملی به جز ورودیشان تغییر میدهند، فراخوانی کند. مثالهایی از تابعهای غیر خالص ()Date.now یا ()Math.random هستند.
البته هیچ الزامی در خصوص موارد فوق وجود ندارد؛ اما شما باید همواره قواعد را رعایت کنید.
کاهندههای چندگانه
از آنجا که حالت یک اپلیکیشن پیچیده میتواند واقعاً پیچیده باشد، در عمل هیچ کاهنده منفردی وجود ندارد؛ بلکه کاهندههای زیادی برای هر نوع اکشن موجود هستند.
شبیهسازی یک کاهنده
Redux در هسته مرکزی خود دارای این مدل ساده است:
حالت
1{
2 list: [
3 { title: "First item" },
4 { title: "Second item" },
5 ],
6 title: 'Groceries list'
7}
لیستی از اکشنها
1{ type: 'ADD_ITEM', title: 'Third item' }
2{ type: 'REMOVE_ITEM', index: 1 }
3{ type: 'CHANGE_LIST_TITLE', title: 'Road trip list' }
یک کاهنده برای هر بخش از حالت
1const title = (state = '', action) => {
2 if (action.type === 'CHANGE_LIST_TITLE') {
3 return action.title
4 } else {
5 return state
6 }
7}
8const list = (state = [], action) => {
9 switch (action.type) {
10 case 'ADD_ITEM':
11 return state.concat([{ title: action.title }])
12 case 'REMOVE_ITEM':
13 return state.map((item, index) =>
14 action.index === index
15 ? { title: item.title }
16 : item
17 default:
18 return state
19 }
20}
یک کاهنده برای کل حالت
1const listManager = (state = {}, action) => {
2 return {
3 title: title(state.title, action),
4 list: list(state.list, action)
5 }
6}
Store
Store یک شیء با خصوصیات زیر است:
- حالت اپلیکیشن را نگهداری میکند.
- حالت را از طریق ()getState افشا میکند.
- امکان بهروزرسانی حالت را از طریق ()dispatch فراهم ساخته است.
- امکان ثبت یک شنونده تغییر حالت را با استفاده از ()subscribe در اختیار ما قرار میدهد.
store برای هر اپلیکیشن منحصر به فرد است.
در ادامه روش ایجاد یک store برای اپلیکیشن listManager را مشاهده میکنید:
1import { createStore } from 'redux'
2import listManager from './reducers'
3let store = createStore(listManager)
آیا میتوانیم Store را با دادههای سرور مقداردهی اولیه بکنیم؟
چنین کاری ممکن است و صرفاً بایستی یک حالت آغازین ارسال شود:
1let store = createStore(listManager, preexistingState)
دریافت کردن حالت
1store.getState()
بهروزرسانی حالت
1store.dispatch(addItem('Something'))
گوش دادن به تغییرات حالت
1const unsubscribe = store.subscribe(() =>
2 const newState = store.getState()
3)
4unsubscribe()
گردش داده
گردش داده در Redux همواره غیر جهتدار است. شما میتوانید ()dispatch را روی store فراخوانی کرده و یک اکشن به آن ارسال کنید. در این حالت store مسئولیت ارسال اکشن به کاهنده و تولید حالت بعدی را بر عهده میگیرد. در ادامه Store حالت خود را بهروزرسانی کرده و به همه شنوندهها هشدار میدهد.
Next.js
کار کردن روی یک اپلیکیشن جاوا اسکریپت مدرن که از React نیرو میگیرد، بسیار جذاب به نظر میرسد؛ تا این که میفهمید چندین مشکل در ارتباط با رندر کردن همه محتوا در سمت کلاینت وجود دارد.
مشکل نخست این است که زمان مورد نیاز برای نمایش صفحه برای کاربر افزایش مییابد؛ چون پیش از آن که محتوا بارگذاری شود، باید همه کدهای جاوا اسکریپت بارگذاری شده باشند و اپلیکیشن نیاز دارد که اجرا شود تا مشخص شود که چیزی باید روی صفحه باید نمایش یابد.
مشکل دوم این است که اگر مشغول ساخت یک وبسایت در دسترس عموم باشید، با مشکلات مرتبط با سئو مواجه میشوید. موتورهای جستجو هم اینک عملکرد خود را در زمینه اجرا و اندیسگذاری اپلیکیشنهای جاوا اسکریپت بهبود بخشیدهاند؛ اما همچنان بسیار بهتر است که به جای این که به آنها اجازه دهیم خودشان محتوای ما را بارگذاری کنند، محتوای آماده خود را به آنها ارسال کنیم.
راهحل هر دو مشکل فوق رندر کردن سمت سرور است که به نام «پیش رندرینگ استاتیک» (Static pre-rendering) نیز نامیده میشود.
Next.js یک فریمورک ریاکت است که با استفاده از آن میتوانید همه این کارها را به روشی بسیار ساده انجام دهید؛ اما محدود به این موارد هم نیست. این فریمورک از سوی خالقانش به نام «مجموعه ابزار تک دستوری با پیکربندی صفر برای اپلیکیشنهای React» نامگذاری شده است.
این فریمورک ساختمان متداولی را ارائه میکند که امکان ساخت آسان اپلیکیشنهای فرانتاند ریاکت را در اختیار شما قرار میدهد و میتوانید به صورت شفافی رندرینگ سمت سروری را مدیریت کنید.
در ادامه فهرست غیر جامعی از ویژگیهای اصلی Next.js را مشاهده میکنید:
- بارگذاری مجدد بیدرنگ کد: Next.js هنگامی که تشخیص دهد هر گونه تغییری روی دیسک ذخیره شده است، صفحه را مجدداً بارگذاری میکند.
- مسیریابی خودکار: هر URL به فایلسیستم و فایلهایی که در پوشه pages قرار دارند نگاشت میشود و دیگر نیاز نیست هیچ گونه پیکربندی انجام دهید. البته امکان سفارشیسازی همچنان وجود دارد.
- کامپوننتهای تک فایلی: با استفاده از styled-jsx که به دلیل ساخته شدن از سوی همان تیم، کاملاً یکپارچهسازی شده است به سادگی میتوانید سبکهایی را در دامنه کامپوننت اضافه کنید.
- رندرینگ سرور: شما میتوانید (در صورت تمایل) کامپوننتهای ریاکت را در سمت سرور و پیش از ارسال TM به کلاینت رندر کنید.
- تطبیق با اکوسیستم: Next.js با بقیه بخشهای اکوسیستم جاوا اسکریپت، Node و React به خوبی کار میکند.
- افراز خودکار کد: صفحهها صرفاً به وسیله کتابخانهها و کد جاوا اسکریپتی که نیاز است رندر میشوند و به چیز دیگری نیاز ندارید.
- «پیشواکشی» (Prefetching): کامپوننت Link برای لینک کردن صفحههای مختلف استفاده میشود و از مشخصه پیشواکشی که به صورت خودکار منابع صفحه را در پسزمینه پیشواکشی میکند نیز پشتیبانی میکند.
- کامپوننتهای دینامیک: شما میتوانید ماژولهای جاوا اسکریپت و کامپوننتهای ریاکت را به صورت دینامیک ایمپورت کنید.
- اکسپورتهای استاتیک: Next.js با استفاده از دستور next export میتواند یک سایت کاملاً استاتیک را از اپلیکیشن شما استخراج و اکسپورت کند.
نصب Next.js
Next.js از همه پلتفرمهای عمده مانند لینوکس، macOS و ویندوز پشتیبانی میکند.
یک پروژه Next.js به سادگی با استفاده از npm به صورت زیر آغاز میشود:
npm install next react react-dom
همچنین با استفاده از Yarn به صورت زیر را اندازی میشود:
yarn add next react react-dom
آغاز به کار
یک فایل package.json با محتوای زیر ایجاد کنید:
1{
2 "scripts": {
3 "dev": "next"
4 }
5}
اگر این دستور را اجرا کنید:
npm run dev
اسکریپت مربوطه خطایی در خصوص عدم یافتن پوشه pages صادر میکند. در واقع این تنها نیازمندی Next.js برای شروع به کار است.
یک پوشه خالی pages ایجاد کرده و دستور را مجدداً اجرا کنید تا Next.js به طور خودکار سرور را در مسیر localhost:3000 آغاز کند. اکنون اگر به این URL مراجعه کنید با یک پیام خطای صفحه 404 مواجه میشوید که البته طراحی زیبایی دارد.
Next.js همه انواع خطاها مانند خطاهای 500 را نیز به خوبی مدیریت میکند.
ایجاد یک صفحه
در پوشه pages یک فایل به نام index.js با کامپوننت کارکردی ساده React ایجاد کنید:
1export default () => (
2 <div>
3 <p>Hello World!</p>
4 </div>
5)
اگر از مسیر localhost:3000 بازدید کنید، این کامپوننت به صورت خودکار رندر خواهد شد. شاید از خود بپرسید چرا این قدر ساده است؟ Next.js از یک ساختار اعلانی برای صفحهها استفاده میکند که مبتنی بر ساختار فایلسیستم است.
به بیان ساده صفحههای درون پوشه pages و URL صفحه از روی نام فایل تعیین میشوند. در واقع فایل سیستم همان API صفحهها محسوب میشود.
رندرینگ سمت سرور
در مرورگر کروم با مراجعه به مسیر View -> Developer -> View Source، کد منبع صفحه را باز کنید. همان طور که میبینید HTML ایجاد شده از سوی کامپوننت مستقیماً در منبع صفحه به مرورگر ارسال شده است. این کد در سمت کلاینت رندر نشده است؛ بلکه در سمت سرور رندر شده است.
تیم Next.js میخواستهاند یک تجربه توسعهدهنده برای صفحههای رندر شده در سمت سرور همانند تجربهای که در زمان ایجاد یک پروژه PHP ابتدایی کسب میکنید ارائه دهند. در زبان PHP فایلها را به سادگی در پوشهها قرار میدهیم و آنها را فراخوانی میکنیم و آنها صفحهها را نمایش میدهند. Next.js نیز به روش مشابهی عمل میکند؛ البته تفاوتهای زیادی دارد؛ اما سادگی استفاده از آن کاملاً مشهود است.
افزودن صفحه دوم
در این بخش یک صفحه دیگر در آدرس pages/contact.js ایجاد میکنیم.
1export default () => (
2 <div>
3 <p>
4 <a href="mailto:my@email.com">Contact us!</a>
5 </p>
6 </div>
7)
اگر مرورگر خود را به آدرس localhost:3000/contact هدایت کنید، این صفحه رندر خواهد شد. همان طور که شاهد هستید این صفحه نیز در سمت سرور رندر شده است.
بارگذاری مجدد بیدرنگ
همان طور که در بخش قبل دیدیم برای بارگذاری صفحه دوم نیازی به ریاستارت کردن npm نبود. Next.js این کار را در پسزمینه برای ما انجام میدهد.
رندرینگ کلاینت
رندرینگ سرور در بارگذاری نخست صفحه امری کاملاً رایج است و دلایل آن را قبلاً بررسی کردیم؛ اما زمانی که قرار است درون وبسایت حرکت کنیم، رندرینگ سمت کلاینت موجب افزایش سرعت بارگذاری صفحه و همچنین بهبود تجربه کاربری میشود.
Next.js یک کامپوننت Link ارائه کرده است که میتواند لینکهایی برای شما بسازد. در ادامه تلاش میکنیم دو صفحهای که در بخش قبل ساختیم را به هم لینک کنیم.
به این منظور کد فایل index.js را به صورت زیر تغییر دهید:
1import Link from 'next/link'
2export default () => (
3 <div>
4 <p>Hello World!</p>
5 <Link href="/contact">
6 <a>Contact me!</a>
7 </Link>
8 </div>
9)
اینک به مرورگر بازگردید و این لینک را امتحان کنید. همان طور که میبینید، صفحه جاری بیدرنگ و بدون نیاز به رفرش کردن صفحه، بارگذاری میشود.
این ناوبری سمت کلاینت به دستی کار میکند و پشتیبانی کامی از History API دارد. معنی آن این است که کاربران میتوانند از دکمه back مرورگر نیز استفاده کنند و چیزی از دست نمیرود.
اگر در این زمان روی لینک cmd-click (در ویندوز Ctrl+click) بکنید، همان صفحه Contact در برگه جدیدی باز میشود و این بار روی سرور رندر میشود.
صفحههای دینامیک
یک کاربرد خوب Next.js در زمینه blog است، چون بلاگ چیزی است که همه توسعهدهندهها با طرز کار آن آشنا هستند و برای توضیح روش مدیریت صفحههای دینامیک نیز مناسب است.
یک صفحه دینامیک صفحهای است که هیچ محتوای ثابتی ندارد؛ بلکه به جای آن مقداری دادههای مبتنی بر برخی پارامترها را نمایش میدهد.
فایل index.js را به صورت زیر تغییر دهید:
1import Link from 'next/link'
2const Post = props => (
3 <li>
4 <Link href={`/post?title=${props.title}`}>
5 <a>{props.title}</a>
6 </Link>
7 </li>
8)
9export default () => (
10 <div>
11 <h2>My blog</h2>
12 <ul>
13 <li>
14 <Post title="Yet another post" />
15 <Post title="Second post" />
16 <Post title="Hello, world!" />
17 </li>
18 </ul>
19 </div>
20)
این وضعیت موجب ایجاد یک سری پست میشود و پارامتر کوئری عنوان را با عناوین مطالب پر میکند:
اینک فایل post.js را در پوشه pages ایجاد کرده و کد زیر را اضافه کنید:
1export default props => <h1>{props.url.query.title}</h1>
در ادامه روی یک مطلب منفرد کلیک کنید تا عنوان پست در یک تگ h1 رندر شود:
شما میتوانید از URL-های تمیز بدون پارامترهای کوئری نیز استفاده کنید. کامپوننت Link در Next.js به ما کمک میکند که یک خصوصیت as را بپذیریم و از آن میتوان برای ارسال یک «نشانی مطلب» (Slug) استفاده کرد:
1import Link from 'next/link'
2const Post = props => (
3 <li>
4 <Link as={`/${props.slug}`} href={`/post?title=${props.title}`}>
5 <a>{props.title}</a>
6 </Link>
7 </li>
8)
9export default () => (
10 <div>
11 <h2>My blog</h2>
12 <ul>
13 <li>
14 <Post slug="yet-another-post" title="Yet another post" />
15 <Post slug="second-post" title="Second post" />
16 <Post slug="hello-world" title="Hello, world!" />
17 </li>
18 </ul>
19 </div>
20)
CSS-in-JS
Next.js به صورت پیشفرض از styled-jsx پشتیبانی میکند که یک راهحل CSS-in-JS ارائه شده از سوی همان تیم توسعه است، اما شما میتوانید از هر کتابخانهای که ترجیح میدهید مانند Styled Components استفاده کنید:
مثال:
1export default () => (
2 <div>
3 <p>
4 <a href="mailto:my@email.com">Contact us!</a>
5 </p>
6 <style jsx>{`
7 p {
8 font-family: 'Courier New';
9 }
10 a {
11 text-decoration: none;
12 color: black;
13 }
14 a:hover {
15 opacity: 0.8;
16 }
17 `}</style>
18 </div>
19)
استایل ها در دامنه کامپوننت هستند؛ اما میتوان استایلهای با دامنه سراسری را نیز با افزودن global به عنصر style ویرایش کرد:
1export default () => (
2 <div>
3 <p>
4 <a href="mailto:my@email.com">Contact us!</a>
5 </p>
6 <style jsx global>{`
7 body {
8 font-family: 'Benton Sans', 'Helvetica Neue';
9 margin: 2em;
10 }
11 h2 {
12 font-style: italic;
13 color: #373fff;
14 }
15 `}</style>
16 </div>
17)
اکسپورت کردن سایت استاتیک
یک اپلیکیشن Next.js میتواند به سادگی به صوت یک سایت استاتیک اکسپورت شود. این سایت را میتوان روی یکی از میزبانهای سایت بسیار سریع مانند Netlify یا Firebase Hosting میزبانی کرد و به این ترتیب نیازی هم به راهاندازی محیط Node وجود نخواهد داشت.
این فرایند نیازمند اعلان URL-هایی است که وبسایت را تشکیل میدهند؛ اما در کل فرایند سرراستی محسوب میشود.
توزیع وبسایت
ایجاد یک کپی آماده انتشار از اپلیکیشن بدون نگاشتهای منبع یا دیگر ابزارهای توزیع که در build نهایی مورد نیاز هستند، کار آسانی محسوب میشود.
در ابتدای این راهنما یک فایل package.json با این محتوا ایجاد کردیم:
1{
2 "scripts": {
3 "dev": "next"
4 }
5}
که روشی برای راهاندازی سرور توزیع با استفاده از npm run dev محسوب میشد. اینک محتوای زیر را به جای آن به فایل package.json اضافه میکنیم:
1{
2 "scripts": {
3 "dev": "next",
4 "build": "next build",
5 "start": "next start"
6 }
7}
بدین ترتیب اپلیکیشن خود را با اجرای npm run build و npm run start آمادهسازی میکنیم.
Now
شرکتی که Next.js را خلق کرده است یک سرویس میزبانی جذاب برای اپلیکیشنهای Node.js به نام Now نیز ارائه کرده است. البته آنها هر دو محصول را با هم ترکیب کردهاند به طوری که میتوانید اپلیکیشنهای Next.js را به صورت بیوقفه زمانی که Now را نصب کردید، با اجرای دستور now در پوشه اپلیکیشن توزیع کنید.
Now در پشت صحنه سرور را برای شما راهاندازی میکند و لازم نیست در مورد هیچ چیز دغدغه داشته باشید و کافی است منتظر باشید تا URL اپلیکیشن آماده شود.
Zone-ها
شما میتوانید چندین وهله از Next.js را راهاندازی کنید تا به URL-های متفاوت گوش دهند و با این وجود اپلیکیشن از نظر یک بیگانه این طور به نظر میرسد که گویا از یک سرور منفرد نیرو میگیرد.
افزونهها
Next.js فهرستی از افزونهها دارند که در این آدرس (+) میتوانید آنها را ملاحظه کنید.
Gatsby
Gatsby پلتفرمی برای ساخت اپلیکیشنها و وبسایتها با استفاده از React است. Gatsby یکی از ابزارهایی است که امکان ساخت یک مجموعه از فناوریها و رویهها را فراهم ساخته است که به صورت جمعی به نام JAMstack شناخته میشوند.
اینک Gatsby یکی از ابزارهای جالب در حوزه توسعه فرانتاند محسوب میشود. دلایل آن به شرح زیر هستند:
- استفاده گسترده از رویکرد JAMstack برای ساخت وباپلیکیشنها و وبسایتها.
- استفاده رو به فزونی از فناوری «وباپلیکیشنهای پیشرونده» (+) در این صنعت که یکی از ویژگیهای کلیدی گتسبی محسوب میشود.
- Gatsby در React و GraphQL ساخته شده است که دو فناوری محبوب و رو به رشد هستند.
- Gatsby واقعاً قدرتمند است.
- Gatsby سریع است.
- مستندات Gatsby عالی است.
- Gatsby موفق شده اثر شبکهای ایجاد کند یعنی افراد از آن استفاده میکنند، وبسایت میسازند، افراد بیشتری در مورد آن کسب اطلاع میکنند و یک چرخه ایجاد میشود.
- همه چیز در Gatsby با استفاده از جاوا اسکریپت نوشته شده است و نیازی به یادگیری زبانهای قالببندی جدید وجود ندارد.
- Gatsby پیچیدگی ذاتی خود را در ابتدا پنهان میکند و البته امکان دسترسی به همه مراحل مورد نیاز برای سفارشیسازی را در اختیار شما قرار میدهد.
Gatsby چگونه کار کند؟
اپلیکیشنهای شما با استفاده از Gatsby به وسیله کامپوننتهای React ساخته میشوند. محتوایی که در یک سایت رندر میکنید، عموماً با استفاده از Markdown نوشته میشود؛ اما میتوانید از هر نوع منبع دادهای مانند یک CSS به صورت headless یا وبسرویس همچون Contentful نیز استفاده کنید.
Gatsby سایت را میسازد و به صورت HTML استاتیک کامپایل میشود که میتواند روی هر وبسروری که میخواهید مانند Netlify، AWS S3، GitHub Pages، هر نوع ارائهدهنده خدمات میزبانی وبسایت و یا PAAS توزیع شود. تنها چیزی که نیاز دارید محلی است که صفحههای ساده HTTP و فایلهای شما را به کاربر ارائه کند.
در لیست فوق از وباپلیکیشنهای پیشرونده نیز یاد کردیم. گتسبی به صورت خودکار سایت شما را به صورت یک PWA میسازد. این کار به کمک یک «سرویس ورکر» انجام مییابد که سرعت بارگذاری صفحه و کش شدن منابع را افزایش میدهد.
شما میتوانید کارکردهای گتسبی را از طریق افزونهها ارتقا بدهید.
نصب Gatsby
Gatsby را میتوان با اجرای دستور زیر در ترمینال نصب کرد:
npm install -g gatsby-cli
دستور فوق ابزار CLI مربوط به Gatsby را نصب میکند. زمانی که نسخه جدیدی انتشار یابد، میتوانید با اجرای مجدد دستور فوق آن را بهروزرسانی کنید. با اجرای دستور زیر یک وبسایت «Hello World» جدید ایجاد میشود.
این دستور یک سایت Gatsby کاملاً جدید در پوشه mysite و با استفاده از starter که در این آدرس (+) قرار دارد، ایجاد میکند.
starter یک سایت ساده است که میتوان بر مبنای آن سایتهای دیگری را ساخت. یک استارتر رایج دیگر default است که در این آدرس (+) موجود است.
فهرستی از همه استارتر هایی که میتوان استفاده کرد را میتوانید در این آدرس (+) مشاهده کنید.
اجرای سایت Gatsby
پس از این که ترمینال نصب استارتر را به پایان برد، میتوانید وبسایت را با فراخوانی دستورهای زیر اجرا کنید:
cd mysite gatsby develop
دستورهای فوق یک وبسرور جدید را راهاندازی میکنند و سایت را روی پورت 8000 روی localhost عرضه میکنند.
تصویر وبسایت Hello World در عمل به صورت زیر است:
بررسی سایت
اگر سایتی را که ایجاد کردید با ویرایشگر کد محبوب خود باز کنید، 4 پوشه در آن میبینید:
- پوشه cache.: این یک پوشه پنهان است که شامل موارد داخلی Gatsby است و چیزی که لازم باشد در حال حاضر تغییر دهید در آن وجود ندارد.
- پوشه public: این پوشه پس از ساخت وبسایت، شامل آن خواهد بود.
- پوشه src: این پوشه شامل کامپوننتهای react است که در این مورد کامپوننت index را شامل میشود.
- پوشه static: این پوشه شامل منابع استاتیک مانند CSS و تصاویر است.
اینک ایجاد یک تغییر ساده در صفحه پیشفرض کار سادهای محسوب میشود. کافی است فایل src/pages/index.js را باز کنید و «Hello world!» را به چیز دیگری تغییر دهید و آن را ذخیره کنید. در این حالت، مرورگر باید بیدرنگ کامپوننت را بارگذاری مجدد بکند. این بدان معنی است که صفحه رفرش نمیشود؛ اما محتوای آن تغییر مییابد. این ترفند از سوی فناوریهای تشکیلدهنده آن ممکن شده است.
برای افزودن یک صفحه دوم کافی است یک فایل js. دیگر در همان پوشه ایجاد کنید و همان محتوای index.js را در آن قرار داده و ذخیره کنید. در ادامه محتوای آن را دستکاری خواهیم کرد.
برای نمونه یک فایل second.js را با محتوای زیر ایجاد میکنیم:
1import React from 'react'
2export default () => <div>Second page!</div>
و مرورگر را باز کرده و به آدرس زیر میرویم:
http://localhost:8000/second
لینک کردن صفحهها
میتوان این صفحهها را با استفاده از ایمپورت کردن یک کامپوننت React به نام Link به هم لینک کرد:
1import { Link } from "gatsby"
همچنین میتوان آن را در JSX کامپوننت مورد استفاده قرار داد:
1<Link to="/second/">Second</Link>
افزودن CSS
میتوان هر فایل CSS را با استفاده از ایمپورت جاوا اسکریپت، ایمپورت کرد:
1import './index.css'
میتوانید از استایلدهی React نیز استفاده کنید:
1<p style={{
2 margin: '0 auto',
3 padding: '20px'
4 }}>Hello world</p>
استفاده از افزونهها
Gatsby چیزهای زیادی را به صورت آماده عرضه میکند؛ اما بسیاری از کارکردهای آن در افزونهها نهفته هستند. گتسبی سه نوع افزونه دارد:
افزونههای سورس
این افزونهها به واکشی دادهها از یک منبع میپردازند. گرههایی را ایجاد میکنند که میتوان در ادامه با استفاده از افزونههای transformer آنها را فیلتر کرد.
افزونههای transformer
این افزونهها دادههای ارائه شده از سوی افزونههای سورس را به چیزی تبدیل میکنند که گتسبی بتواند استفاده کند.
افزونههای کاربردی
این افزونهها به پیادهسازی برخی از انواع کارکردها مانند افزودن پشتیبانی از «نقشه سایت» (sitemap) و موارد دیگر میپردازند.
برخی از افزونههای پرکاربرد گتسبی به شرح زیر هستند:
- gatsby-plugin-react-helmet
این افزونه (+) امکان ویرایش محتوای تگ head را فراهم میسازد.
- gatsby-plugin-catch-links
این افزونه (+)-ای است که از History API استفاده میکند تا جلوی بارگذاری مجدد صفحه در زمان کلیک شدن یک لینک را بگیرد و به جای آن محتوای جدید را با استفاده از AJAX بارگذاری کند.
یک افزونه گتسبی در 2 مرحله نصب میشود. ابتدا آن را با استفاده از npm نصب میکنیم و سپس آن را در فایل gatsby-config.js به پیکربندی گتسبی اضافه میکنیم. برای نمونه میتوانید افزونه Catch Links را با دستور زیر نصب کنید:
npm install gatsby-plugin-catch-links
در فایل gatsby-config.js (اگر این فایل را ندارید در پوشه ریشه وبسایت آن را بسازید) افزونه را به آرایه اکسپورت شده plugins اضافه کنید:
1module.exports = {
2 plugins: ['gatsby-plugin-catch-links']
3}
کار به همین سادگی است، اینک افزونه کار خود را انجام خواهد داد.
ساخت وبسایت استاتیک
زمانی که کار دستکاری سایت پایان یافت و خواستید یک سایت استاتیک نهایی بسازید میتوانید دستور زیر را اجرا کنید:
gatsby build
در این زمان میتوانید با آغاز کردن یک وبسرور محلی با استفاده از دستور زیر، بررسی کنید که آیا همه چیز مطابق انتظار کار میکند یا نه:
gatsby serve
دستور فوق سایت را تا حد امکان شبیه به آنچه در توزیع نهایی خواهید دید، رندر میکند.
توزیع
زمانی که سایت را با استفاده از gatsby build ساختید، تنها کاری که باید انجام دهید توزیع نتیجه سایت حاصل، در پوشه public است.
بسته به این که از چه راهحلی استفاده میکنید به این منظور باید مراحل مختلفی را طی کنید؛ اما به طور کلی لازم است که سایت را به یک ریپازیتوری Git ارسال کنید و اجازه بدهید قلابهای پس از کامیت Git کار توزیع را بر عهده بگیرند. در این آدرس (gatsby serve) میتوانید در این خصوص راهنماییهای بیشتری برای پلتفرمهای محبوب دریافت کنید.
بدین ترتیب به پایان این راهنمای آموزش ریاکت میرسیم. امیدواریم این نوشته جامع برای شما مفید بوده باشد و دستکم نقطه شروعی برای بررسی جنبههای پیشرفتهتر برنامهنویسی React در اختیار شما قرار داده باشد.
جمعبندی
ریاکت در سال 2011 از سوی یکی از مهندسان فیسبوک به نام «جردن ووک» (Jordan Walke) ایجاد شد.
این کتابخانه نخستین بار در سال 2011 در نیوزفید فیسبوک و سپس در سال 2012 در اینستاگرام استفاده شد. کتابخانه مذکور در می 2013 به صورت متن باز عرضه شده است. React Native که امکان توسعه اپلیکیشنهای اندروید و iOS را فراهم ساخته است در سال 2015 از سوی فیسبوک به صوت متن باز عرضه شد. در سال 2017 فیسبوک React Fiber را معرفی کرد که الگوریتم مرکزی جدید کتابخانه فریمورک ریاکت برای ساخت رابطهای کاربری است. ریاکت فیبر به بنیادی برای هر گونه توسعه و بهبود آتی فریمورک ریاکت تبدیل خواهد شد. آخرین نسخه از فریمورک ریاکت با شماره 16.8.4 در 5 مارس 2019 عرضه شده است.
==
ممنون از نویسنده مقاله واقعا کارشون حرف نداشت
آیا این مقاله به صورت pdf هم موجوده چون میخواستم چاپش کنم