创建Vue Cli项目

1. 安装Vue Cli

首先可以通过下面的两个命令来检查npm包下面的路径和设置路径,保证后续的安装和下载会更加快捷

  1. npm get registry
  2. npm config set registry http://registry.npm.taobao.org/

然后全局安装vue cli工具, 并且检查版本号:

  1. npm install -g @vue/cli@4.5.9
  2. vue --version

2. 创建web应用

然后我们要创建vue项目的时候,需要在之前的wiki项目目录里面去执行下面的命令:

  1. vue create web

然后我们选择Manually select features,手工选择我们需要的插件和工具(使用空格选中)

  • Choose Vue version,Babel,Typescript,Router,Vuex,然后点击回车
  • 选择3.x的vue版本
  • 不要选择class-style component风格的写法
  • 不要选择Babel alongside TypeScript
  • 选择history的模式
  • Eslint风格选择最简单的error prevention only即可
  • 选择Lint on save
  • 然后选择Babel和Eslint单独放在一个文件(in dedicated config files)
  • 最后选择是否将其保存成模板,你自己决定。

这里有两个可能会遇到的小问题,第一个是在IDEA的Terminal当中无法使用vue命令,可以在IDEA的setting-> Tools ->Terminal,在shell path可以自选选择cmd.exe还是powershell.exe

3.启动web应用

最后在IDEA当中右击package.json -> show npm scripts,将package.json当中的npm命令全部提到一个小窗口当中,以后直接双击启动,不需要手动输入命令。

集成Ant Design Vue

1. 集成Ant Design Vue

在前端项目的目录下输入下面的命令,来安装Ant Design Vue:

  1. npm install ant-design-vue@2.0.0-rc.3 --save

然后有两种集成方式,可以全部注入,也可以局部注入(按需加载),我们现在使用前者,就是将所有ant design vue的组件都加载,我们需要在main.ts当中这样书写:

  1. import { createApp } from 'vue'
  2. import App from './App.vue'
  3. import router from './router'
  4. import store from './store'
  5. import Antd from 'ant-design-vue'; // 1. 引入antd
  6. import 'ant-design-vue/dist/antd.css'; // 2. 加载css样式
  7. createApp(App)
  8. .use(store)
  9. .use(router)
  10. .use(Antd) // 3. vue.use(antd)即可
  11. .mount('#app')

2. 集成Ant Design icon

然后我们需要集成一下ant Design vue当中的图标库,我们到main.ts文件当中去集成一下:

  1. import * as Icons from '@ant-design/icons-vue'; // a-1.集成图标库
  2. // a-2. 全局使用图标
  3. const icons: any = Icons;
  4. for (const i in icons) {
  5. app.component(i, icons[i]);
  6. }

3. 示例展示

集成之后,就可以将直接ant design vue官网的示例代码粘贴过来进行使用。

  1. <template>
  2. <div class="home">
  3. <a-button type="text">Text Button</a-button>
  4. </div>
  5. </template>

网站首页布局开发

我们这里从App.vue出发,彻底改造一下项目,同时其中也使用了vue3的很多东西,都会讲到,下面是App.vue,代码都很简单,总的布局就是上中下,然后中间部分左边是二级菜单,右侧是内容显示。

  1. <template>
  2. <a-layout id="components-layout-demo-top-side-2">
  3. <the-header></the-header>
  4. <router-view/>
  5. <the-footer></the-footer>
  6. </a-layout>
  7. </template>
  8. <script lang="ts">
  9. import { defineComponent } from 'vue';
  10. import TheHeader from '@/components/the-header.vue';
  11. import TheFooter from '@/components/the-footer.vue';
  12. export default defineComponent({
  13. name: 'app',
  14. components: {
  15. TheHeader,
  16. TheFooter,
  17. },
  18. });
  19. </script>

集成HTTP库axios

1. 集成HTTP库axios

在前端项目当中使用下面的命令去下载axios:

  1. npm install axios@0.21.0 --save

2. 跨域的处理

我们在后端的config下面创建CorsConfig.java文件,内容如下:

  1. package com.taopoppy.wiki.config;
  2. import org.springframework.context.annotation.Configuration;
  3. import org.springframework.web.cors.CorsConfiguration;
  4. import org.springframework.web.servlet.config.annotation.CorsRegistry;
  5. import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
  6. @Configuration
  7. public class CorsConfig implements WebMvcConfigurer {
  8. @Override
  9. public void addCorsMappings(CorsRegistry registry) {
  10. registry.addMapping("/**") // 针对所有的请求地址
  11. .allowedOriginPatterns("*")
  12. .allowedHeaders(CorsConfiguration.ALL)
  13. .allowedMethods(CorsConfiguration.ALL)
  14. .allowCredentials(true) // 允许前端带上凭证信息,比如cookie
  15. .maxAge(3600); // 1小时内不需要再预检(发OPTIONS请求)
  16. }
  17. }

3. 完成电子书列表接口前后端交互

我们前端只需要正确的使用axios就可以实现前后端交互:

  1. <template>
  2. <pre>{{ebooks}} {{ebooks2}}</pre>
  3. </template>
  4. <script lang="ts">
  5. import { defineComponent, onMounted, reactive, ref, toRef} from 'vue'
  6. import axios from 'axios'
  7. export default defineComponent({
  8. name: "Home",
  9. setup() {
  10. const ebooks = ref()
  11. const ebooks1 = reactive({books: []})
  12. // 相当于vue2的mounted生命周期方法
  13. onMounted(()=> {
  14. axios.get("http://127.0.0.1:8081/ebook/list?name=Spring",).then((response) => {
  15. const data = response.data;
  16. ebooks.value = data.content // 使用ref的话,使用xxx.value的方式
  17. ebooks1.books = data.content
  18. });
  19. });
  20. return {
  21. ebooks,
  22. ebooks2: toRef(ebooks1,'books')
  23. }
  24. }
  25. })
  26. </script>
  27. <style scoped></style>

从上面的代码可以看出,我们使用了两种方法做vue3的数据绑定,可以看到reactive的使用比较麻烦,但是要记住的是ref的本质就是reactive,所以前面我们老说ref用在基础数据上,使用的时候必须是xxx.value才行,但实际上ref也可以将引用类型的数据进行数据绑定

4. 动态SQL

因为电子书列表的接口的逻辑是如果不传name就查所有的,所以我们需要在后端书写动态sql:

  1. // createCriteria相当于where条件
  2. EbookExample.Criteria criteria = ebookExample.createCriteria();
  3. // 动态sql的写法
  4. if(!ObjectUtils.isEmpty(req.getName())) {
  5. criteria.andNameLike("%" + req.getName() + "%");
  6. }

5. axios拦截器打印前端日志

我们需要在main.ts当中去书写axios的拦截器,这样我们不需要每次在实现接口的时候都要自己去书写打印日志,但是axios的拦截器功能并非只有打印,还有很多功能都可以在这里实现,比如判断token,已经根据返回状态进行路由跳转等等:

  1. /**
  2. * axios拦截器
  3. */
  4. axios.interceptors.request.use(function (config) {
  5. console.log('请求参数:', config);
  6. return config;
  7. }, error => {
  8. return Promise.reject(error);
  9. });
  10. axios.interceptors.response.use(function (response) {
  11. console.log('返回结果:', response);
  12. return response;
  13. }, error => {
  14. console.log('返回错误:', error);
  15. return Promise.reject(error);
  16. });

代码写到这里实际就可以了,但是我们还是把将来要用到的功能展示出来,暂时帮助大家更深刻的理解axios拦截器的功能:

  1. /**
  2. * axios拦截器
  3. */
  4. axios.interceptors.request.use(function (config) {
  5. console.log('请求参数:', config);
  6. const token = store.state.user.token;
  7. if (Tool.isNotEmpty(token)) {
  8. config.headers.token = token;
  9. console.log("请求headers增加token:", token);
  10. }
  11. return config;
  12. }, error => {
  13. return Promise.reject(error);
  14. });
  15. axios.interceptors.response.use(function (response) {
  16. console.log('返回结果:', response);
  17. return response;
  18. }, error => {
  19. console.log('返回错误:', error);
  20. const response = error.response;
  21. const status = response.status;
  22. if (status === 401) {
  23. // 判断状态码是401 跳转到首页或登录页
  24. console.log("未登录,跳到首页");
  25. store.commit("setUser", {});
  26. message.error("未登录或登录超时");
  27. router.push('/');
  28. }
  29. return Promise.reject(error);
  30. });

Vue CLI多环境配置

1.增加开发和生产配置文件

因为我们之前在代码当中书写死了请求的地址是127.0.0.1:8880,如果是生产环境就不对了,我们在项目当中创建.env.dev和.env.prod文件,将测试和生产环境的不同配置书写在当中,注意:自定义属性需要使用VUEAPP做开头:

NODE_ENV=development VUE_APP_SERVER=http://127.0.0.1:8880 VUE_APP_WS_SERVER=ws://127.0.0.1:8880

下面是生产环境的,这里我们先随便写,后面生产环境上线还要回来再修改:

NODE_ENV=production VUE_APP_SERVER=http://wiki-server.courseimooc.com VUE_APP_WS_SERVER=ws://wiki-server.courseimooc.com

2. 修改编译和启动支持多环境

我们来修改一下package.json当中的启动和编译的命令:

  1. "scripts": {
  2. "serve": "vue-cli-service serve",
  3. "serve-dev": "vue-cli-service serve --mode dev --port 8080",
  4. "serve-prod": "vue-cli-service serve --mode prod",
  5. "build-dev": "vue-cli-service build --mode dev",
  6. "build-prod": "vue-cli-service build --mode prod",
  7. "lint": "vue-cli-service lint"
  8. },

同时我们在main.ts最后写上两句话:

  1. console.log('环境:', process.env.NODE_ENV);
  2. console.log('服务端:', process.env.VUE_APP_SERVER);

这样的话就配置成功了,这样的逻辑是什么,我们来讲一下:如果使用npm run serve-dev命令的话,—mode dev就说明在开发环境启动,那么代码当中通过process.env.xxx调用的参数就是来自于.env.dev文件中定义的xxx,同样使用serve-prod命令启动的话,代码当中通过process.env.xxx调用的参数就是来自于.env.prod文件中定义的xxx。

同时修改启动端口,也可以通过—port 8081这种方式,注意一下即可。

3. 修改axios请求地址支持多环境

我们只需要在main.ts文件当中增加这样的代码:

  1. import axios from 'axios'; // b-1 引入axios
  2. axios.defaults.baseURL = process.env.VUE_APP_SERVER; // b-2 axios配置统一基地址

这样的话,我们请求的时候,地址就可以只书写在各种环境都统一的部分,而基地址不同的部分都在.env.prod和.env.dev文件中进行了配置:

  1. axios.get("/ebook/list",).then((response) => {
  2. const data = response.data;
  3. ebooks.value = data.content
  4. });
  5. axios.get("/ebook/list?name=Spring",).then((response) => {
  6. const data = response.data;
  7. ebooks1.books = data.content
  8. });

SpringBoot过滤器的使用

1. 过滤器打印接口耗时

我们只需要在com.taopoppy.wiki下面新建一个包filter,然后在此包下面新建LogFilter.java,内容如下,可以直接复制:

  1. package com.taopoppy.wiki.filter;
  2. import org.slf4j.Logger;
  3. import org.slf4j.LoggerFactory;
  4. import org.springframework.stereotype.Component;
  5. import javax.servlet.*;
  6. import javax.servlet.http.HttpServletRequest;
  7. import java.io.IOException;
  8. @Component
  9. public class LogFilter implements Filter {
  10. private static final Logger LOG = LoggerFactory.getLogger(LogFilter.class);
  11. @Override
  12. public void init(FilterConfig filterConfig) throws ServletException {
  13. }
  14. @Override
  15. public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
  16. // 打印请求信息
  17. HttpServletRequest request = (HttpServletRequest) servletRequest;
  18. LOG.info("------------- LogFilter 开始 -------------");
  19. // request.getRequestURL().toString() 就是地址,request.getMethod()就是请求方法
  20. LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
  21. // request.getRemoteAddr()就是请求该接口的IP地址
  22. LOG.info("远程地址: {}", request.getRemoteAddr());
  23. long startTime = System.currentTimeMillis(); // 开始计时
  24. filterChain.doFilter(servletRequest, servletResponse); // 继续往后执行
  25. // 打印接口的耗时
  26. LOG.info("------------- LogFilter 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
  27. }
  28. }

这个过滤器实现了Filter这个接口,它是属于servlet这个包的,过滤器是sevlet当中的一个概念,而servlet又是容器当中的一个概念,过滤器是给容器使用的。

所谓的容器,其实就是我们经常听到的tomat, netty 这些都叫做容器,所以我们书写的过滤器是给netty或者tomcat使用的。因为它是一个servlet,servlet其实就是我们的请求,可以理解为就是我们的请求接口。请求进来后,拿到请求的一系列信息,然后开始计时,继续往后执行代码或者后面还有的过滤器,最后计时结束,打印接口的耗时。其中@Component是注解过滤器的,SpringBoot会去扫描,容器就会拿到这个过滤器。

SpringBoot拦截器的使用

1. 拦截器打印接口耗时

我们只需要在com.taopoppy.wiki下面新建一个包interceptor,然后在此包下面新建LogInterceptor.java,内容如下,可以直接复制:

  1. package com.taopoppy.wiki.interceptor;
  2. import org.slf4j.Logger;
  3. import org.slf4j.LoggerFactory;
  4. import org.springframework.stereotype.Component;
  5. import org.springframework.web.servlet.HandlerInterceptor;
  6. import org.springframework.web.servlet.ModelAndView;
  7. import javax.servlet.http.HttpServletRequest;
  8. import javax.servlet.http.HttpServletResponse;
  9. /**
  10. * 拦截器:Spring框架特有的,常用于登录校验,权限校验,请求日志打印 /login
  11. */
  12. @Component
  13. public class LogInterceptor implements HandlerInterceptor {
  14. private static final Logger LOG = LoggerFactory.getLogger(LogInterceptor.class);
  15. @Override
  16. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  17. // 打印请求信息
  18. LOG.info("------------- LogInterceptor 开始 -------------");
  19. LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
  20. LOG.info("远程地址: {}", request.getRemoteAddr());
  21. long startTime = System.currentTimeMillis();
  22. request.setAttribute("requestStartTime", startTime); // 将requestStartTime参数加入到request中
  23. return true; // 如果返回false,拦截器和业务都结束了
  24. }
  25. @Override
  26. public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
  27. long startTime = (Long) request.getAttribute("requestStartTime"); // 业务结束后再拿到一开始定义的开始时间,然后计算耗时
  28. LOG.info("------------- LogInterceptor 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
  29. }
  30. }

拦截器和过滤器有一点区别,就是它前后是分成两个方法的,如上代码,前面方法是preHandle,后面方法是postHandle,而过滤器是一起,中间使用链去调用业务方法,而拦截器调用业务方法不需要你自己去写

然后我们需要书写这样一个配置,让SpringBoot去扫描到拦截器,我们在config下面创建SpringMvcConfig.java,内容如下:

  1. package com.taopoppy.wiki.config;
  2. import com.taopoppy.wiki.interceptor.LogInterceptor;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
  5. import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
  6. import javax.annotation.Resource;
  7. @Configuration
  8. public class SpringMvcConfig implements WebMvcConfigurer {
  9. @Resource
  10. LogInterceptor logInterceptor;
  11. public void addInterceptors(InterceptorRegistry registry) {
  12. registry.addInterceptor(logInterceptor) // 将拦截器进行注册
  13. .addPathPatterns("/**"); // 配置拦截器生效的接口
  14. }
  15. }

上面就是拦截器生效的配置,因为会有很多不同类型的拦截器,所以我们都需要书写后在这里进行注册,并且配置针对那些接口进行拦截,比如配置addPathPatterns(“/**”)就是对所有接口都拦截,那么后续还有一些需要必须登录的接口或者权限校验的接口就会在这里配置其他的拦截器,并对一些特殊的接口进行拦截。

然后我们测试一下,看看拦截器和过滤器的范围:
1644649169(1).png
很明显,过滤器的范围是要大于拦截器的,因为过滤器是在容器里面的,即在tomcat当中的,所以接口一进来先进入容器,容器会发到SpringBoot应用,拦截器是应用内的,应用内的拦截器就拿到请求了,与此同时,过滤器一共是一个方法,而拦截器是有两个方法(preHandle和postHandle),所以拦截器和过滤器是这样一个关系:
1644649632(1).png

SpringBootAOP的使用

  • AOP(Aspect Oriented Programming,面向切面编程),是对OOP(Object Oriented Programming,面向对象编程)的一种补充
  • OOP的关键是类(Class),侧重于对象的封装、继承;AOP的关键是切面(Aspect),侧重于对业务代码中某一公共行为的提取;
  • AOP可以在不修改业务代码的情况下,在连接点(Join point)向现有代码中添加一些自定义行(Advice);
  • SpringBoot中AOP的底层原理是动态代理技术,AOP的主要应用场景是事务管理、日志记录、用户权限校验等;

    1. AOP打印请求和返回参数

    使用AOP呢,我们首先要去下载依赖,在pom.xml当中添加下面的依赖,前者aop是spring内置的模块,后面fastjson不是必须的,只不过我们这里用到了就顺便一起下载了。
    1. <dependency>
    2. <groupId>org.springframework.boot</groupId>
    3. <artifactId>spring-boot-starter-aop</artifactId>
    4. </dependency>
    5. <dependency>
    6. <groupId>com.alibaba</groupId>
    7. <artifactId>fastjson</artifactId>
    8. <version>1.2.70</version>
    9. </dependency>
    我们在com.taopoppy.wiki下面创建一个包aspect,此包下面创建LogAspect,内容如下: ```java package com.taopoppy.wiki.aspect;

import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.support.spring.PropertyPreFilters; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest;

@Aspect @Component public class LogAspect {

  1. private final static Logger LOG = LoggerFactory.getLogger(LogAspect.class);
  2. /** 定义一个切点 */
  3. @Pointcut("execution(public * com.taopoppy.*.controller..*Controller.*(..))")
  4. public void controllerPointcut() {}
  5. /** Before是前置通知 */
  6. @Before("controllerPointcut()")
  7. public void doBefore(JoinPoint joinPoint) throws Throwable {
  8. // 开始打印请求日志
  9. ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
  10. HttpServletRequest request = attributes.getRequest();
  11. Signature signature = joinPoint.getSignature();
  12. String name = signature.getName();
  13. // 打印请求信息
  14. LOG.info("------------- 开始 -------------");
  15. LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
  16. LOG.info("类名方法: {}.{}", signature.getDeclaringTypeName(), name);
  17. LOG.info("远程地址: {}", request.getRemoteAddr());
  18. // 打印请求参数(拿到所有请求字段)
  19. Object[] args = joinPoint.getArgs();
  20. // LOG.info("请求参数: {}", JSONObject.toJSONString(args));
  21. Object[] arguments = new Object[args.length];
  22. for (int i = 0; i < args.length; i++) {
  23. if (args[i] instanceof ServletRequest
  24. || args[i] instanceof ServletResponse
  25. || args[i] instanceof MultipartFile) {
  26. continue;
  27. }
  28. arguments[i] = args[i];
  29. }
  30. // 排除字段,敏感字段或太长的字段不显示
  31. String[] excludeProperties = {"password", "file"};
  32. PropertyPreFilters filters = new PropertyPreFilters();
  33. PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
  34. excludefilter.addExcludes(excludeProperties);
  35. LOG.info("请求参数: {}", JSONObject.toJSONString(arguments, excludefilter));
  36. }
  37. /** Around是环绕通知 */
  38. @Around("controllerPointcut()")
  39. public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
  40. long startTime = System.currentTimeMillis();
  41. Object result = proceedingJoinPoint.proceed(); // 执行业务内容
  42. // 排除字段,敏感字段或太长的字段不显示
  43. String[] excludeProperties = {"password", "file"};
  44. PropertyPreFilters filters = new PropertyPreFilters();
  45. PropertyPreFilters.MySimplePropertyPreFilter excludefilter = filters.addFilter();
  46. excludefilter.addExcludes(excludeProperties);
  47. LOG.info("返回结果: {}", JSONObject.toJSONString(result, excludefilter));
  48. LOG.info("------------- 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
  49. return result;
  50. }

} ``` 当然,上面的代码也可以直接复制粘贴,然后启动项目会发现,先执行的过滤器,再执行拦截器,再执行aop,现在我们简单分析一下上面的aop代码:

  • @Aspect这个注解是固定的,表明这是个aop,@Component注解是最基本的注解,就是表示你要把这个类交给spring来管理,比如像@RestController和@Service在源码当中都是有@Component的注解的。
  • 代码当中定义的切点,是针对所有的Controller当中所有的方法,所有的参数,都会进入AOP当中
  • @Before(“controllerPointcut()”)就是一个前置通知,前置通知的意思就是:执行业务代码之前,我们要去做的事情就放在这个前置通知里面去。
  • AOP和过滤器和拦截器有一点不同,就是他拿参数是通过连接点JoinPoint来拿的,所以从43-61都是循环拿到请求参数的代码。
  • 从63到68是过滤敏感字段,有些敏感字段,比如身份证,密码等都是不能打印的,需要把这些敏感字段排除掉,还有太长的字段也不需要显示。
  • @Around(“controllerPointcut()”)是一个环绕通知,什么是环绕通知呢,就是在业务内容前面执行一点东西,后面再执行一点东西,就是环绕通知。如上代码所示,proceedingJoinPoint.proceed()就是执行业务代码,那么proceedingJoinPoint.proceed()前面的代码就是要在业务内容前面执行的,proceedingJoinPoint.proceed()后面的代码就是要在业务内容后面执行的。由此可见,前置通知的代码也能放在环绕通知的proceedingJoinPoint.proceed()前面

所以拦截器,过滤器,AOP针对打印这个功能,我们三选一即可,我们就选择AOP即可,拦截器和过滤器中的打印接口耗时的代码全部注释。但是有些功能需要放在拦截器或者过滤器当中。