1. 程式人生 > 實用技巧 >從零開始的野路子React(3)打通前後端

從零開始的野路子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,然後在兩個新增按鈕的下方顯示返回的內容。

再啟動一下試試,點選按鈕我們就會得到想要的效果了:

從前向後的資料傳遞也就完成了。

程式碼見:

https://github.com/SilenceGTX/react_front_and_back