React:六、axios请求接口
1 前言
vue和react均可使用axios进行接口数据请求。axios官网文档地址:
http://www.axios-js.com/docs/index.html
React项目,先执行如下命令安装axios库:
npm i axios
2 使用
2.1 接口准备
编写react前,先准备好请求接口,如下搭建起水果查询接口:
mapper文件:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xiaoxu.dao.QueryFruitDao">
<select id="queryFruits"
parameterType="map"
resultType="com.xiaoxu.entity.Fruit.Fruit">
select
fruit_id,create_time,extra_info,
modify_time,cross_out_price,fruit_stock,name,sales,
sup_id,unit_price,unit_weight
from my_fruit
<where>
<if test = "fruitId!=null">
and fruit_id = #{fruitId}
</if>
<if test = "name!=null">
and name like concat('%',#{name},'%')
</if>
<if test = "supId!=null">
and sup_id like concat('%',#{supId},'%')
</if>
</where>
</select>
</mapper>
测试数据如下:
controller:
package com.xiaoxu.controller.Fruit;
import com.google.common.collect.Maps;
import com.xiaoxu.base.FruitMallExceptions.CommExp;
import com.xiaoxu.base.Request.QueryFruitRequest;
import com.xiaoxu.base.Result.CommResult;
import com.xiaoxu.base.Result.results.QueryFruitsResults;
import com.xiaoxu.base.template.ProcessorCallBack;
import com.xiaoxu.base.template.impl.ProcessorImpl;
import com.xiaoxu.service.QueryFruitService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.xiaoxu.vo.Fruit.FruitVo;
import java.util.List;
import java.util.Map;
/**
* @author xiaoxu
* @date 2022-03-26 12:50
* FruitMall:com.xiaoxu.controller.Fruit.FruitController
*/
@Controller
@RequestMapping(value = "/fruit")
@Slf4j
public class FruitController {
@Autowired
QueryFruitService queryFruitService;
@Autowired
ProcessorImpl<QueryFruitRequest, QueryFruitsResults> processor;
@ResponseBody
@RequestMapping(value = "/fruitsBySup",method = RequestMethod.GET )
public List<FruitVo> queryFruitsBySupplier(){
return null;
}
@ResponseBody
@RequestMapping(value = "/queryFruits", method = RequestMethod.GET)
public CommResult<QueryFruitsResults> queryFruits(QueryFruitRequest queryFruitRequest){
Map<String,Object> params = Maps.newHashMap();
return processor.process(queryFruitRequest, new ProcessorCallBack<QueryFruitRequest, QueryFruitsResults>() {
@Override
public void validate(QueryFruitRequest params1) throws CommExp {
}
@Override
public QueryFruitsResults doProcess() throws CommExp {
log.info(String.format("请求进来了,传入的参数是:%s",queryFruitRequest));
List<FruitVo> fruitVos = queryFruitService.queryFruitsByCondition(queryFruitRequest);
QueryFruitsResults queryFruitsResults = new QueryFruitsResults();
if (!CollectionUtils.isEmpty(fruitVos)) {
queryFruitsResults.setFruitVoList(fruitVos);
}
return queryFruitsResults;
}
});
}
}
配置文件中的url注意时区问题:
serverTimezone改为中国时区Asia/Shanghai,如果是serverTimezone=UTC,那么接口返回json数据中,时间会增加8小时(因为若服务器时区为UTC,那么中国时间为东8区,即UTC+8,那么转换成本地时间会增加8小时):
url: jdbc:mysql://localhost:3306/fruitmall?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=Asia/Shanghai
postman请求:
接口返回数据无误:
2.2 react接口请求
2.2.1 src下封装接口请求api:axios
import axios from "axios"
// 请求响应超时时间
axios.defaults.timeout=5000;
let {keys,values,entries} = Object;
// 封装get/post请求
export default{
get:(path="",params={})=>{
let request_url="";
request_url=path?request_url+path:request_url;
if(keys(params).length>0){
request_url+="?"
let count=0;
for(let k of keys(params)){
if(count!=keys(params).length-1){
request_url+=k+"="+params[k]+"&";
count+=1;
}else{
request_url+=k+"="+params[k];
}
}
}
axios.get(request_url).then(res=>{
console.log("get接口请求成功!");
console.log(res);
console.log(typeof res);
}).catch(err=>{
console.log("get接口报错!")
console.log(err)
}).then(r=>{
console.log("get请求接口调用结束!");
});
},
post:(path="",data={})=>{
let request_url="";
request_url=path?request_url+path:request_url;
axios.post(request_url,data).then(res=>{
console.log("post接口请求成功!");
console.log(res);
console.log(typeof res);
}).catch(err=>{
console.log("post接口报错!")
console.log(err)
}).then(r=>{
console.log("post请求接口调用结束!");
});
}
}
2.2.2 组件请求接口
App.js:
import React, { Component } from 'react'
import Search from './Components/Search'
import Table from './Components/Table'
export default class App extends Component {
render() {
return (
<div>
<Search/>
<Table/>
</div>
)
}
}
Search:
index.jsx:
import React, { Component } from 'react'
import Axios from '../../api/axios'
export default class Search extends Component {
componentDidMount(){
Axios.get("http://localhost:8800/fruit/queryFruits")
// console.log(Axios)
}
render() {
return (
<div>index</div>
)
}
}
页面出现跨域错误:Access-Control-Allow-Origin
跨域是浏览器的行为,即浏览器不允许一个aaa.com,能够直接去访问bbb.com的资源,这样别人拿到你网站的资源后,可能会攻击你的网站等。而服务器之间的访问则没有跨域的说法,不同的服务器之间是可以访问的,之所以没有增加服务器间访问的限制,因为一般服务器间都有网关、鉴权等功能,别人也无法轻易访问到你的服务器内容。
如下可证明跨域是浏览器拦截的,即网站可发送请求,但是接受服务器请求时,被浏览器发现跨域,再次做了拦截。
如下服务器打印日志,说明是接收到了前端的请求,只是因为跨域,故而被浏览器拦截了。
注意:在修改跨域前,先引入react-ui组件库:antd
官方文档:https://ant.design/docs/react/introduce-cn
或antd官网镜像地址:https://ant-design.gitee.io/index-cn
npm i antd --save
2.2.3 解决跨域-脚手架配置代理
通过代理,浏览器将XXX:3000的请求,转发到代理服务器XXX:3000,由代理服务器发送请求到服务器端8800,最后返回给代理服务器,代理服务器通过XXX:3000再次转发给浏览器XXX:3000,即不会发生跨域问题。
找到脚手架下的package.json,在最后修改如下:
"proxy":"http://localhost:8800"
另外接口请求的路径,需要去掉前面的http://localhost:8800,修改Search的index.jsx如下:
import React, { Component } from 'react'
import Axios from '../../api/axios'
import {Form,Input,Button} from 'antd'
export default class Search extends Component {
componentDidMount(){
// Axios.get("http://localhost:8800/fruit/queryFruits")
// Axios.get("http://localhost:3000/fruit/queryFruits")
Axios.get("/fruit/queryFruits")
// console.log(Axios)
}
render() {
return (
<div>
<Form>
<Form.Item
label = "水果名称">
<Input/>
</Form.Item>
<Form.Item
label = "水果id">
<Input/>
</Form.Item>
<Form.Item
label = "供应商id">
<Input/>
</Form.Item>
<Form.Item className='flex_btn'>
<Button type = "primary" style={{"fontSize":"12px"}}>搜索</Button>
<Button style={{"fontSize":"12px"}}>重置</Button>
</Form.Item>
</Form>
</div>
)
}
}
或者如下也可:
Axios.get("http://localhost:3000/fruit/queryFruits")
修改了package.json,一定注意重启项目!再次执行npm start,此时接口请求,返回成功:
tips:
修改如下请求:
这样默认优先访问前端public下的资源文件:
如果前端资源不存在,再访问服务器资源,服务器也无资源,则直接报错:
2.2.4 解决跨域-脚手架配置代理二
如上解决跨域的方式,有一个弊端是如果前端请求的接口的ip+端口号来自不同的服务器,那么直接在package.json中配置是无法办到的(package.json中只能配置一个代理),于是更为通用的做法是在setupProxy.js中配置代理。
先注释package.json下的代理,在react的脚手架的src目录下,新建一个setupProxy.js:
setupProxy.js中,使用CommonJS语法(http-proxy-middleware在react脚手架中已经配置了,无需重复npm i http-proxy-middleware):
createProxyMiddleware是需要重写的接口路径(必须要有),然后下面的pathRewrite,再将/api1开头这个路径去掉,这样才能正确访问到后端的接口路径。
const {createProxyMiddleware} = require('http-proxy-middleware')
module.exports = function(app){
app.use(
createProxyMiddleware('/api1',{
target:"http://localhost:8800",
changeOrigin:true,
pathRewrite:{'^/api1':""}
})
)
}
这种代理方式配置好后,注意原先请求的路径要更改下,否则会报错404,接口路径找不到:
可以使用http://localhost:3000/api1/fruit/queryFruits,或者/api1/fruit/queryFruits(一定要带上重写的接口的前缀路径/api1,即上述createProxyMiddleware的第一个参数),app.use中配置的代理,只要接口请求前缀遇到/api1,就会触发相应的代理:
componentDidMount(){
// Axios.get("http://localhost:8800/fruit/queryFruits")
// Axios.get("http://localhost:3000/fruit/queryFruits")
Axios.get("http://localhost:3000/api1/fruit/queryFruits")
// Axios.get("/api1/fruit/queryFruits")
// console.log(Axios)
}
其中,changeOrigin的默认值为false,改为true后,可以控制服务器端收到的请求头的Host值。即为false时,服务器收到请求头的host是localhost:3000,但是改为true后,服务器收到的host就是localhost:8800了(即changeOrigin为true时,会模拟相同的服务器地址来请求后端接口,避免后端有请求来源限制)。
如果涉及到多个后端请求的ip+端口号路径,app.use中可以传多个createProxyMiddleware()参数,逗号隔开即可。
说明:
(1)优点:可以配置多个代理,灵活控制请求是否走代理(请求接口前缀增加/api1与否决定)
(2)缺点:配置繁琐,前端请求资源时必须加前缀
2.2.5 举个搜索的例子
文件目录如下:
api:
参考python的位置参数和关键字参数(args和kwargs),因为axios为异步请求api,直接在then中return结果,定义变量去接收axios请求返回数据会拿到undefined(异步请求的结果还没拿到,变量就已经赋值了),故而增加回调函数参数供axios封装方法使用,此时为了方便get请求等可以不传参,所以回调参数作为axios封装方法的第一个参数,而关键字参数如params有默认值,故而可留空。
axios.js:
import axios from "axios"
// 请求响应超时时间
axios.defaults.timeout=5000;
let {keys,values,entries} = Object;
// 封装get/post请求
export default{
get:(callBack,path="",params={})=>{
let request_url="";
request_url=path?request_url+path:request_url;
if(keys(params).length>0){
request_url+="?"
let count=0;
for(let k of keys(params)){
if(count!=keys(params).length-1){
request_url+=k+"="+params[k]+"&";
count+=1;
}else{
request_url+=k+"="+params[k];
}
}
}
axios.get(request_url).then(res=>{
console.log("get接口请求成功!");
console.log(res);
console.log(typeof res);
callBack(res.data)
}).catch(err=>{
console.log("get接口报错!")
console.log(err)
}).then(r=>{
console.log("get请求接口调用结束!");
});
},
post:(callBack,path="",data={})=>{
let request_url="";
request_url=path?request_url+path:request_url;
axios.post(request_url,data).then(res=>{
console.log("post接口请求成功!");
console.log(res);
console.log(typeof res);
callBack(res.data)
}).catch(err=>{
console.log("post接口报错!")
console.log(err)
}).then(r=>{
console.log("post请求接口调用结束!");
});
}
}
commonTool.js:
// 接口请求参数为小驼峰写法,在这里需要转换下
export function mapToRequestData(mapObj){
if(typeof(mapObj)=="undefined"){
throw Error("请传入非undefined对象")
}
var finalObj = {}
for(let i in mapObj){
if(typeof(mapObj[i])!=="undefined"){
const value = mapObj[i].replace(/\s/g,"")
const newKeyArr = i.split("_")
let objKey = "";
let newVal = ""
for(let k=0;k<newKeyArr.length;k++){
let oldVal = newKeyArr[k];
if(k>=1&&k<=newKeyArr.length-1){
newVal = oldVal.substring(0,1).toUpperCase()+oldVal.substring(1)
}else{
newVal = oldVal;
}
objKey+=newVal
}
finalObj[objKey] = value
}
}
return finalObj
}
Components:
Search(index.css为空):
index.jsx:
import React, { Component } from 'react'
import Axios from '../../api/axios'
import {Form,Input,Button} from 'antd'
import {mapToRequestData} from '../../api/commonTool'
export default class Search extends Component {
fRef = React.createRef()
componentDidMount(){
// Axios.get("http://localhost:8800/fruit/queryFruits")
// Axios.get("http://localhost:3000/fruit/queryFruits")
// Axios.get("/api1/fruit/queryFruits")
// console.log(Axios)
this.onFinish()
}
OnReSet = ()=>{
const form = this.fRef.current
form.resetFields();
// form.getFieldsValue()是不需要校验的,所以可以直接获取值
this.onFinish()
}
// form表单校验通过后的回调
onFinish = async()=>{
// console.log("组件校验成功")
const {searchToFather} = this.props
const form = this.fRef.current
const values = form.getFieldsValue(this.names.slice(1))
// console.log("我是值哈哈哈",values,typeof values)
// form.validateFields返回的是Promise对象
// const values2 = await form.validateFields(this.names.slice(1))
// console.log("我是校验后的值哈哈哈",values2) 在onfinish中,结果
// 和values一样
var params = mapToRequestData(values)
// console.log("我是请求的参数:",params)
const {changeLoad} = this.props
changeLoad(true)
Axios.get((data)=>{
// console.log("哈哈哈,我是回调",data)
const {success} = data
// const {resData:{fruitVoList:value}} = data
// 对象的解构赋值,上面fruitVoList:value,就是对fruitVoList
// 取别名,可以直接使用value,代表的变量就是fruitVoList
const {resData:{fruitVoList}} = data
// &&fruitVoList!==null
if(success===true&&typeof(fruitVoList)!=="undefined"
&&fruitVoList!==null){
// console.log("请求数据成功~",fruitVoList,typeof fruitVoList)
searchToFather(fruitVoList)
}else{
// 其余场景全部默认没查到数据,返回空
searchToFather([])
}
// 组件数据返回给父组件后,更新加载状态
changeLoad(false)
},"http://localhost:3000/api1/fruit/queryFruits",params)
}
names = ["form","name","fruit_id","sup_id"]
// 使用form的validateMessages,可作为全部限制字段的模板,
// 对应字段加上name,以及rules配置required:true即可
validateMessages = {
required: "这个#${label}#,即${name}是必选字段",
};
render() {
// console.log(this.fRef.current)
// console.log(this.fRef)
return (
<div>
<Form
ref = {this.fRef}
labelCol={{
span: 5,
}}
name = {this.names[0]}
validateMessages = {this.validateMessages}
scrollToFirstError
onFinish = {this.onFinish}
>
<Form.Item
label = "水果名称"
name = "name"
>
<Input/>
</Form.Item>
<Form.Item
label = "水果id"
name = "fruit_id"
rules={[
{
required: true,
message: '请输入水果id~',
},
]}>
<Input/>
</Form.Item>
<Form.Item
label = "供应商id"
name = "sup_id"
rules = {[{
required:true
}]}
>
<Input/>
</Form.Item>
<Form.Item className='flex_btn'>
<Button
type = "primary"
style={{"fontSize":"12px"}}
htmlType="submit"
>搜索</Button>
{/* 注意上面搜索按钮,必须加上 htmlType="submit",
否则校验文本不会生效,并且对应的Form.Item的
rules和name都要有,才能生效*/}
<Button
style={{"fontSize":"12px"}}
onClick = {this.OnReSet}
>重置</Button>
</Form.Item>
</Form>
</div>
)
}
}
Table(index.css为空):
index.jsx:
import React, { Component,Fragment } from 'react'
import { Divider,Table } from 'antd'
const tableColumns = [
{
title:"水果编号",
dataIndex:"fruitNumber"
},
{
title:"水果名称",
dataIndex:"fruitName"
},
{
title:"供应商编号",
dataIndex:"supplierId"
},
{
title:"库存",
dataIndex:"fruitStockCount"
},
{
title:"销量",
dataIndex:"fruitSaleCount"
},
{
title:"水果单价",
dataIndex:"unitPrice",
render:(data)=>{
return <>{data} / ¥</>
}
},
{
title:"水果划线价",
dataIndex:"crossOutPrice",
render:(data)=>{
return (<>
<div style = {{"textDecoration":"line-through",
"display":"inline-block",
"textDecorationColor":"red"}}>{data}</div> / ¥
</>);
}
},
{
title:"水果单位质量",
dataIndex:"unitWeight",
render:(data)=>{
return <>{data} / g</>
}
},
{
title:"上架时间",
dataIndex:"createTime"
},
]
const data = []
export default class FruitQueryTable extends Component {
constructor(props){
super(props)
this.fruitData = data
}
render() {
var {lists,isLoad} = this.props
this.fruitData = lists
// console.log("table的数据为:",this.fruitData)
return (
<div>
<Divider/>
<Table
columns={tableColumns}
loading = {isLoad}
locale = {{emptyText:'现在暂无数据哦~'}}
bordered
dataSource={this.fruitData}
>
</Table>
</div>
)
}
}
App.css:
#root .wrapper_input{
width:400px;
height: 266px;
margin: auto;
padding:20px;
border: 1px solid rgb(111, 197, 239);
border-radius: 10px;
}
#root .wrapper_input:hover{
box-shadow: 2px 1px 1px 1px rgb(111, 197, 239);
}
.ant-form-item .ant-btn{
margin-left: 10px;
}
.flex_btn .ant-form-item-control-input-content{
display: flex;
justify-content: flex-end;
}
/* table的样式 */
.wrapper_table{
width: 80%;
margin: auto;
margin-top: 10px;
border: 1px solid #ddd;
border-radius: 10px;
padding: 5px 15px 20px;
}
.wrapper_table .ant-table-wrapper{
border-radius:5px;
box-shadow: 1px 1px 1px 0 rgb(222, 219, 219);
}
App.js:
import React, { Component } from 'react'
import Search from './Components/Search'
import FruitQueryTable from './Components/Table'
import 'antd/dist/antd.css'
import './App.css'
export default class App extends Component {
state = {fruitLists:[],isLoading:false}
// 子组件search将查询出来的数据传递给父组件
searchToFather = (fruitLists)=>{
if(fruitLists instanceof Array&&typeof(fruitLists)!=="undefined"
&&fruitLists!==null){
// console.log("父组件开始啦啦啦")
fruitLists.map(item=>{
let keyVal = item["fruitNumber"]
item["key"] = keyVal
return item
})
this.setState({fruitLists})
this.changeLoding()
}
}
// changeLoding:当子组件Search查询时,通知子组件Table更新加载状态
changeLoding = (flag)=>{
this.setState({isLoading:flag})
}
render() {
return (
<div >
<div className = "wrapper_input">
<Search
searchToFather = {this.searchToFather}
changeLoad = {this.changeLoding}/>
</div>
<div className = "wrapper_table">
<FruitQueryTable
lists = {this.state.fruitLists}
isLoad = {this.state.isLoading}/>
</div>
</div>
)
}
}
效果如下:
默认初始化页面时,无条件时查询全部数据:
无输入时限制表单提交,且不调用接口请求:
输入必填参数查询:
水果名称为模糊匹配:
条件不匹配,查询无数据展示:
点击重置,清空查询条件,并且查询全部数据: