تعامل کاربران با نرم افزارهای ری اکت

29 اسفند 1397
درسنامه درس 10 از سری آموزش react (ری اکت)
React-user-interaction

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

تعامل با کاربران

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

برای مثال می توانیم یک تابع برای رویداد mousemove مرورگر مطابق زیر تعریف کنیم:

export const go = () => {
  const ele = document.getElementById('mousemove');
  ele.innerHTML = 'Move your mouse to see the demo';
  ele.addEventListener('mousemove', function(evt) {
    const { screenX, screenY } = evt;
    ele.innerHTML = '<div>Mouse is at: X: ' +
          screenX + ', Y: ' + screenY +
                    '</div>';
  })
}

نتیجه اجرای کد بالا باعث می شود که شما بتوانید ماوس را در مرورگر حرکت دهید.

در ری اکت ما نمی توانیم با جاوا اسکریپت معمولی با رویدادهای مرورگر کار کنیم، چون ری اکت به ما این امکان را می دهد تا بتوانیم رویدادها را توسط props مدیریت کنیم.

برای مثال برای گوش دادن به رویداد mousemove در برنامه بالا، باید پروپرتی onMouseMove را مطابق زیر تعریف کنیم:

<div onMouseMove={(evt) => console.log(evt)}>
  Move the mouse over this text
</div>

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

برای دیدن نحوه کار با رویدادها، در زیر یک برنامه کوچک نوشته و چند رویداد را از طریق props به عناصر برنامه پاس می دهیم.

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

ما از پروپرتی onClick به طور مکرر در برنامه مان استفاده می کنیم، پس بهتر است که با این رویداد بیشتر آشنا شوید.

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

حال می خواهیم هنگامی که کاربر روی آیکن جستجو کلیک کرد، فیلد جستجو (<input/>) برای او نمایان شود.

کامپوننت Header مطابق زیر است:

class Header extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      searchVisible: false
    }
  }

  // toggle visibility when run on the state
  showSearch() {
    this.setState({
      searchVisible: !this.state.searchVisible
    })
  }

  render() {
    // Classes to add to the <input /> element
    let searchInputClasses = ["searchInput"];

    // Update the class array if the state is visible
    if (this.state.searchVisible) {
      searchInputClasses.push("active");
    }

    return (
      <div className="header">
        <div className="menuIcon">
          <div className="dashTop"></div>
          <div className="dashBottom"></div>
          <div className="circle"></div>
        </div>

        <span className="title">
          {this.props.title}
        </span>

        <input
          type="text"
          className={searchInputClasses.join(' ')}
          placeholder="Search ..." />

        {/* Adding an onClick handler to call the showSearch button */}
        <div
          onClick={this.showSearch.bind(this)}
          className="fa fa-search searchIcon"></div>
      </div>
    )
  }
}

هنگامی که کاربر روی عنصر <div className="fa fa-search searchIcon"></div> کلیک می کند، یک تابع برای بروزرسانی وضعیت کامپوننت اجرا می کنیم، در نتیجه آبجکت searchInputClose بروزرسانی می شود.

با استفاده از onClick می توان اینکار را خیلی ساده تر انجام داد.

حال باید این کامپوننت را stateful کنیم (چون می خواهیم ببینیم که آیا فیلد جستجو باید نمایان شود یا خیر). برای انجام اینکار از تابع constructor() مطابق زیر استفاده می کنیم:

class Header extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      searchVisible: false
    }
  }
  // ...
}

تابع constructor() چیست؟

در جاوا اسکریپت constructor تابعی است که هنگام ایجاد یک آبجکت، اجرا می شود. این تابع یک ارجاع به تابع آبجکتی که نمونه prototype را ایجاد کرده، بر می گرداند.

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

هنگامی که از سینتکس کلاس ES6 برای ساخت یک آبجکت استفاده می کنید، باید متد super() را قبل از هر متد دیگری فراخوانی کنید. فراخوانی تابع super() باعث فراخوانی تابع constructor() در کلاس والد می شود. این متد را با همان آرگومان هایی که در تابع constructor() استفاده کرده بودیم، بکار می بریم. هنگامی که کاربر روی دکمه کلیک می کند، وضعیت searchVisible را بروزرسانی می کنیم. چون می خواهیم کاربر با کلیک دوباره روی آیکن جستجو، فیلد <input/>را پنهان کند، همان وضعیت searchVisible را با استفاده از عملگر ! تغییر وضعیت می دهیم.

حال این متد را برای اتصال به رویداد کلیک، تعریف می کنیم:

class Header extends React.Component {
  // ...
  showSearch() {
    this.setState({
      searchVisible: !this.state.searchVisible
    })
  }
  // ...
}

در انتها یک اداره کننده رویداد click (با استفاده از پروپرتی onClick) روی عنصر آیکن برای فراخوانی متد showSearch() تعریف می کنیم. سورس کد کامل کامپوننت Header مطابق زیر خواهد شد.

class Header extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      searchVisible: false
    }
  }

  // toggle visibility when run on the state
  showSearch() {
    this.setState({
      searchVisible: !this.state.searchVisible
    })
  }

  render() {
    // Classes to add to the <input /> element
    let searchInputClasses = ["searchInput"];

    // Update the class array if the state is visible
    if (this.state.searchVisible) {
      searchInputClasses.push("active");
    }

    return (
      <div className="header">
        <div className="fa fa-more"></div>

        <span className="title">
          {this.props.title}
        </span>

        <input
          type="text"
          className={searchInputClasses.join(' ')}
          placeholder="Search ..." />

        {/* Adding an onClick handler to call the showSearch button */}
        <div
          onClick={this.showSearch.bind(this)}
          className="fa fa-search searchIcon"></div>
      </div>
    )
  }
}

حال با کلیک روی آیکن جستجو می بینید که فیلد جستجو نمایان و پنهان می شود.

رویدادهای input

هر گاه بخواهیم یک فرم در ری اکت ایجاد کنیم می توانیم از رویدادهای inputیی که توسط ری اکت ایجاد شده اند، استفاده کنیم. برای مثال ما معمولا از پروپرتی های onSubmit() و onChange() به دفعات استفاده می کنیم.

حال فیلد جستجوی خود را برای دریافت متن وارد شده در آن بروزرسانی می کنیم. اگر پروپرتی onChange() روی فیلد <input/> تعریف شود، با هر بار تغییری که در مقدار آن فیلد اتفاق بیفتد، تابعی که به پروپرتی onChange نسبت داده شده بود اجرا می شود. پس هنگامی که روی آن کلیک کرده و شروع به تایپ کنیم، این متد اجرا می شود.

با استفاده از این پروپرتی می توانی مقدار وارد شده در این فیلد را دریافت کنیم. اجازه دهید بجای بروزرسانی کامپوننت <Header>، یک کامپوننت فرزند جدید که شامل یک عنصر فرم است را ایجاد کنیم. با انتقال مسئولیت مدیریت فرم به خود آن فرم، کدهای <Header> تمیزتر می شود و می توانیم هنگامی که کاربر فرم را submit کرد، آن را از طریق کامپوننت والد Header دریافت کنیم.

حال یک کامپوننت جدید به نام searchForm ایجاد می کنیم. این کامپوننت جدید یک کامپوننت statesul است، چون ما باید مقدار فیلد جستجو را نگهداری کنیم.

class SearchForm extends React.Component {
  // ...
  constructor(props) {
    super(props);

    this.state = {
      searchText: ''
    }
  }
  // ...
}

ما قبلا یک فرم در کامپوننت <Header> تعریف کرده بودیم. حال آن کدها را برداشته و در داخل متد searchForm.render() قرار می دهیم:

class SearchForm extends React.Component {
  // ...
  render() {
    const { searchVisible } = this.state;
    let searchClasses = ['searchInput']
    if (searchVisible) {
      searchClasses.push('active')
    }

    return (
      <form className='header'>
        <input
          type="search"
          className={searchClasses.join(' ')}
          onChange={this.updateSearchInput.bind(this)}
          placeholder="Search ..." />

        <div
          onClick={this.showSearch.bind(this)}
          className="fa fa-search searchIcon"></div>
      </form>
    );
  }
}

دقت کنید که ما نمی توانیم از استایل ها در فیلد <input> استفاده کنیم، چون ما وضعیت searchVisible را روی این کامپوننت جدید تعریف نکرده ایم، بنابراین نمی توانیم از آن برای استایل دهی <input/> استفاده کنیم. با این حال می توانیم یک پروپرتی از کامپوننت Header به searchForm ارسال کنیم تا فیلد input را نمایان کند.

حال یک پروپرتی به نام searchVisible تعریف کرده و تابع render را برای استفاده از این پروپرتی جدید بروزرسانی می کنیم تا بتواند به درستی فیلد جستجو را نمایان یا پنهان کند. همچنین یک مقدار پیش فرض false برای حالت نمایان بودن فیلد جستجو تعریف می کنیم.

class SearchForm extends React.Component {
  static propTypes = {
    onSubmit: PropTypes.func.isRequired,
    searchVisible: PropTypes.bool
  }
  // ...
}

به این ترتیب توانستیم به فیلد <input/> مان استایل اضافه کنیم. حال می خواهیم قابلیتی به برنامه اضافه کنیم تا بتوانیم مقداری که کاربر در فیلد جستجو وارد کرده را دریافت کنیم.

برای انجام اینکار یک پروپرتی onChange() به عنصر input مان متصل می کنیم و همچنین یک متد به این رویداد پاس می دهیم تا هر زمان که مقدار فیلد input تغییر کرد، این متد اجرا شود.

class SearchForm extends React.Component {
  // ...
  updateSearchInput(e) {
    const val = e.target.value;
    this.setState({
      searchText: val
    });
  }
  // ...
  render() {
    const { searchVisible } = this.state;
    let searchClasses = ['searchInput']
    if (searchVisible) {
      searchClasses.push('active')
    }

    return (
      <form className='header'>
        <input
          type="search"
          className={searchClasses.join(' ')}
          onChange={this.updateSearchInput.bind(this)}
          placeholder="Search ..." />

        <div
          onClick={this.showSearch.bind(this)}
          className="fa fa-search searchIcon"></div>
      </form>
    );
  }
}

هنگامی که چیزی را در فیلد input تایپ کنیم، تابع ()updateSearchInput فراخوانی می شود و با بروزرسانی state می توانیم مقدار فرم را نگهداری کنیم. همچنین متد ()this.setState را در داخل تابع ()updateSearchInput برای بروزرسانی وضعیت کامپوننت فراخوانی می کنیم.

class SearchForm extends React.Component {
  // ...
  updateSearchInput(e) {
    const val = e.target.value;
    this.setState({
      searchText: val
    });
  }
  // ...
}

مقایسه کنترل شده با کنترل نشده

کامپوننتی که ایجاد کردیم، اصطلاحاً یک کامپوننت کنترل نشده نامیده می شود. چون ما مقداری برای عنصر <input/> تنظیم نکرده ایم. همچنین از هیچ اعتبارسنجی یا پردازشی روی مقدار فیلد input استفاده نکرده ایم.

اگر بخواهیم مقدار کامپوننت <input> را اعتبارسنجی کرده یا آن را تغییر دهیم، باید یک کامپوننت کنترل شده ایجاد کنیم، یعنی یک مقدار را توسط پروپرتی value به آن ارسال کنیم.

در روش کنترل شده، متد ()render مطابق زیر است:

class SearchForm extends React.Component {
  render() {
    return (
      <input
        type="search"
        value={this.state.searchText}
        className={searchInputClasses}
        onChange={this.updateSearchInput.bind(this)}
        placeholder="Search ..." />
    );
  }
}

تا به اینجا ما هیچ قابلیتی برای ارسال فرم ایجاد نکردیم، بنابراین کاربر نمی تواند جستجویی انجام دهد. برای اینکار باید فیلد <input/> را در داخل یک تگ <form/> قرار دهیم تا کاربر با فشار دادن دکمه Enter بتواند فرم را ارسال کند. همچنین می توانیم توسط پروپرتی ()onSubmit که روی یک تگ <form/> تعریف می شود، ارسال فرم را کنترل کنیم.

برای انجام اینکار متد ()render را مطابق زیر بروزرسانی می کنیم.

class SearchForm extends React.Component {
  // ...
  submitForm(e) {
    e.preventDefault();

    const {searchText} = this.state;
    this.props.onSubmit(searchText);
  }
  // ...
  render() {
    const { searchVisible } = this.props;
    let searchClasses = ['searchInput']
    if (searchVisible) {
      searchClasses.push('active')
    }

    return (
      <form onSubmit={this.submitForm.bind(this)}>
        <input
          type="search"
          className={searchClasses.join(' ')}
          onChange={this.updateSearchInput.bind(this)}
          placeholder="Search ..." />
      </form>
    );
  }
}

همان طور که می بینید ما از دستور ()event.preventDefault در داخل متد ()onSubmit استفاده کردیم. این دستور از رفرش شدن کامل یک صفحه در هنگام ارسال فرم، جلوگیری می کند.

حال، هنگامی که متنی را در فیلد input تایپ کرده و Enter را فشار دهیم، تابع ()submitForm فراخوانی می شود.

خب، ما توانستیم فرم را ارسال کنیم، اما کجا عمل جستجو را انجام می دهیم؟

در حال حاضر برای اهداف نمایشی، متن جستجو را به کامپوننت های والد-فرزند ارسال می کنیم و کامپوننت Header تصمیم میگیرد که چه چیزی را جستجو کند.

کامپوننت searchForm نمی داند که در حال جستجوی چه چیزی است، چون ما مسئولیت اینکار را به صورت زنجیره وار انجام داده ایم.

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

حال یک پروپرتی به نام onSubmit() تعریف کرده و آن را به کامپوننت searchForm ارسال می کنیم. چون می خواهیم مطمئن شویم که onSubmit() حتماً تعریف می شود، پس قابلیت required را به پروپرتی onSubmit() اضافه می کنیم.

class SearchForm extends React.Component {
  static propTypes = {
    onSubmit: PropTypes.func.isRequired,
    searchVisible: PropTypes.bool
  }
  // ...
  static defaultProps = {
    onSubmit: () => {},
    searchVisible: false
  }
  // ...
}

هنگامی که فرم ارسال شد، این متد را بطور مستقیم از طریق props فراخوانی می کنیم. چون ما متن جستجو را در state نگهداری کرده ایم، می توانیم تابع را با مقدار searchText بر روی state فراخوانی کنیم. بنابراین تابع onSubmit() تنها آن مقدار را دریافت می کند و نیازی به کار کردن با رویداد ندارد.

class SearchForm extends React.Component {
  // ...
  submitForm(event) {
    // prevent the form from reloading the entire page
    event.preventDefault();
    // call the callback with the search value
    this.props.onSubmit(this.state.searchText);
  }
}

حال هنگامی که کاربر روی enter کلیک کرد، می توانیم توسط کامپوننت Header تابعی که به عنوان پروپرتی onSubmit() پاس داده شده را فراخوانی کنیم. همچنین می توانیم در داخل کامپوننت Header از کامپوننت searchForm هم استفاده کنیم و دو پروپرتی که برای آن تعریف کرده ایم (یعنی searchVisible و onSubmit) را به آن ارسال کنیم.

import React from 'react';
import SearchForm from './SearchFormWithSubmit'

class Header extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      searchVisible: false
    }
  }

  // toggle visibility when run on the state
  showSearch() {
    this.setState({
      searchVisible: !this.state.searchVisible
    })
  }

  render() {
    // Classes to add to the <input /> element
    let searchInputClasses = ["searchInput"];

    // Update the class array if the state is visible
    if (this.state.searchVisible) {
      searchInputClasses.push("active");
    }

    return (
      <div className="header">
        <div className="menuIcon">
          <div className="dashTop"></div>
          <div className="dashBottom"></div>
          <div className="circle"></div>
        </div>

        <span className="title">
          {this.props.title}
        </span>

        <SearchForm
          searchVisible={this.state.searchVisible}
          onSubmit={this.props.onSubmit} />

        {/* Adding an onClick handler to call the showSearch button */}
        <div
          onClick={this.showSearch.bind(this)}
          className="fa fa-search searchIcon"></div>
      </div>
    )
  }
}

export default Header

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

پیاده سازی جستجو

برای پیاده سازی جستجو در کامپوننت، مسئولیت جستجو را به یک مرحله بالاتر، از کامپوننت Header به یک کامپوننت container به نام panel ارسال می کنیم.

اول از همه، باید الگوی مشابه با ارسال یک کالبک به یک کامپوننت والد از داخل یک کامپوننت فرزند را از کانتینر panel به کامپوننت Header پیاده سازی کنیم. حال یک پروپرتی به نام onSearch در قسمت propType کامپوننت Header تعریف می کنیم:

class Header extends React.Component {
  // ...
}
Header.propTypes = {
  onSearch: PropTypes.func
}

حال در داخل متد submitForm() کامپوننت Header، این پروپرتی onSearch() را فراخوانی می کنیم.

class Header extends React.Component {
  // ...
  submitForm(val) {
    this.props.onSearch(val);
  }
  // ...
}
Header.propTypes = {
  onSearch: PropTypes.func
}

دقت داشته باشید که درخت مجازی مان مطابق زیر خواهد بود:

<Panel>
  <Header>
    <SearchForm></SearchForm>
  </Header>
</Panel>

هنگامی که searchForm() بروزرسانی شد، هر تغییری که در فیلد جستجو اتفاق بیفتد را به کامپوننت والد خود یعنی <Header> ارسال می کند.

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

حال به کامپوننت Panel که در درس هفتم ایجاد کرده بودیم، بر می گردیم و توسط پروپرتی onSearch() یک تابع به Header() ارسال می کنیم.

سپس هنگامی که فرم جستجو ارسال شد، مقدار فیلد جستجو را به کامپوننت ارسال کرده و سپس توسط کامپوننت panel، جستجو را مدیریت می کنیم.

چون کامپوننت Header جستجوی محتوا را انجام نمی دهد و در عوض کامپوننت panel اینکار را می کند، ما این مسئولیت را به یک سطح بالاتر ارسال می کنیم.

در هر حال، کامپوننت panel یک کپی از کامپوننت content است که ما قبلا با آن کار می کردیم.

class Panel extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      loading: false, // <~ set loading to false
      activities: data,
      filtered: data,
    }
  }

  componentDidMount() {this.updateData();}
  componentWillReceiveProps(nextProps) {
    // Check to see if the requestRefresh prop has changed
    if (nextProps.requestRefresh === true) {
      this.setState({loading: true}, this.updateData);
    }
  }

  handleSearch = txt => {
    if (txt === '') {
      this.setState({
        filtered: this.state.activities
      })
    } else {
      const { activities } = this.state
      const filtered = activities.filter(a => a.actor && a.actor.login.match(txt))
      this.setState({
        filtered
      })
    }
  }

  // Call out to github and refresh directory
  updateData() {
    this.setState({
      loading: false,
      activities: data
    }, this.props.onComponentRefresh);
  }

  render() {
    const {loading, filtered} = this.state;

    return (
      <div>
        <Header
          onSubmit={this.handleSearch}
          title="Github activity" />
        <div className="content">
          <div className="line"></div>
          {/* Show loading message if loading */}
          {loading && <div>Loading</div>}
          {/* Timeline item */}
          {filtered.map((activity) => (
            <ActivityItem
              key={activity.id}
              activity={activity} />
          ))}

        </div>
      </div>
    )
  }
}

حال stateمان را بروزرسانی کرده و یک رشته به نام searchFilter را به آن اضافه می کنیم که در حقیقت مقدار فیلد جستجو را در خود نگه می دارد:

class Panel extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      loading: false,
      searchFilter: '',
      activities: []
    }
  }
}

همچنین برای مدیریت جستجو باید یک تابع onSearch() به کامپوننت Header ارسال کنیم. پس یک تابع onSearch() در کامپوننت Panel تعریف کرده و آن را به پروپرتی Header در متد render() ارسال می کنیم:

class Panel extends React.Component {
  // ...
  // after the content has refreshed, we want to
  // reset the loading variable
  onComponentRefresh() {this.setState({loading: false});}

  handleSearch(val) {
    // handle search here
  }

  render() {
    const {loading} = this.state;

    return (
      <div>
        <Header
          onSearch={this.handleSearch.bind(this)}
          title="Github activity" />
        <Content
          requestRefresh={loading}
          onComponentRefresh={this.onComponentRefresh.bind(this)}
          fetchData={this.updateData.bind(this)} />
      </div>
    )
  }
}

در بالا یک تابع handleSearch() تعریف کرده و آن را به Header ارسال می کنیم. حال هرگاه کاربر متنی را در فیلد جستجو وارد کند، تابع handleSearch() بر روی کامپوننت panel فراخوانی می شود.

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

پس ابتدا باید searchFilter را روی state تنظیم کنید.

همچنین می توانیم با تنظیم loading به true  کامپوننت content را وادار کنیم تا داده ها را از نو بارگذاری کند.

class Panel extends React.Component {
  // ...
  handleSearch(val) {
    this.setState({
      searchFilter: val,
      loading: true
    });
  }
  // ...
}

هر چند اینکار ممکن است پیچیده به نظر برسد، اما در حقیقت تقریباً همان کار متد updateDate() را انجام می دهد، بجز اینکه ما نتایج متد fetch() را برای فراخوانی متد filter() بر روی یک مجموعه داده های json فراخوانی کردیم.

کاری که متد collection.filter() انجام می دهد این است که متدی که به آن پاس داده می شود را روی تمام عناصر اجرا می کند و مقادیری که مورد نیازمان است را برمی گرداند. هرگاه که متد updateData() بروزرسانی شد، عمل جستجوی مان تکمیل می شود.

حال یک کامپوننت برنامه سه لایه داریم که جستجوها را از داخل یک کامپوننت فرزند مدیریت می کند.

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

در درس بعدی به آموزش ساخت کامپوننت های خالص می پردازیم.

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری آموزش react (ری اکت) توصیه می‌کند:
نویسنده شوید

دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.