動(dòng)手實(shí)踐!從零開始實(shí)現(xiàn)Springboot+Vue登錄
作者:Eli Shaw
https://blog.csdn.net/xiaojinlai123/article/details/90694372
一、簡述
最近學(xué)習(xí)使用 Vue 實(shí)現(xiàn)前端后端分離,在 Github 上有一個(gè)很好的開源項(xiàng)目:mall,正所謂百看不如一練,自己動(dòng)手實(shí)現(xiàn)了一個(gè) Springboot+Vue 的登錄操作,在此記錄一下踩過的坑。
文章最后補(bǔ)充兩端的 GitHub 代碼,之所以放在最后,是因?yàn)槲恼聦懙暮芗?xì)致了,動(dòng)手操作一下會(huì)更有幫忙,如果有很大出入可以比對原碼,找找問題。
二、開發(fā)工具
VSCode
IDEA
Vue 的安裝就不說了,有很多文章,但是 Springboot+Vue 整合的完整文章相對較少,所以我主要記錄一下這兩端整合時(shí)的內(nèi)容。
(Vue 安裝后就會(huì)有 npm?或 cnpm,相應(yīng)的介紹也不說了,Vue 官網(wǎng)可查看)
一、打開 cmd 創(chuàng)建 Vue 項(xiàng)目,并添加 Vue 依賴的框架:
1. 創(chuàng)建 Vue?項(xiàng)目 (進(jìn)入自己想創(chuàng)建的文件夾位置,我放在 D:\VSCodeWorkSpace),創(chuàng)建語句 vue create vue-spring-login-summed,方向鍵選擇創(chuàng)建方式,我選擇的默認(rèn)

2. 進(jìn)入到創(chuàng)建的 Vue 項(xiàng)目目錄,添加依賴框架:
cd vue-spring-login-summed (進(jìn)入到項(xiàng)目根目錄)
vue add element (添加 element,一個(gè) element 風(fēng)格的 UI 框架)
npm install axios (安裝 axios,用于網(wǎng)絡(luò)請求)
npm install vuex --save(安裝 Vuex,用于管理狀態(tài))
npm install vue-router (安裝 路由,用于實(shí)現(xiàn)兩個(gè) Vue 頁面的跳轉(zhuǎn))
以上命令截圖如下:
1) 添加 Element


2) 添加 axios

3) 添加 Vuex

4) 添加 路由

到此相關(guān)依賴的架包添加完畢,輸入 code .?打開?VSCode

二、添加目錄結(jié)構(gòu)
在?VSCode?下看到?Vue?整體項(xiàng)目結(jié)構(gòu)如下

現(xiàn)在需要?jiǎng)?chuàng)建相應(yīng)功能的目錄結(jié)構(gòu),進(jìn)行分層開發(fā),需要在?src?目錄下創(chuàng)建下面幾個(gè)目錄
api (網(wǎng)絡(luò)請求接口包)
router (路由配置包)
store (Vuex 狀態(tài)管理包)
utils (工具包)
views (vue 視圖包,存放所有 vue 代碼,可根據(jù)功能模塊進(jìn)行相應(yīng)分包)
創(chuàng)建后的目錄結(jié)構(gòu)如下

三、運(yùn)行項(xiàng)目
現(xiàn)在可以運(yùn)行項(xiàng)目了,在 VSCode 菜單欄依次選擇:終端 ——?運(yùn)行任務(wù)...

這里使用的是 serve?模式,即開發(fā)模式運(yùn)行的項(xiàng)目

在瀏覽器輸入:http://localhost:8080/

這是 Vue 默認(rèn)的頁面,代表項(xiàng)目創(chuàng)建成功了,在進(jìn)行代碼開發(fā)前,先貼上項(xiàng)目整體結(jié)構(gòu),防止不知道在哪創(chuàng)建

四、View?層代碼編寫
編寫三個(gè) vue 文件:login.vue(登錄頁面)、success.vue(登錄成功頁面)、error.vue(登錄失敗頁面)
1.login.vue
代碼如下 (比較懶,直接從?mall?扒下來的代碼,去掉了一些功能)
autocomplete="on"
:model="loginForm"
ref="loginForm"
label-position="left"
>
class="login-mall">
mall-admin-web
"username">
type="text"
v-model="loginForm.username"
autocomplete="on"
placeholder="請輸入用戶名"
>
"prefix">
class="user">
"password">
:type="pwdType"
@keyup.enter.native="handleLogin"
v-model="loginForm.password"
autocomplete="on"
placeholder="請輸入密碼"
>
"prefix">
class="password">
"suffix" @click="showPwd">
class="eye">
type="primary"
:loading="loading"
@click.native.prevent="handleLogin"
>登錄
2.success.vue
Welcome!{{msg}}
3.error.vue
登錄錯(cuò)誤:{{msg}}
五、路由
頁面寫好了,我們需要依次顯示這三個(gè)頁面,這里我們統(tǒng)一使用路由來管理顯示頁面,路由的官方文檔見:vue 路由
本著先實(shí)踐,后理解的碼農(nóng)學(xué)習(xí)方式。我們先使用路由顯示三個(gè)頁面后,再去理解 Vue 路由這個(gè)功能點(diǎn)。
1. 創(chuàng)建路由配置文件
在剛才建立的 router?文件夾下創(chuàng)建一個(gè) index.js?文件,內(nèi)容如下
import Vue from 'vue' //引入 Vue
import VueRouter from 'vue-router' //引入 Vue 路由
Vue.use(VueRouter); //安裝插件
export const constantRouterMap = \[
//配置默認(rèn)的路徑,默認(rèn)顯示登錄頁
{ path: '/', component: () => import('@/views/login')},
//配置登錄成功頁面,使用時(shí)需要使用 path 路徑來實(shí)現(xiàn)跳轉(zhuǎn)
{ path: '/success', component: () => import('@/views/success')},
//配置登錄失敗頁面,使用時(shí)需要使用 path 路徑來實(shí)現(xiàn)跳轉(zhuǎn)
{ path: '/error', component: () => import('@/views/error'), hidden: true }
\]
export default new VueRouter({
// mode: 'history', //后端支持可開
scrollBehavior: () => ({ y: 0 }),
routes: constantRouterMap //指定路由列表
})
2. 將路由添加到程序入口
路由配置文件寫好,我們需要把他引入到 main.js 中,在項(xiàng)目的 src 目錄根節(jié)點(diǎn)下,找到 main.js,添加內(nèi)容如下:
import Vue from 'vue'
import App from './App.vue'
import './plugins/element.js'
import router from './router' //引入路由配置
Vue.config.productionTip = false
new Vue({
render: h => h(App),
router, //使用路由配置
}).$mount('#app')
3. 配置路由的出入口
現(xiàn)在路由已經(jīng)完全引入到項(xiàng)目了,但是路由還需要一個(gè)出入口,這個(gè)出入口用來告訴路由將路由的內(nèi)容顯示在這里。上面 main.js 配置的第一個(gè) vue 顯示頁面為 App.vue ,因此我們修改 App.vue 內(nèi)容如下
現(xiàn)在保存 App.vue 文件后,當(dāng)前項(xiàng)目會(huì)被重新裝載運(yùn)行,在剛才瀏覽的界面就會(huì)看到登錄界面如下:

4. 路由跳轉(zhuǎn)
在 login.vue?中可以使用 this.$router.push({path: "路徑"})?來跳轉(zhuǎn)到指定路徑的路由組件中,下面是通過路由跳轉(zhuǎn)到 error.vue?與 success.vue 的代碼
this.$router.push({path: "/success"}); //跳轉(zhuǎn)到成功頁
或
this.$router.push({path: "/error"}); //跳轉(zhuǎn)到失敗頁
六、使用?Vuex + Axios?方式進(jìn)行網(wǎng)絡(luò)請求
1.Axios
axios 是一個(gè)網(wǎng)絡(luò)請求構(gòu)架,官方推薦使用這種方式進(jìn)行 http 的請求。
1)?在 utils?包下封裝一個(gè)請求工具類 request.js
import axios from 'axios' //引入 axios
import baseUrl from '../api/baseUrl' //使用環(huán)境變量 + 模式的方式定義基礎(chǔ)URL
// 創(chuàng)建 axios 實(shí)例
const service = axios.create({
baseURL: baseUrl, // api 的 base\_url
timeout: 15000, // 請求超時(shí)時(shí)間
})
export default service
這里的 baseUrl 涉及 Vue CLI3 的環(huán)境變量與模式的概念,見:Vue 環(huán)境變量和模式 (設(shè)置通用 baseUrl)
2)?登錄請求接口 API
在 api 文件夾下,創(chuàng)建一個(gè)登錄 API 文件:login.js
import request from '@/utils/request' //引入封裝好的 axios 請求
export function login(username, password) { //登錄接口
return request({ //使用封裝好的 axios 進(jìn)行網(wǎng)絡(luò)請求
url: '/admin/login',
method: 'post',
data: { //提交的數(shù)據(jù)
username,
password
}
})
}
2. 使用 Vuex?封裝 axios
Vuex 是一個(gè)狀態(tài)管理構(gòu)架,官方文檔:Vuex
1) 封裝 Vuex 中的?module
在 store?文件夾下創(chuàng)建一個(gè) modules?文件夾,然后在此文件夾下創(chuàng)建一個(gè) user.js?文件
import { login } from '@/api/login'//引入登錄 api 接口
const user = {
actions: {
// 登錄
Login({ commit }, userInfo) { //定義 Login 方法,在組件中使用 this.$store.dispatch("Login") 調(diào)用
const username = userInfo.username.trim()
return new Promise((resolve, reject) => { //封裝一個(gè) Promise
login(username, userInfo.password).then(response => { //使用 login 接口進(jìn)行網(wǎng)絡(luò)請求
commit('') //提交一個(gè) mutation,通知狀態(tài)改變
resolve(response) //將結(jié)果封裝進(jìn) Promise
}).catch(error => {
reject(error)
})
})
},
}
}
export default user
這里的代碼值得解釋一下:官方文檔對應(yīng):Vuex actions
1. 首先引入 login 接口,之后使用登錄接口進(jìn)行網(wǎng)絡(luò)請求。
2. 定義一個(gè)?名為 Login?的 action?方法,Vue 組件通過 this.$store.dispatch("Login") 調(diào)用
3.Promise,這個(gè)類很有意思,官方的解釋是 “store.dispatch?可以處理被觸發(fā)的 action 的處理函數(shù)返回的 Promise,并且?store.dispatch?仍舊返回 Promise”。這話的意思組件中的 dispatch 返回的仍是一個(gè) Promise 類,因此推測 Promise 中的兩個(gè)方法 resolve() 與 reject() 分別對應(yīng) dispatch 中的 then 與 catch。
2) 創(chuàng)建 Vuex
在 store?文件夾下創(chuàng)建一個(gè) index.js?文件
import Vue from 'vue' //引入 Vue
import Vuex from 'vuex' //引入 Vuex
import user from './modules/user' //引入 user module
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
user //使用 user.js 中的 action
}
})
export default store
3)?將?Vuex?添加到 main.js?文件
修改之前的 main.js 文件如下:
import Vue from 'vue'
import App from './App.vue'
import './plugins/element.js'
import router from './router' //引入路由配置
import store from './store' //引入 Vuex 狀態(tài)管理
Vue.config.productionTip = false
new Vue({
render: h => h(App),
router, //使用路由配置
store //使用 Vuex 進(jìn)行狀態(tài)管理
}).$mount('#app')
重新運(yùn)行項(xiàng)目,在 Chrome 瀏覽器中進(jìn)入調(diào)試模式,點(diǎn)擊登錄按鈕

可以看到有發(fā)送一個(gè) 8088 端口的請求,至此 Vue 端的所有代碼已經(jīng)完成。
-------------------------------Springboot?開發(fā) -------------------------------
項(xiàng)目創(chuàng)建就不提了,網(wǎng)上有很多,只要使用 Spring Assistant 創(chuàng)建就好。
整體目錄結(jié)構(gòu)如下

1. 在 application.yml 修改端口號(hào)
不要和 Vue 在一個(gè) 8080 端口上:
server:
port: 8088
2. 解決跨域問題
這里有一個(gè)跨域問題,即 Vue 使用 8080?端口,要訪問 8088 端口的服務(wù)器,會(huì)報(bào)錯(cuò)。錯(cuò)誤信息如下:
Access to XMLHttpRequest at 'http://localhost:8088/admin/login' from origin 'http://localhost:8080' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No'Access-Control-Allow-Origin' header is present on the requested resource.

這個(gè)問題在 Vue 端或在 Springboot 端處理都可以,我在 Springboot 端處理的,寫一個(gè) CorsConfig 類內(nèi)容如下,不要忘了?@Configuration 注解。
@Configuration
public class CorsConfig {
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("\*"); // 1
corsConfiguration.addAllowedHeader("\*"); // 2
corsConfiguration.addAllowedMethod("\*"); // 3
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/\*\*", buildConfig()); // 4
return new CorsFilter(source);
}
}
3.IErrorCode?接口
Java 版本
public interface IErrorCode {
long getCode();
String getMessage();
}
Kotlin 版本
interface IErrorCode {
fun getCode(): Long
fun getMessage(): String
}
4.CommonResult 類
Java?版本
public class CommonResult<T> {
private long code;
private String message;
private T data;
protected CommonResult() {
}
protected CommonResult(long code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
/\*\*
\* 成功返回結(jié)果
\*
\* @param data 獲取的數(shù)據(jù)
\*/
public static CommonResult success(T data) {
return new CommonResult(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
}
/\*\*
\* 成功返回結(jié)果
\*
\* @param data 獲取的數(shù)據(jù)
\* @param message 提示信息
\*/
public static CommonResult success(T data, String message) {
return new CommonResult(ResultCode.SUCCESS.getCode(), message, data);
}
/\*\*
\* 失敗返回結(jié)果
\*
\* @param errorCode 錯(cuò)誤碼
\*/
public static CommonResult failed(IErrorCode errorCode) {
return new CommonResult(errorCode.getCode(), errorCode.getMessage(), null);
}
/\*\*
\* 失敗返回結(jié)果
\*
\* @param message 提示信息
\*/
public static CommonResult failed(String message) {
return new CommonResult(ResultCode.FAILED.getCode(), message, null);
}
/\*\*
\* 失敗返回結(jié)果
\*/
public static CommonResult failed() {
return failed(ResultCode.FAILED);
}
/\*\*
\* 參數(shù)驗(yàn)證失敗返回結(jié)果
\*/
public static CommonResult validateFailed() {
return failed(ResultCode.VALIDATE\_FAILED);
}
/\*\*
\* 參數(shù)驗(yàn)證失敗返回結(jié)果
\*
\* @param message 提示信息
\*/
public static CommonResult validateFailed(String message) {
return new CommonResult(ResultCode.VALIDATE\_FAILED.getCode(), message, null);
}
/\*\*
\* 未登錄返回結(jié)果
\*/
public static CommonResult unauthorized(T data) {
return new CommonResult(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data);
}
/\*\*
\* 未授權(quán)返回結(jié)果
\*/
public static CommonResult forbidden(T data) {
return new CommonResult(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data);
}
public long getCode() {
return code;
}
public void setCode(long code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
Kotlin?版本
class CommonResult<T> {
var code: Long = 0
var message: String? = null
var data: T? = null
constructor(code: Long, message: String, data: T?) {
this.code = code
this.message = message
this.data = data
}
companion object {
/\*\*
\* 成功返回結(jié)果
\* @param data 獲取的數(shù)據(jù)
\*/
fun success(data: T): CommonResult {
return CommonResult(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data)
}
/\*\*
\* 成功返回結(jié)果
\* @param data 獲取的數(shù)據(jù)
\* @param message 提示信息
\*/
fun success(data: T, message: String): CommonResult {
return CommonResult(ResultCode.SUCCESS.getCode(), message, data)
}
/\*\*
\* 失敗返回結(jié)果
\* @param errorCode 錯(cuò)誤碼
\*/
fun failed(errorCode: IErrorCode): CommonResult {
return CommonResult(errorCode.getCode(), errorCode.getMessage(), null)
}
/\*\*
\* 失敗返回結(jié)果
\* @param message 提示信息
\*/
fun failed(message: String): CommonResult {
return CommonResult(ResultCode.FAILED.getCode(), message, null)
}
/\*\*
\* 失敗返回結(jié)果
\*/
fun failed(): CommonResult {
return failed(ResultCode.FAILED)
}
/\*\*
\* 參數(shù)驗(yàn)證失敗返回結(jié)果
\*/
fun validateFailed(): CommonResult {
return failed(ResultCode.VALIDATE\_FAILED)
}
/\*\*
\* 參數(shù)驗(yàn)證失敗返回結(jié)果
\* @param message 提示信息
\*/
fun validateFailed(message: String): CommonResult {
return CommonResult(ResultCode.VALIDATE\_FAILED.getCode(), message, null)
}
/\*\*
\* 未登錄返回結(jié)果
\*/
fun unauthorized(data: T): CommonResult {
return CommonResult(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data)
}
/\*\*
\* 未授權(quán)返回結(jié)果
\*/
fun forbidden(data: T): CommonResult {
return CommonResult(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data)
}
}
}
5.ResultCode?枚舉
Java?版本
public enum ResultCode implements IErrorCode {
SUCCESS(200, "操作成功"),
FAILED(500, "操作失敗"),
VALIDATE\_FAILED(404, "參數(shù)檢驗(yàn)失敗"),
UNAUTHORIZED(401, "暫未登錄或token已經(jīng)過期"),
FORBIDDEN(403, "沒有相關(guān)權(quán)限");
private long code;
private String message;
private ResultCode(long code, String message) {
this.code = code;
this.message = message;
}
public long getCode() {
return code;
}
public String getMessage() {
return message;
}
}
Kotlin?版本
enum class ResultCode(private val code: Long, private val message: String) : IErrorCode {
SUCCESS(200, "操作成功"),
FAILED(500, "操作失敗"),
VALIDATE\_FAILED(404, "參數(shù)檢驗(yàn)失敗"),
UNAUTHORIZED(401, "暫未登錄或token已經(jīng)過期"),
FORBIDDEN(403, "沒有相關(guān)權(quán)限");
override fun getCode(): Long {
return code
}
override fun getMessage(): String {
return message
}
}
6.User 類
Java?版本
public class User {
private int id;
private String username;
private String password;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
Kotlin?版本
data class User(
val id: Int,
val username: String,
val password: String)
7.LoginController?類
Java?版本
@RestController
public class LoginController {
@RequestMapping(value = "/admin/login", method = RequestMethod.POST)
public CommonResult login(@RequestBody User user) {
if (user.getUsername().equals("admin") && user.getPassword().equals("123456"))
return CommonResult.success("admin");
else
return CommonResult.validateFailed();
}
}
Kotlin?版本
@RestController //此注解是 @ResponseBody 和 @Controller 的組合注解,可返回一個(gè) JSON
class LoginController {
@RequestMapping(value = \["/admin/login"\], method = \[RequestMethod.POST\])
fun admin(@RequestBody user: User): CommonResult<\*> {
return if (user.username == "admin" && user.password == "123456") {
CommonResult.success("admin")
} else {
CommonResult.validateFailed()
}
}
}
啟動(dòng)兩端程序

輸入正確的賬號(hào)密碼

輸入錯(cuò)誤的賬號(hào)密碼

七、GitHub 源碼地址
vue 端:https://github.com/xiaojinlai/vue-spring-login-summed
Java 端:https://github.com/xiaojinlai/vue-login-java
Java 端 - Kotlin 版本:https://github.com/xiaojinlai/vue-login-kotlin
注:Kotlin 版本只是我本人用習(xí)慣了 Kotlin,就功能而言與 Java 是一樣的。大家如果不喜歡可以不用理會(huì),如果有感興趣的可以看看,Kotlin 是 Google 推出的一種簡潔性語言,主推在 Android 上,用習(xí)慣后還是蠻喜歡的。學(xué)習(xí)起來也不難,內(nèi)容也不多,推薦一個(gè)學(xué)習(xí) Kotlin 的網(wǎng)址:https://www.kotlincn.net/docs/reference/
更多精彩推薦
??外包程序員入職螞蟻金服被質(zhì)疑,網(wǎng)友:人生污點(diǎn)??前后端分離三連問:為何分離?如何分離?分離后的接口規(guī)范???如何設(shè)計(jì)一個(gè)通用的權(quán)限管理系統(tǒng)??去一家小公司從0到1搭建后端架構(gòu),做個(gè)總結(jié)!??這應(yīng)該是全網(wǎng)最全的Git分支開發(fā)規(guī)范手冊~
最后,推薦給大家一個(gè)有趣有料的公眾號(hào):寫代碼的渣渣鵬,7年老程序員教你寫bug,回復(fù) 面試或資源 送一你整套開發(fā)筆記 有驚喜哦

