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