一步步使用SpringBoot結合Vue實現登錄和用戶管理功能

前後端分離開發是當今開發的主流。本篇文章從零開始,一步步使用SpringBoot結合Vue來實現日常開發中最常見的登錄功能,以及登錄之後對用戶的管理功能。通過這個例子,可以快速入門SpringBoot+Vue前後端分離的開發。

前言

1、前後端分離簡介

在這裏首先簡單說明一下什麼是前後端分離單頁式應用前後端分離 的核心思想是前端頁面通過 ajax 調用後端的 restuful api 進行數據交互,而 單頁面應用(single page web application,SPA),就是隻有一個頁面,並在用戶與應用程序交互時動態更新該頁面的 Web 應用程序。

2、示例所用技術簡介

簡單說明以下本示例中所用到的技術,如圖所示:

前後端分離Demo

後端

  • SpringBoot:SpringBoot是當前最流行的Java後端框架。可以簡單地看成簡化了的、按照約定開發的SSM(H), 大大提升了開發速度。

    官網地址:https://spring.io/projects/spring-boot

  • MybatisPlus: MyBatis-Plus(簡稱 MP)是一個 MyBatis的增強工具,在 MyBatis 的基礎上只做增強不做改變,爲簡化開發、提高效率而生。

    官網地址:https://mybatis.plus/

前端:

  • Vue :Vue 是一套用於構建用戶界面的漸進式框架。儘管Vue3已經發布,但是至少一段時間內主流應用還是vue2.x,所以示例裏還是採用Vue2.x版本。

    官網地址:https://cn.vuejs.org/

  • ElementUI: ElementUI 是目前國內最流行的Vue UI框架。組件豐富,樣式衆多,也比較符合大衆審美。雖然一度傳出停止維護更新的傳聞,但是隨着Vue3的發佈,官方也Beta了適配Vue3的ElementPlus。

    官網地址:https://element.eleme.cn/#/zh-CN

數據庫:

上面已經簡單介紹了本實例用到的技術,在開始本實例之前,最好能對以上技術具備一定程度的掌握。

一、環境準備

1、前端

1.1、安裝Node.js

前端項目使用 veu-cli腳手架,vue-cli需要通過npm安裝,是而 npm 是集成在 Node.js 中的,所以第一步我們需要安裝 Node.js,訪問官網 https://nodejs.org/en/,首頁即可下載。

image-20210116144913607

下載完成後運行安裝包,一路下一步就行。然後在 cmd 中輸入 node -v,檢查是否安裝成功。

image-20210116145019443

如圖,出現了版本號(根據下載時候的版本確定),說明已經安裝成功了。同時,npm 包也已經安裝成功,可以輸入 npm -v 查看版本號

image-20210116145121709

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

image-20210116153327925

注意此種方式安裝的是 2.x 版本的 Vue CLI,最新版本需要通過 npm install -g @vue/cli 安裝。新版本可以使用圖形化界面初始化項目,並加入了項目健康監控等內容。

1.4、VS Code

前端的開發工具採用的當下最流行的前端開發工具 VS code。

官網:https://code.visualstudio.com

下載對應的版本,一步步安裝即可。安裝之後,初始界面如下:

在這裏插入圖片描述

VS Code安裝後,我們一般還需要搜索安裝一些所需要的插件輔助開發。安裝插件很簡單,在搜索面板中查找到後,直接安裝即可。

image-20210116150528000

一般會安裝這些插件:

  • 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

image-20210123000324282

2、後端

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 是構建單頁面應用的關鍵。

image-20210116153944498

OK,可以看到目錄下完成了項目的構建,基本結構如下。

image-20210116154257320

1.2、項目運行

使用VS code打開初始化完成的vue項目。

image-20210116160035620

在vs code 中點擊終端,輸入命令 npm run dev 運行項目。

image-20210116155515858

項目運行成功:

image-20210116155609488

訪問地址:http://localhost:8080,就可以查看網頁Demo。

image-20210116155800630

1.3、項目結構說明

在vs code 中可以看到項目結構如下:

image-20210116154629979

詳細的目錄項說明:

在這裏插入圖片描述

來重點看下標紅旗的幾個文件。

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/,界面發生改變:

image-20210116164637044

2、後端項目搭建

2.1、後端項目創建

後端項目創建如下:

  • 打開Idea, New Project ,選擇 Spring Intializr

image-20210116173103706

  • 填入項目的相關信息

image-20210117103912171

  • SpringBoot版本選擇了 2.3.8 , 選擇了web 和 MySQL驅動依賴

image-20210117104258493

  • 創建完成的項目

image-20210117105751005

  • 項目完整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、數據庫創建

數據庫設計非常簡單,只有一張表。

image-20210117111045353

建表語句如下:

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","男","[email protected]","滿都鎮");
        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 ,把下面一行註釋或者去掉。

image-20210117140457353

在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 ,就可以訪問登錄頁面:

image-20210117141836421

頁面有點粗糙簡陋對不對,沒關係,我們可以引入ElmentUI ,使用ElementUI中已經成型的組件。

1.3、引入ElementUI美化界面

Element 的官方地址爲 http://element-cn.eleme.io/#/zh-CN ,官方文檔比較好懂,大部分組件複製粘貼即可。

image-20210118200916023

1.3.1、安裝Element UI

在vscode 中打開終端,運行命令 npm i element-ui -S ,就安裝了 element ui 最新版本—當前是 2.15.0

image-20210117142500415

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;
    

好了,看看我們修改之後的登錄界面效果:

image-20210117150416183

OK,登錄界面的面子已經做好了,但是裏子還是空的,沒法和後臺交互。

1.4、引入axios發起請求

相信大家都對 ajax 有所瞭解,前後端分離情況下,前後端交互的模式是前端發出異步式請求,後端返回 json 。

axios 是一個基於Promise 用於瀏覽器和 nodejs 的 HTTP 客戶端,本質上也是對原生XHR的封裝,只不過它是Promise的實現版本,符合最新的ES規範。在這裏我們只需要知道它是非常強大的網絡請求處理庫,且得到廣泛應用即可。

在項目目錄下運行命令 npm install --save axios ,安裝模塊:

image-20210117152013705

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,"登錄失敗","");
    }
}

啓動後端項目:

image-20210117174026776

訪問登錄界面,效果如下:

登錄簡單效果

這樣一個簡答的登錄就完成了,接下來,我們會對這個登錄進一步完善。

四、登錄功能完善

前面雖然實現了登錄,但只是一個簡單的登錄跳轉,實際上並不能對用戶的登錄狀態進行判別,接下來我們進一步完善登錄功能。

首先開始後端的開發。

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
}

最後,測試一下登錄接口:

image-20210118231814997

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 。

首先導入 axiosstore:

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 中看到我們存儲的信息

image-20210123000856893

  • 打開vue 開發工具,在 Vuex 中也能看到我們 store中的數據

image-20210123001144379

  • 再次登錄,打開Network,可以發現異步式請求請求頭裏已經添加了 token

image-20210123103330123

再次說一下,這裏偷了懶,登錄用封裝的公共請求方法是不合理的,畢竟登錄就是爲了獲取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
}

簡單測一下,後端👌

image-20210126172536248

2、前端開發

2.1、首頁

在前面,登錄之後,跳轉到HelloWorld,還是比較簡陋的。本來想直接跳到用戶管理的視圖,覺得不太好看,所以還是寫了一個首頁,當然這一部分不是重點。

見過一些後臺管理系統的都知道,後臺管理系統大概都是像下面的佈局:

後臺佈局

在ElementUI中提供了這樣的佈局組件Container 佈局容器:

image-20210126173415562

大家都知道根組件是 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
          }
        },
        }
      ]
    },

首頁本來不想放什麼東西,後來想想,還是放了點大家愛看的——沒別的意思,快過年了,各位姐夫過年好。🏮😀

image-20210126174723686

圖片來自冰冰微博,見水印。

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>
    

效果如下,點擊用戶管理:

image-20210126184434700

2.3、分頁

在上面的圖裏,我們看到了在最下面有分頁欄,我們接下來看看分頁欄的實現。

我們這裏使用了 Pagination 分頁組件:

image-20210126184833582

      <!--分頁區域-->
      <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>

效果如下:

image-20210126185429397

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();
    },

效果:

image-20210126190500082

在最後一頁可以看到我們添加的用戶:

image-20210126190528809

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);
          });
      }
    },

效果:

image-20210126192208197

2.8、批量刪除用戶

  • api
//批量刪除用戶
export function userBatchDelete(data) {
  return request({
    url: '/user/delete/batch',
    method: 'post',
    data
  })
}
  • 在ElementUI表格組件中有一個多選的方式,手動添加一個el-table-column,設type屬性爲selection即可

image-20210126192421265

<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,所以做一下處理:

image-20210126193018008

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);
          });
      }

效果:

image-20210126193403139

完整代碼有點長,就不貼了,請自行查看源碼。

六、總結

通過這個示例,相信大家已經對 SpringBoot+Vue 前後端分離開發有了一個初步的掌握。

當然,由於這個示例並不是一個完整的項目,所以技術上和功能上都非常潦草😓

有興趣的同學可以進一步地去擴展和完善這個示例。👏👏👏

源碼地址:https://gitee.com/fighter3/springboot-vue-demo.git



參考:

【1】:Vue.js - 漸進式 JavaScript 框架

【2】:Element - 網站快速成型工具

【3】:how2j.cn

【4】:Vue + Spring Boot 項目實戰

【5】:一看就懂!基於Springboot 攔截器的前後端分離式登錄攔截

【6】:手摸手,帶你用vue擼後臺 系列一(基礎篇

【7】:Vue + ElementUI的電商管理系統實例

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章