從零開始的野路子React(3)打通前後端
相信很多人都聽說過前後端分離這個概念,一直以來我比較好奇的一件事是,分離了之後我們怎麼再讓資料在前後端流通呢?最近正好學習了一下。
這次我新建了一個叫connection的專案,我們可以用create-react-app frontend新建一個前端目錄,叫frontend。再用express backend新建一個後端目錄,叫backend。初始化搭建就完成了,結構如圖:
1、搭建後端
這次我們先從後端開始。
作為我本人非常野路子的理解:後端就像一個倉庫,可以根據前端的需求向前端運送需要的貨物。後端有一群倉管,稱為API,每個倉管負責找不同的東西。每個倉管有自己的名字,也就是path,而管理這些名字的花名冊也就是路由系統了。
我們的後端非常簡單,修改一下app.js檔案:
var express = require('express'); var cors = require('cors'); var app = express(); var corsOptions = { credentials:true, origin:'http://localhost:3000', optionsSuccessStatus:200 }; app.use(cors(corsOptions)); app.get('/', function (req, res) { res.send('來者可是諸葛孔明?') }); app.listen(5000, function() { console.log('App listening on port 5000...') });
我們需要2個庫,一個是express,負責後端框架,另一個是cors,負責跨域請求。
我們後端的地址預設也是localhost,監聽的埠是5000。
這裡需要注意的是,如果不使用cors之類的庫處理跨域請求的話,我們會遇到跨域問題,簡而言之,也就是說雖然我們前後端的域名相同,但卻無法分享資訊。你的前端即使引用了後端的內容也會報錯。
在corsOptions中我們定義了一些cors所需要的配置,比如指定前端的地址是http://localhost:3000,之後我們使用app.use來把這些內容提供給app。
最後我們加上一套樸實無華的路由系統——一共只有一個地址,它允許你向http://localhost:3000/傳送一個GET請求,每次你傳送這個請求,你得到的內容將會是“來者可是諸葛孔明?”
後端搭建就完成了,在啟動之前,再做兩件事,第一是把backend目錄下的bin目錄整個刪掉,第二是修改package.json,把“start”對應的值改成node app.js,這樣就會從app.js啟動了。
然後我們就可以在cmd中一通npm install再npm start來啟動後端了。
這樣就是成功了。讓我們用Postman來發送一個GET請求看看:
看來後端運作沒有問題了。
2、搭建前端
接下來的問題就是,前端要如何從後端獲取資料呢?當然是傳送請求給後端的API啦。
找到API對應的後端地址,傳送相應的GET/POST請求,然後API就會返回相應的資料。這就像你說:“王二狗(path),給我一張倉庫所有貨物的清單(GET request)。”,然後名叫王二狗的倉管就給了你一張清單。
前端拿到這些資料後,只要再渲染一下即可。
那麼如何傳送這種請求呢?作為一個複製黏貼工程師,我並沒有正經學過fetch之類的方法,而是直接從大佬那裡抄了axios來用,真正做到大佬用什麼我用什麼。
現在我們來寫個呼叫後端API的元件CallApi.js吧:
import axios from 'axios'; const api = 'http://localhost:5000'; class CallApi { getSomething() { return new Promise((resolve) => resolve(axios.get(`${api}`))); } } export default new CallApi();
這個元件幹了幾件事,首先匯入了axios,然後指定了後端的地址(http://localhost:5000),接著定義了一個類,這個類有一個函式getSomething。每次這個元件被呼叫,就會返回一個CallApi的例項。
getSomething這個函式所做的事情就是向http://localhost:5000這個地址傳送一個GET請求,然後返回一個Promise物件,Promise會給出一個請求是否成功的答覆(當然,我仍然沒有非常理解Promise這個東西)。如果執行成功,會給出一個成功的答覆(resolve),並且包含了返回的資料。我們可以通過後接then來執行回撥函式,把資料挖出來用。
這裡我們再新建一個Page元件,用一種非常簡單的方式,用then來獲取資料(.data),再把後端傳輸過來的資料用一級標題顯示出來:
import React from 'react'; import CallApi from './CallApi'; export default function Page() { const getContent = () => { CallApi.getSomething() .then(response => { console.log(response.data) return response.data }) }; var content = getContent(); console.log(content) return ( <> <h1>{ content }</h1> </> ); }
修改一下App.js,加入Page這個元件:
import React from 'react'; import './App.css'; import Page from './components/Page'; function App() { return ( <div className="App"> <Page/> </div> ); } export default App;
整個結構像這樣:
我們在後端已經啟動的情況下在frontend目錄通過npm start來啟動前端,結果卻發現頁面一片空白……我們看一下console記錄的東西,會發現一些有趣的內容:
首先,content的內容是undefined,而getCotent函式中response.data的內容卻是後端傳來的資料,兩者竟然不一致。其次,我們會發現,console先記錄了content,再記錄了getCotent函式中的response.data,順序跟我們寫的是反的。
查閱資料得知,這是由於非同步的問題,程式會先解決var content = getContent()的部分,因此我們會先得到undefined,等CallApi.getSomething返回的Promise執行成功了之後,我們才會得到相應的資料,也就是getCotent函式中response.data,這也就解釋了這個奇怪的現象。
這可以打個比方(可能不太恰當),老闆讓你打電話找倉管要個倉庫所有貨物的清單,你打電話給王二狗,告訴他你要一份清單,王二狗說沒問題,他去找找,一會兒跟你傳真過來(Promise)。然後你掛了電話,先回復老闆說,我已經打電話問了。老闆如果要問你清單的內容,你當然不知道啦(undefined)。一會兒,王二狗找到了清單,給你傳真過來了,這下你才獲得了資料(response.data)。
那麼如何解決這個問題呢?還得靠第1篇中提到過的useState。我們重寫一下剛才的Page.js這個元件:
import React, {useState} from 'react'; import CallApi from './CallApi'; export default function Page() { const [content, setContent] = useState(""); function getContent() { CallApi.getSomething() .then(response => { console.log(response.data) setContent(response.data) }) } getContent(); console.log(content) return ( <> <h1>{ content }</h1> </> ); }
這一次,我們在getContent中並沒有return,而是通過setContent來改變content的狀態。這樣一來,一旦Promise執行成功了之後,setContent就會把相應的內容賦予content,這樣我們的問題也就解決了。
可以看到最初返回的是””,當Promise獲得了後端傳來的資料之後,頁面就更新了。當然,至於為什麼更新了很多次,我還並不清楚……
如此一來,我們就完成了後端向前端傳輸資料的過程。
3、從前向後
之前我們完成了資料從後向前的傳遞,現在我們來看看資料從前向後的傳遞。從前向後我們可以通過POST請求來完成。
我們先改一下後端:
var express = require('express'); var cors = require('cors'); const greeting = {"劉備":"玄德公乃仁義之士", "曹操":"快與我活捉曹賊"} var app = express(); var corsOptions = { credentials:true, origin:'http://localhost:3000', optionsSuccessStatus:200 }; app.use(cors(corsOptions)); app.use(express.urlencoded({extended: true})); // 必須要加 app.use(express.json()); // 必須要加 app.get('/', function (req, res) { res.send('來者可是諸葛孔明?') }); app.post('/hello', function (req, res) { let grt = greeting[req.body.name] res.send(grt) }); app.listen(5000, function() { console.log('App listening on port 5000...') });
在這裡,我們做了幾件事:
(1)新建了一個名為greeting的Object,可以通過人名來找到對應的問候語;
(2)我們用app.use給app加了express.urlencoded和express.json,這兩個不加的話無法正確解析前端傳來的資料;
(3)我們新加了一個API處理POST請求,對應的path是 /hello 。這就像是新招了一個倉管李二餅,王二狗專門負責查清單,李二餅專門負責盤庫存。
每次 /hello 收到一個POST請求之後,我們需要的內容(人名)就藏在請求的body部分裡,body是個JSON,我們假設body裡name就是我們需要獲取的人名。獲取人名之後,我們再通過greeting這個Object查詢對應問候語,然後將資料傳回去(res.send)。
後端完成之後,我們再改改前端,首先是CallApi這個元件,我們需要新加一個函式對應POST請求:
import axios from 'axios'; const api = 'http://localhost:5000'; class CallApi { getSomething() { return new Promise((resolve) => resolve(axios.get(`${api}`))); } sendSomething(body) { return new Promise((resolve) => resolve(axios.post(`${api}/hello`, body))); } } export default new CallApi();
sendSomething這個函式接收一個body引數(一個JSON),然後會將它傳送給後端的”/hello”。
再改改Page這個元件:
import React, {useState} from 'react'; import CallApi from './CallApi'; export default function Page() { const [content, setContent] = useState(""); const [greeting, setGreeting] = useState(""); function getContent() { CallApi.getSomething() .then(response => { console.log(response.data) setContent(response.data) }) } function handleLB () { CallApi.sendSomething({"name":"劉備"}) .then(response => { console.log(response.data) setGreeting(response.data) }) } function handleCC () { CallApi.sendSomething({"name":"曹操"}) .then(response => { console.log(response.data) setGreeting(response.data) }) } getContent(); console.log(content) return ( <> <h1>{ content }</h1> <div> <button onClick={ handleLB }>劉備來了</button> <button onClick={ handleCC }>曹操來了</button> </div> <div> <p>{ greeting }</p> </div> </> ); }
我們增加了兩個按鈕,以及對應按下按鈕時觸發的函式(handleLB, handleCC),這兩個函式跟getContent非常相似,區別在於它們呼叫的是CallApi.sendSomething,並且會發送一個JSON,而這個JSON裡有我們需要傳遞的人名資料name。我們會將返回的資料賦值給greeting,然後在兩個新增按鈕的下方顯示返回的內容。
再啟動一下試試,點選按鈕我們就會得到想要的效果了:
從前向後的資料傳遞也就完成了。
程式碼見: