發出一個非同步 request 串接後端
回憶
昨天介紹了 React 和完成用 create-react-app 建立了第一個 React app
目標
- 發出一個非同步 request
- 串接後端
在開始前,我們先來談談「非同步」
非同步函數是什麼?
來看看一個同步的例子:
- 打開
hello-react,在public資料夾建立一個檔案ayncRequest.html:內容如下1<!-- ayncRequest.html --> 2<html> 3<body> 4 <div>before script</div> 5 <script type="text/javascript"> 6 console.log('hi'); // 在 Console 印出 "hi" 7 </script> 8 <div>after script</div> 9</body> 10</html> - 執行
npm run start - 開啟Chrome 再開 devTools,並選擇 Console 頁籤,打開
http://localhost:3000/ayncRequest.html剛剛的頁面 會看到以下的結果
console.log()會印出訊息在 Console 中
瀏覽器頁面渲染到一半,一看到 <script/> 就會立刻執行裡面的 javascript,也就是說 console.log() 立刻被執行
1對於頁面渲染,console.log() 是同步地被執行。
試試看:你可以用 debug 模式証實它真的有停在那裡。若不會操作,可以看 Day 10- 一周目- 開始玩轉前端(一)
![]()
![]()
一個非同步的例子:
修改 ayncRequest.html 加入 setTimeout()
1<!-- ayncRequest.html -->
2<html>
3<body>
4 <div>before script</div>
5 <script type="text/javascript">
6 console.log('hi'); // 在 Console 印出 "hi"
7 setTimeout(() => {
8 console.log('time up'); // 兩秒後印出 "time up"
9 }, 2000);
10 </script>
11 <div>after script</div>
12</body>
13</html>
再次刷新頁面
console.log('time up')。
瀏覽器在執行渲染時, 下面的箭頭函
1() => {
2 console.log('time up'); // 兩秒後印出 "time up"
3}
被 setTimeout() 存在某個地方後,頁面渲染結束後(看到 before script / after script),等個兩秒,箭頭函數才被執行。此時的箭頭函數叫 callback function。
1對於頁面渲染,console.log() 是非同步地被執行。因為箭頭函數的執行不是透過「頁面渲染」執行的。
下面的專案很有趣 loupe by Philip Roberts,可以看到瀏覽器非同步執行過程。
所以…結論是?
很有趣的是,我找不到有人給出非同步函數正式定義,因為非同步函數無法單獨定義,它是由行為所導致結果,如:「頁面渲染」因為 setTimeout()導致「頁面渲染」是非同步的執行(見:Node.js Asynchronous Function Definition Ask Question)。
本節最後,我說說我對非同步函數的看法是:
當一函數無法立刻得到執行函數的目地,就是非同步函數。一般利用 callback function 回傳結果。
所以未來看到一個函數參數有定義 callback function 大部分都是非同步函數。
以後在二周目我們將會看到如何做出非同步執行的函數:
- 利用
setTimeout()…系列 - Promise
- async/await
串接後端
上章提出了「非同步」的概念,現在來使用非同步函數,來發出非同步 request 的與伺服溝通。
準備環境
- 打開並執行
hello-react前端開發網頁伺服器 - 打開 Day 9 - 一周目- 開始玩轉後端(二) 建的
hello-express,它有POST /api/echoAPI - 因為前端占用 3000 prot,後端要更改 port,修改
package.json1"scripts": { 2 "start": "PORT=3001 node ./bin/www" 3},PORT是環境變數,port 換成 3001 - 處理 Cross-origin resource sharing (CORS)問題。因為我們把前後端分開,在前端網頁會打非同步api(request)到不同主機的位置,瀏覽器 基於安全性會拒絕 request,且會出現下圖:
- 後端安裝套件
npm install cors --save - 後端修改
./app.js1// 加在前面 2var cors = require('cors'); 3 4// 加在 var app = express(); 之後 5app.use(cors()); // 加入一個 middleware
- 後端安裝套件
- 執行
npm run start
前端主機是 http://localhost:3000
後端主機是 http://localhost:3001
你可以用瀏覽器試試是否有正常運作或用 Postman
發出一個非同步 request
主要有兩個方法
XMLHttpRequest物件fetch()函數
XMLHttpRequest
- 開啟前端
./src/App.js,加入componentDidMount()成員函數1class App extends Component { 2 componentDidMount() { 3 // 送到後端的資料 4 const data = { 5 name: 'Billy' 6 }; 7 8 // 用 XMLHttpRequest 發起一個非同步的 request 9 const xhr = new XMLHttpRequest(); 10 xhr.onreadystatechange = () => { 11 if (xhr.readyState === 4) { // readyState == 4 為 request 完成 12 const contentType = xhr.getResponseHeader('content-type'); 13 if (xhr.status === 200 && contentType && contentType.indexOf('application/json') > -1) { // 依回應的資料格式處理,我們只處理 200 && application/json 14 try { 15 var result = JSON.parse(xhr.responseText); 16 console.log(result) 17 } catch(e) { 18 console.error(e); 19 } 20 } else { 21 console.error(new Error('無法得到資料'), xhr.responseText); 22 } 23 } 24 } 25 xhr.open("POST", "http://localhost:3001/api/echo"); // 開啟連線 26 xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); // 設定 header 27 xhr.send(JSON.stringify(data)); // 以文字字串送出JSON資料 28 } 29 30 render() { 31 ... 32 } 33} - 得到結果
這裡 componentDidMount() 是當 component 的渲染結果(html)插入 DOM tree 中就會呼叫。
使用 XMLHttpRequest 物件,以文字字串送出(send()) JSON 資料。因為有設定 Content-Type: application/json;charset=UTF-8,所以後端可以解讀。
每次 request 狀態改變,onreadystatechange 就會被呼叫,當 readyState 等於 4 就是 request 完成(不論成功還是失敗)。更多狀態見:AJAX - onreadystatechange 事件
fetch()
Promise 物件。 Promise 可以說是 javascript 最重要的物件,它包裝非同步操作,只要用 then(resolveCallback).catch(rejectCallback),就可以接收非同步的結果。我們在二周目會再次見到它。
把上 使用 XMLHttpRequest 物件的 componentDidMount(),全部註解掉,貼上下面程式,也會得到一樣的結果
1componentDidMount() {
2 // 送到後端的資料
3 const data = {
4 name: 'Billy'
5 };
6
7 // 用 fetch() 發起一個非同步的 request
8 fetch("http://localhost:3001/api/echo", {
9 method: "POST",
10 headers: {
11 "Content-Type": "application/json; charset=utf-8",
12 },
13 body: JSON.stringify(data),
14 })
15 .then(response => response.json()) // 取出 JSON 資料,並還原成 Object。response.json() 一樣回傳 Promise 物件
16 .then(data => {
17 console.log(data);
18 })
19 .catch(e => {
20 console.error(e);
21 });
22 }
fetch() 是不是用起來比較自然一點了,可讀性增加了。fetch().then(resolveCallback1).then(resolveCallback2).catch(rejectCallback) 是一種鏈式語法,就是一直串下去要做的事。
流程如下:(你先不要看解釋,看程式就大概可以猜到過程了)
fetch()一但成功取得資料,假設叫request,就會把request往下一個then()裡面的 callback 傳,就如同執行resolveCallback1(request)。resolveCallback1(request)回傳一個由response.json()產生的 Promise 物件,也會繼續如同之前一樣。response.json()成功時取得資料,假設叫data,就會往下一個then()裡面的 callback 傳,就如同執行resolveCallback2(data)。- 最後
catch(rejectCallback)中的rejectCallback什麼時後被叫呢?就是fetch()、resolveCallback1()或resolveCallback2有任何失敗或產生例外。
then(resolveCallback).catch(rejectCallback)用起來有點像 try...catch,但強多了,配合 async/await 這語法糖衣,又可以拆掉鍊式語法得到像同步的程式碼。我只寫出結果,看看它利害的地方,以後我們再談它們
1componentDidMount() {
2 // 送到後端的資料
3 const data = {
4 name: 'Billy'
5 };
6
7 const workerPromise = (async () => {
8 // 用 fetch() 發起一個非同步的 request,等待回傳結果
9 const response = await fetch("http://localhost:3001/api/echo", {
10 method: "POST",
11 headers: {
12 "Content-Type": "application/json; charset=utf-8",
13 },
14 body: JSON.stringify(data),
15 })
16
17 // 等待 response.json() 回傳的 JSON 物件
18 const resultData = await response.json();
19 return resultData;
20 })();
21
22 workerPromise
23 .then(data => {
24 console.log(data);
25 })
26 .catch(e => {
27 console.error(e);
28 });
29 }
把結果顯示在網頁上
接下來,後端的結果顯示在網頁上。
- 加入 App 的成員變數
state,用來記錄 App component 的內部狀態1state = { 2 name: '', 3} - 在
console.log(data)後面加入1this.setState({ 2 name: data.name, 3});setState()會更新的 state 內的值,並引起 App component 重新渲染render()。 - 使用內部狀態
state,修改render(),1render() { 2 return ( 3 <div className="App" > 4 Hello React: {this.state.name} 5 </div > 6 ); 7}
完整的 App component 如下:
1class App extends Component {
2 state = {
3 name: '',
4 }
5
6 componentDidMount() {
7 // 送到後端的資料
8 const data = {
9 name: 'Billy'
10 };
11
12 // 用 fetch() 發起一個非同步的 request
13 fetch("http://localhost:3001/api/echo", {
14 method: "POST",
15 headers: {
16 "Content-Type": "application/json; charset=utf-8",
17 },
18 body: JSON.stringify(data),
19 })
20 .then(response => response.json()) // 取出 JSON 資料,並還原成 Object。response.json() 一樣回傳 Promise 物件
21 .then(data => {
22 console.log(data);
23
24 // 更新的 state 內的值,並再一次引起渲染 render()
25 this.setState({
26 name: data.name,
27 });
28 })
29 .catch(e => {
30 console.error(e);
31 });
32 }
33
34 render() {
35 return (
36 <div className="App" >
37 Hello React: {this.state.name}
38 </div >
39 );
40 }
41}
得到結果
總結
今天引進了非同步的概念,並利用 XMLHttpRequest 物件 和 fetch() 函數引發一個 非同步的 request,得到結果後重新渲染畫面。
評論