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
。
發送路徑 :
- 發一個 get request 到
https://www.google.com/?username=www
位址。用 get 方法,我的參數會被附加的網址上去。從網址上就可以知道參數是甚麼,所以登入一般用 post 帳密會帶在 body。 - 接著 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
在瀏覽器上發請求一定是非同步,因為如果是同步,就要等,等到 response 回來,才可以做其他事情,在這段期間,頁面無法做其他事情,唯一能做的事情就是等待。
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 標籤拿到資料。
代碼說明:
- load 一個 script:
<script src="https://test.com/user.js"></script>
。 - 假設 user.js 是他回傳的內容,回傳的內容可以是 function,function 內夾帶的是回傳的資料。
- 回傳的資料為 js 物件資料,會在 server 端做填充,client 端就可以利用 function 拿到資料。
setData([
{
id: 1,
name: 'hello'
},
{
id: 2,
name: 'byby'
}
])
- 所以我們可以在 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
- 需要 new 一個 xhr ,用變數 request 接起來 =>
const request = new XMLHttpRequest()
可以理解成 new 一個 request - 幫 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')
}
})
- 一旦 request 被載入,就可以拿一些資訊,以上述代碼為例,依據 request 的 status 判斷。status 就是 http response 的 status code。
- 指定用哪個 method 發 request 到哪個網址 =>
request.open('GET', 'https://reqres.in/api/users', true)
,第三個參數傳遞要否非同步,true 表示非同步,open 的意思是:你要發request 到這個地方。 - 將 request 送出 =>
request.send()
response 為純文字,需要用 JSON.parse() 成 JSON 才能做一些事情
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、主機位置。
小節:
- 相同 domain(網域) 就是同源。https 跟 http 是分開的,也就是說用 http 存取 https 不算同源,相反過來也非同源。
- 別人的網站跟你不一樣,除非我們共用同個 domain。
- 瀏覽器預設將不同源的 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 都可以存取。
總結:如何解決跨來源問題
- 變成同來源
- server 端加上
Access-Control-Allow-Origin: *
瀏覽器住海邊嗎,為什麼管這麼寬?
為甚麼要有這些限制呢?因為安全性。另外要強調,上述的政策都僅與瀏覽器有關。他是瀏覽器幫我們加上的限制。如果今天沒有瀏覽器,就沒有同源政策的限制。<參照> node.js 發 request、瀏覽器發 request。
換句話說,今天我用 node.js 寫一個 request,不論是否同源、server 的 response 是否有 Access-Control-Allow-Origin: *
,我都拿的到結果。因為我沒有透過瀏覽器。
五、單向傳送資料的延伸應用(email開啟追蹤 與 廣告追蹤)
單向傳送資料:有些時候不想拿到 response ,只是想傳一些資料過去。
舉例:某些發信網站發信後可以檢視有多少人打開信件,但我要怎麼知道使用者有無打開信件?
背後原理:
- email 中放入一個非常小的(透明)圖片,他的 src 是一個網址,user open 後面接的是你的 user id
<img width='1' height='1' src='https://example.com/users_open/123456'/>
- 一旦打開這封信,開信軟體就會去 load 這個圖片讓它顯示出來
- 圖片顯示出來後,就會發 request 到
https://example.com/users_open/123456
- server 端就知道有人打開這個網址,換句話說有人打開這封信
綜合示範、抓取資料並顯示
步驟概況
- 版面刻好
- 拿資料,將寫死的資料動態換成我要的資料
- 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>