前后端分離開發是當今開發的主流。本篇文章從零開始,一步步使用SpringBoot結合Vue來實現日常開發中最常見的登錄功能,以及登錄之后對用戶的管理功能。通過這個例子,可以快速入門SpringBoot+Vue前后端分離的開發。
前言
1、前后端分離簡介
在這里首先簡單說明一下什么是前后端分離和單頁式應用:前后端分離 的核心思想是前端頁面通過 ajax 調用后端的 restuful api 進行數據交互,而 單頁面應用(single page web application,SPA),就是只有一個頁面,並在用戶與應用程序交互時動態更新該頁面的 Web 應用程序。
2、示例所用技術簡介
簡單說明以下本示例中所用到的技術,如圖所示:
后端
-
SpringBoot:SpringBoot是當前最流行的Java后端框架。可以簡單地看成簡化了的、按照約定開發的SSM(H), 大大提升了開發速度。
-
MybatisPlus: MyBatis-Plus(簡稱 MP)是一個 MyBatis的增強工具,在 MyBatis 的基礎上只做增強不做改變,為簡化開發、提高效率而生。
前端:
-
Vue :Vue 是一套用於構建用戶界面的漸進式框架。盡管Vue3已經發布,但是至少一段時間內主流應用還是vue2.x,所以示例里還是采用Vue2.x版本。
-
ElementUI: ElementUI 是目前國內最流行的Vue UI框架。組件豐富,樣式眾多,也比較符合大眾審美。雖然一度傳出停止維護更新的傳聞,但是隨着Vue3的發布,官方也Beta了適配Vue3的ElementPlus。
數據庫:
-
MySQL:MySQL是一個流行的開源關系型數據庫。
上面已經簡單介紹了本實例用到的技術,在開始本實例之前,最好能對以上技術具備一定程度的掌握。
一、環境准備
1、前端
1.1、安裝Node.js
前端項目使用 veu-cli
腳手架,vue-cli
需要通過npm
安裝,是而 npm 是集成在 Node.js 中的,所以第一步我們需要安裝 Node.js,訪問官網 https://nodejs.org/en/,首頁即可下載。
下載完成后運行安裝包,一路下一步就行。然后在 cmd 中輸入 node -v
,檢查是否安裝成功。
如圖,出現了版本號(根據下載時候的版本確定),說明已經安裝成功了。同時,npm 包也已經安裝成功,可以輸入 npm -v
查看版本號
1.2、配置NPM源
NPM原始的源是在國外的服務器上,下載東西比較慢。
可以通過兩種方式來提升下載速度。
-
下載時指定源
//本次從淘寶倉庫源下載 npm --registry=https://registry.npm.taobao.org install
-
配置源為淘寶倉庫
//設置淘寶源 npm config set registry https://registry.npm.taobao.org
也可以安裝 cnpm ,但是使用中可能會遇到一些問題。
1.3、安裝vue-cli腳手架
使用如下命令安裝 vue-cli
腳手架:
npm install -g vue-cli
注意此種方式安裝的是 2.x 版本的 Vue CLI,最新版本需要通過 npm install -g @vue/cli
安裝。新版本可以使用圖形化界面初始化項目,並加入了項目健康監控等內容。
1.4、VS Code
前端的開發工具采用的當下最流行的前端開發工具 VS code。
官網:https://code.visualstudio.com
下載對應的版本,一步步安裝即可。安裝之后,初始界面如下:
VS Code安裝后,我們一般還需要搜索安裝一些所需要的插件輔助開發。安裝插件很簡單,在搜索面板中查找到后,直接安裝即可。
一般會安裝這些插件:
- Chinese:中文語言插件
- Vetur:Vue多功能集成插件,包括:語法高亮,智能提示,emmet,錯誤提示,格式化,自動補全,debugger。vscode官方欽定Vue插件,Vue開發者必備。
- ESLint:ESLint 是一個語法規則和代碼風格的檢查工具,可以用來保證寫出語法正確、風格統一的代碼。
- VS Code - Debugger for Chrome:結合Chrome進行調試的插件。
- Beautify:Beautify 插件可以快速格式化你的代碼格式,讓你在編寫代碼時雜亂的代碼結構瞬間變得非常規整。
1.5、Chrome
Chrome 是比較流行的瀏覽器,也是我們前端開發的常用工具。
Chrome 下載途徑很多,請自行搜索下載安裝。
Chrome下載安裝完成之后,建議安裝一個插件 Vue.js devtools
,是非常好用的 vue 調試工具。
谷歌商店下載地址:https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd
2、后端
- 后端采用的jdk版本是1.8,具體安裝可以參考 Win10系統安裝與配置JDK1.8
- 采用的maven版本是3.5,安裝配置可參考 Maven系列教材 (二)- 下載與配置Maven。
- 開發工具采用的是Idea,安裝請自行查找。
3、數據庫
數據庫采用的是MySQL5.7,安裝可以參考: Win10配置免安裝版MySQL5.7
二、項目搭建
1、前端項目搭建
1.1、創建項目
這里使用命令行來創建項目,在工作文件下新建目錄。
然后執行命令 vue init webpack demo-vue
,這里 webpack 是以 webpack 為模板指生成項目,還可以替換為 pwa、simple 等參數,這里不再贅述。 demo-vue 是項目名稱,也可以起別的名字。
在程序執行的過程中會有一些提示,可以按照默認的設定一路回車下去,也可以按需修改。
需要注意的是詢問是否安裝 vue-router,一定要選是,也就是回車或按 Y,vue-router 是構建單頁面應用的關鍵。
OK,可以看到目錄下完成了項目的構建,基本結構如下。
1.2、項目運行
使用VS code打開初始化完成的vue項目。
在vs code 中點擊終端,輸入命令 npm run dev
運行項目。
項目運行成功:
訪問地址:http://localhost:8080,就可以查看網頁Demo。
1.3、項目結構說明
在vs code 中可以看到項目結構如下:
詳細的目錄項說明:
來重點看下標紅旗的幾個文件。
1.3.1、index.html
首頁文件的初始代碼如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>demo-vue</title>
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
需要注意的是 <div id="app"></div>
這一行帶代碼,下面有一行注釋,構建的文件將會被自動注入,也就是說我們編寫的其它的內容都將在這個 div 中展示。
所謂單頁面應用,就是整個項目只有這一個 html 文件,當我們打開這個應用,表面上可以有很多頁面,實際上它們都是動態地加載在一個 div
中。
1.3.2、App.vue
這個文件稱為“根組件”,因為其它的組件又都包含在這個組件中。
.vue
文件是一種自定義文件類型,在結構上類似 html,一個 .vue 文件即是一個 vue 組件。先看它的初始代碼:
<template>
<div id="app">
<img src="./assets/logo.png">
<router-view/>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
這里也有一句 <div id="app">
,但跟 index.html 里的那個是沒有關系的。這個只是普通的div塊。
<script>
標簽里的內容即該組件的腳本,也就是 js 代碼,export default 是 ES6 的語法,意思是將這個組件整體導出,之后就可以使用 import 導入組件了。大括號里的內容是這個組件的相關屬性。
這個文件最關鍵的一點其實是第四行, <router-view/>
,是一個容器,名字叫“路由視圖”,意思是當前路由( URL)指向的內容將顯示在這個容器中。也就是說,其它的組件即使擁有自己的路由(URL,需要在 router 文件夾的 index.js 文件里定義),也只不過表面上是一個單獨的頁面,實際上只是在根組件 App.vue 中。
1.3.3、main.js
App.vue 和 index.html是怎么聯系的?關鍵點就在於這個文件:
import Vue from 'vue'
import App from './App'
import router from './router'
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})
最上面 import 了幾個模塊,其中 vue 模塊在 node_modules 中,App 即 App.vue 里定義的組件,router 即 router 文件夾里定義的路由。
Vue.config.productionTip = false ,作用是阻止vue 在啟動時生成生產提示。
在這個 js 文件中,我們創建了一個 Vue 對象(實例),el 屬性提供一個在頁面上已存在的 DOM 元素作為 Vue 對象的掛載目標,router 代表該對象包含 Vue Router,並使用項目中定義的路由。components 表示該對象包含的 Vue 組件,template 是用一個字符串模板作為 Vue 實例的標識使用,類似於定義一個 html 標簽。
1.3.4、router/index.js
前面說到了vue-router是單式應用的關鍵,這里我們來看一下 router/index.js
文件:
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'HelloWorld',
component: HelloWorld
}
]
})
最上面 import 了幾個組件,在 routes
這個數組里定義了路由,可以看到 /
路徑路由到了 HelloWorld
這個組件,所以訪問 http://localhost:8080/ 會看到上面的界面。為了更直觀的理解,這里可以對 src\components\HelloWorld.vue
組件進行修改,修改如下:
<template>
<div id="demo">
{{msg}}
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data () {
return {
msg: 'Hello Vue!'
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
#demo{
background-color: bisque;
font-size: 20pt;
color:darkcyan;
margin-left: 30%;
margin-right: 30%;
}
</style>
vue-cli會我們的更改進行熱更新,再次打開 http://localhost:8080/,界面發生改變:
2、后端項目搭建
2.1、后端項目創建
后端項目創建如下:
- 打開Idea,
New Project
,選擇Spring Intializr
- 填入項目的相關信息
- SpringBoot版本選擇了 2.3.8 , 選擇了web 和 MySQL驅動依賴
- 創建完成的項目
- 項目完整pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.8.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.fighter3</groupId>
<artifactId>demo-java</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo-java</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.3、引入MybatisPlus
如果對MybatisPlus不熟悉,入門可以參考 SpringBoot學習筆記(十七:MyBatis-Plus )
想了解更多可以直接查看官網。
2.3.1、引入MP依賴
<!--mybatis-plus依賴-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
由於本實例的數據庫表非常簡單,只有一個單表,所以這里我們直接將基本的增刪改查寫出來
2.3.2、數據庫創建
數據庫設計非常簡單,只有一張表。
建表語句如下:
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`login_name` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '登錄名',
`user_name` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用戶名',
`password` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密碼',
`sex` varchar(8) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '性別',
`email` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '郵箱',
`address` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '地址',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
2.3.3、配置
在application.properties
中寫入相關配置:
# 服務端口號
server.port=8088
# 數據庫連接配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/demo?characterEncoding=utf-8&allowMultiQueries=true&serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root
在啟動類里添加 @MapperScan
注解,掃描 Mapper 文件夾:
@SpringBootApplication
@MapperScan("cn.fighter3.mapper")
public class DemoJavaApplication {
public static void main(String[] args) {
SpringApplication.run(DemoJavaApplication.class, args);
}
}
2.3.3、相關代碼
MP提供了代碼生成器的功能,可以按模塊生成Controller、Service、Mapper、實體類的代碼。在數據庫表比較多的情況下,能提升開發效率。官網給出了一個Demo,有興趣的可以自行查看。
- 實體類
/**
* @Author: 三分惡
* @Date: 2021/1/17
* @Description: 用戶實體類
**/
@TableName(value = "user")
public class User {
@TableId(type = IdType.AUTO)
private Integer id;
private String loginName;
private String userName;
private String password;
private String sex;
private String email;
private String address;
//省略getter、setter等
}
- Mapper接口:繼承BaseMapper即可
/**
* @Author: 三分惡
* @Date: 2021/1/17
* @Description: TODO
**/
public interface UserMapper extends BaseMapper<User> {
}
OK,到此單表的增刪改查功能已經完成了,是不是很簡單。
可以寫一個單元測試測一下。
2.3.4、單元測試
@SpringBootTest
class UserMapperTest {
@Autowired
UserMapper userMapper;
@Test
@DisplayName("插入數據")
public void testInsert(){
User user=new User("test1","test","t123","男","test1@qq.com","滿都鎮");
Integer id=userMapper.insert(user);
System.out.printf(id.toString());
}
@Test
@DisplayName("根據id查找")
public void testSelectById(){
User user=userMapper.selectById(1);
System.out.println(user.toString());
}
@Test
@DisplayName("查找所有")
public void testSelectAll(){
List userList=userMapper.selectObjs(null);
System.out.println(userList.size());
}
@Test
@DisplayName("更新")
public void testUpdate(){
User user=new User();
user.setId(1);
user.setAddress("金葫蘆鎮");
Integer id=userMapper.updateById(user);
System.out.println(id);
}
@Test
@DisplayName("刪除")
public void testDelete(){
userMapper.deleteById(1);
}
}
至此前后端項目基本搭建完成,接下來開始進行功能開發。
三、登錄功能開發
1、前端開發
1.1、登錄界面
在前面訪問頁面的時候,有一個 V logo,看起來比較奇怪,我們先把它去掉,這個圖片的引入是在根組件中——src\App.vue
,把下面一行注釋或者去掉。
在src目錄下新建文件夾views,在views下新建文件 login.vue
<template>
<div>
<h3>登錄</h3>
用戶名:<input type="text" v-model="loginForm.loginName" placeholder="請輸入用戶名"/>
<br><br>
密碼: <input type="password" v-model="loginForm.password" placeholder="請輸入密碼"/>
<br><br>
<button>登錄</button>
</div>
</template>
<script>
export default {
name: 'Login',
data () {
return {
loginForm: {
loginName: '',
password: ''
},
responseResult: []
}
},
methods: {
}
}
</script>
1.2、添加路由
在 config\index.js
里添加路由,代碼如下:
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
//導入登錄頁面組件
import Login from '@/views/login.vue'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'HelloWorld',
component: HelloWorld
},
//添加登錄頁面路由
{
path:'/login',
name: 'Login',
component: Login
}
]
})
OK,現在在瀏覽器里輸入 http://localhost:8080/#/login
,就可以訪問登錄頁面:
頁面有點粗糙簡陋對不對,沒關系,我們可以引入ElmentUI
,使用ElementUI中已經成型的組件。
1.3、引入ElementUI美化界面
Element 的官方地址為 http://element-cn.eleme.io/#/zh-CN ,官方文檔比較好懂,大部分組件復制粘貼即可。
1.3.1、安裝Element UI
在vscode 中打開終端,運行命令npm i element-ui -S
,就安裝了 element ui 最新版本—當前是 2.15.0
1.3.2、引入 Element
引入分為完整引入和按需引入兩種模式,按需引入可以縮小項目的體積,這里我們選擇完整引入。
根據文檔,我們需要修改 main.js 為如下內容:
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
//引入ElementUI
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.config.productionTip = false
/* eslint-disable no-new */
Vue.use(ElementUI)
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})
1.3.3、使用ElementUI美化登錄頁面
現在開始使用 ElementUI和 css美化我們的登錄界面,修改后的login.vue
代碼如下:
<template>
<body id="login-page">
<el-form class="login-container" label-position="left" label-width="0px">
<h3 class="login_title">系統登錄</h3>
<el-form-item>
<el-input
type="text"
v-model="loginForm.loginName"
auto-complete="off"
placeholder="賬號"
></el-input>
</el-form-item>
<el-form-item>
<el-input
type="password"
v-model="loginForm.password"
auto-complete="off"
placeholder="密碼"
></el-input>
</el-form-item>
<el-form-item style="width: 100%">
<el-button
type="primary"
style="width: 100%; border: none"
>登錄</el-button
>
</el-form-item>
</el-form>
</body>
</template>
<script>
export default {
name: "Login",
data() {
return {
loginForm: {
loginName: "",
password: "",
},
responseResult: [],
};
},
methods: {},
};
</script>
<style scoped>
#login-page {
background: url("../assets/img/bg.jpg") no-repeat;
background-position: center;
height: 100%;
width: 100%;
background-size: cover;
position: fixed;
}
body {
margin: 0px;
}
.login-container {
border-radius: 15px;
background-clip: padding-box;
margin: 90px auto;
width: 350px;
padding: 35px 35px 15px 35px;
background: #fff;
border: 1px solid #eaeaea;
box-shadow: 0 0 25px #cac6c6;
}
.login_title {
margin: 0px auto 40px auto;
text-align: center;
color: #505458;
}
</style>
需要注意:
-
在
src\assets
路徑下新建一個一個文件夾img
,在 img 里放了一張網上找到的無版權圖片作為背景圖 -
App.vue
里刪了一行代碼,不然會有空白:margin-top: 60px;
好了,看看我們修改之后的登錄界面效果:
OK,登錄界面的面子已經做好了,但是里子還是空的,沒法和后台交互。
1.4、引入axios發起請求
相信大家都對 ajax 有所了解,前后端分離情況下,前后端交互的模式是前端發出異步式請求,后端返回 json 。
axios 是一個基於Promise 用於瀏覽器和 nodejs 的 HTTP 客戶端,本質上也是對原生XHR的封裝,只不過它是Promise的實現版本,符合最新的ES規范。在這里我們只需要知道它是非常強大的網絡請求處理庫,且得到廣泛應用即可。
在項目目錄下運行命令 npm install --save axios
,安裝模塊:
在 main.js
里全局注冊 axios:
var axios = require('axios')
// 全局注冊,之后可在其他組件中通過 this.$axios 發送數據
Vue.prototype.$axios = axios
那么怎么使用 axios
發起請求呢?
在 login.vue
中添加方法:
methods: {
login () {
this.$axios
.post('/login', {
loginName: this.loginForm.loginName,
password: this.loginForm.password
})
.then(successResponse => {
if (successResponse.data.code === 200) {
this.$router.replace({path: '/'})
}
})
.catch(failResponse => {
})
}
},
這個方法里通過 axios 向后台發起了請求,如果返回成功的結果就跳轉到 /
路由下。
在登錄按鈕里觸發這個方法:
<el-button
type="primary"
style="width: 100%; border: none"
@click="login"
>登錄</el-button
>
那么現在就能向后台發起請求了嗎?還沒完。
1.5、前端相關配置
-
反向代理
修改
src\main.js
,添加反向代理的配置:// 設置反向代理,前端請求默認發送到 http://localhost:8888/api axios.defaults.baseURL = 'http://localhost:8088/api'
這么一來,我們在前面寫的登錄請求,訪問的后台地址實際就是 http://localhost:8088/api/login
-
跨域配置
前后端分離會帶來一個問題—跨域,關於跨域,這里就不展開講解。在
config\index.js
中,找到 proxyTable 位置,修改為以下內容:proxyTable: { '/api': { target: 'http://localhost:8088', changeOrigin: true, pathRewrite: { '^/api': '' } } },
2、后端開發
2.1、統一結果封裝
這里我們創建了一個 Result 類,用於異步統一返回的結果封裝。一般來說,結果里面有幾個要素必要的
- 是否成功,可用 code 表示(如 200 表示成功,400 表示異常)
- 結果消息
- 結果數據
/**
* @Author: 三分惡
* @Date: 2021/1/17
* @Description: 統一結果封裝
**/
public class Result {
//相應碼
private Integer code;
//信息
private String message;
//返回數據
private Object data;
//省略getter、setter、構造方法
}
實際上由於響應碼是固定的,code
屬性應該是一個枚舉值,這里作了一些簡化。
2.2、登錄業務實體類
為了接收前端登錄的數據,我們這里創建了一個登錄用的業務實體類:
public class LoginDTO {
private String loginName;
private String password;
//省略getter、setter
}
2.3、控制層
LoginController,進行業務響應:
/**
* @Author: 三分惡
* @Date: 2021/1/17
* @Description: TODO
**/
@RestController
public class LoginController {
@Autowired
LoginService loginService;
@PostMapping(value = "/api/login")
@CrossOrigin //后端跨域
public Result login(@RequestBody LoginDTO loginDTO){
return loginService.login(loginDTO);
}
}
2.4、業務層
業務層進行實際的業務處理。
- LoginService:
public interface LoginService {
public Result login(LoginDTO loginDTO);
}
- LoginServiceImpl:
/**
* @Author: 三分惡
* @Date: 2021/1/17
* @Description:
**/
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private UserMapper userMapper;
@Override
public Result login(LoginDTO loginDTO) {
if (StringUtils.isEmpty(loginDTO.getLoginName())){
return new Result(400,"賬號不能為空","");
}
if (StringUtils.isEmpty(loginDTO.getPassword())){
return new Result(400,"密碼不能為空","");
}
//通過登錄名查詢用戶
QueryWrapper<User> wrapper = new QueryWrapper();
wrapper.eq("login_name", loginDTO.getLoginName());
User uer=userMapper.selectOne(wrapper);
//比較密碼
if (uer!=null&&uer.getPassword().equals(loginDTO.getPassword())){
return new Result(200,"",uer);
}
return new Result(400,"登錄失敗","");
}
}
啟動后端項目:
訪問登錄界面,效果如下:
這樣一個簡答的登錄就完成了,接下來,我們會對這個登錄進一步完善。
四、登錄功能完善
前面雖然實現了登錄,但只是一個簡單的登錄跳轉,實際上並不能對用戶的登錄狀態進行判別,接下來我們進一步完善登錄功能。
首先開始后端的開發。
1、后端開發
1.1、攔截器
在前后端分離的情況下,比較流行的認證方案是 JWT認證
認證,和傳統的session認證不同,jwt是一種無狀態的認證方法,也就是服務端不再保存任何認證信息。出於篇幅考慮,我們這里不再引入 JWT
,只是簡單地判斷一下前端的請求頭里是否存有 token
。對JWT 認證感興趣的可以查看文章:SpringBoot學習筆記(十三:JWT ) 。
- 創建
interceptor
包,包下新建攔截器LoginInterceptor
/**
* @Author: 三分惡
* @Date: 2021/1/18
* @Description: 用戶登錄攔截器
**/
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
//從header中獲取token
String token = request.getHeader("token");
//如果token為空
if (StringUtils.isBlank(token)) {
setReturn(response,401,"用戶未登錄,請先登錄");
return false;
}
//在實際使用中還會:
// 1、校驗token是否能夠解密出用戶信息來獲取訪問者
// 2、token是否已經過期
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
}
//返回json格式錯誤信息
private static void setReturn(HttpServletResponse response, Integer code, String msg) throws IOException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtil.getOrigin());
//UTF-8編碼
httpResponse.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
Result result = new Result(code,msg,"");
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(result);
httpResponse.getWriter().print(json);
}
}
- 為了能給前端返回 json 格式的結果,這里還用到了一個工具類,新建
util
包,util 包下新建工具類HttpContextUtil
/**
* @Author: 三分惡
* @Date: 2021/1/18
* @Description: http上下文
**/
public class HttpContextUtil {
public static HttpServletRequest getHttpServletRequest() {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
}
public static String getDomain() {
HttpServletRequest request = getHttpServletRequest();
StringBuffer url = request.getRequestURL();
return url.delete(url.length() - request.getRequestURI().length(), url.length()).toString();
}
public static String getOrigin() {
HttpServletRequest request = getHttpServletRequest();
return request.getHeader("Origin");
}
}
1.2、攔截器配置
攔截器創建完成之后,還需要進行配置。
/**
* @Author: 三分惡
* @Date: 2021/1/18
* @Description: web配置
**/
@Configuration
public class DemoWebConfig implements WebMvcConfigurer {
/**
* 攔截器配置
*
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
//添加攔截器
registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/api/**")
//放行路徑,可以添加多個
.excludePathPatterns("/api/login");
}
}
1.3、跨域配置
細致的同學可能會發現,在之前的后台接口,有一個注解@CrossOrigin
,這個注解是用來跨域的,每個接口都寫一遍肯定是不太方便的,這里我們 創建跨域配置類並添加統一的跨域配置:
/**
* @Author 三分惡
* @Date 2021/1/25
* @Description 跨域配置
*/
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
//允許源,這里允許所有源訪問,實際應用會加以限制
corsConfiguration.addAllowedOrigin("*");
//允許所有請求頭
corsConfiguration.addAllowedHeader("*");
//允許所有方法
corsConfiguration.addAllowedMethod("*");
source.registerCorsConfiguration("/**", corsConfiguration);
return new CorsFilter(source);
}
}
1.3、登錄service
這樣一來,后端就需要生成一個 token
返回給前端,所以更改 LoginServiceImpl
里的登錄方法。
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private UserMapper userMapper;
@Override
public Result login(LoginDTO loginDTO) {
if (StringUtils.isEmpty(loginDTO.getLoginName())){
return new Result(400,"賬號不能為空","");
}
if (StringUtils.isEmpty(loginDTO.getPassword())){
return new Result(400,"密碼不能為空","");
}
//通過登錄名查詢用戶
QueryWrapper<User> wrapper = new QueryWrapper();
wrapper.eq("login_name", loginDTO.getLoginName());
User uer=userMapper.selectOne(wrapper);
//比較密碼
if (uer!=null&&uer.getPassword().equals(loginDTO.getPassword())){
LoginVO loginVO=new LoginVO();
loginVO.setId(uer.getId());
//這里token直接用一個uuid
//使用jwt的情況下,會生成一個jwt token,jwt token里會包含用戶的信息
loginVO.setToken(UUID.randomUUID().toString());
loginVO.setUser(uer);
return new Result(200,"",loginVO);
}
return new Result(401,"登錄失敗","");
}
}
其中對返回的data
封裝了一個VO:
/**
* @Author: 三分惡
* @Date: 2021/1/18
* @Description: 登錄VO
**/
public class LoginVO implements Serializable {
private Integer id;
private String token;
private User user;
//省略getter、setter
}
最后,測試一下登錄接口:
OK,沒有問題。
2、前端開發
前面我們使用了后端攔截器,接下來我們嘗試用前端實現相似的功能。
實現前端登錄器,需要在前端判斷用戶的登錄狀態。我們可以像之前那樣在組件的 data 中設置一個狀態標志,但登錄狀態應該被視為一個全局屬性,而不應該只寫在某一組件中。所以我們需要引入一個新的工具——Vuex,它是專門為 Vue 開發的狀態管理方案,我們可以把需要在各個組件中傳遞使用的變量、方法定義在這里。
2.1引入Vuex
首先在終端里使用命令 npm install vuex --save
來安裝 Vuex 。
在 src 目錄下新建一個文件夾 store,並在該目錄下新建 index.js 文件,在該文件中引入 vue 和 vuex,代碼如下:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
接下來,在index.js
里設置我們需要的狀態變量和方法。為了實現登錄攔截器,我們需要一個記錄token的變量量。同時為了全局使用用戶信息,我們還需要一個記錄用戶信息的變量。還需要改變變量值的mutations。完整的代碼如下:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
token: sessionStorage.getItem("token"),
user: JSON.parse(sessionStorage.getItem("user"))
},
mutations: {
// set
SET_TOKENN: (state, token) => {
state.token = token
sessionStorage.setItem("token", token)
},
SET_USER: (state, user) => {
state.user = user
sessionStorage.setItem("user", JSON.stringify(user))
},
REMOVE_INFO : (state) => {
state.token = ''
state.user = {}
sessionStorage.setItem("token", '')
sessionStorage.setItem("user", JSON.stringify(''))
}
},
getters: {
},
actions: {
},
modules: {
}
})
這里我們還用到了 sessionStorage
,使用sessionStorage
,關掉瀏覽器的時候會被清除掉,和 localStorage
相比,比較利於保證實時性。
2.2、修改路由配置
為了能夠區分哪些路由需要被攔截,我們在路由里添上一個元數據requireAuth
來做是否需要攔截的判斷:
{
path: '/',
name: 'HelloWorld',
component: HelloWorld,
meta: {
requireAuth: true
}
},
完整的 src\router\index.js
代碼如下:
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
//導入登錄頁面組件
import Login from '@/views/login.vue'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'HelloWorld',
component: HelloWorld,
meta: {
requireAuth: true
}
},
//添加登錄頁面路由
{
path:'/login',
name: 'Login',
component: Login
}
]
})
2.3、使用鈎子函數判斷是否攔截
上面我們添加了 requireAuth
, 接下來就要用到它了。
鈎子函數及在某些時機會被調用的函數。這里我們使用 router.beforeEach()
,意思是在訪問每一個路由前調用。
打開 src\main.js
,首先添加對 store
的引用
import store from './store'
並修改vue對象里的內容,使 store 能全局使用:
new Vue({
el: '#app',
router,
// 注意這里
store,
components: { App },
template: '<App/>'
})
解下來,我們寫beforeEach()
函數,邏輯很簡單,判斷是否需要登錄,如果是,判斷 store中是否存有token ,是則放行,否則跳轉到登錄頁。
//鈎子函數,訪問路由前調用
router.beforeEach((to, from, next) => {
//路由需要認證
if (to.meta.requireAuth) {
//判斷store里是否有token
if (store.state.token) {
next()
} else {
next({
path: 'login',
query: { redirect: to.fullPath }
})
}
} else {
next()
}
}
)
完整的 main.js 代碼如下:
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
//引入ElementUI
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import store from './store'
var axios = require('axios')
// 全局注冊,之后可在其他組件中通過 this.$axios 發送數據
Vue.prototype.$axios = axios
// 設置反向代理,前端請求默認發送到 http://localhost:8888/api
axios.defaults.baseURL = 'http://localhost:8088/api'
Vue.config.productionTip = false
/* eslint-disable no-new */
Vue.use(ElementUI)
//鈎子函數,訪問路由前調用
router.beforeEach((to, from, next) => {
//路由需要認證
if (to.meta.requireAuth) {
//判斷store里是否有token
if (store.state.token) {
next()
} else {
next({
path: 'login',
query: { redirect: to.fullPath }
})
}
} else {
next()
}
}
)
new Vue({
el: '#app',
router,
// 注意這里
store,
components: { App },
template: '<App/>'
})
2.4、請求封裝
我們前面寫的后端攔截器,對請求進行了攔截,要求請求頭里攜帶token,這個怎么處理呢?
答案是封裝axios
。
在 src 目錄下新建目錄 utils ,在uitls 目錄下新建文件 request.js 。
首先導入 axios
和 store
:
import axios from 'axios'
import store from '@/store'
接下來在請求攔截器中,給請求頭添加 token
:
// request 請求攔截
service.interceptors.request.use(
config => {
if (store.state.token) {
config.headers['token'] = window.sessionStorage.getItem("token")
}
return config
},
error => {
// do something with request error
console.log(error) // for debug
return Promise.reject(error)
}
)
完整的request.js:
import axios from 'axios'
import store from '@/store'
//const baseURL="localhost:8088/api"
//創建axios實例
const service = axios.create({
baseURL: process.env.BASE_API, // api的base_url
})
// request 請求攔截
service.interceptors.request.use(
config => {
if (store.getters.getToken) {
config.headers['token'] = window.sessionStorage.getItem("token")
}
return config
},
error => {
// do something with request error
console.log(error) // for debug
return Promise.reject(error)
}
)
//response響應攔截
axios.interceptors.response.use(response => {
let res = response.data;
console.log(res)
if (res.code === 200) {
return response
} else {
return Promise.reject(response.data.msg)
}
},
error => {
console.log(error)
if (error.response.data) {
error.message = error.response.data.msg
}
if (error.response.status === 401) {
router.push("/login")
}
return Promise.reject(error)
}
)
export default service
注意創建axios實例里用到了 baseUrl ,在 config\dev.env.js
里修改配置:
module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
BASE_API: '"http://localhost:8088/api"',
})
這樣一封裝,我們就不用每個請求都手動來塞 token,或者來做一些統一的異常處理,一勞永逸。 而且我們的 api 可以根據 env
環境變量動態切換。
2.5、封裝api
request.js 既然已經封裝了,那么接下來就要開始用它。
我們可以像上面的 axios
添加到 main.js 中,這樣就能被全局調用。但是有更好的用法。
一般項目中,viess
下放的是我們各個業務模塊的視圖,對應這些業務模塊,我們創建對應的 api
來封裝對后台的請求,這樣即使業務模塊很多,但關系仍然是比較清晰的。
在 src 下新建 api
文件夾,在 api
文件夾下新建 user.js
,在user.js 中我們封裝了登錄的后台請求:
import request from '@/utils/request'
export function userLogin(data) {
return request({
url: '/login',
method: 'post',
data
})
}
當然,事實上登錄用 request.js
不合適,因為request.js
攔截了token,但登錄就是為了獲取token——所以😅湊合着看吧,誰叫現在就這一個接口呢。
2.6、login.vue
之前的登錄組件中,我們只是判斷后端返回的狀態碼,如果是 200,就重定向到首頁。在經過前面的配置后,我們需要修改一下登錄邏輯,以最終實現登錄攔截。
修改后的邏輯如下:
1.點擊登錄按鈕,向后端發送數據
2.受到后端返回的成功代碼時,觸發 store
中的 mutation
,存儲token 和user,
3.獲取登錄前頁面的路徑並跳轉,如果該路徑不存在,則跳轉到首頁
修改后的 login()
方法如下:
login() {
var _this = this;
userLogin({
loginName: this.loginForm.loginName,
password: this.loginForm.password,
}).then((resp) => {
let code=resp.data.code;
if(code===200){
let data=resp.data.data;
let token=data.token;
let user=data.user;
//存儲token
_this.$store.commit('SET_TOKENN', token);
//存儲user,優雅一點的做法是token和user分開獲取
_this.$store.commit('SET_USER', user);
console.log(_this.$store.state.token);
var path = this.$route.query.redirect
this.$router.replace({path: path === '/' || path === undefined ? '/' : path})
}
});
完整的login.vue
:
<template>
<body id="login-page">
<el-form class="login-container" label-position="left" label-width="0px">
<h3 class="login_title">系統登錄</h3>
<el-form-item>
<el-input
type="text"
v-model="loginForm.loginName"
auto-complete="off"
placeholder="賬號"
></el-input>
</el-form-item>
<el-form-item>
<el-input
type="password"
v-model="loginForm.password"
auto-complete="off"
placeholder="密碼"
></el-input>
</el-form-item>
<el-form-item style="width: 100%">
<el-button
type="primary"
style="width: 100%; border: none"
@click="login"
>登錄</el-button
>
</el-form-item>
</el-form>
</body>
</template>
<script>
import { userLogin } from "@/api/user";
export default {
name: "Login",
data() {
return {
loginForm: {
loginName: "",
password: "",
},
responseResult: [],
};
},
methods: {
login() {
var _this = this;
userLogin({
loginName: this.loginForm.loginName,
password: this.loginForm.password,
}).then((resp) => {
let code=resp.data.code;
if(code===200){
let data=resp.data.data;
let token=data.token;
let user=data.user;
//存儲token
_this.$store.commit('SET_TOKENN', token);
//存儲user,優雅一點的做法是token和user分開獲取
_this.$store.commit('SET_USER', user);
console.log(_this.$store.state.token);
var path = this.$route.query.redirect
this.$router.replace({path: path === '/' || path === undefined ? '/' : path})
}
});
},
},
};
</script>
<style scoped>
#login-page {
background: url("../assets/img/bg.jpg") no-repeat;
background-position: center;
height: 100%;
width: 100%;
background-size: cover;
position: fixed;
}
body {
margin: 0px;
}
.login-container {
border-radius: 15px;
background-clip: padding-box;
margin: 90px auto;
width: 350px;
padding: 35px 35px 15px 35px;
background: #fff;
border: 1px solid #eaeaea;
box-shadow: 0 0 25px #cac6c6;
}
.login_title {
margin: 0px auto 40px auto;
text-align: center;
color: #505458;
}
</style>
2.7、HelloWorld.vue
大家應該還記得,到目前為止,我們 的 /
路徑還是指向 HelloWorld.vue
這個組件,為了演示 vuex
狀態的全局使用,我們做一些更改,添加一個生命周期的鈎子函數,來獲取 store
中存儲的用戶名:
computed: {
userName() {
return this.$store.state.user.userName
}
}
完整的 HelloWorld.vue
:
<template>
<div id="demo">
{{userName}}
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data () {
return {
msg: 'Hello Vue!'
}
},
computed: {
userName() {
return this.$store.state.user.userName
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
#demo{
background-color: bisque;
font-size: 20pt;
color:darkcyan;
margin-left: 30%;
margin-right: 30%;
}
</style>
我們看一下修改之后的整體效果:
訪問首頁會自動跳轉到登錄頁,登錄成功之后,會記錄登錄狀態。
F12
打開谷歌開發者工具:
- 打開
Application
,在Session Storage
中看到我們存儲的信息
- 打開
vue
開發工具,在Vuex
中也能看到我們store
中的數據
- 再次登錄,打開Network,可以發現異步式請求請求頭里已經添加了
token
再次說一下,這里偷了懶,登錄用封裝的公共請求方法是不合理的,畢竟登錄就是為了獲取token,request.js又對token進行了攔截,所以我懟我自己😂 比較好的做法可以參考 vue-element-admin
,在 store 中寫 action
用來登錄。
五、用戶管理功能
上面我們已經寫了一個簡單的登錄功能,通過這個功能,基本可以對SpringBoot+Vue前后端分離開發有有一個初步了解,在實際工作中,一般的工作都是基於基本框架已經成型的項目,登錄、鑒權、動態路由、請求封裝這些基礎功能可能都已經成型。所以后端的日常工作就是寫接口
、寫業務
,前端的日常工作就是 調接口
、寫界面
,通過接下來的用戶管理功能,我們能熟悉這些日常的開發。
1、后端開發
后端開發,crud就完了。
1.1、自定義分頁查詢
按照官方文檔,來進行MP的分頁。
1.1.1、分頁配置
首先需要對分頁進行配置,創建分頁配置類
/**
* @Author 三分惡
* @Date 2021/1/23
* @Description MP分頁設置
*/
@Configuration
@MapperScan("cn.fighter3.mapper.*.mapper*")
public class MybatisPlusConfig {
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// 設置請求的頁面大於最大頁后操作, true調回到首頁,false 繼續請求 默認false
// paginationInterceptor.setOverflow(false);
// 設置最大單頁限制數量,默認 500 條,-1 不受限制
// paginationInterceptor.setLimit(500);
// 開啟 count 的 join 優化,只針對部分 left join
paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
return paginationInterceptor;
}
}
1.1.2、自定義sql
作為Mybatis的增強工具,MP自然是支持自定義sql的。其實在MP中,單表操作基本上是不用自己寫sql。這里只是為了演示MP的自定義sql,畢竟在實際應用中,批量操作、多表操作還是更適合自定義sql實現。
- 修改pom.xml,在 <build>中添加:
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
- 配置文件:在application.properties中添加mapper掃描路徑及實體類別名包
# mybatis-plus
mybatis-plus.mapper-locations=classpath:cn/fighter3/mapper/*.xml
mybatis-plus.type-aliases-package=cn.fighter3.entity
- 在UserMapper.java 中定義分頁查詢的方法
IPage<User> selectUserPage(Page<User> page,String keyword);
- 在UserMapper.java 同級目錄下新建 UserMapper.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.fighter3.mapper.UserMapper">
<select id="selectUserPage" resultType="User">
select * from user
<where>
<if test="keyword !=null and keyword !='' ">
or login_name like CONCAT('%',#{keyword},'%')
or user_name like CONCAT('%',#{keyword},'%')
or email like CONCAT('%',#{keyword},'%')
or address like CONCAT('%',#{keyword},'%')
</if>
</where>
</select>
</mapper>
這個查詢也比較簡單,根據關鍵字查詢用戶。
OK,我們的自定義分頁查詢就完成了,可以寫個單元測試測一下。
1.2、控制層
新建UserControler,里面也沒什么東西,增刪改查的接口:
/**
* @Author 三分惡
* @Date 2021/1/23
* @Description 用戶管理
*/
@RestController
public class UserController {
@Autowired
private UserService userService;
/**
* 分頁查詢
* @param queryDTO
* @return
*/
@PostMapping("/api/user/list")
public Result userList(@RequestBody QueryDTO queryDTO){
return new Result(200,"",userService.selectUserPage(queryDTO));
}
/**
* 添加
* @param user
* @return
*/
@PostMapping("/api/user/add")
public Result addUser(@RequestBody User user){
return new Result(200,"",userService.addUser(user));
}
/**
* 更新
* @param user
* @return
*/
@PostMapping("/api/user/update")
public Result updateUser(@RequestBody User user){
return new Result(200,"",userService.updateUser(user));
}
/**
* 刪除
* @param id
* @return
*/
@PostMapping("/api/user/delete")
public Result deleteUser(Integer id){
return new Result(200,"",userService.deleteUser(id));
}
/**
* 批量刪除
* @param ids
* @return
*/
@PostMapping("/api/user/delete/batch")
public Result batchDeleteUser(@RequestBody List<Integer> ids){
userService.batchDelete(ids);
return new Result(200,"","");
}
}
這里寫的也比較簡單,直接調用服務層的方法。
1.3、服務層
接口這里就不再貼出了,實現類如下:
/**
* @Author 三分惡
* @Date 2021/1/23
* @Description
*/
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
/**
* 分頁查詢
**/
@Override
public IPage<User> selectUserPage(QueryDTO queryDTO) {
Page<User> page=new Page<>(queryDTO.getPageNo(),queryDTO.getPageSize());
return userMapper.selectUserPage(page,queryDTO.getKeyword());
}
@Override
public Integer addUser(User user) {
return userMapper.insert(user);
}
@Override
public Integer updateUser(User user) {
return userMapper.updateById(user);
}
@Override
public Integer deleteUser(Integer id) {
return userMapper.deleteById(id);
}
@Override
public void batchDelete(List<Integer> ids) {
userMapper.deleteBatchIds(ids);
}
}
這里也比較簡單,也沒什么業務邏輯。
實際上,業務層至少也會做一些參數校驗的工作——我見過有的系統,只是在客戶端進行了參數校驗,實際上,服務端參數校驗是必需的(如果不做,會被懟😔),因為客戶端校驗相比較服務端校驗是不可靠的。
在分頁查詢 public IPage<User> selectUserPage(QueryDTO queryDTO)
里用了一個業務對象,這種寫法,也可以用一些參數校驗的插件。
1.4、業務實體
上面用到了一個業務實體對象,創建一個 業務實體類QueryDTO
,定義了一些參數,這個類主要用於前端向后端傳輸數據,可以可以使用一些參數校驗插件添加參數校驗規則。
/**
* @Author 三分惡
* @Date 2021/1/23
* @Description 查詢業務實體
* 這里僅僅定義了三個參數,在實際應用中可以定義多個參數
*/
public class QueryDTO {
private Integer pageNo; //頁碼
private Integer pageSize; //頁面大小
private String keyword; //關鍵字
//省略getter、setter
}
簡單測一下,后端👌
2、前端開發
2.1、首頁
在前面,登錄之后,跳轉到HelloWorld,還是比較簡陋的。本來想直接跳到用戶管理的視圖,覺得不太好看,所以還是寫了一個首頁,當然這一部分不是重點。
見過一些后台管理系統的都知道,后台管理系統大概都是像下面的布局:
在ElementUI中提供了這樣的布局組件Container 布局容器:
大家都知道根組件是 App.vue ,當然在App.vue中寫整體布局是不合適的,因為還有登錄頁面,所以在 views 下新建 home.vue
,采用Container 布局容器來進行布局,使用NavMenu 導航菜單來創建側邊欄。
當然,比較好的做法是home.vue
里不寫什么內容,將頂部和側邊欄都抽出來作為子頁面(組件)。
<template>
<el-container class="home-container">
<!--頂部-->
<el-header style="margin-right: 15px; width: 100%">
<span class="nav-logo">😀</span>
<span class="head-title">Just A Demo</span>
<el-avatar
icon="el-icon-user-solid"
style="color: #222; float: right; padding: 20px"
>{{ this.$store.state.user.userName }}</el-avatar
>
</el-header>
<!-- 主體 -->
<el-container>
<!-- 側邊欄 -->
<el-aside width="13%">
<el-menu
:default-active="$route.path"
router
text-color="black"
active-text-color="red"
>
<el-menu-item
v-for="(item, i) in navList"
:key="i"
:index="item.name"
>
<i :class="item.icon"></i>
{{ item.title }}
</el-menu-item>
</el-menu>
</el-aside>
<el-main>
<!--路由占位符-->
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</template>
<script>
export default {
name: "Home",
data() {
return {
navList: [
{ name: "/index", title: "首頁", icon: "el-icon-s-home" },
{ name: "/user", title: "用戶管理",icon:"el-icon-s-custom" },
],
};
},
};
</script>
<style >
.nav-logo {
position: absolute;
padding-top: -1%;
left: 5%;
font-size: 40px;
}
.head-title {
position: absolute;
padding-top: 20px;
left: 15%;
font-size: 20px;
font-weight: bold;
}
</style>
注意 <el-main>
用了路由占位符 <router-view></router-view>
,在路由src\router\index.js
里進行配置,就可以加載我們的子路由了:
{
path: '/',
name: 'Default',
redirect: '/home',
component: Home
},
{
path: '/home',
name: 'Home',
component: Home,
meta: {
requireAuth: true
},
redirect: '/index',
children:[
{
path:'/index',
name:'Index',
component:() => import('@/views/home/index'),
meta:{
requireAuth:true
}
},
}
]
},
首頁本來不想放什么東西,后來想想,還是放了點大家愛看的——沒別的意思,快過年了,各位姐夫過年好。🏮😀
圖片來自冰冰微博,見水印。
2.2、用戶列表
在views
下新建 user
目錄,在 user
目錄下新建 index.vue
,然后添加為home的子路由:
{
path: '/home',
name: 'Home',
component: Home,
meta: {
requireAuth: true
},
redirect: '/index',
children:[
{
path:'/index',
name:'Index',
component:() => import('@/views/home/index'),
meta:{
requireAuth:true
}
},
{
path:'/user',
name:'User',
component:()=>import('@/views/user/index'),
meta:{
requireAuth:true
}
}
]
},
接下來開始用戶列表功能的編寫。
- 首先封裝一下api,在user.js中添加調用分頁查詢接口的api
//獲取用戶列表
export function userList(data) {
return request({
url: '/user/list',
method: 'post',
data
})
}
- 在
user/index.vue
中導入userList
import { userList} from "@/api/user";
- 為了在界面初始化的時候加載用戶列表,使用了生命周期鈎子來調用接口獲取用戶列表,代碼直接一鍋燉了
export default {
data() {
return {
userList: [], // 用戶列表
total: 0, // 用戶總數
// 獲取用戶列表的參數對象
queryInfo: {
keyword: "", // 查詢參數
pageNo: 1, // 當前頁碼
pageSize: 5, // 每頁顯示條數
},
}
created() { // 生命周期函數
this.getUserList()
},
methods: {
getUserList() {
userList(this.queryInfo)
.then((res) => {
if (res.data.code === 200) {
//用戶列表
this.userList = res.data.data.records;
this.total = res.data.data.total;
} else {
this.$message.error(res.data.message);
}
})
.catch((err) => {
console.log(err);
});
},
}
-
取到的數據,我們用一個表格組件來進行綁定
<!--表格--> <el-table :data="userList" border stripe > <el-table-column type="index" label="序號"></el-table-column> <el-table-column prop="userName" label="姓名"></el-table-column> <el-table-column prop="loginName" label="登錄名"></el-table-column> <el-table-column prop="sex" label="性別"></el-table-column> <el-table-column prop="email" label="郵箱"></el-table-column> <el-table-column prop="address" label="地址"></el-table-column> <el-table-column label="操作"> </el-table-column> </el-table>
效果如下,點擊用戶管理:
2.3、分頁
在上面的圖里,我們看到了在最下面有分頁欄,我們接下來看看分頁欄的實現。
我們這里使用了 Pagination 分頁組件:
<!--分頁區域-->
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="queryInfo.pageNo"
:page-sizes="[1, 2, 5, 10]"
:page-size="queryInfo.pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
>
</el-pagination>
兩個監聽事件:
// 監聽 pageSize 改變的事件
handleSizeChange(newSize) {
// console.log(newSize)
this.queryInfo.pageSize = newSize;
// 重新發起請求用戶列表
this.getUserList();
},
// 監聽 當前頁碼值 改變的事件
handleCurrentChange(newPage) {
// console.log(newPage)
this.queryInfo.pageNo = newPage;
// 重新發起請求用戶列表
this.getUserList();
},
2.4、檢索用戶
搜索框已經綁定了queryInfo.keyword
,只需要給頂部的搜索區域添加按鈕點擊和清空事件——重新獲取用戶列表:
<!--搜索區域-->
<el-input
placeholder="請輸入內容"
v-model="queryInfo.keyword"
clearable
@clear="getUserList"
>
<el-button
slot="append"
icon="el-icon-search"
@click="getUserList"
></el-button>
</el-input>
效果如下:
2.5、添加用戶
- 還是先寫api,導入后面就略過了
//添加用戶
export function userAdd(data) {
return request({
url: '/user/add',
method: 'post',
data
})
}
- 添加用戶我們用到了兩個組件 Dialog 對話框組件和 Form 表單組件。
<!--添加用戶的對話框-->
<el-dialog
title="添加用戶"
:visible.sync="addDialogVisible"
width="30%"
@close="addDialogClosed"
>
<!--內容主體區域-->
<el-form :model="userForm" label-width="70px">
<el-form-item label="登錄名" prop="loginName">
<el-input v-model="userForm.loginName"></el-input>
</el-form-item>
<el-form-item label="用戶名" prop="userName">
<el-input v-model="userForm.userName"></el-input>
</el-form-item>
<el-form-item label="密碼" prop="password">
<el-input v-model="userForm.password" show-password></el-input>
</el-form-item>
<el-form-item label="性別" prop="sex">
<el-radio v-model="userForm.sex" label="男">男</el-radio>
<el-radio v-model="userForm.sex" label="女">女</el-radio>
</el-form-item>
<el-form-item label="郵箱" prop="email">
<el-input v-model="userForm.email"></el-input>
</el-form-item>
<el-form-item label="地址" prop="address">
<el-input v-model="userForm.address"></el-input>
</el-form-item>
</el-form>
<!--底部按鈕區域-->
<span slot="footer" class="dialog-footer">
<el-button @click="addDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="addUser">確 定</el-button>
</span>
</el-dialog>
- 使用
addDialogVisible
控制對話框可見性,使用userForm
綁定修改用戶表單:
addDialogVisible: false, // 控制添加用戶對話框是否顯示
userForm: {
//用戶
loginName: "",
userName: "",
password: "",
sex: "",
email: "",
address: "",
},
- 兩個函數,
addUser
添加用戶,addDialogClosed
在對話框關閉時清空表單
//添加用戶
addUser() {
userAdd(this.userForm)
.then((res) => {
if (res.data.code === 200) {
this.addDialogVisible = false;
this.getUserList();
this.$message({
message: "添加用戶成功",
type: "success",
});
} else {
this.$message.error("添加用戶失敗");
}
})
.catch((err) => {
this.$message.error("添加用戶異常");
console.log(err);
});
},
// 監聽 添加用戶對話框的關閉事件
addDialogClosed() {
// 表單內容重置為空
this.$refs.addFormRef.resetFields();
},
效果:
在最后一頁可以看到我們添加的用戶:
2.6、修改用戶
- 先寫api
//修改用戶
export function userUpdate(data) {
return request({
url: '/user/update',
method: 'post',
data
})
}
- 在修改用戶這里,我們用到一個作用域插槽,通過
slot-scope="scope"
接收了當前作用域的數據,然后通過scope.row拿到對應這一行的數據,再綁定具體的屬性值就行了。
<el-table-column label="操作">
<!-- 作用域插槽 -->
<template slot-scope="scope">
<!--修改按鈕-->
<el-button
type="primary"
size="mini"
icon="el-icon-edit"
@click="showEditDialog(scope.row)"
></el-button>
</template>
</el-table-column>
- 具體的修改仍然是用對話框加表單的形式
<!--修改用戶的對話框-->
<el-dialog title="修改用戶" :visible.sync="editDialogVisible" width="30%">
<!--內容主體區域-->
<el-form :model="editForm" label-width="70px">
<el-form-item label="用戶名" prop="userName">
<el-input v-model="editForm.userName" :disabled="true"></el-input>
</el-form-item>
<el-form-item label="郵箱" prop="email">
<el-input v-model="editForm.email"></el-input>
</el-form-item>
<el-form-item label="地址" prop="address">
<el-input v-model="editForm.address"></el-input>
</el-form-item>
</el-form>
<!--底部按鈕區域-->
<span slot="footer" class="dialog-footer">
<el-button @click="editDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="editUser">確 定</el-button>
</span>
</el-dialog>
editDialogVisible
控制對話框顯示,editForm
綁定修改用戶表單
editDialogVisible: false, // 控制修改用戶信息對話框是否顯示
editForm: {
id: "",
loginName: "",
userName: "",
password: "",
sex: "",
email: "",
address: "",
},
showEditDialog
除了處理對話框顯示,還綁定了修改用戶對象。editUser
修改用戶。
// 監聽 修改用戶狀態
showEditDialog(userinfo) {
this.editDialogVisible = true;
console.log(userinfo);
this.editForm = userinfo;
},
//修改用戶
editUser() {
userUpdate(this.editForm)
.then((res) => {
if (res.data.code === 200) {
this.editDialogVisible = false;
this.getUserList();
this.$message({
message: "修改用戶成功",
type: "success",
});
} else {
this.$message.error("修改用戶失敗");
}
})
.catch((err) => {
this.$message.error("修改用戶異常");
console.loge(err);
});
},
2.7、刪除用戶
- api
//刪除用戶
export function userDelete(id) {
return request({
url: '/user/delete',
method: 'post',
params: {
id
}
})
}
-
在操作欄的作用域插槽里添加刪除按鈕,直接將作用域的id屬性傳遞進去
<el-table-column label="操作"> <!-- 作用域插槽 --> <template slot-scope="scope"> <!--修改按鈕--> <el-button type="primary" size="mini" icon="el-icon-edit" @click="showEditDialog(scope.row)" ></el-button> <!--刪除按鈕--> <el-button type="danger" size="mini" icon="el-icon-delete" @click="removeUserById(scope.row.id)" ></el-button> </template> </el-table-column>
-
removeUserById
根據用戶id刪除用戶
// 根據ID刪除對應的用戶信息
async removeUserById(id) {
// 彈框 詢問用戶是否刪除
const confirmResult = await this.$confirm(
"此操作將永久刪除該用戶, 是否繼續?",
"提示",
{
confirmButtonText: "確定",
cancelButtonText: "取消",
type: "warning",
}
).catch((err) => err);
// 如果用戶確認刪除,則返回值為字符串 confirm
// 如果用戶取消刪除,則返回值為字符串 cancel
// console.log(confirmResult)
if (confirmResult == "confirm") {
//刪除用戶
userDelete(id)
.then((res) => {
if (res.data.code === 200) {
this.getUserList();
this.$message({
message: "刪除用戶成功",
type: "success",
});
} else {
this.$message.error("刪除用戶失敗");
}
})
.catch((err) => {
this.$message.error("刪除用戶異常");
console.loge(err);
});
}
},
效果:
2.8、批量刪除用戶
- api
//批量刪除用戶
export function userBatchDelete(data) {
return request({
url: '/user/delete/batch',
method: 'post',
data
})
}
- 在ElementUI表格組件中有一個多選的方式,手動添加一個
el-table-column
,設type
屬性為selection
即可
<el-table-column type="selection" width="55"> </el-table-column>
在表格里添加事件:
@selection-change="handleSelectionChange"
下面是官方的示例:
export default {
data() {
return {
multipleSelection: []
}
},
methods: {
handleSelectionChange(val) {
this.multipleSelection = val;
}
}
}
這個示例里取出的參數multipleSelection
結構是這樣的,我們只需要id,所以做一下處理:
export default {
data() {
return {
multipleSelection: [],
ids: [],
}
},
methods: {
handleSelectionChange(val) {
this.multipleSelection = val;
//向被刪除的ids賦值
this.multipleSelection.forEach((item) => {
this.ids.push(item.id);
console.log(this.ids);
});
}
}
}
- 接下來就簡單了,批量刪除操作直接cv上面的刪除,改一下api函數和參數就可以了
//批量刪除用戶
async batchDeleteUser(){
// 彈框 詢問用戶是否刪除
const confirmResult = await this.$confirm(
"此操作將永久刪除用戶, 是否繼續?",
"提示",
{
confirmButtonText: "確定",
cancelButtonText: "取消",
type: "warning",
}
).catch((err) => err);
// 如果用戶確認刪除,則返回值為字符串 confirm
// 如果用戶取消刪除,則返回值為字符串 cancel
if (confirmResult == "confirm") {
//批量刪除用戶
userBatchDelete(this.ids)
.then((res) => {
if (res.data.code === 200) {
this.$message({
message: "批量刪除用戶成功",
type: "success",
});
this.getUserList();
} else {
this.$message.error("批量刪除用戶失敗");
}
})
.catch((err) => {
this.$message.error("批量刪除用戶異常");
console.log(err);
});
}
效果:
完整代碼有點長,就不貼了,請自行查看源碼。
六、總結
通過這個示例,相信大家已經對 SpringBoot+Vue
前后端分離開發有了一個初步的掌握。
當然,由於這個示例並不是一個完整的項目,所以技術上和功能上都非常潦草😓
有興趣的同學可以進一步地去擴展和完善這個示例。👏👏👏
參考:
【1】:Vue.js - 漸進式 JavaScript 框架
【3】:how2j.cn