当前位置:   article > 正文

手把手教你前后分离架构(四) 前后端数据交互_前后端分离怎么实现交互

前后端分离怎么实现交互

前面的章节,系统雏形已经初步形成,前端项目的展示数据为固定数据活mock数据,今天我们来一起完善前后端项目数据交互。

1、后台统一接口

日常工作中,我们开发接口时,一般都会涉及到参数校验、异常处理、封装结果返回等处理。如果每个后端开发人员在参数校验、异常处理等都是各写各的,没有统一处理的话,代码即不优雅,也不容易维护。前端也很难对数据统一操作。所以,作为一名合格的后端开发工程师,我们需要统一校验参数,统一异常处理、统一结果返回,让代码更加规范、可读性更强、更容易维护。

1.1、集成swagger

后端服务的API接口可以查看文档和调试,通过swagger可减少与前端人员沟通成本,也可帮助后端人员了解后端API详情。

添加pom依赖

  1. <dependency>
  2. <groupId>io.springfox</groupId>
  3. <artifactId>springfox-swagger2</artifactId>
  4. <version>2.7.0</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>io.springfox</groupId>
  8. <artifactId>springfox-swagger-ui</artifactId>
  9. <version>2.7.0</version>
  10. </dependency>
  11. <dependency>
  12. <groupId>com.github.xiaoymin</groupId>
  13. <artifactId>swagger-bootstrap-ui</artifactId>
  14. <version>1.9.1</version>
  15. </dependency>

添加启动类

  1. @Component
  2. @Configuration
  3. @EnableSwagger2
  4. public class Swagger2Config extends WebMvcConfigurationSupport {
  5. @Bean
  6. public Docket createRestApi(){
  7. ParameterBuilder tokenPar = new ParameterBuilder();
  8. List<Parameter> pars = new ArrayList<>();
  9. tokenPar.name(Constant.token).description("令牌").
  10. modelRef(new ModelRef("string")).
  11. parameterType("header").required(false).build();
  12. pars.add(tokenPar.build());
  13. return new Docket(DocumentationType.SWAGGER_2)
  14. .apiInfo(apiInfo())
  15. .select()
  16. .apis(RequestHandlerSelectors.basePackage("com.xgg"))
  17. .paths(PathSelectors.any())
  18. .build().globalOperationParameters(pars);
  19. }
  20. private ApiInfo apiInfo(){
  21. return new ApiInfoBuilder().build();
  22. }
  23. @Override
  24. protected void addResourceHandlers(ResourceHandlerRegistry registry) {
  25. registry.addResourceHandler("swagger-ui.html")
  26. .addResourceLocations("classpath:/META-INF/resources/");
  27. registry.addResourceHandler("doc.html").
  28. addResourceLocations("classpath:/META-INF/resources/");
  29. registry.addResourceHandler("/webjars/**")
  30. .addResourceLocations("classpath:/META-INF/resources/webjars/");
  31. //将所有/static/** 访问都映射到classpath:/static/ 目录下
  32. registry.addResourceHandler("/static/**")
  33. .addResourceLocations(ResourceUtils.CLASSPATH_URL_PREFIX +"/static/");
  34. super.addResourceHandlers(registry);
  35. }
  36. }

1.2、统一返回格式

success 接口返回是否成功状态;code返回的状态码;message状态码说明,info返回数据对象。

  1. {
  2. "success": true, //是否成功 true 成功, false 失败
  3. "code": 200, //返回状态码
  4. "message": "成功", //返回状态说明
  5. "info": "" //返回数据对象
  6. }

返回实体定义

  1. @Slf4j
  2. @ApiModel(value = "统一返回结果")
  3. public class Result<T> {
  4. @ApiModelProperty("是否成功")
  5. private boolean success;
  6. @ApiModelProperty("返回码")
  7. private Integer code;
  8. @ApiModelProperty("返回码说明")
  9. private String message;
  10. @ApiModelProperty("返回对象数据")
  11. private T info;
  12. /**
  13. * 成功
  14. */
  15. public static Result ok() {
  16. Result r = new Result();
  17. r.setSuccess(true);
  18. r.setCode(ResultEnum.OK.getCode());
  19. r.setMessage(ResultEnum.OK.getName());
  20. return r;
  21. }
  22. /**
  23. * 错误
  24. */
  25. public static Result error() {
  26. Result r = new Result();
  27. r.setSuccess(false);
  28. r.setCode(ResultEnum.ERROR.getCode());
  29. r.setMessage(ResultEnum.ERROR.getName());
  30. return r;
  31. }
  32. /**
  33. * 无权限
  34. */
  35. public static Result noAccess() {
  36. Result r = new Result();
  37. r.setSuccess(false);
  38. r.setCode(ResultEnum.SIGNATURE_NOT_MATCH.getCode());
  39. r.setMessage(ResultEnum.SIGNATURE_NOT_MATCH.getName());
  40. return r;
  41. }
  42. public boolean isSuccess() {
  43. return success;
  44. }
  45. public void setSuccess(boolean success) {
  46. this.success = success;
  47. }
  48. public Integer getCode() {
  49. return code;
  50. }
  51. public void setCode(Integer code) {
  52. this.code = code;
  53. }
  54. public String getMessage() {
  55. return message;
  56. }
  57. public void setMessage(String message) {
  58. this.message = message;
  59. }
  60. public T getInfo() {
  61. return info;
  62. }
  63. public void setInfo(T info) {
  64. this.info = info;
  65. }
  66. public Result code(Integer value) {
  67. this.setCode(value);
  68. return this;
  69. }
  70. public Result message(String value) {
  71. this.setMessage(value);
  72. return this;
  73. }
  74. public Result info(T value) {
  75. this.setInfo(value);
  76. return this;
  77. }
  78. }

1.3、统一参数校验

添加依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-validation</artifactId>
  4. </dependency

添加参数实体类

  1. @Data
  2. public class TestParam {
  3. @NotBlank(message = "姓名不能为空")
  4. @ApiModelProperty(value = "姓名",required = true)
  5. private String username;
  6. @NotNull(message = "年龄不能为空")
  7. @ApiModelProperty(value = "年龄",required = true)
  8. private Long age;
  9. @ApiModelProperty(value = "毕业学校")
  10. private String school;
  11. }

@NotNull适用于基本数据类型(Integer,Long,Double等等),当 @NotNull 注解被使用在 String 类型的数据上,则表示该数据不能为 Null(但是可以为 Empty)

@NotBlank适用于 String 类型的数据上,加了@NotBlank 注解的参数不能为 Null 且 trim() 之后 size > 0

@NotEmpty适用于 String、Collection集合、Map、数组等等,加了@NotEmpty 注解的参数不能为 Null 或者 长度为 0

Spring Validation验证框架对参数的验证机制提供了@Validated(Spring's JSR-303规范,是标准JSR-303的一个变种),javax提供了@Valid(标准JSR-303规范),配合BindingResult可以直接提供参数验证结果。

@Valid属于javax.validation包下,是jdk给提供的 是使用Hibernate validation的时候使用

@Validated是org.springframework.validation.annotation包下的,是spring提供的 是只用Spring Validator校验机制使用

说明:java的JSR303声明了@Valid这类接口,而Hibernate-validator对其进行了实现
@Validation对@Valid进行了二次封装,在使用上并没有区别,但在分组、注解位置、嵌套验证等功能上有所不同,这里主要就这几种情况进行说明。
在检验Controller的入参是否符合规范时,使用@Validated或者@Valid在基本验证功能上没有太多区别。但是在分组、注解地方、嵌套验证等功能上两个有所不同:
分组
@Validated:提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制,这个网上也有资料,不详述。@Valid:作为标准JSR-303规范,还没有吸收分组的功能。
注解位置
@Validated:用在类型、方法和方法参数上。但不能用于成员属性(field)
@Valid:可以用在方法、构造函数、方法参数和成员属性(field)上 所以可以用@Valid实现嵌套验证。

1.4、统一异常

前面咱们已经实现了前后交互的数据规范和基本流程,但是如果后端出现异常前端如何应对呢。

测试发现返回的数据和咱们制定的数据规范不一致,前端只能通过catch来捕获异常,这显然不利于前端同学研发的便利性。

 所以我们需要再后端建立统一异常规范,全局捕获已知和未知异常,按照我们之前制定的数据交互规范来统一返回数据。

添加全局异常捕获

  1. @Slf4j
  2. @RestControllerAdvice
  3. public class ExceptionHandlerAdvice {
  4. @ExceptionHandler({HttpMessageNotReadableException.class, ConstraintViolationException.class,MissingServletRequestParameterException.class})
  5. public Result messageExceptionHandler(Exception ex) {
  6. log.error(ResultEnum.BODY_NOT_MATCH.getName(), ex);
  7. return Result.error().code(ResultEnum.BODY_NOT_MATCH.getCode()).message(ResultEnum.BODY_NOT_MATCH.getName());
  8. }
  9. @ExceptionHandler(value = {MethodArgumentNotValidException.class, BindException.class})
  10. public Result controllerException(Exception e, BindingResult bindingResult) {
  11. List<FieldError> listErrors = bindingResult.getFieldErrors();
  12. if (!listErrors.isEmpty()) {
  13. FieldError fieldError = listErrors.get(0);
  14. return Result.error().code(ResultEnum.BODY_NOT_MATCH.getCode()).message(ResultEnum.BODY_NOT_MATCH.getName()).info(fieldError.getField()+""+fieldError.getDefaultMessage());
  15. }
  16. return Result.error().code(ResultEnum.BODY_NOT_MATCH.getCode()).message(ResultEnum.BODY_NOT_MATCH.getName());
  17. }
  18. @ExceptionHandler({HttpRequestMethodNotSupportedException.class, MethodArgumentTypeMismatchException.class})
  19. public Result requestExceptionHandler(Exception ex ) {
  20. log.error(ResultEnum.BODY_NOT_MATCH.getName(), ex);
  21. return Result.error().code(ResultEnum.BODY_NOT_MATCH.getCode()).message(ResultEnum.BODY_NOT_MATCH.getName());
  22. }
  23. /**
  24. * 自定义异常
  25. */
  26. @ExceptionHandler({ BizException.class })
  27. public Result bizException(BizException e) {
  28. log.error("自定义系统异常", e);
  29. return Result.error().message(e.getMsg()).code(e.getCode());
  30. }
  31. /**
  32. * 全局异常
  33. */
  34. @ExceptionHandler({ Exception.class })
  35. // @ResponseStatus(HttpStatus.BAD_REQUEST)
  36. public Result sysException(Exception e) {
  37. log.error("系统异常", e);
  38. return Result.error();
  39. }

添加自定义异常

  1. @Data
  2. public class BizException extends RuntimeException {
  3. private Integer code;
  4. private String msg;
  5. public BizException(Integer code, String msg) {
  6. this.msg = msg;
  7. this.code = code;
  8. }
  9. public BizException(String msg) {
  10. this.msg = msg;
  11. this.code = ResultEnum.ERROR.getCode();
  12. }
  13. public BizException(String msg, Throwable t) {
  14. super(t);
  15. this.msg = msg;
  16. this.code = ResultEnum.ERROR.getCode();
  17. }
  18. }

使用自定义异常

throw new SqException(201,"测试自定义异常");

1.5、验证测试

  1. @ApiOperation(value = "测试查询")
  2. @GetMapping("/test")
  3. public Result test(@Valid TestParam testParam, BindingResult result) throws Exception{
  4. List<FieldError> fieldErrors = result.getFieldErrors();
  5. if (!fieldErrors.isEmpty()) {
  6. return Result.error().info(fieldErrors.get(0).getDefaultMessage());
  7. }
  8. return Result.ok().info(testParam);
  9. }
  10. @ApiOperation(value = "测试接口1")
  11. @PostMapping("/test1")
  12. public Result test1(@Valid TestParam testParam) throws Exception{
  13. throw new BizException(201,"测试自定义异常");
  14. testService.test("哈哈哈哈哈哈哈");
  15. return Result.ok().info(testParam);
  16. }

1.6、接口规范

前后端交互接口遵循统一RESTful接口原则,构建优良的 REST API

避免在 URI 中使用动词

HTTP Method动词与 URI 的组合,比如 GET: / user/。一个端点可以被解释为对某种资源进行的某个动作。比如, POST: /user 代表“创建一个新的 user”而不是saveUser;查询用户:GET: /user而不是getUser。
HTTP Method: GET 代表查,POST代表增,PUT代表改, DELETE 代表删。

2、前后端交互

2.1前端整合axios

axios时目前最流行的ajax封装库之一,用于很方便地实现ajax请求的发送。

添加依赖

npm install axios

使用axios

我们使用之前的test.vue页面和SpringBoot后台的测试接口做测试。

  1. axios
  2. .get('http://127.0.0.1:8888/test', {
  3. params: {
  4. username: 12345,
  5. age: 20
  6. }
  7. }).then(({data}) => {
  8. if(data && data.success){
  9. this.$message.success(data.message)
  10. }else{
  11. this.$message.error(data.info)
  12. }
  13. }).catch(error => { // 请求失败处理
  14. this.$message.error("系统异常,请稍后重试!");
  15. });
  16. }

点击测试按钮会发现浏览器控制台报错了

这是跨域问题引起的,因为我们浏览器访问的是9081端口,而访问后台的端口是8888端口,而且部署到服务器以后可能不光两者端口不同,连ip可能都会不同了。这就是跨域问题。

2.2 、SpringBoot跨域支持

后端跨域有很多种方式,咱们这里采用过滤器的方式。

  1. @Configuration
  2. public class CorsConfig {
  3. @Bean
  4. public CorsFilter corsFilter() {
  5. CorsConfiguration corsConfiguration = new CorsConfiguration();
  6. //1,允许任何来源
  7. corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*"));
  8. //2,允许任何请求头
  9. corsConfiguration.addAllowedHeader(CorsConfiguration.ALL);
  10. //3,允许任何方法
  11. corsConfiguration.addAllowedMethod(CorsConfiguration.ALL);
  12. //4,允许凭证
  13. corsConfiguration.setAllowCredentials(true);
  14. UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
  15. source.registerCorsConfiguration("/**", corsConfiguration);
  16. return new CorsFilter(source);
  17. }
  18. }

重启后端服务,测试点击按钮发现成功返回后端数据。

2.3、表单验证实践

  1. <template>
  2. <div>
  3. <el-form :inline="true" :model="formInline" :rules="rules" ref="formInline" class="demo-form-inline">
  4. <el-form-item label="姓名" prop="username">
  5. <el-input v-model="formInline.username" placeholder="姓名"></el-input>
  6. </el-form-item>
  7. <el-form-item label="年龄" prop="age">
  8. <el-input v-model="formInline.age" placeholder="年龄"></el-input>
  9. </el-form-item>
  10. <el-form-item>
  11. <el-button type="primary" @click="onSubmit('formInline')">查询</el-button>
  12. </el-form-item>
  13. <el-form-item>
  14. <el-button type="primary" @click="test" icon="el-icon-search">测试</el-button>
  15. </el-form-item>
  16. </el-form>
  17. </div>
  18. </template>
  19. <script>
  20. import axios from 'axios'
  21. export default {
  22. name: "test1",
  23. data() {
  24. return {
  25. formInline: {
  26. username: '',
  27. age: ''
  28. },
  29. rules: {
  30. username: [
  31. {required: true, message: '请姓名', trigger: 'blur'},
  32. {min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur'}
  33. ],
  34. age: [
  35. { required: true, message: '年龄不能为空', trigger: 'blur'},
  36. { type: 'number', message: '年龄必须为数字值'}
  37. ]
  38. }
  39. }
  40. },
  41. methods:{
  42. test(){
  43. axios
  44. .get('http://127.0.0.1:8888/test', {
  45. params: {
  46. username: 12345,
  47. age: 20
  48. }
  49. }).then(({data}) => {
  50. if(data && data.success){
  51. this.$message.success(data.message)
  52. }else{
  53. this.$message.warning(data.info)
  54. }
  55. }).catch(error => { // 请求失败处理
  56. this.$message.error("系统异常,请稍后重试!");
  57. });
  58. },
  59. onSubmit(formName){
  60. this.$refs[formName].validate((valid) => {
  61. if (valid) {
  62. axios.get('http://127.0.0.1:8888/test', {
  63. params: this.formInline
  64. }).then(({data}) => {
  65. if (data && data.success) {
  66. this.$message.success(data.message)
  67. } else {
  68. this.$message.warning(data.info)
  69. }
  70. }).catch(error => { // 请求失败处理
  71. this.$message.error("系统异常,请稍后重试!");
  72. });
  73. }
  74. })
  75. },
  76. }
  77. }
  78. </script>

 

关注公众号”小猿架构“,发送 "前后分离架构" ,下载课程视频+课程源码+课件。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/115183
推荐阅读
相关标签
  

闽ICP备14008679号