یادگیری Fetch API با ساخت یه کتابخونه ساده HTTP

درخواست های asynchronous شبکه جزو جدا نشدنی از وب اپ های امروزی هستن و تقریبا وب اپی رو امروزه نمیبینیم که درخواست شبکه ای نداشته باشه بنابراین خیلی مهم و حیاتیه که اپی که مینویسیم قابلیت درخواست شبکه را داشته باشه و این مدل درخواست هارو بلد باشیم پیاده سازی کنیم. در این پست میخوام شما رو با Fetch آشنا کنم و حتی یه کتابخونه ساده باهاش پیاده سازی کنیم.

این Fetch چی هست اصلا؟

برای درخواست های شبکه و یا Ajax ئی مون مجبوریم از API هایی که جاوااسکریپت در اختیارمون گذاشته استفاده کنیم. قبلا این درخواست هارو با کمک XMLHttpRequest ارسال میکردیم که استفاده ازش پیچیده و آزار دهنده بود و برای همین از کتابخونه هایی مثل jquery برای راحتتر شدن کار استفاده میکردیم. که پیچیدگی و سختیش رو مخفی میکردن. حالا با داشتن Fetch API که مدرن هست و از promise ها استفاده میکنه و APIئی ساده تر و آسون تر داره و دیگه از callback hell توش خبری نیست زمان خیلی خوبیه که با این API آشنا بشیم و ازش کمک بگیریم.

در مورد پشتیبانی مرورگر ها هم خیالتون راحت باشه الان که این پست رو مینویسم پشتیبانی Fetch API بسیار خوبه و به غیر از یه مرورگر(حدس میزنین چی؟) بقیه مرورگرها ازش پشتیبانی میکنن و برای پشتیبانیش در IE هم میتونین از این polyfill استفاده کنین.

چجوری استفاده کنیم ازش؟

شروع استفاده از Fetch بسیار راحته :

fetch('/example.json'); 

تمام! fetch یه درخواست httpئی رو به آدرسی که دادیم میفرسته( این آدرس میتونه Relative/نسبی یا Absolute/مطلق باشه ) و اینجا فایل example.json رو که در دامنه‌مون هست دریافت میکنه.
حالا وقتشه که از محتویات دریافتی از فایل استفاده کنیم اینجوری:

fetch('./example.json') 
.then((response) => { 
response.json().then((data) => { 
    console.log(data) ;
      }) ;
 });

همانطور که میبینین fetch پس از درخواست یک promise برمیگردونه و شما response obj رو میتونین دریافت کنین و البته response obj شامل تمامی اطلاعات request و response تون هست.

چجوری خطا هارو handle کنیم؟

همونطور که با promise ها آشنا هستین در صورتی که خطایی رخ بده شما میتونین اونو از catch دریافت کنین. بدین شکل:

fetch('./example.json')
.then((response) => {
  //.....
  }) 
  .catch((err) => { 
    console.error(err) ;
  });

این Respone Object که بالا گفتی چی بود؟

هنگامی که response رو دریافت میکنین از fetch شما یک response object دارین که توش اطلاعات بدرد بخوری هست مثل:

۱- هدر(header) های response
۲- کد وضعیت (status)
۳- متن/پیام وضعیت (statusText)
۴- آدرس کاملی که شما درخواستتون رو فرستادین (url)
۵- و بالاخره بدنه/داده که سرور برگردونده (body)

شما میتونین موارد بالا رو از response obj بگیرین مثل مثال پایین:

fetch('./example.json') 
.then((response) => { 
  console.log(response.headers.get('Content-Type'));
  console.log(response.status);
  console.log(response.statusText) ;
  console.log(response.url); 
    response.json().then((data) => { 
      console.log(data) 
     }) 
 })

برای دریافت بدنه/داده response نیاز دارین که متدی از response obj رو صدا بزنین :
۱- به صورت متن response.text()
۲- به صورت جیسون response.json()
۳- در صورتی که داده فایل بود مثلا عکس response.blob()
توجه داشته باشین که این متد ها هم خودشون یک promise برمیگردونن.

پس چجوری درخواست POST یا متد های دیگه بفرستیم؟

خب fetch میتونه ۲ تا آرگومان دریافت کنه که دومی اختیاریه و options obj هست که شما میتونین باهاش داده های دیگه مثل متد رو مشخص کنین.
شما در options میتونین موارد زیر رو مشخص کنین:
۱- متد( method ) : متدی که میخواین درخواستتون ارسال بشه متد GET به صورت default هست و شما میتونین علاوه بر اون POST,PUT, DELETE,HEAD برای متد درخواستتون مشخص کنین.
۲- هدرها ( headers ) : هدر های درخواست اینجا مشخص میشن.
۳- بدنه/داده ( body ) : اگر درخواست شما داده ای اضافه تر باید بفرسته میتونین اینجا مشخصش کنین که میتونه string/formData/... باشه.




یه کتابخونه ساده HTTP

تا الان در مورد fetch خیلی چیزا فهمیدیم بهتره که باقی موارد با جزئیات بیشتر رو سر این پروژمون یاد بگیریم. کتابخونه ای که مینویسیم یه کتابخونه بسیار سادست که شما میتونین به صورت ساده تر با fetch کار کنین. برای این که کار با کتابخونه راحتتر بشه پروژه رو جوری مینویسیم که راحت بتونیم با async/await باهاش کار کنیم و توی این کتابخونه برای جلوگیری از پیچیده شدن فقط با response های json کار میکنیم. قبلش یه موردی رو نیازه توضیح بدم.

همونطور که میدونین promise ها اگر با خطا مواجه بشن میتونین با catch اون مورد رو هندل کنیم async/await ها خیلی خوب با promise ها کار میکنن فقط مشکل اینجاست که لازم هست داخل یه بلاک try...catch قرار بگیرن تا وقتی promise مون خطا داد وارد catch بشن. من این مورد رو زیاد خوشم نمیاد و جالب نمیدونم و اگر چندین مورد await داشته باشیم نهایتا کد جالبی نخواهیم داشت بنابراین ازونجایی که با زبون Golang مقداری کار کردم از نحوه error handling اش لذت بردم که به صورت زیر هست تقریبا الهام میگیرم تا مجبور به استفاده از try...catch نباشم. error handling توی golang بدین شکله:

data, err := request(url)
if err != nil { return err }

ما همین کارو تقریبا انجام خواهیم داد.

خب برگردیم به کتابخونه مون.
اول لازمه که یک کلاس داشته باشیم :

class SimpleHttp {
}

مرحله اول پیاده سازی متد get هست. get توی fetch اصلا کاری نداره درنتیجه پیاده سازیش راحته.

async get(url) {
    let response = undefined;
    try{
            const responseObj = await fetch(url);
            const resData = await responseObj.json();
            response = [ null , resData ];
     }
     catch(e){
            response = [ e , null ];
     }
     return response ;
  }

متد رو به صورت async تعریف میکنیم. await رو داخل try...catch قرار میدیم و پاسخ نهایی رو به صورت یه آرایه به کاربر میدیم. که خونه اول آرایه خطا و خونه دوم آرایه داده ایست که از سرور دریافت میکنیم.

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

async post(url,data) {
    let response = undefined;
    try{
        const responseObj = await fetch(url, {
            method: 'POST',
            headers: {
                 'Content-type': 'application/json'
            },
            body: JSON.stringify(data)
        });
        const resData = await responseObj.json();
        response = [ null , resData ];
    }
    catch(e){
        response = [ e , null ];
    } 
    return response ;
}

در این متد ازونجایی که میتونیم dataئی رو هم به سرور بفرستیم یک پارامتر data هم دریافت میکنیم و پارامتر دومی رو هم fetch پاس میدهیم که یک options obj هست. و متد رو post انتخاب میکنیم و data ئی که دریافت کردیم که یک object هست رو با JSON.stringify تبدیل به string میکنیم.

مرحله سوم پیاده سازی متد put هست که مشابه همان post است.

async put(url,data) {
    let response = undefined;
    try{
        const responseObj = await fetch(url, {
            method: 'PUT',
            headers: {
                 'Content-type': 'application/json'
             },
             body: JSON.stringify(data)
         });
         const resData = await responseObj.json();
         response = [ null , resData ];
    }
    catch(e){
        response = [ e , null ];
    }
    return response ;
}

مرحله چهارم پیاده سازی متد delete هست که مشابه همان post است این بار بدون دریافت data

async delete(url) {
    let response = undefined;
    try{
        const responseObj = await fetch(url, {
            method: 'DELETE',
            headers: {
             'Content-type': 'application/json'
             },
             body: JSON.stringify(data)
     });
         const resData = await 'OK';
         response = [ null , resData ];
      }
      catch(e){
          response = [ e , null ];
      }
      return response ;
}

در این متد نیازی نیست که response را به صورت json دریافت کنیم و فقط پاس دادن 'OK' کفایت میکند.

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

نمونه کارکرد این کتابخونه کوچیک هم میتونین تو کد زیر ببینین:

const http = new SimpleHttp();
document.querySelector('button').addEventListener('click',async () => {
    const [ err , data ] = http.get('https://jsonplaceholder.typicode.com/posts/1');
    if(err){
        alert(err);
        return;
     }
    console.log(data);
 });

خیلی هم عالی... کتابخونمون یه سری ضعف ها داره مثلا برای ارسال فایل راهی نزاشتیم یا یه چیز دیگه هم هست fetch هر درخواستی که میفرستی و اگر بتونه به سرور وصل شه و ازش اطلاعات بگیره رو موفق در نظر میگیره ولی ممکنه این درخواست ، درخواست درستی نباشه و مثلا status مون ۴۰۳ باشه که عملا درخواستمون موفق نبوده میتونیم handling این قسمت رو توی کتابخونمون اضافه کنیم یا این که فقط با json کار نکنیم و بتونیم text,blob رو هم با کتابخونه انجام بدیم.