JS Day 47: AJAX Request - XMLHttpRequest, Fetch, AXIOS

有 N 人看过

XMLHttpRequest

  • JavaScript 原生的方法傳送請求
  • 不支援 Promises
  • 語法攏長,較多 callbacks,不好記

GET request

// GET request
const myReq = new XMLHttpRequest();

myReq.onload = function() {
  const data = JSON.parse(this.responseText);
};

myReq.onerror = function(err) {
  console.log('Error!', err);
};

myReq.open('get', 'https://www.icanhazdadjoke.com/', true');
myReq.setRequestHeader('Accept', 'application/json');
myReq.send();

POST request

// POST request
let xhr = new XMLHttpRequest();
xhr.open('POST', 'https://hexschool-tutorial.herokuapp.com/api/signup', true);

// 格式:application/x-www-form-urlencoded
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhr.send('email=abcde@gmail.com');

// 格式:JSON
// let account = {
//   email:'abcdef@gmail.com',
//   password: '1234'
// };

// xhr.setRequestHeader("Content-type", "application/json");
// xhr.send(JSON.stringify(account));

範例:GET Request StarWars API (www.swapi.dev)

const req = new XMLHttpRequest();
req.addEventListener('load', function() {
  console.log('請求成功!');
  const data = JSON.parse(this.responseText);
  for (let planet of data.results) {
    console.log(planet.name); // 列出星球名稱
  }
});

req.addEventListener('error', () => {
  console.log('錯誤');
});

req.open('GET', 'https://swapi.dev/api/planets/')
req.send();
console.log('request send')

Planet Request 結果:第一筆資料

GET /api/planets/
HTTP 200 OK
Content-Type: application/json
Vary: Accept
Allow: GET, HEAD, OPTIONS

{
    "count": 60, 
    "next": "http://swapi.dev/api/planets/?page=2", 
    "previous": null, 
    "results": [
        {
            "name": "Tatooine", 
            "rotation_period": "23", 
            "orbital_period": "304", 
            "diameter": "10465", 
            "climate": "arid", 
            "gravity": "1 standard", 
            "terrain": "desert", 
            "surface_water": "1", 
            "population": "200000", 
            "residents": [
                "http://swapi.dev/api/people/1/", 
                "http://swapi.dev/api/people/2/", 
                "http://swapi.dev/api/people/4/", 
                "http://swapi.dev/api/people/6/", 
                "http://swapi.dev/api/people/7/", 
                "http://swapi.dev/api/people/8/", 
                "http://swapi.dev/api/people/9/", 
                "http://swapi.dev/api/people/11/", 
                "http://swapi.dev/api/people/43/", 
                "http://swapi.dev/api/people/62/"
            ], 
            "films": [
                "http://swapi.dev/api/films/1/", 
                "http://swapi.dev/api/films/3/", 
                "http://swapi.dev/api/films/4/", 
                "http://swapi.dev/api/films/5/", 
                "http://swapi.dev/api/films/6/"
            ], 
            "created": "2014-12-09T13:50:49.641000Z", 
            "edited": "2014-12-20T20:58:18.411000Z", 
            "url": "http://swapi.dev/api/planets/1/"
        },

...略

Chaining XMLHttpRequest

每一個星球下還有很多資料,例如 films,但是要取的 films 還需要發送 request 才可以取得資料。

Films request 結果

// Film Instance

GET /api/films/5/
HTTP 200 OK
Content-Type: application/json
Vary: Accept
Allow: GET, HEAD, OPTIONS

{
    "title": "Attack of the Clones", 
    "episode_id": 2, 
    "opening_crawl": "There is unrest in the Galactic\r\nSenate. Several thousand solar\r\nsystems have declared their\r\nintentions to leave the Republic.\r\n\r\nThis separatist movement,\r\nunder the leadership of the\r\nmysterious Count Dooku, has\r\nmade it difficult for the limited\r\nnumber of Jedi Knights to maintain \r\npeace and order in the galaxy.\r\n\r\nSenator Amidala, the former\r\nQueen of Naboo, is returning\r\nto the Galactic Senate to vote\r\non the critical issue of creating\r\nan ARMY OF THE REPUBLIC\r\nto assist the overwhelmed\r\nJedi....", 
    "director": "George Lucas", 
    "producer": "Rick McCallum", 
    "release_date": "2002-05-16", 
    "characters": [
        "http://swapi.dev/api/people/2/", 
        "http://swapi.dev/api/people/3/", 
        "http://swapi.dev/api/people/6/", 
        "http://swapi.dev/api/people/7/", 
        "http://swapi.dev/api/people/10/", 

       .... 略

Film 底下還有很多資料可以 request,例如人物。如果用 XMLHttpRequest 就可以一直 nested 下去沒完沒了。

Fetch

  • ES6 原生方法
  • 支援 Promises
  • 不支援 IE

POST Request

// 來發個 POST Request:

postData('http://example.com/answer', {answer: 42})
  .then(data => console.log(data)) // JSON from `response.json()` call
  .catch(error => console.error(error))

function postData(url, data) {
  // Default options are marked with *
  return fetch(url, {
    body: JSON.stringify(data), // must match 'Content-Type' header
    cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
    credentials: 'same-origin', // include, same-origin, *omit
    headers: {
      'user-agent': 'Mozilla/4.0 MDN Example',
      'content-type': 'application/json'
    },
    method: 'POST', // *GET, POST, PUT, DELETE, etc.
    mode: 'cors', // no-cors, cors, *same-origin
    redirect: 'follow', // manual, *follow, error
    referrer: 'no-referrer', // *client, no-referrer
  })
  .then(response => response.json()) // 輸出成 json
}

HexSchool 註冊帳號範例:

const api = 'https://hexschool-tutorial.herokuapp.com/api/signup';
let account = {
    email:'abcdef@gmail.com',
    password: '1234'
  };

postData(api, account)
  .then(data => console.log(data)) // JSON from `response.json()` call
  .catch(error => console.error(error))

function postData(url, data) {
  return fetch(url, {
    body: JSON.stringify(data),
    headers: {
      'content-type': 'application/json'
    },
    method: 'POST',
  })
  .then(response => response.json()) // 輸出成 json
}

// {success: false, result: {…}, message: "此帳號已被使用"}

GET Request

const myReq = fetch('http://example.com/movies.json')
  .then(function(response) {
    return response.json();
  })
  .then(function(myJson) {
    console.log(myJson);
  });

範例:改寫 XMLHttpRequest 例子

const fetchedData = fetch('https://swapi.dev/api/planets/')
  .then( response => {
    // status: 404/500 promise 一樣會是 resolved (ok 值會從 true 變成 false)
    console.log(response);
    if (response.status !== 202) {
      console.log('發生錯誤:', response.status);
      return;
    }
    response.json().then ( data => {
      for (let planet of data.results) {
        console.log(planet.name);
      }
    })
  })
  .catch( err => {
    // 只有在網路發生錯誤或是請求中斷才會是 promise rejected
    console.log('Error: ', err);
  })

fetch() 回傳的 promise 物件, resolve 和 reject 的使用方式有差異, 當遇到 HTTP Status 404, 500 時會使用 resolve 但會將 status 的值從 ok 變為 false, reject 只有在網路發生錯誤或是任何會中斷網路請求時才會使用。 — MDN web docs

除了上方的方式,也可以利用下方的 catch error,即是把錯誤丟給他

const fetchedData = fetch('https://swapi.dev/api/planetsss/') // 網址錯誤
  .then( response => {
    // status: 404/500 promise 一樣會是 resolved (ok 值會從 true 變成 false)
    console.log(response);
    if (response.status !== 202) { // 這邊寫成 if (!response.ok) ** not ok ** 也可以
      throw new Error(`Status: ${response.status}`);
            return;
    }
    response.json().then ( data => {
      for (let planet of data.results) {
        console.log(planet.name);
      }
    })
  })
  .catch( err => {
    // 只有在網路發生錯誤或是請求中斷才會是 promise rejected
    console.log('Catch 錯誤: ', err);
  })

錯誤跑到下方顯示了。
https://drive.google.com/uc?export=view&id=1dK5aSjMRe4_TdzL6xya9dKoKzZSTKVhC

const fetchedData = fetch('https://swapi.dev/api/planets/')
  .then( response => {
    // status: 404/500 promise 一樣會是 resolved (ok 值會從 true 變成 false)
    console.log(response);
    if (!response.ok) {
      throw new Error(`Status: ${response.status}`);
      return;
    }
    return response.json(); // return promise
  })

  ////////////////////////////////
  // 從上方 response.json() 往下移,less nested
  .then ( data => {
    console.log(data);
    for (let planet of data.results) {
      console.log(planet.name);
    }
  })
  ////////////////////////////////

    .catch( err => {
    // 只有在網路發生錯誤或是請求中斷才會是 promise rejected
    console.log('Catch 錯誤: ', err);
  })

Chaining Fetch Requests

const fetchedData = fetch('https://swapi.dev/api/planets/')
  .then( response => {
    // status: 404/500 promise 一樣會是 resolved (ok 值會從 true 變成 false)
    console.log('星球', response);
    if (!response.ok) {
      throw new Error(`Status: ${response.status}`);
      return;
    }
    return response.json(); // return promise
  })
  .then ( data => {
    //////////////////////////////
    //  request FILM
    const filmAPI = data.results[0].films[0];
    fetch(filmAPI)
      .then( response => {
        console.log('影片', response);
        if (!response.ok) {
          throw new Error(`Status: ${response.status}`);
          return;
        }
        return response.json(); // return promise
      })
      .then ( filmData => {
        console.log('filmData', filmData);
      })
      .catch( err => {
        console.log('Catch 錯誤: ', err);
      })
  })
  ////////////////////////////////
  .catch( err => {
    // 只有在網路發生錯誤或是請求中斷才會是 promise rejected
    console.log('Catch 錯誤: ', err);
  })
const fetchedData = fetch('https://swapi.dev/api/planets/')
  .then( response => {
    // status: 404/500 promise 一樣會是 resolved (ok 值會從 true 變成 false)
    console.log(response);
    if (!response.ok) {
      throw new Error(`Status: ${response.status}`);
      return;
    }
    return response.json(); // return promise
  })

  ////////////////////////////////
  // 請求 film data
  .then ( data => {
    const filmAPI = data.results[0].films[0];
    return fetch(filmAPI); // return promise
  })
  //** 這邊跟上方的 response 是做一樣的事 **//
  .then( response => {
    console.log(response);
    if (!response.ok) {
      throw new Error(`Status: ${response.status}`);
      return;
    }
    return response.json(); // return promise
  })
  .then( filmData => {
    console.log(filmData);
  })
  //**                                **//
  ////////////////////////////////

  .catch( err => {
    // 只有在網路發生錯誤或是請求中斷才會是 promise rejected
    console.log('Catch 錯誤: ', err);
  })

這樣子看起來比較平整話,而且有部分重複的語法。

AXIOS

  • 支援 promises
  • 在瀏覽器中創建 XMLHttpRequests
  • 支援IE

Post Request

axios.post('/user', {
    firstName: 'Fred',
    lastName: 'Flintstone'
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

HexSchool 註冊帳號範例:

const api = 'https://hexschool-tutorial.herokuapp.com/api/signup';
axios.post(api, {
  email:'abcdefddd@gmail.com',
  password: '1234'
})
.then(function (response) {
  console.log(response);
})
.catch(function (error) {
  console.log(error);
});

Reference:

The Modern JavaScript Bootcamp
Using Fetch — MDN web docs
AXIOS

本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可。