Spring Boot之前后端分離(二):后端、前后端集成

前言

來啦老鐵!

筆者學(xué)習(xí)Spring Boot有一段時間了,附上Spring Boot系列學(xué)習(xí)文章,歡迎取閱、賜教:

  1. 5分鐘入手Spring Boot;
  2. Spring Boot數(shù)據(jù)庫交互之Spring Data JPA;
  3. Spring Boot數(shù)據(jù)庫交互之Mybatis;
  4. Spring Boot視圖技術(shù);
  5. Spring Boot之整合Swagger;
  6. Spring Boot之junit單元測試踩坑;
  7. 如何在Spring Boot中使用TestNG;
  8. Spring Boot之整合logback日志;
  9. Spring Boot之整合Spring Batch:批處理與任務(wù)調(diào)度;
  10. Spring Boot之整合Spring Security: 訪問認(rèn)證;
  11. Spring Boot之整合Spring Security: 授權(quán)管理;
  12. Spring Boot之多數(shù)據(jù)庫源:極簡方案;
  13. Spring Boot之使用MongoDB數(shù)據(jù)庫源;
  14. Spring Boot之多線程、異步:@Async;
  15. Spring Boot之前后端分離(一):Vue前端;

在上一篇文章Spring Boot之前后端分離(一):Vue前端中我們建立了Vue前端,打開了Spring Boot前后端分離的第一步,今天我們將建立后端,并且與前端進行集成,搭建一個完整的前后端分離的web應(yīng)用!

  • 該web應(yīng)用主要演示登錄操作!

整體步驟

  1. 后端技術(shù)棧選型;
  2. 后端搭建;
  3. 完成前端登錄頁面;
  4. 前后端集成與交互;
  5. 前后端交互演示;

1. 后端技術(shù)棧選型;

有了之前Spring Boot學(xué)習(xí)的基礎(chǔ),我們可以很快建立后端,整體選型:

  1. 持久層框架使用Mybatis;
  2. 集成訪問認(rèn)證與權(quán)限控制;
  3. 集成單元測試;
  4. 集成Swagger生成API文檔;
  5. 集成logback日志系統(tǒng);

筆者還預(yù)想過一些Spring Boot中常用的功能,如使用Redis、消息系統(tǒng)Kafka等、Elasticsearch、應(yīng)用監(jiān)控Acutator等,但由于還未進行這些方面的學(xué)習(xí),咱將來再把它們集成到我們的前后端分離的項目中。

2. 后端搭建;

1). 持久層框架使用Mybatis(暫未在本項目中使用,后續(xù)加上);
2). 集成訪問認(rèn)證與權(quán)限控制(已使用,主要演示訪問認(rèn)證);
3). 集成單元測試(暫未在本項目中演示,后續(xù)加上);
4). 集成Swagger生成API文檔(暫未在本項目中使用,后續(xù)加上);
5). 集成logback日志系統(tǒng)(已使用);

全部是之前學(xué)過的知識,是不是有種冥冥中一切都安排好了的感覺?哈哈!?。?/p>

整個過程描述起來比較費勁,這里就不再贅述了,需要的同學(xué)請參考git倉庫代碼:
項目整體結(jié)構(gòu)
  • 關(guān)鍵代碼,WebSecurityConfig類:
package com.github.dylanz666.config;

import com.alibaba.fastjson.JSONArray;
import com.github.dylanz666.constant.UserRoleEnum;
import com.github.dylanz666.domain.AuthorizationException;
import com.github.dylanz666.domain.SignInResponse;
import com.github.dylanz666.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.web.cors.CorsUtils;

import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.Collection;

/**
 * @author : dylanz
 * @since : 10/04/2020
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsServiceImpl userDetailsService;
    @Autowired
    private AuthorizationException authorizationException;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .requestMatchers(CorsUtils::isPreFlightRequest)
                .permitAll()
                .antMatchers("/", "/ping").permitAll()//這3個url不用訪問認(rèn)證
                .antMatchers("/admin/**").hasRole(UserRoleEnum.ADMIN.toString())
                .antMatchers("/user/**").hasRole(UserRoleEnum.USER.toString())
                .anyRequest()
                .authenticated()//其他url都需要訪問認(rèn)證
                .and()
                .formLogin()
                .loginProcessingUrl("/login")
                .permitAll()
                .failureHandler((request, response, ex) -> {//登錄失敗
                    response.setContentType("application/json");
                    response.setStatus(400);

                    SignInResponse signInResponse = new SignInResponse();
                    signInResponse.setCode(400);
                    signInResponse.setStatus("fail");
                    signInResponse.setMessage("Invalid username or password.");
                    signInResponse.setUsername(request.getParameter("username"));

                    PrintWriter out = response.getWriter();
                    out.write(signInResponse.toString());
                    out.flush();
                    out.close();
                })
                .successHandler((request, response, authentication) -> {//登錄成功
                    response.setContentType("application/json");
                    response.setStatus(200);

                    SignInResponse signInResponse = new SignInResponse();
                    signInResponse.setCode(200);
                    signInResponse.setStatus("success");
                    signInResponse.setMessage("success");
                    signInResponse.setUsername(request.getParameter("username"));
                    Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
                    JSONArray userRoles = new JSONArray();
                    for (GrantedAuthority authority : authorities) {
                        String userRole = authority.getAuthority();
                        if (!userRole.equals("")) {
                            userRoles.add(userRole);
                        }
                    }
                    signInResponse.setUserRoles(userRoles);

                    PrintWriter out = response.getWriter();
                    out.write(signInResponse.toString());
                    out.flush();
                    out.close();
                })
                .and()
                .logout()
                .permitAll()//logout不需要訪問認(rèn)證
                .and()
                .exceptionHandling()
                .accessDeniedHandler(((httpServletRequest, httpServletResponse, e) -> {
                    e.printStackTrace();
                    httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
                    httpServletResponse.setContentType("application/json");
                    authorizationException.setCode(HttpServletResponse.SC_FORBIDDEN);
                    authorizationException.setStatus("FAIL");
                    authorizationException.setMessage("FORBIDDEN");
                    authorizationException.setUri(httpServletRequest.getRequestURI());
                    PrintWriter printWriter = httpServletResponse.getWriter();
                    printWriter.write(authorizationException.toString());
                    printWriter.flush();
                    printWriter.close();
                }))
                .authenticationEntryPoint((httpServletRequest, httpServletResponse, e) -> {
                    e.printStackTrace();
                    httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                    httpServletResponse.setContentType("application/json");
                    authorizationException.setCode(HttpServletResponse.SC_UNAUTHORIZED);
                    authorizationException.setStatus("FAIL");
                    authorizationException.setMessage("UNAUTHORIZED");
                    authorizationException.setUri(httpServletRequest.getRequestURI());
                    PrintWriter printWriter = httpServletResponse.getWriter();
                    printWriter.write(authorizationException.toString());
                    printWriter.flush();
                    printWriter.close();
                });
        try {
            http.userDetailsService(userDetailsService());
        } catch (Exception e) {
            http.authenticationProvider(authenticationProvider());
        }
        //開啟跨域訪問
        http.cors().disable();
        //開啟模擬請求,比如API POST測試工具的測試,不開啟時,API POST為報403錯誤
        http.csrf().disable();
    }

    @Override
    public void configure(WebSecurity web) {
        //對于在header里面增加token等類似情況,放行所有OPTIONS請求。
        web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
    }

    @Bean
    @Override
    public UserDetailsService userDetailsService() {
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        UserDetails dylanz =
                User.withUsername("dylanz")
                        .password(bCryptPasswordEncoder.encode("666"))
                        .roles(UserRoleEnum.ADMIN.toString())
                        .build();
        UserDetails ritay =
                User.withUsername("ritay")
                        .password(bCryptPasswordEncoder.encode("888"))
                        .roles(UserRoleEnum.USER.toString())
                        .build();
        UserDetails jonathanw =
                User.withUsername("jonathanw")
                        .password(bCryptPasswordEncoder.encode("999"))
                        .roles(UserRoleEnum.USER.toString())
                        .build();
        return new InMemoryUserDetailsManager(dylanz, ritay, jonathanw);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public RoleHierarchy roleHierarchy() {
        RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
        roleHierarchy.setHierarchy("ROLE_" + UserRoleEnum.ADMIN.toString() + " > ROLE_" + UserRoleEnum.USER.toString());
        return roleHierarchy;
    }

    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(userDetailsService);
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
        return daoAuthenticationProvider;
    }
}

特別是這幾行:

try {
    http.userDetailsService(userDetailsService());
} catch (Exception e) {
    http.authenticationProvider(authenticationProvider());
}
//開啟跨域訪問
http.cors().disable();
//開啟模擬請求,比如API POST測試工具的測試,不開啟時,API POST為報403錯誤
http.csrf().disable();

同時我們在WebSecurityConfig類中自定義了登錄失敗與成功的處理failureHandler和successHandler,用postman掉用登錄API,返回例子如:

登錄-單角色
登錄-多角色
登錄-密碼錯誤
登錄-不存在的用戶名
6). 后端編寫的演示API;
package com.github.dylanz666.controller;

import org.springframework.web.bind.annotation.*;

/**
 * @author : dylanz
 * @since : 10/04/2020
 */
@RestController
public class PingController {
    @GetMapping("/ping")
    public String ping() {
        return "success";
    }
}
7). 后端登錄API;

這塊我使用了Spring Security默認(rèn)的登錄API,無需再自行寫登錄API,即http://127.0.0.1:8080/login , 這樣我們就可以把登錄交給Spring Security來做啦,省時省力?。?!

8). 跨域設(shè)置;

由于我們要完成的是前后端分離,即前端與后端分開部署,二者使用不同的服務(wù)器或相同服務(wù)器的不同端口(我本地就是這種情況),因此我們需要使后端能夠跨域調(diào)用,方法如下:

  • config包下的WebMvcConfig類代碼:
package com.github.dylanz666.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.*;

/**
 * @author : dylanz
 * @since : 10/04/2020
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Autowired
    public CorsInterceptor corsInterceptor;

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowCredentials(true)
                .allowedHeaders("*")
                .allowedMethods("*")
                .allowedOrigins("*")
                .maxAge(3600);
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(corsInterceptor);
    }
}
  • 這里頭我還引入了攔截器CorsInterceptor類(WebMvcConfig中引入攔截器:addInterceptors),用于打印請求信息:
package com.github.dylanz666.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author : dylanz
 * @since : 10/04/2020
 */
@Component
public class CorsInterceptor extends HandlerInterceptorAdapter {
    private static final Logger logger = LoggerFactory.getLogger(CorsInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String logPattern = "[%s][%s]:%s";
        logger.info(String.format(logPattern, request.getMethod(), request.getRemoteAddr(), request.getRequestURI()));
        return true;
    }
}
至此,后端已準(zhǔn)備好,接下來我們來做前端具體頁面以及前后端集成!

3. 完成前端登錄頁面;

1). 安裝node-sass和sass-loader模塊;

Sass 是世界上最成熟、穩(wěn)定、強大的專業(yè)級 CSS 擴展語言。
Sass 是一個 CSS 預(yù)處理器。
Sass 是 CSS 擴展語言,可以幫助我們減少 CSS 重復(fù)的代碼,節(jié)省開發(fā)時間。
Sass 完全兼容所有版本的 CSS。
Sass 擴展了 CSS3,增加了規(guī)則、變量、混入、選擇器、繼承、內(nèi)置函數(shù)等等特性。
Sass 生成良好格式化的 CSS 代碼,易于組織和維護。

筆者根據(jù)過往的一些項目中使用了Sass來寫CSS代碼,因此也學(xué)著使用,學(xué)互聯(lián)網(wǎng)技術(shù),有時候要先使用后理解,用著用著就能理解了!
使用Sass需要安裝node-sass和sass-loader模塊:

npm install node-sass --save-dev

而sass-loader則不能安裝最新版本,否則項目運行會報錯,推薦安裝低一些的版本,如7.3.1:

npm install sass-loader@7.3.1 --save-dev
2). 修改src/App.vue文件;

刪除#app樣式中的margin-top: 60px; 這樣頁面頂部就不會有一塊空白,如:

頂部空白
3). 修改項目根目錄的index.html文件;
修改前:
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <title>spring-boot-vue-frontend</title>
</head>

<body>
  <div id="app"></div>
  <!-- built files will be auto injected -->
</body>

</html>
修改后:
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <title>spring-boot-vue-frontend</title>
</head>

<body>
  <div id="app"></div>
  <!-- built files will be auto injected -->
</body>

</html>

<style type="text/css">
  body {
    margin: 0;
  }
</style>

其實就加了個body的樣式,這是因為如果沒有這個樣式,項目啟動后,頁面會有“白邊”,例如:

頁面白邊
4). 編寫前端代碼views/login/index.vue;
<template>
  <div align="center" class="login-container">
    <div style="margin-top: 100px">
      <h2 style="color: white">Sign in to Magic</h2>
      <el-card
        shadow="always"
        style="width: 380px; height: 290px; padding: 10px"
      >
        <el-form
          :model="ruleForm"
          status-icon
          :rules="rules"
          ref="ruleForm"
          class="demo-ruleForm"
        >
          <div align="left">
            <span>Username</span>
          </div>
          <el-form-item prop="username">
            <el-input v-model="ruleForm.username" autocomplete="off"></el-input>
          </el-form-item>

          <div align="left">
            <span>Password</span>
          </div>

          <el-form-item prop="password">
            <el-input
              type="password"
              v-model="ruleForm.password"
              autocomplete="off"
            ></el-input>
          </el-form-item>

          <el-form-item>
            <el-button
              type="primary"
              @click="login('ruleForm')"
              style="width: 100%"
              >Sign in</el-button
            >
          </el-form-item>
        </el-form>

        <el-row>
          <el-col :span="12" align="left"
            ><el-link href="/#/resetPassword.html" target="_blank"
              >Forgot password?</el-link
            >
          </el-col>
          <el-col :span="12" align="right">
            <el-link href="/#/createAccount.html" target="_blank"
              >Create an account.</el-link
            >
          </el-col>
        </el-row>
      </el-card>
    </div>
  </div>
</template>

<script>
export default {
  name: "login",
  data() {
    var validateUsername = (rule, value, callback) => {
      if (value === "") {
        callback(new Error("Please input the username"));
      } else {
        if (this.ruleForm.checkUsername !== "") {
          this.$refs.ruleForm.validateField("password");
        }
        callback();
      }
    };
    var validatePassword = (rule, value, callback) => {
      if (value === "") {
        callback(new Error("Please input the password"));
      } else if (value !== this.ruleForm.password) {
        callback(new Error("Two inputs don't match!"));
      } else {
        callback();
      }
    };
    return {
      ruleForm: {
        username: "",
        password: "",
      },
      rules: {
        username: [{ validator: validateUsername, trigger: "blur" }],
        password: [{ validator: validatePassword, trigger: "blur" }],
      },
    };
  },
  methods: {
    login(formName) {
      this.$refs[formName].validate((valid) => {
        if (valid) {
          alert("sign in!");
        } else {
          alert("error sign in!");
          return false;
        }
      });
    },
  },
};
</script>

<style rel="stylesheet/scss" lang="scss">
$bg: #2d3a4b;
$dark_gray: #889aa4;
$light_gray: #eee;

.login-container {
  position: fixed;
  height: 100%;
  width: 100%;
  background-color: $bg;
  input {
    background: transparent;
    border: 0px;
    -webkit-appearance: none;
    border-radius: 0px;
    padding: 12px 5px 12px 15px;
    height: 47px;
  }
  .el-input {
    height: 47px;
    width: 85%;
  }
  .tips {
    font-size: 14px;
    color: #fff;
    margin-bottom: 10px;
  }
  .svg-container {
    padding: 6px 5px 6px 15px;
    color: $dark_gray;
    vertical-align: middle;
    width: 30px;
    display: inline-block;
    &_login {
      font-size: 20px;
    }
  }
  .title {
    font-size: 26px;
    font-weight: 400;
    color: $light_gray;
    margin: 0px auto 40px auto;
    text-align: center;
    font-weight: bold;
  }
  .login-form {
    position: absolute;
    left: 0;
    right: 0;
    width: 400px;
    padding: 35px 35px 15px 35px;
    margin: 120px auto;
  }
  .el-form-item {
    border: 1px solid rgba(255, 255, 255, 0.1);
    background: rgba(117, 137, 230, 0.1);
    border-radius: 5px;
    color: #454545;
  }
  .show-pwd {
    position: absolute;
    right: 10px;
    top: 7px;
    font-size: 16px;
    color: $dark_gray;
    cursor: pointer;
    user-select: none;
  }
  .thirdparty-button {
    position: absolute;
    right: 35px;
    bottom: 28px;
  }
}

.title-container {
  position: relative;
  .title {
    font-size: 26px;
    font-weight: 400;
    color: $light_gray;
    margin: 0px auto 40px auto;
    text-align: center;
    font-weight: bold;
  }
  .set-language {
    color: #fff;
    position: absolute;
    top: 5px;
    right: 0px;
  }
}
</style>
5). 前端登錄頁面樣子;
登錄頁面

怎么樣,還算美觀吧!

5. 前后端集成與交互;

1). 設(shè)置代理與跨域;

config/index.js中有個proxyTable,設(shè)置target值為后端API基礎(chǔ)路徑,進行代理轉(zhuǎn)發(fā)映射,將changeOrigin值設(shè)置為true,這樣就不會有跨域問題了。
如:

proxyTable: {
  '/api': {
    target: 'http://127.0.0.1:8080/',
    changeOrigin: true,
    pathRewrite: {
      '^/api': '/api'
    }
  },
  '/login': {
    target: 'http://127.0.0.1:8080/',
    changeOrigin: true,
    pathRewrite: {
      '^/login': '/login'
    }
  }
}

這里有幾個注意點:

  • 這個proxyTable代表了所有前端以/api開頭的請求,均調(diào)用后端http://127.0.0.1:8080/api開頭的API,如我們前端請求http://127.0.0.1:9528/api/test,則背后實際上調(diào)用的是http://127.0.0.1:8080/api/test。
  • target中要帶上http://
  • pathRewrite中'^/api'代表當(dāng)匹配到前端請求url中以/api為開頭的請求,則該請求轉(zhuǎn)發(fā)到指定代理去,即http://127.0.0.1:8080/api/XXX,如果'^/api'對應(yīng)的值為'',則轉(zhuǎn)發(fā)至http://127.0.0.1:8080/XXX,這里的'^/api'和"api"可依實際情況自行設(shè)定,如'^/test': '/new',代表當(dāng)匹配到前端請求url中以/test為開頭的請求,則該請求轉(zhuǎn)發(fā)到指定代理去http://127.0.0.1:8080/new/XXX;
  • 設(shè)置完成后,需要重啟前端應(yīng)用!注意是重啟,不是熱部署!??!

最后一點特別重要,因為vue熱部署默認(rèn)沒有使用全部代碼重啟(可配置),而這個代理剛好不在熱部署代碼范圍內(nèi),所以,必須要重啟前端,除非用戶已事先解決這個問題,否則代理是沒有生效的。筆者就因為這個問題,困惑了一個下午,說多了都是淚?。。?!
2). 封裝login API請求;

在src/api新建login.js文件,在login.js文件中寫入代碼:

import request from '@/utils/request'

export function login(username, password) {
    let form = new FormData();
    form.append("username", username);
    form.append("password", password);
    return request({
        url: '/login',
        method: 'post',
        data: form
    });
}

export function ping() {
    return request({
        url: '/ping',
        method: 'get',
        params: {}
    })
}
說明一下:

我們基于axios,根據(jù)后端Spring Security登錄API 127.0.0.1:8080/login 進行封裝,方法名為login,入?yún)閡sername和password,請求方法為post。完成封裝后,前端.vue文件中,就能很輕松的進行使用了,同時也一定程度上避免了重復(fù)性的代碼,減少冗余!

3). .vue文件中使用login API;

在src/views/login/index.vue中的login方法中使用login方法:
<script>標(biāo)簽內(nèi)先import進login方法

...
import { login } from "@/api/login";
...

然后修改src/views/login/index.vue中的methods:

methods: {
  login(formName) {
    this.$refs[formName].validate((valid) => {
      if (!valid) {
        alert("error sign in!");
        return false;
      }
      login(this.ruleForm.username, this.ruleForm.password).then(
        (response) => {
          this.showSuccessNotify(response);
          this.ruleForm.username = "";
          this.ruleForm.password = "";
        }
      );
    });
  },
  showSuccessNotify(response) {
    if ("success" == response.status) {
      this.$notify({
        title: "Success",
        message: response.message,
        type: "success",
        offset: 100,
      });
    }
  }
},

至此,我們完成了從前端登錄頁面調(diào)用后端登錄接口的代碼部分,接下來我們來一起見證奇跡!

6. 前后端交互演示;

1). 啟動前端:
啟動前端
2). 啟動后端:
啟動后端
3). 前端登錄操作:
前端登錄操作
image.png
4). 后端接收到請求:
后端接收到請求
5). 前端登錄后行為:

登錄成功后,我清空了登錄表單,并且彈出登錄成功信息(暫未做跳轉(zhuǎn)):

前端登錄后行為-登錄成功
前端登錄后行為-登錄失敗

可見,前端的登錄操作(9528端口)已經(jīng)能夠成功調(diào)用后端的接口(8080端口)!

至此,我們實現(xiàn)了前后端分離,并完成了前后端集成調(diào)試。本案例僅作演示和學(xué)習(xí),雖然沒有實現(xiàn)復(fù)雜的功能,但具體應(yīng)用無非是在這基礎(chǔ)之上堆砌功能與代碼,開發(fā)實際有用的應(yīng)用指日可待!

如果本文對您有幫助,麻煩點贊+關(guān)注!

謝謝!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容