上一节(手把手撸一个wiki系统(2):前端vue整合)我们完成了前端部分vue-cli的搭建,这节我们完成前后端的交互。
集成HTTP库axios
Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。
特性
- 从浏览器中创建 XMLHttpRequests
- 从 node.js 创建 http 请求
- 支持 Promise API
- 拦截请求和响应
- 转换请求数据和响应数据
- 取消请求
- 自动转换 JSON 数据
- 客户端支持防御 XSRF
在web目录下安装:
yarn add axios
可以看到package.json和yarn.lock都发生了变化。(提交至commit 3-1)
跨域处理
后端端口现在是8081,前端是8080,前端发送的请求不被后端信任 No 'Access-Control-Allow-Origin' header is present on the requested resource.当一个请求 url 的协议、域名、端口三者之间任意一个与当前页面 url 不同即为跨域
在config包下新建CorsConfig.java,实现webMvcConfigurer类,重写addCorsMapping方法
/**
* 这里我们的CORSConfig配置类继承了WebMvcConfigurer父类并且重写了addCorsMappings方法,我们来简单介绍下我们的配置信息
* allowedOrigins:允许设置的请求域名访问我们的跨域资源,可以固定单条或者多条内容,如:"http://www.baidu.com",只有百度可以访问我们的跨域资源。
* addMapping:配置可以被跨域的路径,可以任意配置,可以具体到直接请求路径。
* allowedMethods:设置允许的请求方法类型访问该跨域资源服务器,如:POST、GET、PUT、OPTIONS、DELETE等。
* allowedHeaders:允许所有的请求header访问,可以自定义设置任意请求头信息,如:"X-YYYY-TOKEN"
* allowCredentials: 是否允许请求带有验证信息,用户是否可以发送、处理 cookie
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedHeaders(CorsConfiguration.ALL)
.allowedMethods(CorsConfiguration.ALL)
.allowCredentials(true)
.maxAge(3600); //一小时内不再预检(发送options请求看接口是否正常)
}
}
此处介绍了多种跨域的处理方式:https://www.cnblogs.com/sueyyyy/p/10129575.html
然后在Home.vue里用setup(相当于方法)来请求后端。
export default defineComponent({
name:'Home',
setup(){
console.log("log"),
axios.get("http://localhost:8081/ebook/search?name=y").then((response) => {
console.log(response);
})
}
})
前端电子书列表显示
将list列表组件选择一个样式拷过来,Home.vue如下:
<template>
<a-layout>
<a-layout-sider width="200" style="background: #fff">
<a-menu
mode="inline"
v-model:selectedKeys="selectedKeys2"
v-model:openKeys="openKeys"
:style="{ height: '100%', borderRight: 0 }"
>
<a-sub-menu key="sub1">
<template #title>
<span>
<user-outlined />
菜单 1
</span>
</template>
<a-menu-item key="1">option1</a-menu-item>
<a-menu-item key="2">option2</a-menu-item>
<a-menu-item key="3">option3</a-menu-item>
<a-menu-item key="4">option4</a-menu-item>
</a-sub-menu>
<a-sub-menu key="sub2">
<template #title>
<span>
<laptop-outlined />
菜单 2
</span>
</template>
<a-menu-item key="5">option5</a-menu-item>
<a-menu-item key="6">option6</a-menu-item>
<a-menu-item key="7">option7</a-menu-item>
<a-menu-item key="8">option8</a-menu-item>
</a-sub-menu>
<a-sub-menu key="sub3">
<template #title>
<span>
<notification-outlined />
菜单 3
</span>
</template>
<a-menu-item key="9">option9</a-menu-item>
<a-menu-item key="10">option10</a-menu-item>
<a-menu-item key="11">option11</a-menu-item>
<a-menu-item key="12">option12</a-menu-item>
</a-sub-menu>
</a-menu>
</a-layout-sider>
<a-layout-content
:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
>
<a-list item-layout="vertical" size="large" :pagination="pagination" :data-source="listData">
<template #footer>
<div>
<b>ant design vue</b>
footer part
</div>
</template>
<template #renderItem="{ item }">
<a-list-item key="item.title">
<template #actions>
<span v-for="{ type, text } in actions" :key="type">
<component v-bind:is="type" style="margin-right: 8px" />
{{ text }}
</span>
</template>
<template #extra>
<img
width="272"
alt="logo"
src="https://gw.alipayobjects.com/zos/rmsportal/mqaQswcyDLcXyDKnZfES.png"
/>
</template>
<a-list-item-meta :description="item.description">
<template #title>
<a :href="item.href">{{ item.title }}</a>
</template>
<template #avatar><a-avatar :src="item.avatar" /></template>
</a-list-item-meta>
{{ item.content }}
</a-list-item>
</template>
</a-list>
</a-layout-content>
</a-layout>
</template>
<script lang="ts">
import {defineComponent,reactive,toRef} from "vue";
import axios from "axios";
import { StarOutlined, LikeOutlined, MessageOutlined } from '@ant-design/icons-vue';
const listData: Record<string, string>[] = [];
for (let i = 0; i < 23; i++) {
listData.push({
href: 'https://www.antdv.com/',
title: `ant design vue part ${i}`,
avatar: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png',
description:
'Ant Design, a design language for background applications, is refined by Ant UED Team.',
content:
'We supply a series of design principles, practical patterns and high quality design resources (Sketch and Axure), to help people create their product prototypes beautifully and efficiently.',
});
}
export default defineComponent({
name:'Home',
components: {
StarOutlined,
LikeOutlined,
MessageOutlined,
},
setup(){
const ebooks = reactive({books:[]});
axios.get("http://localhost:8081/ebook/search?name=教程").then((response) => {
const data = response.data;
ebooks.books = data.content;
});
const pagination = {
onChange: (page: number) => {
console.log(page);
},
pageSize: 3,
};
const actions: Record<string, string>[] = [
{ type: 'StarOutlined', text: '156' },
{ type: 'LikeOutlined', text: '156' },
{ type: 'MessageOutlined', text: '2' },
];
return {
books: toRef(ebooks,"books"),
listData,
pagination,
actions,
}
},
})
</script>
然后进行改造:
-
datasource:数据来源,我们改成books
-
删掉template #footer
-
item是对books的循环,可改名可不改,通过 . 来获取内容
-
删除右边的大图template
-
字段修正为数据库对应的字段,例如title改为name
-
通过
:grid="{ gutter: 16, column: 4 }"
属性改为栅格列表,gutter是间距px,column是列数改为4 -
pageSize每页数目改为5
-
修改图标为方形,添加css样式,scoped表示仅在当前文件生效
改完后:
<template>
<a-layout>
<a-layout-sider width="200" style="background: #fff">
<a-menu
mode="inline"
v-model:selectedKeys="selectedKeys2"
v-model:openKeys="openKeys"
:style="{ height: '100%', borderRight: 0 }"
>
<a-sub-menu key="sub1">
<template #title>
<span>
<user-outlined />
菜单 1
</span>
</template>
<a-menu-item key="1">option1</a-menu-item>
<a-menu-item key="2">option2</a-menu-item>
<a-menu-item key="3">option3</a-menu-item>
<a-menu-item key="4">option4</a-menu-item>
</a-sub-menu>
<a-sub-menu key="sub2">
<template #title>
<span>
<laptop-outlined />
菜单 2
</span>
</template>
<a-menu-item key="5">option5</a-menu-item>
<a-menu-item key="6">option6</a-menu-item>
<a-menu-item key="7">option7</a-menu-item>
<a-menu-item key="8">option8</a-menu-item>
</a-sub-menu>
<a-sub-menu key="sub3">
<template #title>
<span>
<notification-outlined />
菜单 3
</span>
</template>
<a-menu-item key="9">option9</a-menu-item>
<a-menu-item key="10">option10</a-menu-item>
<a-menu-item key="11">option11</a-menu-item>
<a-menu-item key="12">option12</a-menu-item>
</a-sub-menu>
</a-menu>
</a-layout-sider>
<a-layout-content
:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
>
<a-list item-layout="vertical" size="large" :pagination="pagination" :data-source="books" :grid="{ gutter: 16, column: 4 }" >
<template #renderItem="{ item }">
<a-list-item key="item.name">
<template #actions>
<span v-for="{ type, text } in actions" :key="type">
<component v-bind:is="type" style="margin-right: 8px" />
{{ text }}
</span>
</template>
<a-list-item-meta :description="item.description">
<template #title>
<a :href="item.href">{{ item.name }}</a>
</template>
<template #avatar><a-avatar :src="item.avatar" /></template>
</a-list-item-meta>
{{ item.content }}
</a-list-item>
</template>
</a-list>
</a-layout-content>
</a-layout>
</template>
<script lang="ts">
import {defineComponent,reactive,toRef} from "vue";
import axios from "axios";
import { StarOutlined, LikeOutlined, MessageOutlined } from '@ant-design/icons-vue';
const listData: Record<string, string>[] = [];
export default defineComponent({
name:'Home',
components: {
StarOutlined,
LikeOutlined,
MessageOutlined,
},
setup(){
const ebooks = reactive({books:[]});
axios.get("http://localhost:8081/ebook/search?name=教程").then((response) => {
const data = response.data;
ebooks.books = data.content;
});
const pagination = {
onChange: (page: number) => {
console.log(page);
},
pageSize: 5,
};
const actions: Record<string, string>[] = [
{ type: 'StarOutlined', text: '156' },
{ type: 'LikeOutlined', text: '156' },
{ type: 'MessageOutlined', text: '2' },
];
return {
books: toRef(ebooks,"books"),
listData,
pagination,
actions,
}
},
})
</script>
<style scoped>
.ant-avatar {
width: 50px;
height: 50px;
line-height: 50px;
border-radius: 8%;
margin: 5px 0;
}
</style>
后端对查询的service进行判空处理:
public List<EbookResp> search(EbookReq req){
if (!ObjectUtils.isEmpty(req)) {
List<Ebook> ebookList = ebookMapper.selectByName("%" + req.getName() + "%");
//将List<Ebook>转换为List<EbookResp>
List<EbookResp> respList = CopyUtil.copyList(ebookList, EbookResp.class);
return respList;
}
return null;
}
具体见commit 3-3.
Vue cli多环境配置
开发环境和生产环境是不同的,需要对环境的参数进行抽离,单独管理。
web下新建两个文件,分别叫.env.dev和.env.prod。内容分别如下:
NODE_ENV = development
VUE_APP_SERVER = http://localhost:8081
NODE_ENV = production
VUE_APP_SERVER = 生产环境域名
VUE_APP_SERVER是对后端请求的地址
在package.json里修改启动命令:
"scripts": {
"serve-dev": "vue-cli-service serve --mode dev",
"serve-prod": "vue-cli-service serve --mode prod",
"build-dev": "vue-cli-service build --mode dev",
"build-prod": "vue-cli-service build --mode prod",
"lint": "vue-cli-service lint"
},
dev、prod是后缀,会自动读取。如果要指定VUE的启动端口,则在参数后加上 --port 端口号 即可:
"serve-dev": "vue-cli-service serve --mode dev --port 9999",
环境定义好了,那么如何在代码里使用呢?直接引用配置信息会比较麻烦,因为每次都要去写一次,我们可以直接定义整个前端项目的base_url即可。
先看看直接引用的方式:
使用process.env.
即可获取环境变量:
axios.get(process.env.VUE_APP_SERVER + "/ebook/search?name=教程").then((response) => {
const data = response.data;
ebooks.books = data.content;
});
如何统一配置呢?在main.ts里配置:
import axios from "axios";
axios.defaults.baseURL = process.env.VUE_APP_SERVER;
此时Home.vue里就不用写域地址了:
axios.get("/ebook/search?name=教程").then((response) => {
const data = response.data;
ebooks.books = data.content;
});
具体代码见commit 3-4.
使用axios拦截器打日志
手动console.log会比较麻烦,要指定打什么,而且还要写很多次,因此我们使用axios提供的拦截器来打日志,会很方便。
在main.ts里编写拦截器:
/**
* axios拦截器
*/
axios.interceptors.request.use(function (config) {
console.log('请求参数:', config);
return config;
}, error => {
return Promise.reject(error);
});
axios.interceptors.response.use(function (response) {
console.log('返回结果:', response);
return response;
}, error => {
return Promise.reject(error);
});
此时可以在浏览器控制台看到详细日志。
见commit 3-5
使用SpringBoot过滤器统计接口耗时
后端新建filter包,新建LogFilter类,内容如下:
@Component
public class LogFilter implements Filter {
private static final Logger LOG = LoggerFactory.getLogger(LogFilter.class);
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 打印请求信息
HttpServletRequest request = (HttpServletRequest) servletRequest;
LOG.info("------------- LogFilter 开始 -------------");
LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
LOG.info("远程地址: {}", request.getRemoteAddr());
//打印接口耗时
long startTime = System.currentTimeMillis(); //开始时间
filterChain.doFilter(servletRequest, servletResponse);
LOG.info("------------- LogFilter 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
}
}
然后访问接口,就能在控制台里看到耗时了。
见commit 3-6.
使用SpringBoot拦截器
新建interceptor层,新建LogInterceptor.java,内容如下:
/**
* 拦截器:Spring框架特有的,常用于登录校验,权限校验,请求日志打印 /login
*/
@Component
public class LogInterceptor implements HandlerInterceptor {
private static final Logger LOG = LoggerFactory.getLogger(LogInterceptor.class);
//先执行的方法
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 打印请求信息
LOG.info("------------- LogInterceptor 开始 -------------");
LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
LOG.info("远程地址: {}", request.getRemoteAddr());
long startTime = System.currentTimeMillis();
request.setAttribute("requestStartTime", startTime);
return true; //false时不会执行业务代码
}
//后执行的方法
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
long startTime = (Long) request.getAttribute("requestStartTime");
LOG.info("------------- LogInterceptor 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
}
}
Config层下新建SpringMvcConfig.java,内容如下:
/**
* 拦截器打印耗时
* Created by tangssst@qq.com on 2021/06/09
*/
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
@Resource
private LogInterceptor logInterceptor;
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(logInterceptor)
.addPathPatterns("/**").excludePathPatterns("/login");
}
}
针对除了/login的所有路径都执行拦截器。
过滤器的范围比拦截器大。
见commit 3-7.
使用AOP
pom.xml:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
新建aspect层,新建LogAspect.java
@Aspect
@Component
public class LogAspect {
private final static Logger LOG = LoggerFactory.getLogger(LogAspect.class);
/**
* 定义一个切点,作用于所有的controller的所有的.Controller的所有方法(所有参数)
*/
@Pointcut("execution(public * com.example.*.controller..*Controller.*(..))")
public void controllerPointcut() {
}
/**
* 前置通知before,业务代码之前执行,joinPoint是业务代码的参数
* @param joinPoint
* @throws Throwable
*/
@Before("controllerPointcut()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
// 开始打印请求日志
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
Signature signature = joinPoint.getSignature();
String name = signature.getName();
// 打印请求信息
LOG.info("------------- 开始 -------------");
LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
LOG.info("类名方法: {}.{}", signature.getDeclaringTypeName(), name);
LOG.info("远程地址: {}", request.getRemoteAddr());
// 打印请求参数
Object[] args = joinPoint.getArgs();
// LOG.info("请求参数: {}", JSONObject.toJSONString(args));
Object[] arguments = new Object[args.length];
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof ServletRequest
|| args[i] instanceof ServletResponse
|| args[i] instanceof MultipartFile) {
continue;
}
arguments[i] = args[i];
}
// 排除一些不该打印出来的敏感字段,或太长的字段不显示。此处排除password和file字段
String[] excludeProperties = {"password", "file"};
PropertyPreFilters filters = new PropertyPreFilters();
PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
excludefilter.addExcludes(excludeProperties);
LOG.info("请求参数: {}", JSONObject.toJSONString(arguments, excludefilter));
}
/**
* 环绕通知Around,在业务内容前面和后面执行
* @param proceedingJoinPoint
* @return
* @throws Throwable
*/
@Around("controllerPointcut()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
//前面
long startTime = System.currentTimeMillis();
//业务代码
Object result = proceedingJoinPoint.proceed();
//后面
// 排除字段,敏感字段或太长的字段不显示
String[] excludeProperties = {"password", "file"};
PropertyPreFilters filters = new PropertyPreFilters();
PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
excludefilter.addExcludes(excludeProperties);
LOG.info("返回结果: {}", JSONObject.toJSONString(result, excludefilter));
LOG.info("------------- 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
return result;
}
}
fastjson用于排除敏感、太长字段。
打日志的话过滤器、拦截器、aop用一个就行,这里用aop,把log过滤器、拦截器、SpringMvcConfig注释掉。
见comm 3-8.