# 0421 透過 JavaScript 進行 HTTP 請求 & 非同步
###### tags: `JavaScript`
- [老師講義](https://hackmd.io/@pastleo/Byqk5WeUO#/)
- [隨堂練習包](https://ppt.cc/f514zx)
## HTTP request (請求)
- 網頁的運作:[HTTP Wiki 介紹](https://zh.wikipedia.org/wiki/%E8%B6%85%E6%96%87%E6%9C%AC%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE)
- DevTool 的 Network 可以拿來觀察
- 網站重新整理後跑出來的每一行都是一個瀏覽器送出的 HTTP Request
- 打開 Header 觀察細節
- 裡面會有 General, Response Header, Request Header
- 一般 Request Header 會有:`cookie`
- Response 也會有新的 cookie 夾帶一起送回來

- 電腦跟電腦溝通就像寄信傳輸一樣
- 信封上有既定的慣例寫寄件者、收件者、地址....
- 電腦跟電腦間有特別的通訊協定
- Header 就像信封上寫的東西一樣
- 網站的 Response

- 利用 JS 寫程式跟後端拿資料
- 前端需要即時跟後端拿資料再即時渲染到目前的網頁
## HTTP request via JavaScript
### fetch API
- fetch 字面上意思:==拿取==
- [MDN 介紹](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)
- 在 fetch(...) 出現之前
- 使用 `$.ajax()`
- `XMLHttpRequest`
#### 練習用的 Posts API server
- [練習使用](https://pastleo-posts-api.herokuapp.com/)
- [Git Repo](https://github.com/5xTraining/pastleo-js-posts-api)
### Web API 常見的回傳格式:JSON
- [JSON維基百科](https://zh.wikipedia.org/wiki/JSON)
- 提供一個結構化的資料給你看
- 方便 JavaScript 處理資料
```json=
[
{
"id": 128,
"title": "hello json!"
}
]
```
- 範例:

- 要小心 JSON 有一些不合常規的格式
- 單引號不接受,只可用雙引號(`" "`)
- 不能接受任何運算(function)
- 只能寫計算完成的值
- 字串、數字、陣列、物件
- 大部分是由程式產生的
- `Expecting 'STRING', 'NUMBER', 'NULL', 'TRUE', 'FALSE', '{', '[', got 'undefined'`
- JS 及 JSON 間轉換
- 用 `JSON.stringify()` 把 JS 裡的物件、陣列、字串或數字轉換為 JSON 格式
-
- [MDN 說明](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify)
- 用 `JSON.parse()` 把 JSON 格式的東西轉回 JS 格式
-
- [MDN 說明](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse)

- [JSON-Validator / JSON 格式驗證](https://jsonlint.com/)
- JSON 是 JS 內建的 API
### 實作:fetch-first-post
- 網頁讀取完之後用 fetch 把各個資料塞進去
- 任務:使用 fetch 抓取並顯示 API 回傳的第一篇文章
- fetch 資料 (JavaScript 一個在 window 底下的 function)
```javascript=
fetch('...放一段字串 / 網址') // 網址也要用字串模式
.then(request => request.json())
.then(posts => { ... })
```
- 試著從 api 裡面抓取所有資料
```javascript=
document.addEventListener('DOMContentLoaded', function(){
fetch('https://pastleo-posts-api.herokuapp.com/api/posts')
.then(request => request.json())
.then(posts => {
console.log(posts)
})
})
```
- 拿到的資料是陣列
- 把拿到的東西塞到 HTML 裡面
```javascript=
document.addEventListener('DOMContentLoaded', function(){
fetch('https://pastleo-posts-api.herokuapp.com/api/posts')
.then(request => request.json())
.then(posts => {
console.log(posts)
const firstPost = posts[0]
const postTitleA = document.querySelector('.post-title')
postTitleA.textContent = firstPost.title
postTitleA.href = firstPost.url
document.querySelector('.post-created-at').textContent = firstPost.created_at
document.querySelector('.post-author').textContent = firstPost.author
document.querySelector('.post-description').textContent = firstPost.description
const postLinkA = document.querySelector('.post-link')
postLinkA.classList.remove('hidden')
postLinkA.href = firstPost.url
})
})
```
### CORS 跨來源資源共享
- Cross-origin resource sharing
- [WIKI 介紹](https://zh.wikipedia.org/zh-tw/%E8%B7%A8%E4%BE%86%E6%BA%90%E8%B3%87%E6%BA%90%E5%85%B1%E4%BA%AB)

* CORS headers 設定
* 伺服器預設為沒有設定(不開放)
* 設定完之後 server 就知道可以放哪個 domain 的人進來拿資料
* 看設定的 DNS 位址
* [西瓜範例](https://github.com/5xTraining/pastleo-js-posts-api/blob/bdcb851fb392b20179f9521584d67f35af9d37ec/app/controllers/posts_controller.rb#L99)
* 原始碼把 allow_cors 放在 before_action 所以都會執行


* 一般狀況:沒有 CORS headers
* [西瓜範例(Posts 另一組沒有 CORS 的 API)](https://pastleo-posts-api.herokuapp.com/posts.json)
* 其實伺服器還是會完成所有動作,只是會擋掉 JS 拿資料的動作
* 實際通常不會直接寫進 controller 裡面
* 實務上通常會直接寫進 GEM 裡面 => 直接把 CORS 允許名單做進 Route 裡面
## Asynchronous / 非同步
- 什麼是非同步?
- 等待的時候可以去處理別的事情
### 什麼是同步?
- 第一行執行完,執行第二行。第二行執行完,執行第三行。
```json=
const two = 1 + 1;
const post = fetchPostSync(); // IO operation
postTitleA.textContent = post.title;
```
- 範例中,第二行會需要比較久的時間執行
#### 同步的問題

- 同步沒有回應的原因
- JS Event Loop 的特性:
- 單執行緒
- [Run-to-completion](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#run-to-completion)
- 像超商店員服務完前一個客人以後,才會服務你
- input/output (IO) operation:
- 存取網路、檔案等,通常 CPU 不忙,但是需要等待時間
### 同步及非同步案例
- 使用 07_sync-async-fetch
- 20 行 setInterval 每秒會執行一次
- 24 行如果執行下去,程式就會卡在那無法繼續進行
- 33 行傳出 false,34 行送出之後一樣會卡住(照樣同步)
- 40 行傳出 true 就是開啟非同步行為(fetch 預設為 true)
- 因為 40 行是非同步,所以要設 42 行的 callback 接他的結果,因為在 41 行結束時結果還沒回來

- fetch 不允許同步的事件
- 給一個 function 來告知瀏覽器 IO 完成的時候要做什麼
- 非同步寫法
- 
* Callback hell
```javascript=
const xhr1 = new XMLHttpRequest();
xhr1.open('GET', /* ... */);
xhr1.send();
xhr1.addEventListener('load',() => {
const xhr2 = new XMLHttpRequest();
xhr2.open('GET', /* ... */);
xhr2.send();
xhr2.addEventListener('load',() => {
const xhr3 = new XMLHttpRequest();
xhr3.open('GET', /* ... */);
xhr3.send();
xhr3.addEventListener('load',() => {
const xhr4 = new XMLHttpRequest();
xhr4.open('GET', /* ... */);
xhr4.send();
xhr4.addEventListener('load',() => {
// ...
})
})
})
})
```
### 非同步寫法 Promise
- [MDN 介紹](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)
- [MDN 介紹 Promise catch](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch)
- promise 有點像是一個未來的事件
- 他保證未來會發回來給你
- 我承諾你未來會有
- 發明了 promise 之後,制定標準的人就把一些需要非同步執行的指令都加上 promise
- `fetch` 就會回傳 `promise`
- JavaScript 官方的非同步處理介面
- `.then(callback)`
- 在 resolve 時執行 callback
- `callback` 如果回傳 `Promise`
- 可以繼續串 (chain)
- `then(callback).then(callback)...`
- 預設 callback 裡的 .json() 會回傳一個 promise
- [callback西瓜範例](https://hackmd.io/eobvUTY_TK2ZPJdxJysNEQ#Callback)
#### 實作範例
- 使用 08_fetch-first-post-content
- 取得文章列表之後,再抓文章內文來顯示
- `.json()`
- 接收網站的 response 再把資料從 json 格式轉成 js 格式
- 會回傳一個包含 js 內容的 `promise`
* 不好的寫法:(再次陷入callback hell)
```javascript=
fetch('https://pastleo-posts-api.herokuapp.com/api/posts')
.then(function(response){
response.json().then(function(response){
response......
})
})
```
* 比較好的方法:
* .then 最後面加 return
* 後面 .then 分開來放
```javascript=
document.addEventListener('DOMContentLoaded', () => {
const descriptionDiv = document.querySelector('.post-description')
fetch('https://pastleo-posts-api.herokuapp.com/api/posts')
.then(response => response.json())
.then(posts => {
const post = posts[0];
document.querySelector('.post-created-at').textContent = post.created_at;
document.querySelector('.post-author').textContent = `By ${post.author}`;
const postTitleA = document.querySelector('a.post-title');
postTitleA.textContent = post.title;
postTitleA.href = post.url;
descriptionDiv.textContent = post.description;
return fetch(post.api_url)
})
.then(response => response.json())
.then(fullPost => {
document.querySelector('.post-content').textContent = fullPost.content
descriptionDiv.classList.add('hidden')
})
});
```
### 非同步寫法 async / await
- 只是把寫法改掉,運作模式跟效能都是一樣的
- [MDN 介紹 async](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function)
- 用 async 的 function 一定會回傳 promise
- 只有 async function 才能用 await
- 寫法
```json=
async () => {}
async function() {}
async function name() {}
```
- 一律回傳 Promise
- 可用 await 關鍵字
- 加入 await 就像 fetch 後面有 .then 一樣
- 有點像 `接著要執行的東西 = await 要執行比較久的東西`
```javascript=
resolved = await promise
```
- 改寫之前的程式碼
```javascript=
document.addEventListener('DOMContentLoaded', async () => { // 加入 sync
const descriptionDiv = document.querySelector('.post-description')
const postsResponse = await fetch('https://pastleo-posts-api.herokuapp.com/api/posts')
const posts = await postsResponse.json()
const post = posts[0];
document.querySelector('.post-created-at').textContent = post.created_at;
document.querySelector('.post-author').textContent = `By ${post.author}`;
const postTitleA = document.querySelector('a.post-title');
postTitleA.textContent = post.title;
postTitleA.href = post.url;
descriptionDiv.textContent = post.description;
const fullPostResponse = await fetch(post.api_url)
const fullPost = await fullPostResponse.json()
document.querySelector('.post-content').textContent = fullPost.content
descriptionDiv.classList.add('hidden')
});
```
- 但是... await 會有出狀況的時候
- 在送出表單(`await fetch`)前沒有 `event.preventDefault()`
- 這樣表單就會依原本的方式送出,會無法得到預期的非同步執行結果
- 瀏覽器會以為已經做完了,所以就執行預設程序
- 如果有舊版網頁沒有使用 async 跟 await 時,最好在新區塊加入新功能就好,不要改寫舊的部分,避免造成不相容或壞掉的狀況發生
---
## HTTP `POST` via JavaScript
- 用了之後可以把東西送出去
- Header 裡面會出現 Form Data
- [MDN 說明 fetch 相關參數](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch)
- fetch(url, options)
- url 與 posts index 相同
- options
- `method: 'POST'`
- `headers: { Authorization: '...' }`
- 例:pastleo-js-posts-api-secret
- 讓伺服器知道你是誰,用於認證或篩選使用者
- 輸入錯誤的話會顯示`422`訊息,顯示認證失敗
- 實務上會是隨機產生的內容
- `body: new FormData(form)`
- FormData
- 收集 form 裡面的資料
- 對照 input tag 裡面的 name 產生對應
- [MDN 說明](https://developer.mozilla.org/en-US/docs/Web/API/FormData/FormData)
#### 範例說明
* FormData

* 網站建置時限制存取的人

* 使用 09_create_post 示範
- 錯誤處理(顯示錯誤訊息)
```javascript=
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('create-post-form');
const submitBtn = form.querySelector('button[type=submit]');
form.addEventListener('submit', async event => {
event.preventDefault();
form.querySelectorAll('label').forEach(
label => label.querySelector('.error').textContent = ''
)
const response = await fetch('https://pastleo-posts-api.herokuapp.com/api/posts',
{
method: 'POST',
headers: {
Authorization: 'pastleo-js-posts-api-secret'
},
body: new FormData(form)
}
)
if (response.ok) {
submitBtn.textContent = 'Success!'
} else {
submitBtn.textContent = 'Error!'
const errors = await response.json()
form.querySelectorAll('label').forEach(
label => {
const error = errors[label.htmlFor]
if (error) {
label.querySelector('.error').textContent = error.join(', ')
}
}
)
}
});
})
```
- Loading 時 button 會有變色效果
## 作業
- 用 new Promise 改寫 setTimeout
- 可參考 [MDN 說明 promise constructor](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Global_Objects/Promise)
* 改寫內容:
```javascript=
-setTimeout(() => {
- submitBtn.textContent = 'Submit';
- submitBtn.disabled = false;
-}, 2000);
+await timeoutPromise(2000);
+submitBtn.textContent = 'Submit'
+submitBtn.disabled = false;
```
## 題外話
### 開發者工具(右鍵檢查)
- 平常沒開的時候是不會記錄東西的
- 因為會浪費記憶體
- 打開之後會是空白
- 打開後按重新整理就會有東西跑出來
- Disable Cache (開發時隨時更新)

- 模擬比較慢的手機網路(像是 Slow 3G)

- 選取特定的 Request/Response

- 找出物件裡包裹的東西
- 在 Element view 裡面可以點到物件
- 在 console 裡打 `$0`
- 會顯示出現在點到的東西跟詳細屬性
---
### 取得使用者輸入數值
- 一個同步執行的內建方法
```javascript=
const input = prompt();
// prompt 回傳值為輸入框得到的資料
putBlock('Prompt:', input);
// purBlock 自動產出一個區塊
```
---
### Arrow function
[MDN 介紹](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions)
- function 裡可以??
- function 如果只有一個參數,`()`可以省略
```javascript=
// 傳統函式宣告
function asdf(a){
return a + 1
}
// asdf 變數宣告接一個匿名函式
let asdf = function (a) {
return a + 1
}
// 變成 arrow function ( 把asdf放到前面 )
let asdf = (a) => {
return a + 1
}
// 省略花括號跟 return
asdf = (a) => a + 1
// 如果只有一個參數的話,小括號可以省略
asdf = a => a + 1
```
---
### Template Strings
Template literals are enclosed by the backtick (`) (grave accent) character instead of double or single quotes.
- 裡面可以放運算式
- Ruby: `"qwer #{....}"`
- JavaScript: `"qwer ${....}"`
> [MDN對Template strings的解釋](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals)