مقدمه‌ای بر React و نحوه عملکرد آن - بخش دوم
ﺯﻣﺎﻥ ﻣﻄﺎﻟﻌﻪ: 11 دقیقه

مقدمه‌ای بر React و نحوه عملکرد آن - بخش دوم

در قسمت اول مروری داشتیم بر نحوه عملکرد ری‌اکت و مقایسه آن با سایر فریمورک‌های فرانت-اند. همچنین یک مثال ساده از راه‌اندازی پروژه و ابزارهای مورد نیاز را نیز بررسی کردیم.

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

ایجاد اولین کامپوننت

برای اینکه همه چیز تمیز و مرتب باشد، ابتدا پوشه‌ای به نام components در دایرکتوری src ایجاد می‌کنیم. اولین کامپوننت ما Header.js خواهد بود.

توجه داشته باشید این یک فایل جاوا اسکریپت است که از حرف اول بزرگ در نام گذاریش استفاده می‌شود.

notesapp
├── public
│ └── index.html
└── src
├── components // New folder
│ └── Header.js // New component
├── App.css
├── App.js
└── index.js

از آنجایی که ما از توابع برای تعریف یک کامپوننت کمک می‌گیریم، به دو روش می‌توانیم این کار را انجام دهیم:

  • تابع Regular
  • تابع Arrow

در این آموزش همه کامپوننت‌های ما از توابع Arrow استفاده می‌کنند و فقط کامپوننت App است که از یک تابع Regular بهره می‌گیرد. هنگامی که شروع به یکپارچه‌سازی propها می‌کنیم، توابع Arrow به کمک ما می‌آیند که در ادامه بیشتر راجع به این موضوع توضیح خواهیم داد.

درست مانند نام فایل کامپوننت، مطمئن شوید که نام تابع نیز با حرف بزرگ شروع می‌شود.

// notesapp > src > components > Header.js

 

const Header = () => {
    return (
        <div>
            <h1>My Header</h1>
     </div>
    )
}

 

export default Header;

همچنین برای استفاده از کامپوننت Header در برنامه خود، ابتدا باید آن را ایمپورت کنیم.

// notesapp > src > App.js
 
import './App.css';
import Header from './components/Header' // Import component
 
function App() {
  return (
    <div className="App">
      <Header />
    </div>
  );
}
 
export default App;

 

به طور مشابه بیایید یک کامپوننت Body.js نیز ایجاد کنیم. این بار از میانبر ارائه شده توسط پلاگین ES7 React snippets که در قسمت اول آن را نصب کردیم، استفاده می‌نماییم. به همین منظور برای ایجاد یک کامپوننت ری‌اکت با تابع Arrow، از میانبر زیر کمک بگیرید:

rafce

در این صورت اسنیپت زیر برای ما ایجاد می‌شود تا آن را کامل کنیم:

// notesapp > src > components > Body.js
 
import React from 'react'
 
const Body = () => {
    return (
        <div>
           
        </div>
    )
}
 
export default Body

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

با استفاده از یک میانبر دیگر می‌توانیم کامپوننت Body خود را در App.js به صورت زیر ایمپورت کنیم:

imp + tab

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

import moduleName from 'module'

پس از ایمپورت کردن می‌توانیم کامپوننت Body خود را وارد برنامه کنیم.

// notesapp > src > App.js
 
import './App.css';
import Header from './components/Header'
import Body from './components/Body' // Import component
 
function App() {
  return (
    <div className="App">
      <Header />
      <Body />
    </div>
  );
}
 
export default App;

 

نکاتی که هنگام ساخت کامپوننت‌ها باید به خاطر بسپارید:

  • هر کامپوننت باید یک عنصر والد داشته باشد که همه چیزهای دیگر را پوشش دهد.
  • به جای استفاده از <div> به‌عنوان والد، می‌توانید از فرگمنت‌ها (یعنی </><>) استفاده کنید.

افزودن صفحات

صفحات در ری‌اکت همانند کامپوننت‌ها هستند، اما برای نگهداریشان آن‌ها را در فولدرهایی قرار می‌دهیم. بیایید یک پوشه جدید در src به نام pages ایجاد کنیم. در داخل آن یک کامپوننت صفحه جدید به نام NotesListPage.js می‌سازیم که به عنوان صفحه اصلی برنامه Notes عمل می‌کند.

notesapp
├── public
│ └── index.html
└── src
├── pages // New folder
│ └── NotesListPage.js // New file
├── components
│ ├── Header.js
│ └── Body.js
├── App.css
├── App.js
└── index.js

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

در داخل کامپوننت صفحه، محتوایی را به آن می‌دهیم.

// notesapp > src > pages > NotesListPage.js
 
import React from 'react'
 
const NotesListPage = () => {
    return (
        <div>
            Notes
        </div>
    )
}
 
export default NotesListPage

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

// notesapp > src > App.js
 
import './App.css';
import Header from './components/Header'
import NotesListPage from './pages/NotesListPage'
 
function App() {
  return (
    <div className="App">
      <Header />
      <NotesListPage />
    </div>
  );
}
 
export default App;

کار با داده‌ها

تا به اینجا تمام محتوا را برای notesapp کدنویسی کردیم. خوب است به نحوه رندر داده‌هایی که در جای دیگری ذخیره شده‌اند نیز نگاهی بیاندازیم. در حال حاضر ما با داده‌هایی که در یک فایل محلی ذخیره شده‌اند کار خواهیم کرد.

یک فایل جدید به نام data.js ایجاد کنید که در داخل assets پوشه جدید قرار دارد.

notesapp
├── public
│ └── index.html
└── src
├── assets // New folder
│ └── data.js // New file
├── pages
│ └── NotesListPage.js
├── components
│ ├── Header.js
│ └── Body.js
├── App.css
├── App.js
└── index.js

داخل data.js آرایه‌ای از اشیاء note با سه خصوصیت ایجاد می‌کنیم: id، body و updated.

// notesapp > src > assets > data.js
 
let notes = [
    {
        "id": 1,
        "body": "Todays Agenda\\n\\n- Walk Dog\\n- Feed fish\\n- Play basketball\\n- Eat a salad",
        "updated": "2021-07-14T13:49:02.078653Z"
    },
    {
        "id": 2,
        "body": "Bob from bar down the \\n\\n- Take out trash\\n- Eat food",
        "updated": "2021-07-13T20:43:18.550058Z"
    },
    {
        "id": 3,
        "body": "Wash car",
        "updated": "2021-07-13T19:46:12.187306Z"
    }
]
 
export default notes;

برای دسترسی به data.js باید آن را ایمپورت کنیم. سپس یک کانتینر نیاز داریم تا تمام محتوای note را از طریق تابع map جاوا اسکریپت فهرست کند.

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

// notesapp > src > pages > NotesListPage.js
 
import React from 'react'
import notes from '../assets/data' // Import data
 
const NotesListPage = () => {
    return (
        <div>
            <div className='notes-list'>
                {notes.map(note => (
                    <p>{note.body}</p>
                ))}
            </div>
        </div>
    )
}
 
export default NotesListPage

 

اکنون کامپوننت صفحه NotesListPage در حال رندر داده‌هاست. با این حال می‌خواهیم هر note را به کامپوننت خاص خود تبدیل کنیم. بنابراین یک کامپوننت جدید ListItem.js ایجاد می‌کنیم که می‌توانیم جدا از کامپوننت صفحه آن را سفارشی کنیم.

// notesapp > src > components > ListItem.js
 
import React from 'react'
 
const ListItem = () => {
    return (
        <div>
            <h3>List Item</h3>
        </div>
    )
}
 
export default ListItem

سپس کامپوننت ListItem را به صفحه ایمپورت کرده و تگ پاراگراف خود را جایگزین می‌کنیم.

// notesapp > src > pages > NotesListPage.js
 
import React from 'react'
import notes from '../assets/data'
import ListItem from '../components/ListItem'
 
const NotesListPage = () => {
    return (
        <div>
            <div className='notes-list'>
                {notes.map(note => (
                    <ListItem/>
                ))}
            </div>
        </div>
    )
}
 
export default NotesListPage

 

Propها

در حال حاضر نمی‌توانیم یادداشت‌های خود را ببینیم، زیرا هیچ داده‌ای به جز ListItem ارسال نشده است. برای انجام این کار باید از چیزی به نام prop استفاده کنیم.

در ری‌اکت دو نوع داده وجود دارد:

  • Prop: شکل تغییرناپذیری از داده‌هاست. به این معنی که پس از انتشار نمی‌توان آن را تغییر داد.
  • State: داده‌‌ای است که می‌توانیم آن را به‌روزرسانی کنیم (در ادامه بیشتر در این مورد بحث می‌کنیم).

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

بیایید ببینیم وقتی از شی props در کامپوننت ListItem، consol.log می‌گیریم چه اتفاقی می‌افتد.

// notesapp > src > components > ListItem.js
 
import React from 'react'const ListItem = (props) => {
           
    console.log("PROPS:", props)
           
    return (
        <div>
            <h3>List Item</h3>
        </div>
    )
}
 
export default ListItem

 

اولین چیزی که متوجه خواهید شد این است که یک خطای warning وجود دارد. دلیل خطا هم این است که داخل NotesListPage.js در حال حلقه زدن و برگرداندن هر ListItem هستیم. با این حال زمانی که ما این کار را انجام می‌دهیم، ری‌اکت باید هر آیتم را در DOM مجازی شناسایی کند. بنابراین هر زمان که یک آیتم را به‌روزرسانی می‌کنیم، به جای به‌روزرسانی هر آیتم می‌داند که کدام مورد را باید براساس ویژگی key تغییر دهد.

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

// notesapp > src > pages > NotesListPage.js
 
import React from 'react'
import notes from '../assets/data'
import ListItem from '../components/ListItem'
 
const NotesListPage = () => {
    return (
        <div>
            <div className='notes-list'>
                {notes.map((note, index) =>
                    <ListItem key={index} />
                )}
            </div>
        </div>
    )
}
 
export default NotesListPage

دومین چیزی که متوجه خواهید شد این است که مقدار props یک شی خالی در هر آیتم لیست خواهد بود. مشابه ویژگی key می‌توانیم محتوای یادداشت را همانطور که یک ویژگی HTML سفارشی اضافه می‌شود، ارسال کنیم. در این حالت از آنجایی که قبلا note را از طریق تابع map استخراج کرده‌ایم، می‌توانیم شی note را به ListItem ارسال کنیم تا به محتوای آن دسترسی داشته باشیم.

// notesapp > src > pages > NotesListPage.js
 
import React from 'react'
import notes from '../assets/data'
import ListItem from '../components/ListItem'
 
const NotesListPage = () => {
    return (
        <div>
            <div className='notes-list'>
                {notes.map((note, index) =>
                    <ListItem key={index} note={note}/>
                )}
            </div>
        </div>
    )
}
 
export default NotesListPage

console.log نشان می‌دهد که props دیگر خالی نیست و هر شی note را برمی‌گرداند.

برای رندر محتوای هر note در کامپوننت فرزند، به ListItem.js برمی‌گردیم تا مشخص کنیم کدام پارامتر را از شی note می‌خواهیم.

دسترسی به پارامتر به صورت زیر است:

props → attribute → key/parameter of object

// notesapp > src > components > ListItem.js
 
import React from 'react'const ListItem = (props) => {
    console.log("PROPS:", props)
    return (
        <div>
            <h3>{props.note.body}</h3>
        </div>
    )
}
 
export default ListItem

 

از بین بردن Propها

یک راه میانبر برای استفاده از propها، تخریب شیء است. در این مثال ما prop را با note جایگزین می‌کنیم، به این معنی که نیازی به تایپ کردن props در ابتدا نخواهیم داشت.

// notesapp > src > components > ListItem.js
 
import React from 'react'const ListItem = ({note}) => {
    return (
        <div>
            <h3>{note.body}</h3>
        </div>
    )
}
 
export default ListItem

ایجاد یک کامپوننت Subpage

تا کنون تنها یک صفحه با استفاده از کامپوننت NotesListPage برای فهرست کردن همه یادداشت‌های فعلی ایجاد کرده‌ایم. با این حال اگر بخواهیم کامپوننت صفحه دیگری برای جابجایی ایجاد کنیم، باید از چیزی به نام React Router کمک بگیریم.

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

به همین منظور یک کامپوننت صفحه جدید به نام NotePage.js ایجاد کنید که در دایرکتوری pages قرار بگیرد. هدف NotePage نشان دادن هر یادداشت جداگانه در صفحه خواهد بود.

notesapp
├── public
│ └── index.html
└── src
├── assets
│ └── data.js
├── pages
│ ├── NotePage.js // New file
│ └── NotesListPage.js
├── components
│ ├── Header.js
│ └── Body.js
├── App.css
├── App.js
└── index.js

داخل NotePage یک کامپوننت عمومی را با استفاده از میانبر rafce + tab برمی‌گردانیم و سپس یک رشته در داخل <div> اضافه کرده تا زمانی که آن را رندر می‌کنیم، بتوانیم شناساییش کنیم.

// notesapp > src > pages > NotePage.js
 
import React from 'react'const NotePage = () => {
    return (
        <div>
            <h1>This is a single note page</h1>
        </div>
    )
}
 
export default NotePage

قبل‌تر ایمپورت کردن صفحه به App.js و آوردن آن به کامپوننت App را آموختیم. پس بیایید این کار را انجام دهیم.

// notesapp > src > App.jsimport './App.css';

import Header from './components/Header'
import NotesListPage from './pages/NotesListPage'
import NotePage from './pages/NotePage'
 
function App() {
  return (
    <div className="App">
      <Header />
      <NotesListPage />
      <NotePage />
    </div>
  );
}
 
export default App;export default App;

 

اما چیزی که متوجه خواهید شد نمایان شدن هر دو صفحه در یک view است (تصویر بالا). حال کاری که می‌خواهیم انجام دهیم سویچ کردن بین دو صفحه NotesListPage و NotePage خواهد بود.

نصب React Router

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

npm install react-router-dom@5.2.1

پس از نصب باید آن را از react-router-dom در App.js ایمپورت کنیم.

// notesapp > src > App.js
 
import { BrowserRouter as Router, Route } from "react-router-dom";

ایجاد مسیرها

برای استفاده از React Router، کل کامپوننت برنامه باید در عنصر <Router> قرار گیرد.

// notesapp > src > App.js
 
import { BrowserRouter as Router, Route } from "react-router-dom";
import './App.css';
import Header from './components/Header'
import NotesListPage from './pages/NotesListPage'
import NotePage from './pages/NotePage'
 
function App() {
  return (
    <Router>
      <div className="App">
        <Header />
        <NotesListPage />
      </div>
    </Router>
  );
}
 
export default App;

Router کنترل می‌کند که کدام کامپوننت‌ها را ببینیم. بنابراین هر کامپوننت صفحه یک Route در نظر گرفته می‌شود و هر چیزی که با کامپوننت <Route> ایجاد نشده باشد (مانند Header) همیشه در برنامه ظاهر می‌شود.

// notesapp > src > App.js
 
import { BrowserRouter as Router, Route } from "react-router-dom";
import "./App.css";
import Header from "./components/Header";
import NotesListPage from "./pages/NotesListPage";
import NotePage from "./pages/NotePage";
 
function App() {
 return (
  <Router>
   <div className='App'>
    <Header />
    <Route path='/' exact component={NotesListPage} />
   </div>
  </Router>
 );
}
 
export default App;

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

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

حالا بیایید NotePage را اضافه کنیم که از طریق آدرس http://localhost:3000/note به آن دسترسی خواهیم داشت.

// notesapp > src > App.js
 
import { BrowserRouter as Router, Route } from "react-router-dom";
import "./App.css";
import Header from "./components/Header";
import NotesListPage from "./pages/NotesListPage";
import NotePage from "./pages/NotePage";
 
function App() {
 return (
  <Router>
   <div className='App'>
    <Header />
    <Route path='/' exact component={NotesListPage} />
    <Route path='/note' component={NotePage} />
   </div>
  </Router>
 );
}
 
export default App;

مسیرهای داینامیک

تاکنون دو مسیر ایجاد کرده‌ایم: / و note/. هر دوی اینها همیشه محتوای یکسانی را برمی‌گردانند. با این حال نمی‌خواهیم مسیرهای جداگانه‌ای برای محتوای هر یادداشت منحصربه‌فرد ایجاد کنیم.

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

پارامترهای سفارشی باید با یک : شروع شوند، اما می‌توان به جای id هر نامی را به آن‌ها اختصاص داد.

// notesapp > src > App.js
 
import { BrowserRouter as Router, Route } from "react-router-dom";
import "./App.css";
import Header from "./components/Header";
import NotesListPage from "./pages/NotesListPage";
import NotePage from "./pages/NotePage";
 
function App() {
 return (
  <Router>
   <div className='App'>
    <Header />
    <Route path='/' exact component={NotesListPage} />
    <Route path='/note/:id' component={NotePage} />
   </div>
  </Router>
 );
}
 
export default App;

حالا وقتی به note/ می‌رویم فقط هدر ظاهر می‌شود. اما اگر یک مسیر فرعی مانند note/test/ اضافه کنیم، محتوای NotePage را برمی‌گرداند.

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

// notesapp > src > pages > NotePage.js
 
import React from 'react'const NotePage = (props) => {
    console.log("PROPS:", props)
    return (
        <div>
            <h1>This is a single note page</h1>
        </div>
    )
}
 
export default NotePage

روتر اطلاعاتی در مورد URLها به ما داد. موردی که ما با آن کار خواهیم کرد match است، زیرا id صفحه‌ای که در آن برای کوئری از پایگاه داده استفاده می‌کنیم را ارائه می‌دهد.

به طور خاص می‌خواهیم در داخل match از آنچه در پارامترها ذخیره شده است، استفاده کنیم.

چیزی که می‌خواهیم به آن دسترسی پیدا کنیم، params → id است.

// notesapp > src > pages > NotePage.js
 
import React from 'react'const NotePage = (props) => {
    console.log("PROPS:", props)
    console.log("PROPS:", props.match.params.id)
    return (
        <div>
            <h1>This is a single note page</h1>
        </div>
    )
}
 
export default NotePage

مجددا بیایید کد خود را با تخریب (destructing) پاک کنیم.

// notesapp > src > pages > NotePage.js
 
import React from 'react'const NotePage = ({match}) => {
    let noteId = match.params.id
    console.log("noteId:", noteId)

    return (
        <div>
            <h1>This is a single note page</h1>
        </div>
    )
}
 
export default NotePage

 

اکنون که می‌توانیم به هر note بر اساس id آن برویم، بیایید لینک‌هایی را برای رفتن به هر یک از این صفحات تنظیم کنیم. برای انجام این کار اجازه دهید به ListItem.js برگردیم.

در آن تگ Link را از react-router-dom وارد می‌کنیم که معادل تگ <a> در html است. تگ Link به یک خصوصیت to نیاز دارد که ما یک مقدار داینامیک را از طریق template literal به آن اختصاص می‌دهیم.

// notesapp > src > components > ListItem.js
 
import React from 'react'
import { Link } from 'react-router-dom'const ListItem = ({note}) => {
    return (
        <Link to={`/note/${note.id}`}>
            <h3>{note.body}</h3>
        </Link>
    )
}
 
export default ListItem

 

اکنون که هر لینکی به مسیر صحیح خود می‌رود، باید محتوای note را به هر صفحه برگردانیم. برای انجام این کار، باید داده‌ها را به NotePage.js ایمپورت کنیم.

با دسترسی به داده‌های noteها می‌توانیم از Vanilla JavaScript برای یافتن یادداشت بر اساس noteId بهره بگیریم.

توجه: مطمئن شده‌ایم که متغیر noteId به عدد تبدیل می‌شود، زیرا فیلتری که برای یافتن id داریم یک عدد است، نه یک رشته.

// notesapp > src > pages > NotePage.js
 
import React from 'react'
import notes from '../assets/data'
 
const NotePage = ({match}) => {
    let noteId = match.params.id
    let note = notes.find(note => note.id === Number(noteId));    
 
          console.log("noteId:", noteId)
    return (
        <div>
            <p>{note.body}</p>
        </div>
    )
}
 
export default NotePage

 

مدیریت Note‌هایی که وجود ندارند

برای نمایش خطای یادداشت‌های موجود، یک علامت سوال (که به عنوان Optional Chaining شناخته می‌شود) اضافه می‌کنیم تا یک مقدار تعریف نشده یعنی یک مقدار خالی را برگردانیم که فقط Header ظاهر شود.

// notesapp > src > pages > NotePage.js
 
import React from 'react'
import notes from '../assets/data'
 
const NotePage = ({match}) => {
    let noteId = match.params.id
       let note = notes.find(note => note.id === Number(noteId));    
           console.log("noteId:", noteId)
    return (
        <div>
            <p>{note?.body}</p>
        </div>
    )
}
 
export default NotePage

مقاله را تا همینجا به پایان می‌رسانیم. امیدوارم از این آموزش لذت برده باشید. سعی می‌کنیم مباحث بعدی را در آینده‌ای نزدیک منتشر کنیم.

منبع

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

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

/@erfanheshmati
عرفان حشمتی
Full-Stack Web Developer

کارشناس معماری سیستم های کامپیوتری، طراح و توسعه دهنده وب سایت، تولیدکننده محتوا

دیدگاه و پرسش

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

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

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