从零开始的野路子React/Node(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

原文地址:https://www.cnblogs.com/silence-gtx/p/13332793.html