برنامه نویسی 1438 بازدید

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

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

مشکل چیست؟

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

واکشی داده

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

Ajax آغازین

مشکلی که در بخش قبل توضیح دادیم، منجر به ایجاد فناوری‌هایی شد که به صفحه‌های وب امکان می‌داد بخش‌های کوچکی از داده (مانند HTML ،XML ،JSON یا متن ساده) را درخواست کنند و آن‌ها را در موارد ضروری نمایش دهند. بدین ترتیب مشکل توصیف شده در بخش فوق حل می‌شد.

این کار از طریق استفاده از API-هایی مانند XMLHttpRequest یا در سال‌های اخیر Fetch API صورت می‌گیرد. این فناوری‌ها به صفحه‌های وب امکان داده‌اند که مستقیماً درخواست‌های HTTP برای منابع خاصی که روی سرور قرار دارند ارسال کنند و داده‌های بازگشتی را در صورت نیاز پیش از نمایش قالب‌بندی کنند.

نکته: در روزهای آغازین این تکنیک عمومی به نام جاوا اسکریپت و XML ناهمگام یا به اختصار Ajax شناخته می‌شود و از XMLHttpRequest برای درخواست داده‌های XML استفاده می‌کرد. البته این وضعیت امروزه دیگر به آن صورت سابق نیست، چون ما امروزه از XMLHttpRequest یا Fetch برای درخواست JSON استفاده می‌کنیم، اما نتیجه کار همان است و در نتیجه هنوز از همان نام Ajax برای توصیف این فناوری استفاده می‌شود.

واکشی داده

مدل Ajax شامل استفاده از API به عنوان پراکسی برای درخواست هوشمندانه‌تر داده‌ها به جای واداشتن مرورگر به بارگذاری مجدد کل صفحه است. مزیت‌های این روش به شرح زیر هستند:

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

این وضعیت مناسبی است زیرا:

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

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

یک درخواست ساده Ajax

در این بخش به بررسی شیوه مدیریت یک چنین درخواست‌هایی با استفاده از XMLHttpRequest و Fetch که در بخش قبل توضیح دادیم، می‌پردازیم. در این مثال‌ها، داده‌ها را از فایل‌های مختلف متنی درخواست می‌کنیم و از آن‌ها برای مقداردهی یک ناحیه محتوایی بهره می‌گیریم.

این سری از فایل‌ها به عنوان نوعی پایگاه داده استفاده می‌شوند. در واقع در یک اپلیکیشن واقعی ما عموماً از یک زبان سمت سرور مانند PHP یا پایتون یا Node برای درخواست داده‌ها از یک پایگاه داده استفاده می‌کنیم. با این حال در این بخش برای این که همه چیز ساده‌تر باشد و روی سمت کلاینت متمرکز شویم از این روش استفاده می‌کنیم.

XMLHttpRequest

XMLHttpRequest که غالباً به اختصار XHR نامیده می‌شود، یک فناوری نسبتاً قدیمی است. این فناوری از سوی مایکروسافت در اواخر دهه 90 ابداع شده و مدت‌ها پیش روی مرورگرهای مختلف استانداردسازی شده است.

برای آغاز این مثال، ابتدا یک فایل به نام ajax-start.html روی سیستم خود ایجاد کنید و کد زیر را نیز در آن فایل کپی کنید:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="viewport" content="width=device-width">

    <title>Ajax starting point</title>

    <style>
      html, pre {
        font-family: sans-serif;
      }
      body {
        width: 500px;
        margin: 0 auto;
        background-color: #ccc;
      }
      pre {
        line-height: 1.5;
        letter-spacing: 0.05rem;
        padding: 1rem;
        background-color: white;
      }
      label {
        width: 200px;
        margin-right: 33px;
      }
      select {
        width: 350px;
        padding: 5px;
      }
    </style>
    <!--[if lt IE 9]>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.js"></script>
    <![endif]-->
  </head>

  <body>
    <h1>Ajax starting point</h1>

    <form>
      <label for="verse-choose">Choose a verse</label>
      <select id="verse-choose" name="verse-choose">
        <option>Verse 1</option>
        <option>Verse 2</option>
        <option>Verse 3</option>
        <option>Verse 4</option>
      </select>
    </form>

    <h2>The Conqueror Worm, <em>Edgar Allen Poe, 1843</em></h2>

    <pre>

    </pre>

    <script>
    </script>
  </body>
</html>

همچنین چهار فایل verse1.txt ،verse2.txt ،verse3.txt و verse4.txt را نیز در همان دایرکتوری در کنار فایل فوق قرار دهید. در این مثال، ما بیت‌های مختلفی از یک شعر را از طریق XHR در یک منوی بازشدنی بارگذاری می‌کنیم.

درون عنصر <script> کد زیر را اضافه کنید. این کد ارجاعی به عناصر <select> و <pre> در متغیرها نگهداری می‌کند و یک تابع دستگیره رویداد به نام onchange تعریف می‌کند که در زمان تغییر یافتن مقدار منتخب به صورت پارامتر به یک تابع فراخوانی شده به نام ()updateDisplay ارسال می‌شود.

var verseChoose = document.querySelector('select');
var poemDisplay = document.querySelector('pre');

verseChoose.onchange = function() {
  var verse = verseChoose.value;
  updateDisplay(verse);
};

در ادامه تابع ()updateDisplay را تعریف می‌کنیم. قبل از هر چیز کد زیر را در ادامه بلوک کد قبلی قرار دهید. این در واقع پوسته خالی تابع ما است:

function updateDisplay(verse) {

};

ما تابع خود را با ساختن یک URL نسبی آغاز می‌کنیم که به یک فایل متنی اشاره می‌کند که می‌خواهیم بارگذاری شود، چون در ادامه به آن نیاز خواهیم داشت. مقدار عنصر <select> در هر زمان برابر با همان متن درون <option> منتخب است. بنابراین برای نمونه یک مقدار Verse 1 است. فایل متن بیت متناظر نیز Verse1.txt است که در همان دایرکتوری فایل HTML قرار دارد و از این رو صرفاً نام فایل کفایت می‌کند.

با این حال وب‌سرورها به کوچکی و بزرگی حروف حساس هستند و نام فایل نباید فاصله در خود داشته باشد. برای تبدیل Verse 1 به vesre1.txt باید V را به حرف کوچک تبدیل و فاصله را حذف کنیم و یک مقدار txt. به انتهای آن اضافه کنیم. این کار به وسیله تابع‌های ()replace() ،toLowerCase و نوعی الحاق رشته‌ای ساده میسر خواهد بود. خطوط کد زیر را درون تابع ()updateDisplay اضافه کنید:

verse = verse.replace(" ", "");
verse = verse.toLowerCase();
var url = verse + '.txt';

برای آغاز ایجاد یک درخواست باید یک شیء درخواست جدید با استفاده از سازنده ()XMLHttpRequest ایجاد کنید. این شیء را می‌توان به هر نامی خواند، ما آن را request نام‌گذاری می‌کنیم تا همه چیز روشن باشد. کد زیر را در ادامه خطوط قبلی وارد کنید:

var request = new XMLHttpRequest();

سپس باید از متد ()open برای تعیین این که کدام متد درخواست HTTP باید برای درخواست منابع از شبکه مورد استفاده قرار گیرد و URL مربوطه چیست، بهره می‌گیریم. ما در اینجا صرفاً از متد GET استفاده می‌کنیم و URL را به صورت متغیر url تعیین می‌کنیم. کد زیر را در ادامه خط قبلی وارد کنید:

request.open('GET', url);

سپس نوع پاسخی که انتظار داریم را تعیین می‌کنیم که به وسیله مشخصه به صورت text تعریف شده است. این مشخصه لزوماً ضروری نیست، چون XHR به صورت پیش‌فرض متن بازگشت می‌دهد، اما عادت کردن به درج این تنظیمات در مواردی که می‌خواهید انواع دیگری از داده‌ها را در آینده واکشی کنید، ایده مناسبی خواهد بود. کد زیر را نیز در ادامه اضافه کنید:

request.responseType = 'text';

واکشی یک منبع از شبکه یک عملیات ناهمگام است، یعنی باید منتظر بمانید که عملیات تکمیل شود تا بتوانید با آن پاسخ هر کاری که می‌خواهید را انجام دهید، در غیر این صورت خطایی رخ خواهد داد. XHR امکان مدیریت این مسئله را با استفاده از دستگیره رویداد onload می‌دهد. این دستگیره رویداد زمانی اجرا می‌شود که رویداد load اجرا شود یعنی زمانی که پاسخ بازگشت یابد. هنگامی که این واقعه رخ دهد، داده‌های پاسخ در مشخصه response شیء درخواست XHR آماده خواهند بود. کد زیر را به عنوان آخرین بخش کار اضافه کنید. درون دستگیره رویداد onload می‌بینید که textContent مربوط به poemDisplay را برابر با مقدار مشخصه request.response تعیین کردیم.

request.onload = function() {
  poemDisplay.textContent = request.response;
};

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

request.send();

یک مشکل این مثال در حال حاضر آن است که در زمان بارگذاری اولیه هیچ شعری را نمایش نمی‌دهد. برای اصلاح این وضعیت دو خط زیر را در انتهای کد موجود اضافه کنید تا به صورت پیش‌فرض بیت 1 بارگذاری شود. همچنین مطمئن شوید که عنصر <select> همواره مقدار صحیح را نمایش می‌دهد.

updateDisplay('Verse 1');
verseChoose.value = 'Verse 1';

ارائه مثال ذکر شده از طریق یک سرور

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

Fetch

API دیگری نیز به نام Fetch وجود دارد که اساساً یک جایگزین مدرن برای XHR محسوب می‌شود و در سال‌های اخیر در مرورگرها برای اجرای آسان‌تر درخواست‌های HTTP ناهمگام در جاوا اسکریپت، برای توسعه‌دهندگان و همچنین برای API-های دیگری که بر مبنای آن ساخته شده‌اند، معرفی شده است.

در این بخش مثال آخر را با استفاده از Fetch بازسازی می‌کنیم. ابتدا یک کپی از دایرکتوری مثال قبل روی سیستم خود بگیرید. درون تابع ()updateDisplay کد XHR را پیدا کنید:

var request = new XMLHttpRequest();
request.open('GET', url);
request.responseType = 'text';

request.onload = function() {
  poemDisplay.textContent = request.response;
};

request.send();

همه کدهای XHR را با کد زیر جایگزین کنید:

fetch(url).then(function(response) {
  response.text().then(function(text) {
    poemDisplay.textContent = text;
  });
});

مثال را در مرورگر خود بارگذاری کنید (آن را از طریق یک وب‌سرور اجرا کنید) تا ببینید که به شرط اجرا روی یک مرورگر مدرن، همانند نسخه XHR قبلی کار می‌کند.

در کد Fetch چه می‌گذرد؟

قبل از هر چیز باید گفت که ما متد ()fetch را با ارسال URL منبعی که می‌خواهیم واکشی کنیم مورد فراخوانی قرار می‌دهیم. این همان معادل مدرن‌تر ()request.open در XHR است. به اضافه این که دیگر به هیچ معادلی برای ()send. نیاز نداریم.

پس از آن می‌بینید که متد ()then. به انتخاب ()fetch زنجیر شده است. این متد بخشی از Promises است که قابلیت مدرنی در جاوا اسکریپت برای اجرای عملیات ناهمگام محسوب می‌شود. ()fetch یک promise بازگشت می‌دهد که پاسخ بازگشتی از سرور را resolve می‌کند. ما از ()then. برای اجرای نوعی کد پیگیری پس از resolve شدن promise استفاده می‌کنیم که تابعی که تعریف کرده‌ایم، درون آن قرار دارد. این وضعیت معادل دستگیره رویداد onload در نسخه XHR است.

این تابع به صورت خودکار پاسخ دریافتی از سرور را در زمان resolve شدن ()fetch به صورت یک پارامتر دریافت می‌کند. در سوی دیگر ما درون تابع، پاسخ را دریافت می‌کنیم و متد ()text را اجرا می‌کنیم که اساساً پاسخی به صورت متن خام بازگشت می‌دهد. این وضعیت نیز معادل ‘request.responseType = ‘text در نسخه XHR است.

چنان که می‌بینید ()text نیز یک promise بازگشت می‌دهد، بنابراین یک ()then. دیگر نیز به آن زنجیر می‌کنیم و درون آن یک تابع دریافت می‌کنیم که متن خامی را که promise به نام ()text به آن resolve می‌شود را دریافت می‌کند.

درون تابع promise داخلی دقیقاً همان کاری را که در نسخه XHR انجام دادیم اجرا می‌کنیم، یعنی محتوای متنی عناصر <pre> را برابر با مقدار متن تعیین می‌کنیم.

نکاتی در مورد Promise

Promise-ها در نخستین مواجهه کمی پیچیده به نظر می‌آیند، اما فعلاً لازم نیست در مورد آن‌ها نگران باشید. پس از مدتی کار کردن با آن‌ها به خصوص زمانی که با API-های مدرن جاوا اسکریپت آشنا شدید، به کاربردش عادت می‌کنید، زیرا اغلب API-های جدید بر مبنای Promise هستند.

در ادامه ساختار Promise فوق را یک بار دیگر بررسی می‌کنیم و می‌بینیم که آیا می‌توانیم معنای بیشتری از آن درک کنیم:

fetch(url).then(function(response) {
  response.text().then(function(text) {
    poemDisplay.textContent = text;
  });
});

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

عملاً تابع ارسالی به ()then یک بلوک کد است که بی‌درنگ اجرا نخواهد شد. بلکه در زمانی در آینده هنگامی که پاسخ بازگشت یابد اجرا می‌شود. توجه کنید که می‌توانید گزینه ذخیره‌سازی promise در یک متغیر را نیز انتخاب کنید و ()then. را در آن ذخیره کنید. کد زیر همین کار را انجام می‌دهد:

var myFetch = fetch(url);

myFetch.then(function(response) {
  response.text().then(function(text) {
    poemDisplay.textContent = text;
  });
});

از آنجا که متد ()fetch یک promise بازگشت می‌دهد که پاسخ HTTP را resolve می‌کند، هر تابعی که درون یک ()then. تعریف شده و به انتهای آن زنجیر شود، به صورت خودکار پاس را به صورت یک پارامتر دریافت می‌کند. شما می‌توانید این پارامتر را هر چیزی که دوست دارید نام‌گذاری کنید. مثال زیر همچنان کار می‌کند:

fetch(url).then(function(dogBiscuits) {
  dogBiscuits.text().then(function(text) {
    poemDisplay.textContent = text;
  });
});

اما فراخوانی پارامتر به صورتی که محتوای آن را توصیف کند معنی‌دارتر است. اکنون روی تابع تمرکز می‌کنیم:

function(response) {
  response.text().then(function(text) {
    poemDisplay.textContent = text;
  });
}

شیء پاسخ دارای یک متد به نام ()text است که داده‌های خام را که در بدنه پاسخ قرار دارد می‌گیرد و آن‌ها را به متن خام یعنی فرمتی که ما می‌خواهیم تبدیل می‌کند. همچنین یک promise بازگشت می‌دهد که به یک رشته متنی resolve می‌شود و از این رو ما از یک ()then. دیگر درون آن استفاده می‌کنیم که تابع دیگری را تعریف می‌کند. این تابع اقدام به توصیف کاری می‌کند که قرار است روی رشته انجام دهیم. ما مشخصه textContent مربوط به عنصر <pre> شعر را برابر با رشته متنی قرار می‌دهیم و از این رو به سادگی کار می‌کند.

همچنین باید اشاره کرد که می‌توان مستقیماً چندین بلوک promise را به انتهای هم زنجیر کرد و نتیجه هر یک را به بلوک بعدی در زنجیره ارسال نمود. بدین ترتیب promise-ها بسیار قدرتمند می‌شوند.

بلوک کد زیر همان کار مثال اولیه ما را انجام می‌دهد، اما به شیوه متفاوتی نوشته شده است:

fetch(url).then(function(response) {
  return response.text()
}).then(function(text) {
  poemDisplay.textContent = text;
});

توسعه‌دهندگان زیادی این استایل را بهتر یافته‌اند چون مسطح‌تر است و خواندن آن نیز به طور مشخص در مورد زنجیره‌های peomise طولانی‌تر آسان‌تر خواهد بود. هر promise بعد از promise قبلی می‌آید و دیگر داخل آن قرار نمی‌گیرد. تنها تفاوت دیگر این است که باید یک گزاره return در ابتدای ()response.text قرار دهیم تا نتیجه آن را به لینک بعدی در زنجیره ارسال کنیم.

از چه ساز و کاری باید استفاده کرد؟

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

اگر لازم است از مرورگرهای قدیمی پشتیبانی کنید، در این صورت راه‌حل XHR می‌تواند ترجیح بیشتری داشته باشید. با این حال اگر روی پروژه مدرن‌تری کار می‌کنید و در مورد مرورگرهای قدیمی نگرانی ندارید در این صروت Fetch گزینه مناسبی محسوب می‌شود.

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

بررسی یک مثال پیچیده‌تر

برای جمع‌بندی این مقاله به بررسی مثال پیچیده‌تری می‌پردازیم که برخی کاربردهای جالب Fetch را نمایش می‌دهد. ما یک سایت ساده به نام The Can Store ایجاد کرده‌ایم که یک سوپرمارکت خیالی برای فروش محصولات غذایی کنسروی است. برای مشاهده این وب‌سایت به این صفحه (+) مراجعه کنید. سورس کد آن را نیز در این صفحه (+) می‌توانید ملاحظه کنید.

واکشی داده

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

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

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

fetch('products.json').then(function(response) {
  return response.json();
}).then(function(json) {
  products = json;
  initialize();
}).catch(function(err) {
  console.log('Fetch problem: ' + err.message);
});

تابع ()fetch یک promise بازگشت می‌دهد. اگر این کار به صورت موفقی صورت بگیرد، تابع درون بلوک ()then شامل response بازگشتی از شبکه خواهد بود.

ما درون این تابع، ()json و نه ()text را روی response اجرا می‌کنیم و می‌خواهیم پاسخ خودمان را به صورت داده‌های ساخت‌یافته JSON و نه متن خام بازگشت دهیم. سپس ()then دیگری را به انتهای اولی یعنی تابع موفقیتی که شامل json بازگشتی از promise به نام ()response.json است، زنجیر می‌کنیم. ما این مقدار را برابر با مقدار شیء سراسری محصولات قرار می‌دهیم و سپس ()initialize را اجرا می‌کنیم که فرایند را با نمایش دادن همه محصولات در رابط کاربری آغاز می‌کند.

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

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

شما می‌توانید حالت ناموفق بودن را خودتان به این صورت تست کنید که یک کپی از فایل‌های مثال ایجاد کنید (به این منظور این فایل فشرده (+) را دانلود کرده و از حالت فشرده خارج کنید). کد را درون یک وب‌سرور اجرا کنید. مسیر فایلی که قرار است واکشی شود را طوری تغییر دهید که عبارت نادرستی به دست آید. برای مثال مانند produc.json باشد.

اکنون فایل index را درون مرورگر بارگذاری کنید (از طریق localhost:8000) و به کنسول توسعه‌دهنده مرورگر خود نگاه کید. در این زمان پیامی مشاهده خواهید کرد که مشابه «Network request for products.json failed with response 404: File not found» است.

بلوک fetch دوم می‌تواند درون تابع ()fetchBlob باشد:

fetch(url).then(function(response) {
    return response.blob();
}).then(function(blob) {
  // Convert the blob to an object URL — this is basically an temporary internal URL
  // that points to an object stored inside the browser
  var objectURL = URL.createObjectURL(blob);
  // invoke showProduct
  showProduct(objectURL, product);
});

این کد دقیقاً به همان روش کد قبلی عمل می‌کند، به جز این که به جای استفاده از ()json از ()blob استفاده شده است. در این حالت می‌خواهیم پاسخ خود را از یک فایل تصویر بگیریم و فرمت داده‌هایی که استفاده می‌کنیم به صورت Blob باشد. Blob اختصاری برای عبارت «Binary Large Object» (شیء باینری بزرگ) است که اساساً برای ارائه اشیای فایل-مانند بزرگ مانند فایل‌های تصاویر و ویدئوها استفاده می‌شود.

زمانی که blob خود را با موفقیت دریافت کردیم، با استفاده از ()createObjectURL، یک URL شیء از آن ایجاد می‌کنیم. بدین ترتیب URL داخلی موقتی بازگشت می‌یابد که به شیئی درون مرورگر اشاره دارد. این موارد چندان خوانا نیستند، اما می‌توانید با باز کردن اپلیکیشن Can Store با کنترل+کلیک یا راست+کلیک کردن روی تصویر و انتخاب گزینه View image بسته به مرورگری که استفاده می‌کنید، آن‌ها را ببینید. URL شیء درون نوار آدرس نمایان است و باید چیزی مانند زیر باشد:

blob:http://localhost:7800/9b75250e-5279-e249-884f-d03eb1fd84f4

چالش: نسخه XHR برای Can Store

در این بخش از شما می‌خواهیم که نسخه Fetch اپلیکیشن را طوری تبدیل کنید که از XHR استفاده کنید. بدین منظور یک کپی از این فایل فشرده (+) بگیرید و جاوا اسکریپت را به طرز مناسبی تغییر دهید.

در ادامه برخی سرنخ‌های مفید برای انجام این کار را ملاحظه می‌کنید:

  • احتمالاً ارجاع MLHttpRequest را مفید خواهید یافت.
  • اساساً باید از همان الگویی که در بخش ابتدایی در مثال XHR-basic.html دیدید استفاده کنید.
  • با این حال، باید مدیریت خطا را نیز که در نسخه Fetch مربوط به Can Store مشاهده کردید، اضافه کنید. به این منظور توجه داشته باشید که:
    • پاسخ موجود در request.response پس از اجرای رویداد load قرار دارد و نه در promise به نام ()then.
    • بهترین معادل response.ok مربوط به Fetch در XHR، بررسی 200 بودن request.status یا 4 بودن request.readyState است.
    • مشخصه‌های دریافت وضعیت (Status) و پیام وضعیت همان هستند، اما در شیء request حضور دارند و نه در شیء response.

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

// create a variable to store the products 'database' in
var products;

// use XHR to retrieve it, setting the responseType as json, and report any errors
// that occur in the XHR operation. If the respnse is successful, set products to equal
// request.response, then run the initialize() function
var request = new XMLHttpRequest();
request.open('GET', 'products.json');
request.responseType = 'json';

request.onload = function() {
  if(request.status === 200) {
    products = request.response;
    initialize();
  } else {
    console.log('Network request for products.json failed with response ' + request.status + ': ' + request.statusText)
  }
};

request.send();

// sets up the app logic, declares required variables, contains all the other functions
function initialize() {
  // grab the UI elements that we need to manipulate
  var category = document.querySelector('#category');
  var searchTerm = document.querySelector('#searchTerm');
  var searchBtn = document.querySelector('button');
  var main = document.querySelector('main');

  // keep a record of what the last category and search term entered were
  var lastCategory = category.value;
  // no search has been made yet
  var lastSearch = '';

  // these contain the results of filtering by category, and search term
  // finalGroup will contain the products that need to be displayed after
  // the searching has been done. Each will be an array containing objects.
  // Each object will represent a product
  var categoryGroup;
  var finalGroup;

  // To start with, set finalGroup to equal the entire products database
  // then run updateDisplay(), so ALL products are displayed initially.
  finalGroup = products;
  updateDisplay();

  // Set both to equal an empty array, in time for searches to be run
  categoryGroup = [];
  finalGroup = [];

  // when the search button is clicked, invoke selectCategory() to start
  // a search running to select the category of products we want to display
  searchBtn.onclick = selectCategory;

  function selectCategory(e) {
    // Use preventDefault() to stop the form submitting — that would ruin
    // the experience
    e.preventDefault();

    // Set these back to empty arrays, to clear out the previous search
    categoryGroup = [];
    finalGroup = [];

    // if the category and search term are the same as they were the last time a
    // search was run, the results will be the same, so there is no point running
    // it again — just return out of the function
    if(category.value === lastCategory && searchTerm.value.trim() === lastSearch) {
      return;
    } else {
      // update the record of last category and search term
      lastCategory = category.value;
      lastSearch = searchTerm.value.trim();
      // In this case we want to select all products, then filter them by the search
      // term, so we just set categoryGroup to the entire JSON object, then run selectProducts()
      if(category.value === 'All') {
        categoryGroup = products;
        selectProducts();
      // If a specific category is chosen, we need to filter out the products not in that
      // category, then put the remaining products inside categoryGroup, before running
      // selectProducts()
      } else {
        // the values in the <option> elements are uppercase, whereas the categories
        // store in the JSON (under "type") are lowercase. We therefore need to convert
        // to lower case before we do a comparison
        var lowerCaseType = category.value.toLowerCase();
        for(var i = 0; i < products.length ; i++) {
          // If a product's type property is the same as the chosen category, we want to
          // dispay it, so we push it onto the categoryGroup array
          if(products[i].type === lowerCaseType) {
            categoryGroup.push(products[i]);
          }
        }

        // Run selectProducts() after the filtering has bene done
        selectProducts();
      }
    }
  }

  // selectProducts() Takes the group of products selected by selectCategory(), and further
  // filters them by the tnered search term (if one has bene entered)
  function selectProducts() {
    // If no search term has been entered, just make the finalGroup array equal to the categoryGroup
    // array — we don't want to filter the products further — then run updateDisplay().
    if(searchTerm.value.trim() === '') {
      finalGroup = categoryGroup;
      updateDisplay();
    } else {
      // Make sure the search term is converted to lower case before comparison. We've kept the
      // product names all lower case to keep things simple
      var lowerCaseSearchTerm = searchTerm.value.trim().toLowerCase();
      // For each product in categoryGroup, see if the search term is contained inside the product name
      // (if the indexOf() result doesn't return -1, it means it is) — if it is, then push the product
      // onto the finalGroup array
      for(var i = 0; i < categoryGroup.length ; i++) {
        if(categoryGroup[i].name.indexOf(lowerCaseSearchTerm) !== -1) {
          finalGroup.push(categoryGroup[i]);
        }
      }

      // run updateDisplay() after this second round of filtering has been done
      updateDisplay();
    }

  }

  // start the process of updating the display with the new set of products
  function updateDisplay() {
    // remove the previous contents of the <main> element
    while (main.firstChild) {
      main.removeChild(main.firstChild);
    }

    // if no products match the search term, display a "No results to display" message
    if(finalGroup.length === 0) {
      var para = document.createElement('p');
      para.textContent = 'No results to display!';
      main.appendChild(para);
    // for each product we want to display, pass its product object to fetchBlob()
    } else {
      for(var i = 0; i < finalGroup.length; i++) {
        fetchBlob(finalGroup[i]);
      }
    }
  }

  // fetchBlob uses XHR to retrieve the image for that product, and then sends the
  // resulting image display URL and product object on to showProduct() to finally
  // display it
  function fetchBlob(product) {
    // construct the URL path to the image file from the product.image property
    var url = 'images/' + product.image;
    // Use XHR to fetch the image, as a blob
    // Again, if any errors occur we report them in the console.
    var request = new XMLHttpRequest();
    request.open('GET', url);
    request.responseType = 'blob';

    request.onload = function() {
      if(request.status === 200) {
          // Convert the blob to an object URL — this is basically an temporary internal URL
          // that points to an object stored inside the browser
          var blob = request.response;
          var objectURL = URL.createObjectURL(blob);
          // invoke showProduct
          showProduct(objectURL, product);
      } else {
        console.log('Network request for "' + product.name + '" image failed with response ' + request.status + ': ' + request.statusText);
      }
    };

    request.send();
  }

  // Display a product inside the <main> element
  function showProduct(objectURL, product) {
    // create <section>, <h2>, <p>, and <img> elements
    var section = document.createElement('section');
    var heading = document.createElement('h2');
    var para = document.createElement('p');
    var image = document.createElement('img');

    // give the <section> a classname equal to the product "type" property so it will display the correct icon
    section.setAttribute('class', product.type);

    // Give the <h2> textContent equal to the product "name" property, but with the first character
    // replaced with the uppercase version of the first character
    heading.textContent = product.name.replace(product.name.charAt(0), product.name.charAt(0).toUpperCase());

    // Give the <p> textContent equal to the product "price" property, with a $ sign in front
    // toFixed(2) is used to fix the price at 2 decimal places, so for example 1.40 is displayed
    // as 1.40, not 1.4.
    para.textContent = '$' + product.price.toFixed(2);

    // Set the src of the <img> element to the ObjectURL, and the alt to the product "name" property
    image.src = objectURL;
    image.alt = product.name;

    // append the elements to the DOM as appropriate, to add the product to the UI
    main.appendChild(section);
    section.appendChild(heading);
    section.appendChild(para);
    section.appendChild(image);
  }
}

سخن پایانی

در این مقاله به بررسی شیوه آغاز به واکشی داده‌ها از سرور کار با هر دو روش XHR و Fetch پرداختیم.

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

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

==

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

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

یک نظر ثبت شده در “واکشی داده ها از سرور در جاوا اسکریپت — راهنمای جامع

  • عالی بود من در حین آنالیز سایت soundcloud با fetch آشنا شدم فچ یه چیزی مثل گراف میمونه سیستم حرفه ای و جالبی داره

نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *