[FE102] 前端必備:網頁與伺服器的溝通


Posted by s103071049 on 2021-05-31

Recall:API 與網頁伺服器

client 會發一個 request 到 server 去,server 會發一個 response 回來。

實際上 response 有一下兩類

一、 .html 檔案:如重新整理,瀏覽器發一個 request 到網頁上,這個網址回傳的 response 就是瀏覽器渲染出的頁面。平常在使用的瀏覽器就是 client 與 server 間的溝通。

二、 .JSON 檔案

結論、我們會透過瀏覽器發 request,拿到 response 後再做處理。


一、用 node.js 呼叫 API 與在網頁上呼叫的根本差異是什麼?

注:我們先省略收發中間那層-作業系統。(內文仍略提及)

node.js 直接發 request 到 server。更精確地說,node.js 是透過作業系統,作業系統再發 request 到 server。server 的 response 可以直接拿到。換句話說,中間沒有任何人進行干擾。

瀏覽器上的 JS ,會先透過瀏覽器,所以是瀏覽器幫你發 request 到 server。更精確地說瀏覽器上的 JS 透過瀏覽器,瀏覽器再透過作業系統發 request 到 server。同理,server 發 response 回來也是透過瀏覽器,才把 response 傳回來。所以中間經過瀏覽器。基於資安瀏覽器會阻止我們做一些事情,另外瀏覽器也會幫我們加一些東西,例如:瀏覽器的版本,這會使 server 端收到一些額外資訊。

差異:node.js 發送中間沒有受到限制,瀏覽器發送受瀏覽器限制。因為是透過瀏覽器發送,所以會受限瀏覽器的規則。又因為規則的差異,會有其他額外事項需要學習。

二、傳送資料的方式

第一種方式:表單 form

<!DOCTYPE html>
<html>
<head>
  <title>test</title>
</head>
<body>
  <div class='app'>
    <form method='GET' action='/test'>
      username:<input name="username"/>
      <input type='submit'/>
    </form>
  </div>
  <script type="text/javascript">

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

輸入 123 提交,發現 ERR_FILE_NOT_FOUND。因為找不到頁面,所以頁面壞掉。

開啟 devtool 點 network,將 preserve log 打勾,檢視他發送到哪裡。結果是失敗的,因為沒有檔案負責處理這個請求。

現在,調整 action='https://google.com',發請求到估狗。輸入 www,現在到估狗頁面,頁面有 ?username=www

發送路徑 :

  1. 發一個 get request 到 https://www.google.com/?username=www 位址。用 get 方法,我的參數會被附加的網址上去。從網址上就可以知道參數是甚麼,所以登入一般用 post 帳密會帶在 body。
  2. 接著 301 轉址,轉址到 https://google.com/?username=www

小結 : 透過表單發 request 到 servr 會換頁。

也就是說,透過瀏覽器發請求到伺服器,(這邊我們傳的是 username = www 的 request 然後 method 是 GET,到 action='https://google.com' 這個位置。)
當伺服器將回應回傳,瀏覽器會直接 render 出這個 response。用 post 也是相同,google.com 回甚麼 response 我的頁面就會是甚麼樣子:(405. That’s an error.)

所以他會換頁,他從我們原本的頁面,到我們提交 action 的頁面。接下來產生出的結果,都看 action 網頁回傳甚麼結果就是甚麼結果。

總結:發一個新的 request 到一個新的頁面去,瀏覽器再 render 他的 response。這種方式與 js 完全沒關係,純粹是透過 html 元素進行傳遞。比較像是我要到達 action 這個頁面,我需要帶上甚麼參數才可以順利到達。

第二種方式:AJAX

透過表單與伺服器要資料,每次都要換頁,可是有時拿的資料只是畫面部分有改變,並非全部畫面有改變。所以如果透過 JS 發請求到伺服器然後拿到結果,就可以解決換頁困擾。這個方式,就是 AJAX (Asynchronous JavaScript and XML),任何非同步與伺服器交換資料的 JS 都可以叫做 AJAX。

早期都透過 XML 作為資料格式,所以用 AJAX 命名。

差異:

(1) AJAX : 透過瀏覽器發 request 到 server,server 回傳給瀏覽器,瀏覽器再轉傳到 JS

(2) 表單 : 透過瀏覽器發 request 到 server,server 回傳給瀏覽器,瀏覽器渲染出結果

代碼說明

// 兩個用法概念上相同
btn.addEventListener('click', function()...)
btn.onClick = function()... // 只是無法使用第二種用法s
  1. 在瀏覽器上發請求一定是非同步,因為如果是同步,就要等,等到 response 回來,才可以做其他事情,在這段期間,頁面無法做其他事情,唯一能做的事情就是等待。

  2. Access to XMLHttpRequest at 'https://google.com/' from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. => 瀏覽器有限制,不允許我們在這個網頁上發 request 到 google.com => 無法取得結果

<!DOCTYPE html>
<html>
<head>
  <title>test</title>
</head>
<body>
  <div class='app'>

  </div>
  <script type="text/javascript">
    // 使用 ajax 也就是用瀏覽器所提供的東西
    // XMLHttpRequest 是瀏覽器提供的 class,用 new 的方式可以 new 出一個 instance => 產生一個 XMLHttpRequest
    const request = new XMLHttpRequest() // XMLHttpRequest 簡寫 xhr
    // 放一個 function 到 onloda 上,當 request 拿到結果,就會觸發 onload 事件
    // 可以想成幫 request 加一個 onload 的 eventListener
    request.onload = function() {
      // 因為結果會放置在 request 上,所以我可以針對 request 寫下列判斷
      // 表 request 是成功的
      if (request.status >= 200 && request.status <400) {
        console.log(request.responseText)
      } else {
        console.log('err')
      }
    }

    // 也可以幫 request 加另一個 listener onerror log 下有錯的狀況
    request.onerror = function () {
      console.log('error')
    }
    // open 的意思是:你要發request 到這個地方
    // 第三個參數傳遞要否非同步,true 表示非同步
    request.open('GET', 'https://google.com', true)
    // 將 request 傳出
    request.send()
  </script>
</body>
</html>

更換網址,request.open('GET', 'https://reqres.in/api/users', true) => 拿的到這個 api 給我們的結果,同時瀏覽器並未換頁,也就是透過 JS 發 request 然後拿到 response。

// api 拿到的結果
{"page":1,"per_page":6,"total":12,"total_pages":2,"data":[{"id":1,"email":"george.bluth@reqres.in","first_name":"George","last_name":"Bluth","avatar":"https://reqres.in/img/faces/1-image.jpg"},{"id":2,"email":"janet.weaver@reqres.in","first_name":"Janet","last_name":"Weaver","avatar":"https://reqres.in/img/faces/2-image.jpg"},{"id":3,"email":"emma.wong@reqres.in","first_name":"Emma","last_name":"Wong","avatar":"https://reqres.in/img/faces/3-image.jpg"},{"id":4,"email":"eve.holt@reqres.in","first_name":"Eve","last_name":"Holt","avatar":"https://reqres.in/img/faces/4-image.jpg"},{"id":5,"email":"charles.morris@reqres.in","first_name":"Charles","last_name":"Morris","avatar":"https://reqres.in/img/faces/5-image.jpg"},{"id":6,"email":"tracey.ramos@reqres.in","first_name":"Tracey","last_name":"Ramos","avatar":"https://reqres.in/img/faces/6-image.jpg"}],"support":{"url":"https://reqres.in/#support-heading","text":"To keep ReqRes free, contributions towards server costs are appreciated!"}}

第三種方式:JSONP (補充)

現在已經不太有人使用,作為補充科普知識。JSONP,全名 JSON with padding。

有些標籤不受同源政策影響,例如:<img src=''/><script src=''></script>,src 可以引入其他 domain 的 JS,目的是方便,也沒有安全性的疑慮。<參照> AJAX (一定受同源政策管理)

所以也可以利用 script 標籤拿到資料。

代碼說明:

  1. load 一個 script:<script src="https://test.com/user.js"></script>
  2. 假設 user.js 是他回傳的內容,回傳的內容可以是 function,function 內夾帶的是回傳的資料。
  3. 回傳的資料為 js 物件資料,會在 server 端做填充,client 端就可以利用 function 拿到資料。
setData([
  {
    id: 1,
    name: 'hello'
  }, 
  {
    id: 2,
    name: 'byby'
  }

])
  1. 所以我們可以在 load 之前宣告一個 function,叫做 setData(),我們就可以在這裡面拿到他的資料。
  <script >
    function setData(users) {
      console.log(users)
    }
  </script>
  <script src="https://test.com/user.js"></script>

這個 script 可以動態產生

const element = document.createElement('script')
    element.src = 'https://test.com/user.js?id=1'

三、詳細解析 XMLHttpRequest

  1. 需要 new 一個 xhr ,用變數 request 接起來 => const request = new XMLHttpRequest() 可以理解成 new 一個 request
  2. 幫 request 加 eventListener
// 方式一、
request.onload = function() {}

//方式二、
request.addEventListener('load', function() {
})
     const request = new XMLHttpRequest() // XMLHttpRequest 簡寫 xhr
    // 放一個 function 到 onloda 上,當 request 拿到結果,就會觸發 onload 事件
    // 可以想成幫 request 加一個 onload 的 eventListener
    request.onload = function() {
      // 因為結果會放置在 request 上,所以我可以針對 request 寫下列判斷
      // 表 request 是成功的
      if (request.status >= 200 && request.status <400) {
        console.log(request.responseText)
      } else {
        console.log('err')
      }
    }
    //方式二
    request.addEventListener('load', function() {
      if (request.status >= 200 && request.status <400) {
        console.log(request.responseText)
      } else {
        console.log('err')
      }
    })
  1. 一旦 request 被載入,就可以拿一些資訊,以上述代碼為例,依據 request 的 status 判斷。status 就是 http response 的 status code。
  2. 指定用哪個 method 發 request 到哪個網址 => request.open('GET', 'https://reqres.in/api/users', true),第三個參數傳遞要否非同步,true 表示非同步,open 的意思是:你要發request 到這個地方。
  3. 將 request 送出 => request.send()
  4. response 為純文字,需要用 JSON.parse() 成 JSON 才能做一些事情

  5. note:

  • request.status 檢視回傳的狀態
  • request.responseText 拿到 response 的文字 => 不一定每一個 response 都有 responseText,看對方 api 如何實作而決定。

四、專屬於瀏覽器甜蜜:Same origin policy 與跨網域問題

指定閱讀

前言:

前面在送 request 給估狗時,我們收到 Access to XMLHttpRequest at 'https://google.com/' from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. 不能從 origin 'null' 到 google.com 因為被 CORS 擋住。因為沒有這個 header 在 requested resource,所以我們不能進行存取。

Same origin policy 同源政策

甚麼是同源?同源是指兩份網頁具備相同協定、port、主機位置。

小節:

  1. 相同 domain(網域) 就是同源。https 跟 http 是分開的,也就是說用 http 存取 https 不算同源,相反過來也非同源。
  2. 別人的網站跟你不一樣,除非我們共用同個 domain。
  3. 瀏覽器預設將不同源的 response 擋掉。

所以被擋掉怎麼辦?有時可能會到他人的 server 拿資料

Cross origin resourse sharing 跨來源資源共用

如果想要跨來源存取,規範中規定必須在 response 中加一個 header:Access-Control-Allow-Origin。

這裡面的內容是說那些來源的人可以存取這個 api 的 response。from origin 這個來源是甚麼?當我們發 request 瀏覽器會幫我們在 request 的 header 加上一個 header 叫做 origin:現在網頁的 domain,瀏覽器會幫我加上來源,我在 server 就可以決定我要否給他存取的權限。

如果 Access-Control-Allow-Origin: * * 表示所有,也就是所有的 origin 都可以存取。

總結:如何解決跨來源問題

  1. 變成同來源
  2. server 端加上 Access-Control-Allow-Origin: *

瀏覽器住海邊嗎,為什麼管這麼寬?

為甚麼要有這些限制呢?因為安全性。另外要強調,上述的政策都僅與瀏覽器有關。他是瀏覽器幫我們加上的限制。如果今天沒有瀏覽器,就沒有同源政策的限制。<參照> node.js 發 request、瀏覽器發 request

換句話說,今天我用 node.js 寫一個 request,不論是否同源、server 的 response 是否有 Access-Control-Allow-Origin: *,我都拿的到結果。因為我沒有透過瀏覽器。


五、單向傳送資料的延伸應用(email開啟追蹤 與 廣告追蹤)

單向傳送資料:有些時候不想拿到 response ,只是想傳一些資料過去。

舉例:某些發信網站發信後可以檢視有多少人打開信件,但我要怎麼知道使用者有無打開信件?

背後原理:

  1. email 中放入一個非常小的(透明)圖片,他的 src 是一個網址,user open 後面接的是你的 user id <img width='1' height='1' src='https://example.com/users_open/123456'/>
  2. 一旦打開這封信,開信軟體就會去 load 這個圖片讓它顯示出來
  3. 圖片顯示出來後,就會發 request 到 https://example.com/users_open/123456
  4. server 端就知道有人打開這個網址,換句話說有人打開這封信

綜合示範、抓取資料並顯示

步驟概況

  1. 版面刻好
  2. 拿資料,將寫死的資料動態換成我要的資料
  3. append 到畫面上

Clinet side rendering

利用 JS 動態產生出資料,換句話說,右鍵 => 檢視網頁原始碼 => body s看不到任何東西,但右鍵 => 檢查 => 看的到。Clinet side 就是指用 js 動態新增內容,渲染出頁面。

優點:server 資料一旦有變,我就可以拿到最新資料
缺點:搜尋引擎,看到頁面會認為頁面無任何內容,因為搜尋引擎是檢視網頁原始碼,多數搜尋引擎不會幫忙執行 js。

第零步、ajax 寫好

<!DOCTYPE html>
<html>
<head>
  <title>test</title>
</head>
<body>
  <div class='app'>

  </div>
  <script type="text/javascript">
    const request = new XMLHttpRequest() 
    request.onload = function() {
      if (request.status >= 200 && request.status <400) {
        console.log(request.responseText)
      } else {
        console.log('err')
      }
    }
    request.onerror = function () {
      console.log('error')
    }
    request.open('GET', 'https://reqres.in/api/users', true)
    request.send()
  </script>
</body>
</html>

第一步:切版
1-1、寫 html

  <div class='app'>
    <div class='profile'>
      <div class='profile__name'>George Bluth</div>
      <img class='profile__img' src="https://reqres.in/img/faces/1-image.jpg"/>
    </div>
    <div class='profile'>
      <div class='profile__name'>George Bluth</div>
      <img class='profile__img' src="https://reqres.in/img/faces/1-image.jpg"/>
    </div>
  </div>

1-2、加上 css

  <style type="text/css">
    body {
      font-size: 24px;
    }

    .profile {
      border: 1px solid gray;
      width: 300px;
      margin-top: 10px;
      display: inline-flex;
      align-items: center; 
    }
    .profile__name {
      margin: 0 auto;
    }
  </style>

第二步:拿資料

    const request = new XMLHttpRequest() 
    request.onload = function() {
      if (request.status >= 200 && request.status <400) {
        // 拿 response
        const response = request.responseText
        // parse 這個東西
        const json = JSON.parse(response)
        console.log(json) // 印出結果觀察
      } else {
        console.log('err')
      }
    }

選取我需要的部份,將 html 全部放進去

        const users = json.data
        for (let i=0; i<users.length; i++) {
          const html = `
         <div class='profile'>
           <div class='profile__name'>George Bluth</div>
           <img class='profile__img' src="https://reqres.in/img/faces/1-image.jpg"/>
         </div>
          `
        }

建立出最外層的 <div class='profile'></div>

const div = document.createElement('div')
div.classList.add('profile')

將內層的 html 用 innerhtml 塞入

div.innerHTML = `
<div class='profile__name'>George Bluth</div>
<img class='profile__img' src="https://reqres.in/img/faces/1-image.jpg"/>`

加入 template

div.innerHTML = `
<div class='profile__name'>${users[i].first_name}    ${users[i].last_name}</div>
<img class='profile__img' src="${users[i].avatar}"/> `

選好你要在哪個父層,新增這個子層

const request = new XMLHttpRequest()
// 迴圈內層加入這個
container.appendChild(div)

最後代碼長相:

<!DOCTYPE html>
<html>
<head>
  <title>test</title>
  <style type="text/css">
    body {
      font-size: 24px;
    }

    .profile {
      border: 1px solid gray;
      width: 300px;
      margin-top: 10px;
      display: inline-flex;
      align-items: center; 
    }
    .profile__name {
      margin: 0 auto;
    }
  </style>
</head>
<body>
  <div class='app'>
  </div>
  <script type="text/javascript">
    const container = document.querySelector('.app')
    const request = new XMLHttpRequest() 
    request.onload = function() {
      if (request.status >= 200 && request.status <400) {
        // 拿 response
        const response = request.responseText
        // parse 這個東西
        const json = JSON.parse(response)
        //  console.log(json) 印出結果觀察 
        const users = json.data
        for (let i=0; i<users.length; i++) {
          const div = document.createElement('div')
          div.classList.add('profile')
          div.innerHTML = `
         <div class='profile__name'>${users[i].first_name} ${users[i].last_name}</div>
         <img class='profile__img' src="${users[i].avatar}"/>
          `
          container.appendChild(div)
        }
      } else {
        console.log('err')
      }
    }
    request.onerror = function () {
      console.log('error')
    }
    request.open('GET', 'https://reqres.in/api/users', true)
    request.send()
  </script>
</body>
</html>









Related Posts

Day06 SharePrefernce+滑動刪除+Update功能(上)

Day06 SharePrefernce+滑動刪除+Update功能(上)

[BE101]  PHP 與 MySQL  (語法)

[BE101] PHP 與 MySQL (語法)

[Day06] Applicative

[Day06] Applicative


Comments