前言
來啦老鐵!
筆者學(xué)習(xí)Spring Boot有一段時間了澎埠,附上Spring Boot系列學(xué)習(xí)文章嫉沽,歡迎取閱、賜教:
- 5分鐘入手Spring Boot;
- Spring Boot數(shù)據(jù)庫交互之Spring Data JPA;
- Spring Boot數(shù)據(jù)庫交互之Mybatis;
- Spring Boot視圖技術(shù);
- Spring Boot之整合Swagger;
- Spring Boot之junit單元測試踩坑;
- 如何在Spring Boot中使用TestNG;
- Spring Boot之整合logback日志;
- Spring Boot之整合Spring Batch:批處理與任務(wù)調(diào)度;
- Spring Boot之整合Spring Security: 訪問認證;
- Spring Boot之整合Spring Security: 授權(quán)管理;
- Spring Boot之多數(shù)據(jù)庫源:極簡方案;
- Spring Boot之使用MongoDB數(shù)據(jù)庫源;
- Spring Boot之多線程、異步:@Async;
- Spring Boot之前后端分離(一):Vue前端;
在上一篇文章Spring Boot之前后端分離(一):Vue前端中我們建立了Vue前端,打開了Spring Boot前后端分離的第一步,今天我們將建立后端俗慈,并且與前端進行集成,搭建一個完整的前后端分離的web應(yīng)用遣耍!
-
該web應(yīng)用主要演示登錄操作闺阱!
整體步驟
- 后端技術(shù)棧選型;
- 后端搭建舵变;
- 完成前端登錄頁面酣溃;
- 前后端集成與交互;
- 前后端交互演示纪隙;
1. 后端技術(shù)棧選型赊豌;
有了之前Spring Boot學(xué)習(xí)的基礎(chǔ),我們可以很快建立后端绵咱,整體選型:
- 持久層框架使用Mybatis碘饼;
- 集成訪問認證與權(quán)限控制;
- 集成單元測試悲伶;
- 集成Swagger生成API文檔艾恼;
- 集成logback日志系統(tǒng);
筆者還預(yù)想過一些Spring Boot中常用的功能拢切,如使用Redis蒂萎、消息系統(tǒng)Kafka等、Elasticsearch淮椰、應(yīng)用監(jiān)控Acutator等,但由于還未進行這些方面的學(xué)習(xí),咱將來再把它們集成到我們的前后端分離的項目中主穗。
2. 后端搭建泻拦;
1). 持久層框架使用Mybatis(暫未在本項目中使用,后續(xù)加上)忽媒;
2). 集成訪問認證與權(quán)限控制(已使用争拐,主要演示訪問認證);
- 訪問認證參考:Spring Boot之整合Spring Security: 訪問認證;
- 權(quán)限控制參考:Spring Boot之整合Spring Security: 授權(quán)管理;
3). 集成單元測試(暫未在本項目中演示晦雨,后續(xù)加上)架曹;
4). 集成Swagger生成API文檔(暫未在本項目中使用,后續(xù)加上)闹瞧;
5). 集成logback日志系統(tǒng)(已使用)绑雄;
全部是之前學(xué)過的知識,是不是有種冥冥中一切都安排好了的感覺奥邮?哈哈M蛭!洽腺!
整個過程描述起來比較費勁脚粟,這里就不再贅述了,需要的同學(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不用訪問認證
.antMatchers("/admin/**").hasRole(UserRoleEnum.ADMIN.toString())
.antMatchers("/user/**").hasRole(UserRoleEnum.USER.toString())
.anyRequest()
.authenticated()//其他url都需要訪問認證
.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不需要訪問認證
.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默認的登錄API膜楷,無需再自行寫登錄API,即http://127.0.0.1:8080/login , 這樣我們就可以把登錄交給Spring Security來做啦贞奋,省時省力6奶!轿塔!
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;
}
}
至此毒嫡,后端已準備好癌蚁,接下來我們來做前端具體頁面以及前后端集成!
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)用!注意是重啟阱表,不是熱部署5钊纭!捶枢!
最后一點特別重要握截,因為vue熱部署默認沒有使用全部代碼重啟(可配置),而這個代理剛好不在熱部署代碼范圍內(nèi)烂叔,所以谨胞,必須要重啟前端,除非用戶已事先解決這個問題蒜鸡,否則代理是沒有生效的胯努。筆者就因為這個問題牢裳,困惑了一個下午,說多了都是淚耙杜妗F蜒丁!灰署!
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). 前端登錄操作:
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)注奏属!
謝謝跨跨!