关于springboot:一步步使用SpringBoot结合Vue实现登录和用户管理功能

10次阅读

共计 41272 个字符,预计需要花费 104 分钟才能阅读完成。

前后端拆散开发是当今开发的支流。本篇文章从零开始,一步步应用 SpringBoot 联合 Vue 来实现日常开发中最常见的登录性能,以及登录之后对用户的治理性能。通过这个例子,能够疾速入门 SpringBoot+Vue 前后端拆散的开发。

前言

1、前后端拆散简介

在这里首先简略阐明一下什么是 前后端拆散 单页式利用 前后端拆散 的核心思想是前端页面通过 ajax 调用后端的 restuful api 进行数据交互,而 单页面利用(single page web application,SPA),就是只有一个页面,并在用户与应用程序交互时动静更新该页面的 Web 应用程序。

2、示例所用技术简介

简略阐明以下本示例中所用到的技术,如图所示:

后端

  • SpringBoot:SpringBoot 是以后最风行的 Java 后端框架。能够简略地看成简化了的、依照约定开发的 SSM(H),大大晋升了开发速度。

    官网地址:https://spring.io/projects/sp…

  • 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

数据库:

  • MySQL:MySQL 是一个风行的开源关系型数据库。

    官网地址:https://www.mysql.com/

下面曾经简略介绍了本实例用到的技术,在开始本实例之前,最好能对以上技术具备肯定水平的把握。

一、环境筹备

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/web…

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">
    ![](./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/#/…,官网文档比拟好懂,大部分组件复制粘贴即可。

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。

首先导入 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 中看到咱们存储的信息

  • 关上 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 前后端拆散开发有了一个初步的把握。

当然,因为这个示例并不是一个残缺的我的项目,所以技术上和性能上都十分潦草????

有趣味的同学能够进一步地去扩大和欠缺这个示例。????????????

<big>源码地址:https://gitee.com/fighter3/sp…</big>

<big>参考:</big>

【1】:Vue.js – 渐进式 JavaScript 框架

【2】:Element – 网站疾速成型工具

【3】:how2j.cn

【4】:Vue + Spring Boot 我的项目实战

【5】:一看就懂!基于 Springboot 拦截器的前后端分离式登录拦挡

【6】:手摸手,带你用 vue 撸后盾 系列一(根底篇

【7】:Vue + ElementUI 的电商管理系统实例

正文完
 0