瞌睡不够烟来凑 發表於 2021-1-26 20:28:00

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

<p>前后端分离开发是当今开发的主流。本篇文章从零开始,一步步使用SpringBoot结合Vue来实现日常开发中最常见的登录功能,以及登录之后对用户的管理功能。通过这个例子,可以快速入门SpringBoot+Vue前后端分离的开发。</p>
<h1 id="前言">前言</h1>
<h2 id="1前后端分离简介">1、前后端分离简介</h2>
<p>在这里首先简单说明一下什么是<b>前后端分离</b>和<b>单页式应用</b>:<strong>前后端分离</strong> 的核心思想是前端页面通过 ajax 调用后端的 restuful api 进行数据交互,而 <strong>单页面应用</strong>(single page web application,SPA),就是只有一个页面,并在用户与应用程序交互时动态更新该页面的 Web 应用程序。</p>
<h2 id="2示例所用技术简介">2、示例所用技术简介</h2>
<p>简单说明以下本示例中所用到的技术,如图所示:</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210116123617.png" alt="前后端分离Demo" loading="lazy"></p>
<p><strong>后端</strong></p>
<ul>
<li>
<p>SpringBoot:SpringBoot是当前最流行的Java后端框架。可以简单地看成简化了的、按照约定开发的SSM(H), 大大提升了开发速度。</p>
<p>官网地址:https://spring.io/projects/spring-boot</p>
</li>
<li>
<p>MybatisPlus: MyBatis-Plus(简称 MP)是一个 MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。</p>
<p>官网地址:https://mybatis.plus/</p>
</li>
</ul>
<p><strong>前端:</strong></p>
<ul>
<li>
<p>Vue :Vue是一套用于构建用户界面的<strong>渐进式框架</strong>。尽管Vue3已经发布,但是至少一段时间内主流应用还是vue2.x,所以示例里还是采用Vue2.x版本。</p>
<p>官网地址:https://cn.vuejs.org/</p>
</li>
<li>
<p>ElementUI: ElementUI 是目前国内最流行的Vue UI框架。组件丰富,样式众多,也比较符合大众审美。虽然一度传出停止维护更新的传闻,但是随着Vue3的发布,官方也Beta了适配Vue3的ElementPlus。</p>
<p>官网地址:https://element.eleme.cn/#/zh-CN</p>
</li>
</ul>
<p><strong>数据库:</strong></p>
<ul>
<li>
<p>MySQL:MySQL是一个流行的开源关系型数据库。</p>
<p>官网地址:https://www.mysql.com/</p>
</li>
</ul>
<p>上面已经简单介绍了本实例用到的技术,在开始本实例之前,最好能对以上技术具备一定程度的掌握。</p>
<h1 id="一环境准备">一、环境准备</h1>
<h2 id="1前端">1、前端</h2>
<h3 id="11安装nodejs">1.1、安装Node.js</h3>
<p>前端项目使用 <code>veu-cli</code>脚手架,<code>vue-cli</code>需要通过<code>npm</code>安装,是而 npm 是集成在 Node.js 中的,所以第一步我们需要安装 Node.js,访问官网 https://nodejs.org/en/,首页即可下载。</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210116144916.png" alt="image-20210116144913607" loading="lazy"></p>
<p>下载完成后运行安装包,一路下一步就行。然后在 cmd 中输入 <code>node -v</code>,检查是否安装成功。</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210116145021.png" alt="image-20210116145019443" loading="lazy"></p>
<p>如图,出现了版本号(根据下载时候的版本确定),说明已经安装成功了。同时,npm 包也已经安装成功,可以输入 <code>npm -v</code> 查看版本号</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210116145123.png" alt="image-20210116145121709" loading="lazy"></p>
<h3 id="12配置npm源">1.2、配置NPM源</h3>
<p>NPM原始的源是在国外的服务器上,下载东西比较慢。</p>
<p>可以通过两种方式来提升下载速度。</p>
<ul>
<li>
<p>下载时指定源</p>
<pre><code class="language-bash">//本次从淘宝仓库源下载
npm --registry=https://registry.npm.taobao.org install
</code></pre>
</li>
<li>
<p>配置源为淘宝仓库</p>
<pre><code class="language-bash">//设置淘宝源
npm config set registry https://registry.npm.taobao.org
</code></pre>
</li>
</ul>
<p>也可以安装 cnpm ,但是使用中可能会遇到一些问题。</p>
<h3 id="13安装vue-cli脚手架">1.3、安装vue-cli脚手架</h3>
<p>使用如下命令安装 <code>vue-cli</code> 脚手架:</p>
<pre><code class="language-bash">npm install -g vue-cli
</code></pre>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210116153329.png" alt="image-20210116153327925" loading="lazy"></p>
<p>注意此种方式安装的是 2.x 版本的 Vue CLI,最新版本需要通过 <code>npm install -g @vue/cli</code> 安装。新版本可以使用图形化界面初始化项目,并加入了项目健康监控等内容。</p>
<h3 id="14vs-code">1.4、VS Code</h3>
<p>前端的开发工具采用的当下最流行的前端开发工具 VS code。</p>
<p>官网:https://code.visualstudio.com</p>
<p>下载对应的版本,一步步安装即可。安装之后,初始界面如下:</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210116150300.png" alt="在这里插入图片描述" loading="lazy"></p>
<p>VS Code安装后,我们一般还需要搜索安装一些所需要的插件辅助开发。安装插件很简单,在搜索面板中查找到后,直接安装即可。</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210116150533.png" alt="image-20210116150528000" loading="lazy"></p>
<p>一般会安装这些插件:</p>
<ul>
<li><strong>Chinese</strong>:中文语言插件</li>
<li><strong>Vetur</strong>:Vue多功能集成插件,包括:语法高亮,智能提示,emmet,错误提示,格式化,自动补全,debugger。vscode官方钦定Vue插件,Vue开发者必备。</li>
<li><strong>ESLint</strong>:ESLint 是一个语法规则和代码风格的检查工具,可以用来保证写出语法正确、风格统一的代码。</li>
<li><strong>VS Code - Debugger for Chrome</strong>:结合Chrome进行调试的插件。</li>
<li><strong>Beautify</strong>:Beautify 插件可以快速格式化你的代码格式,让你在编写代码时杂乱的代码结构瞬间变得非常规整。</li>
</ul>
<h3 id="15chrome">1.5、Chrome</h3>
<p>Chrome 是比较流行的浏览器,也是我们前端开发的常用工具。</p>
<p>Chrome 下载途径很多,请自行搜索下载安装。</p>
<p>Chrome下载安装完成之后,建议安装一个插件 <code>Vue.js devtools</code> ,是非常好用的 vue 调试工具。</p>
<p>谷歌商店下载地址:https://chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210123000325.png" alt="image-20210123000324282" loading="lazy"></p>
<h2 id="2后端">2、后端</h2>
<ul>
<li>后端采用的jdk版本是1.8,具体安装可以参考Win10系统安装与配置JDK1.8</li>
<li>采用的maven版本是3.5,安装配置可参考 Maven系列教材 (二)- 下载与配置Maven。</li>
<li>开发工具采用的是Idea,安装请自行查找。</li>
</ul>
<h2 id="3数据库">3、数据库</h2>
<p>数据库采用的是MySQL5.7,安装可以参考: Win10配置免安装版MySQL5.7</p>
<h1 id="二项目搭建">二、项目搭建</h1>
<h2 id="1前端项目搭建">1、前端项目搭建</h2>
<h3 id="11创建项目">1.1、创建项目</h3>
<p>这里使用命令行来创建项目,在工作文件下新建目录。</p>
<p>然后执行命令 <code>vue init webpack demo-vue</code>,这里 webpack 是以 webpack 为模板指生成项目,还可以替换为 pwa、simple 等参数,这里不再赘述。 demo-vue 是项目名称,也可以起别的名字。</p>
<p>在程序执行的过程中会有一些提示,可以按照默认的设定一路回车下去,也可以按需修改。</p>
<p>需要注意的是询问是否安装 vue-router,一定要选是,也就是回车或按 Y,vue-router 是构建单页面应用的关键。</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210116153946.png" alt="image-20210116153944498" loading="lazy"></p>
<p>OK,可以看到目录下完成了项目的构建,基本结构如下。</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210116154258.png" alt="image-20210116154257320" loading="lazy"></p>
<h3 id="12项目运行">1.2、项目运行</h3>
<p>使用VS code打开初始化完成的vue项目。</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210116160037.png" alt="image-20210116160035620" loading="lazy"></p>
<p>在vs code 中点击终端,输入命令 <code>npm run dev</code> 运行项目。</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210116155517.png" alt="image-20210116155515858" loading="lazy"></p>
<p>项目运行成功:</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210116155611.png" alt="image-20210116155609488" loading="lazy"></p>
<p>访问地址:http://localhost:8080,就可以查看网页Demo。</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210116155802.png" alt="image-20210116155800630" loading="lazy"></p>
<h3 id="13项目结构说明">1.3、项目结构说明</h3>
<p>在vs code 中可以看到项目结构如下:</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210116154631.png" alt="image-20210116154629979" loading="lazy"></p>
<p>详细的目录项说明:</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210116155052.png" alt="在这里插入图片描述" loading="lazy"></p>
<p>来重点看下标红旗的几个文件。</p>
<h4 id="131indexhtml">1.3.1、index.html</h4>
<p>首页文件的初始代码如下:</p>
<pre><code class="language-javascript">&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
    &lt;meta charset="utf-8"&gt;
    &lt;meta name="viewport" content="width=device-width,initial-scale=1.0"&gt;
    &lt;title&gt;demo-vue&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;div id="app"&gt;&lt;/div&gt;
    &lt;!-- built files will be auto injected --&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>需要注意的是 <code>&lt;div id="app"&gt;&lt;/div&gt;</code>这一行带代码,下面有一行注释,构建的文件将会被自动注入,也就是说我们编写的其它的内容都将在这个 div 中展示。</p>
<p>所谓<strong>单页面应用</strong>,就是整个项目只有这一个 html 文件,当我们打开这个应用,表面上可以有很多页面,实际上它们都是动态地加载在一个 <code>div</code> 中。</p>
<h4 id="132appvue">1.3.2、App.vue</h4>
<p>这个文件称为“根组件”,因为其它的组件又都包含在这个组件中。</p>
<p><code>.vue</code> 文件是一种自定义文件类型,在结构上类似 html,一个 .vue 文件即是一个 vue 组件。先看它的初始代码:</p>
<pre><code class="language-javascript">&lt;template&gt;
&lt;div id="app"&gt;
    &lt;img src="./assets/logo.png"&gt;
    &lt;router-view/&gt;
&lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
name: 'App'
}
&lt;/script&gt;

&lt;style&gt;
#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;
}
&lt;/style&gt;
</code></pre>
<p>这里也有一句 <code>&lt;div id="app"&gt;</code>,但跟 index.html 里的那个是没有关系的。这个只是普通的div块。</p>
<p><code>&lt;script&gt;</code>标签里的内容即该组件的脚本,也就是 js 代码,export default 是 ES6 的语法,意思是将这个组件整体导出,之后就可以使用 import 导入组件了。大括号里的内容是这个组件的相关属性。</p>
<p>这个文件最关键的一点其实是第四行, <code>&lt;router-view/&gt;</code>,是一个容器,名字叫“路由视图”,意思是当前路由( URL)指向的内容将显示在这个容器中。也就是说,其它的组件即使拥有自己的路由(URL,需要在 router 文件夹的 index.js文件里定义),也只不过表面上是一个单独的页面,实际上只是在根组件 App.vue 中。</p>
<h4 id="133mainjs">1.3.3、main.js</h4>
<p>App.vue 和 index.html是怎么联系的?关键点就在于这个文件:</p>
<pre><code class="language-javascript">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: '&lt;App/&gt;'
})
</code></pre>
<p>最上面 import 了几个模块,其中 vue 模块在 node_modules 中,App 即 App.vue 里定义的组件,router 即 router 文件夹里定义的路由。</p>
<p>Vue.config.productionTip = false ,作用是阻止vue 在启动时生成生产提示。</p>
<p>在这个 js 文件中,我们创建了一个 <strong>Vue 对象(实例)</strong>,el 属性提供一个在页面上已存在的 DOM元素作为 Vue 对象的挂载目标,router 代表该对象包含 Vue Router,并使用项目中定义的路由。components表示该对象包含的 Vue 组件,template 是用一个字符串模板作为 Vue 实例的标识使用,类似于定义一个 html 标签。</p>
<h4 id="134routerindexjs">1.3.4、router/index.js</h4>
<p>前面说到了vue-router是单式应用的关键,这里我们来看一下 <code>router/index.js </code>文件:</p>
<pre><code class="language-javascript">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
    }
]
})
</code></pre>
<p>最上面 import 了几个组件,在 <code>routes</code>这个数组里定义了路由,可以看到 <code>/</code> 路径路由到了<code>HelloWorld</code> 这个组件,所以访问http://localhost:8080/ 会看到上面的界面。为了更直观的理解,这里可以对 <code>src\components\HelloWorld.vue</code> 组件进行修改,修改如下:</p>
<pre><code class="language-javascript">&lt;template&gt;
&lt;div id="demo"&gt;
    {{msg}}
&lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
name: 'HelloWorld',
data () {
    return {
      msg: 'Hello Vue!'
    }
}
}
&lt;/script&gt;

&lt;!-- Add "scoped" attribute to limit CSS to this component only --&gt;
&lt;style scoped&gt;
#demo{
background-color: bisque;
font-size: 20pt;
color:darkcyan;
margin-left: 30%;
margin-right: 30%;
}
&lt;/style&gt;
</code></pre>
<p>vue-cli会我们的更改进行热更新,再次打开 http://localhost:8080/,界面发生改变:</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210116164638.png" alt="image-20210116164637044" loading="lazy"></p>
<h2 id="2后端项目搭建">2、后端项目搭建</h2>
<h3 id="21后端项目创建">2.1、后端项目创建</h3>
<p>后端项目创建如下:</p>
<ul>
<li>打开Idea, <code>New Project</code> ,选择 <code>Spring Intializr</code></li>
</ul>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210116173105.png" alt="image-20210116173103706" loading="lazy"></p>
<ul>
<li>填入项目的相关信息</li>
</ul>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210117103913.png" alt="image-20210117103912171" loading="lazy"></p>
<ul>
<li>SpringBoot版本选择了 2.3.8 , 选择了web 和 MySQL驱动依赖</li>
</ul>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210117104259.png" alt="image-20210117104258493" loading="lazy"></p>
<ul>
<li>创建完成的项目</li>
</ul>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210117105752.png" alt="image-20210117105751005" loading="lazy"></p>
<ul>
<li>项目完整pom.xml</li>
</ul>
<pre><code class="language-java">&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;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"&gt;
    &lt;modelVersion&gt;4.0.0&lt;/modelVersion&gt;
    &lt;parent&gt;
      &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
      &lt;artifactId&gt;spring-boot-starter-parent&lt;/artifactId&gt;
      &lt;version&gt;2.3.8.RELEASE&lt;/version&gt;
      &lt;relativePath/&gt; &lt;!-- lookup parent from repository --&gt;
    &lt;/parent&gt;
    &lt;groupId&gt;cn.fighter3&lt;/groupId&gt;
    &lt;artifactId&gt;demo-java&lt;/artifactId&gt;
    &lt;version&gt;0.0.1-SNAPSHOT&lt;/version&gt;
    &lt;name&gt;demo-java&lt;/name&gt;
    &lt;description&gt;Demo project for Spring Boot&lt;/description&gt;

    &lt;properties&gt;
      &lt;java.version&gt;1.8&lt;/java.version&gt;
    &lt;/properties&gt;

    &lt;dependencies&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
      &lt;/dependency&gt;

      &lt;dependency&gt;
            &lt;groupId&gt;mysql&lt;/groupId&gt;
            &lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt;
            &lt;scope&gt;runtime&lt;/scope&gt;
      &lt;/dependency&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt;
            &lt;scope&gt;test&lt;/scope&gt;
            &lt;exclusions&gt;
                &lt;exclusion&gt;
                  &lt;groupId&gt;org.junit.vintage&lt;/groupId&gt;
                  &lt;artifactId&gt;junit-vintage-engine&lt;/artifactId&gt;
                &lt;/exclusion&gt;
            &lt;/exclusions&gt;
      &lt;/dependency&gt;
    &lt;/dependencies&gt;

    &lt;build&gt;
      &lt;plugins&gt;
            &lt;plugin&gt;
                &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
                &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;
            &lt;/plugin&gt;
      &lt;/plugins&gt;
    &lt;/build&gt;

&lt;/project&gt;
</code></pre>
<h3 id="23引入mybatisplus">2.3、引入MybatisPlus</h3>
<p>如果对MybatisPlus不熟悉,入门可以参考 SpringBoot学习笔记(十七:MyBatis-Plus )</p>
<p>想了解更多可以直接查看官网。</p>
<h4 id="231引入mp依赖">2.3.1、引入MP依赖</h4>
<pre><code class="language-java">      &lt;!--mybatis-plus依赖--&gt;
      &lt;dependency&gt;
            &lt;groupId&gt;com.baomidou&lt;/groupId&gt;
            &lt;artifactId&gt;mybatis-plus-boot-starter&lt;/artifactId&gt;
            &lt;version&gt;3.4.1&lt;/version&gt;
      &lt;/dependency&gt;
</code></pre>
<p><strong>由于本实例的数据库表非常简单,只有一个单表,所以这里我们直接将基本的增删改查写出来</strong></p>
<h4 id="232数据库创建">2.3.2、数据库创建</h4>
<p>数据库设计非常简单,只有一张表。</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210117111047.png" alt="image-20210117111045353" loading="lazy"></p>
<p>建表语句如下:</p>
<pre><code class="language-sql">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;
</code></pre>
<h4 id="233配置">2.3.3、配置</h4>
<p>在<code>application.properties</code> 中写入相关配置:</p>
<pre><code class="language-java"># 服务端口号
server.port=8088
# 数据库连接配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/demo?characterEncoding=utf-8&amp;allowMultiQueries=true&amp;serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root
</code></pre>
<p>在启动类里添加 <code>@MapperScan </code>注解,扫描 Mapper 文件夹:</p>
<pre><code class="language-java">@SpringBootApplication
@MapperScan("cn.fighter3.mapper")
public class DemoJavaApplication {

    public static void main(String[] args) {
      SpringApplication.run(DemoJavaApplication.class, args);
    }
}
</code></pre>
<h4 id="233相关代码">2.3.3、相关代码</h4>
<p>MP提供了代码生成器的功能,可以按模块生成Controller、Service、Mapper、实体类的代码。在数据库表比较多的情况下,能提升开发效率。官网给出了一个Demo,有兴趣的可以自行查看。</p>
<ul>
<li>实体类</li>
</ul>
<pre><code class="language-java">/**
* @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等
}
</code></pre>
<ul>
<li>Mapper接口:继承BaseMapper即可</li>
</ul>
<pre><code class="language-java">/**
* @Author: 三分恶
* @Date: 2021/1/17
* @Description: TODO
**/

public interface UserMapper extends BaseMapper&lt;User&gt; {
}
</code></pre>
<p>OK,到此单表的增删改查功能已经完成了,是不是很简单。</p>
<p>可以写一个单元测试测一下。</p>
<h4 id="234单元测试">2.3.4、单元测试</h4>
<pre><code class="language-java">@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);
    }

}
</code></pre>
<p>至此前后端项目基本搭建完成,接下来开始进行功能开发。</p>
<h1 id="三登录功能开发">三、登录功能开发</h1>
<h2 id="1前端开发">1、前端开发</h2>
<h3 id="11登录界面">1.1、登录界面</h3>
<p>在前面访问页面的时候,有一个 V logo,看起来比较奇怪,我们先把它去掉,这个图片的引入是在根组件中——<code>src\App.vue</code> ,把下面一行注释或者去掉。</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210117140506.png" alt="image-20210117140457353" loading="lazy"></p>
<p>在src目录下新建文件夹views,在views下新建文件 <code>login.vue</code></p>
<pre><code class="language-javascript">&lt;template&gt;
&lt;div&gt;
      &lt;h3&gt;登录&lt;/h3&gt;
      用户名:&lt;input type="text" v-model="loginForm.loginName" placeholder="请输入用户名"/&gt;
      &lt;br&gt;&lt;br&gt;
      密码: &lt;input type="password" v-model="loginForm.password" placeholder="请输入密码"/&gt;
      &lt;br&gt;&lt;br&gt;
      &lt;button&gt;登录&lt;/button&gt;
&lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;

export default {
    name: 'Login',
    data () {
      return {
      loginForm: {
          loginName: '',
          password: ''
      },
      responseResult: []
      }
    },
    methods: {
    }
}
&lt;/script&gt;
</code></pre>
<h3 id="12添加路由">1.2、添加路由</h3>
<p>在 <code>router\index.js</code> 里添加路由,代码如下:</p>
<pre><code class="language-javascript">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
    }
]
})

</code></pre>
<p>OK,现在在浏览器里输入 <code>http://localhost:8080/#/login</code> ,就可以访问登录页面:</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210117141837.png" alt="image-20210117141836421" loading="lazy"></p>
<p>页面有点粗糙简陋对不对,没关系,我们可以引入<code>ElmentUI</code> ,使用ElementUI中已经成型的组件。</p>
<h3 id="13引入elementui美化界面">1.3、引入ElementUI美化界面</h3>
<p>Element 的官方地址为 http://element-cn.eleme.io/#/zh-CN ,官方文档比较好懂,大部分组件复制粘贴即可。</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210118200917.png" alt="image-20210118200916023" loading="lazy"></p>
<h4 id="131安装element-ui">1.3.1、安装Element UI</h4>
<p>在vscode 中打开终端,运行命令<code> npm i element-ui -S</code> ,就安装了 element ui 最新版本—当前是 2.15.0</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210117142501.png" alt="image-20210117142500415" loading="lazy"></p>
<h4 id="132引入-element">1.3.2、引入 Element</h4>
<p>引入分为完整引入和按需引入两种模式,按需引入可以缩小项目的体积,这里我们选择完整引入。</p>
<p>根据文档,我们需要修改 main.js 为如下内容:</p>
<pre><code class="language-javascript">// 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: '&lt;App/&gt;'
})

</code></pre>
<h4 id="133使用elementui美化登录页面">1.3.3、使用ElementUI美化登录页面</h4>
<p>现在开始使用 ElementUI和 css美化我们的登录界面,修改后的<code>login.vue</code>代码如下:</p>
<pre><code class="language-javascript">&lt;template&gt;
&lt;body id="login-page"&gt;
    &lt;el-form class="login-container" label-position="left" label-width="0px"&gt;
      &lt;h3 class="login_title"&gt;系统登录&lt;/h3&gt;
      &lt;el-form-item&gt;
      &lt;el-input
          type="text"
          v-model="loginForm.loginName"
          auto-complete="off"
          placeholder="账号"
      &gt;&lt;/el-input&gt;
      &lt;/el-form-item&gt;
      &lt;el-form-item&gt;
      &lt;el-input
          type="password"
          v-model="loginForm.password"
          auto-complete="off"
          placeholder="密码"
      &gt;&lt;/el-input&gt;
      &lt;/el-form-item&gt;
      &lt;el-form-item style="width: 100%"&gt;
      &lt;el-button
          type="primary"
          style="width: 100%;border: none"
          &gt;登录&lt;/el-button
      &gt;
      &lt;/el-form-item&gt;
    &lt;/el-form&gt;
&lt;/body&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
name: "Login",
data() {
    return {
      loginForm: {
      loginName: "",
      password: "",
      },
      responseResult: [],
    };
},
methods: {},
};
&lt;/script&gt;

&lt;style scoped&gt;
#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;
}
&lt;/style&gt;


</code></pre>
<p>需要注意:</p>
<ul>
<li>
<p>在 <code>src\assets</code> 路径下新建一个一个文件夹 <code>img</code>,在 img 里放了一张网上找到的无版权图片作为背景图</p>
</li>
<li>
<p><code>App.vue</code> 里删了一行代码,不然会有空白:</p>
<pre><code class="language-javascript">margin-top: 60px;
</code></pre>
</li>
</ul>
<p>好了,看看我们修改之后的登录界面效果:</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210117150417.png" alt="image-20210117150416183" loading="lazy"></p>
<p>OK,登录界面的面子已经做好了,但是里子还是空的,没法和后台交互。</p>
<h3 id="14引入axios发起请求">1.4、引入axios发起请求</h3>
<p>相信大家都对 ajax 有所了解,前后端分离情况下,前后端交互的模式是前端发出异步式请求,后端返回 json 。</p>
<p>axios 是一个基于Promise 用于浏览器和 nodejs 的 HTTP 客户端,本质上也是对原生XHR的封装,只不过它是Promise的实现版本,符合最新的ES规范。在这里我们只需要知道它是非常强大的网络请求处理库,且得到广泛应用即可。</p>
<p>在项目目录下运行命令 <code>npm install --save axios</code> ,安装模块:</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210117152015.png" alt="image-20210117152013705" loading="lazy"></p>
<p>在 <code>main.js</code> 里全局注册 axios:</p>
<pre><code class="language-javascript">var axios = require('axios')
// 全局注册,之后可在其他组件中通过 this.$axios 发送数据
Vue.prototype.$axios = axios
</code></pre>
<p>那么怎么使用 <code>axios</code> 发起请求呢?</p>
<p>在 <code>login.vue</code>中添加方法:</p>
<pre><code class="language-javascript">methods: {
   login () {
      this.$axios
          .post('/login', {
            loginName: this.loginForm.loginName,
            password: this.loginForm.password
          })
          .then(successResponse =&gt; {
            if (successResponse.data.code === 200) {
            this.$router.replace({path: '/'})
            }
          })
          .catch(failResponse =&gt; {
          })
      }
},
</code></pre>
<p>这个方法里通过 axios 向后台发起了请求,如果返回成功的结果就跳转到 <code>/</code> 路由下。</p>
<p>在登录按钮里触发这个方法:</p>
<pre><code class="language-javascript">      &lt;el-button
          type="primary"
          style="width: 100%;border: none"
          @click="login"
          &gt;登录&lt;/el-button
      &gt;
</code></pre>
<p>那么现在就能向后台发起请求了吗?还没完。</p>
<h3 id="15前端相关配置">1.5、前端相关配置</h3>
<ul>
<li>
<p>反向代理</p>
<p>修改 <code>src\main.js</code> ,添加反向代理的配置:</p>
<pre><code class="language-javascript">// 设置反向代理,前端请求默认发送到 http://localhost:8888/api
axios.defaults.baseURL = 'http://localhost:8088/api'
</code></pre>
</li>
</ul>
<p>这么一来,我们在前面写的登录请求,访问的后台地址实际就是 <code>http://localhost:8088/api/login</code></p>
<ul>
<li>
<p>跨域配置</p>
<p>前后端分离会带来一个问题—跨域,关于跨域,这里就不展开讲解。在 <code>config\index.js</code> 中,找到 proxyTable 位置,修改为以下内容:</p>
<pre><code class="language-javascript">    proxyTable: {
      '/api': {
      target: 'http://localhost:8088',
      changeOrigin: true,
      pathRewrite: {
          '^/api': ''
      }
      }
    },
</code></pre>
</li>
</ul>
<h2 id="2后端开发">2、后端开发</h2>
<h3 id="21统一结果封装">2.1、统一结果封装</h3>
<p>这里我们创建了一个 Result 类,用于异步统一返回的结果封装。一般来说,结果里面有几个要素必要的</p>
<ul>
<li>是否成功,可用 code 表示(如 200 表示成功,400 表示异常)</li>
<li>结果消息</li>
<li>结果数据</li>
</ul>
<pre><code class="language-java">/**
* @Author: 三分恶
* @Date: 2021/1/17
* @Description: 统一结果封装
**/

public class Result {
    //相应码
    private Integer code;
    //信息
    private String message;
    //返回数据
    private Object data;
    //省略getter、setter、构造方法
}
</code></pre>
<p>实际上由于响应码是固定的,<code>code</code> 属性应该是一个枚举值,这里作了一些简化。</p>
<h3 id="22登录业务实体类">2.2、登录业务实体类</h3>
<p>为了接收前端登录的数据,我们这里创建了一个登录用的业务实体类:</p>
<pre><code class="language-java">public class LoginDTO {
    private String loginName;
    private String password;
    //省略getter、setter
}
</code></pre>
<h3 id="23控制层">2.3、控制层</h3>
<p>LoginController,进行业务响应:</p>
<pre><code class="language-java">/**
* @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);
    }
}
</code></pre>
<h3 id="24业务层">2.4、业务层</h3>
<p>业务层进行实际的业务处理。</p>
<ul>
<li>LoginService:</li>
</ul>
<pre><code class="language-java">public interface LoginService {
    public Result login(LoginDTO loginDTO);
}
</code></pre>
<ul>
<li>LoginServiceImpl:</li>
</ul>
<pre><code class="language-java">/**
* @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&lt;User&gt; wrapper = new QueryWrapper();
      wrapper.eq("login_name", loginDTO.getLoginName());
      User uer=userMapper.selectOne(wrapper);
      //比较密码
      if (uer!=null&amp;&amp;uer.getPassword().equals(loginDTO.getPassword())){
            return new Result(200,"",uer);
      }
      return new Result(400,"登录失败","");
    }
}
</code></pre>
<p>启动后端项目:</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210117174028.png" alt="image-20210117174026776" loading="lazy"></p>
<p>访问登录界面,效果如下:</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210117174354.gif" alt="登录简单效果" loading="lazy"></p>
<p>这样一个简答的登录就完成了,接下来,我们会对这个登录进一步完善。</p>
<h1 id="四登录功能完善">四、登录功能完善</h1>
<p>前面虽然实现了登录,但只是一个简单的登录跳转,实际上并不能对用户的登录状态进行判别,接下来我们进一步完善登录功能。</p>
<p>首先开始后端的开发。</p>
<h2 id="1后端开发">1、后端开发</h2>
<h3 id="11拦截器">1.1、拦截器</h3>
<p>在前后端分离的情况下,比较流行的认证方案是 <code>JWT认证</code> 认证,和传统的session认证不同,jwt是一种无状态的认证方法,也就是服务端不再保存任何认证信息。出于篇幅考虑,我们这里不再引入 <code>JWT</code> ,只是简单地判断一下前端的请求头里是否存有 <code>token</code> 。对JWT 认证感兴趣的可以查看文章:SpringBoot学习笔记(十三:JWT ) 。</p>
<ul>
<li>创建 <code>interceptor</code> 包,包下新建拦截器 <code>LoginInterceptor</code></li>
</ul>
<pre><code class="language-java">/**
* @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);
    }

}
</code></pre>
<ul>
<li>为了能给前端返回 json 格式的结果,这里还用到了一个工具类,新建 <code>util</code> 包,util 包下新建工具类 <code>HttpContextUtil</code></li>
</ul>
<pre><code class="language-java">/**
* @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");
    }
}
</code></pre>
<h3 id="12拦截器配置">1.2、拦截器配置</h3>
<p>拦截器创建完成之后,还需要进行配置。</p>
<pre><code class="language-java">/**
* @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");
    }
}
</code></pre>
<h3 id="13跨域配置">1.3、跨域配置</h3>
<p>细致的同学可能会发现,在之前的后台接口,有一个注解<code>@CrossOrigin</code> ,这个注解是用来跨域的,每个接口都写一遍肯定是不太方便的,这里我们 创建跨域配置类并添加统一的跨域配置:</p>
<pre><code class="language-java">/**
* @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);
    }
}
</code></pre>
<h3 id="13登录service">1.3、登录service</h3>
<p>这样一来,后端就需要生成一个 <code>token</code> 返回给前端,所以更改 <code>LoginServiceImpl</code> 里的登录方法。</p>
<pre><code class="language-java">@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&lt;User&gt; wrapper = new QueryWrapper();
      wrapper.eq("login_name", loginDTO.getLoginName());
      User uer=userMapper.selectOne(wrapper);
      //比较密码
      if (uer!=null&amp;&amp;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,"登录失败","");
    }
}
</code></pre>
<p>其中对返回的<code>data</code> 封装了一个VO:</p>
<pre><code class="language-java">/**
* @Author: 三分恶
* @Date: 2021/1/18
* @Description: 登录VO
**/

public class LoginVO implements Serializable {
    private Integer id;
    private String token;
    private User user;
    //省略getter、setter
}
</code></pre>
<p>最后,测试一下登录接口:</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210118231816.png" alt="image-20210118231814997" loading="lazy"></p>
<p>OK,没有问题。</p>
<h2 id="2前端开发">2、前端开发</h2>
<p>前面我们使用了后端拦截器,接下来我们尝试用前端实现相似的功能。</p>
<p>实现前端登录器,需要在前端判断用户的登录状态。我们可以像之前那样在组件的 data 中设置一个状态标志,但登录状态应该被视为一个<strong>全局属性</strong>,而不应该只写在某一组件中。所以我们需要引入一个新的工具——Vuex,它是专门为 Vue开发的状态管理方案,我们可以把需要在各个组件中传递使用的变量、方法定义在这里。</p>
<h3 id="21引入vuex">2.1引入Vuex</h3>
<p>首先在终端里使用命令 <code>npm install vuex --save</code>来安装 Vuex 。</p>
<p>在 src 目录下新建一个文件夹 store,并在该目录下新建 index.js 文件,在该文件中引入 vue 和 vuex,代码如下:</p>
<pre><code class="language-javascript">import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)
</code></pre>
<p>接下来,在<code> index.js</code>里设置我们需要的状态变量和方法。为了实现登录拦截器,我们需要一个记录token的变量量。同时为了全局使用用户信息,我们还需要一个记录用户信息的变量。还需要改变变量值的mutations。完整的代码如下:</p>
<pre><code class="language-javascript">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) =&gt; {
            state.token = token
            sessionStorage.setItem("token", token)
      },
      SET_USER: (state, user) =&gt; {
            state.user = user
            sessionStorage.setItem("user", JSON.stringify(user))
      },
      REMOVE_INFO : (state) =&gt; {
            state.token = ''
            state.user = {}
            sessionStorage.setItem("token", '')
            sessionStorage.setItem("user", JSON.stringify(''))
      }
    },
    getters: {

    },
    actions: {
    },
    modules: {
    }
})
</code></pre>
<p>这里我们还用到了 <code>sessionStorage</code>,使用<code>sessionStorage</code> ,关掉浏览器的时候会被清除掉,和 <code>localStorage</code> 相比,比较利于保证实时性。</p>
<h3 id="22修改路由配置">2.2、修改路由配置</h3>
<p>为了能够区分哪些路由需要被拦截,我们在路由里添上一个元数据<code> requireAuth</code>来做是否需要拦截的判断:</p>
<pre><code class="language-javascript">    {
      path: '/',
      name: 'HelloWorld',
      component: HelloWorld,
      meta: {
      requireAuth: true
      }
    },
</code></pre>
<p>完整的 <code>src\router\index.js</code> 代码如下:</p>
<pre><code class="language-javascript">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
    }
]
})

</code></pre>
<h3 id="23使用钩子函数判断是否拦截">2.3、使用钩子函数判断是否拦截</h3>
<p>上面我们添加了 <code>requireAuth</code> , 接下来就要用到它了。</p>
<p>钩子函数及在某些时机会被调用的函数。这里我们使用 <code>router.beforeEach()</code>,意思是在访问每一个路由前调用。</p>
<p>打开 <code>src\main.js</code> ,首先添加对 <code>store</code> 的引用</p>
<pre><code class="language-javascript">import store from './store'
</code></pre>
<p>并修改vue对象里的内容,使 store 能全局使用:</p>
<pre><code class="language-javascript">new Vue({
el: '#app',
router,
// 注意这里
store,
components: { App },
template: '&lt;App/&gt;'
})
</code></pre>
<p>解下来,我们写<code>beforeEach()</code> 函数,逻辑很简单,判断是否需要登录,如果是,判断 store中是否存有token ,是则放行,否则跳转到登录页。</p>
<pre><code class="language-javascript">//钩子函数,访问路由前调用
router.beforeEach((to, from, next) =&gt; {
//路由需要认证
if (to.meta.requireAuth) {
    //判断store里是否有token
    if (store.state.token) {
      next()
    } else {
      next({
      path: 'login',
      query: { redirect: to.fullPath }
      })
    }
} else {
    next()
}
}
)
</code></pre>
<p>完整的 main.js 代码如下:</p>
<pre><code class="language-javascript">// 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) =&gt; {
//路由需要认证
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: '&lt;App/&gt;'
})

</code></pre>
<h3 id="24请求封装">2.4、请求封装</h3>
<p>我们前面写的后端拦截器,对请求进行了拦截,要求请求头里携带token,这个怎么处理呢?</p>
<p>答案是封装<code>axios</code>。</p>
<p>在 src 目录下新建目录 utils ,在uitls 目录下新建文件 request.js 。</p>
<p>首先导入 <code>axios</code> 和<code>store</code>:</p>
<pre><code class="language-javascript">import axios from 'axios'
import store from '@/store'
</code></pre>
<p>接下来在请求拦截器中,给请求头添加 <code>token</code> :</p>
<pre><code class="language-javascript">// request 请求拦截
service.interceptors.request.use(
    config =&gt; {

      if (store.state.token) {
            config.headers['token'] = window.sessionStorage.getItem("token")
      }
      return config
    },
    error =&gt; {
      // do something with request error
      console.log(error) // for debug
      return Promise.reject(error)
    }
)
</code></pre>
<p>完整的request.js:</p>
<pre><code class="language-javascript">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 =&gt; {

      if (store.getters.getToken) {
            config.headers['token'] = window.sessionStorage.getItem("token")
      }
      return config
    },
    error =&gt; {
      // do something with request error
      console.log(error) // for debug
      return Promise.reject(error)
    }
)

//response响应拦截
axios.interceptors.response.use(response =&gt; {
    let res = response.data;
    console.log(res)

    if (res.code === 200) {
      return response
    } else {
      return Promise.reject(response.data.msg)
    }
},
    error =&gt; {
      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


</code></pre>
<p>注意创建axios实例里用到了 baseUrl ,在 <code>config\dev.env.js</code> 里修改配置:</p>
<pre><code class="language-javascript">module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
BASE_API: '"http://localhost:8088/api"',
})
</code></pre>
<p>这样一封装,我们就不用每个请求都手动来塞 token,或者来做一些统一的异常处理,一劳永逸。 而且我们的 api 可以根据 <code>env</code> 环境变量动态切换。</p>
<h3 id="25封装api">2.5、封装api</h3>
<p>request.js 既然已经封装了,那么接下来就要开始用它。</p>
<p>我们可以像上面的 <code>axios</code> 添加到 main.js 中,这样就能被全局调用。但是有更好的用法。</p>
<p>一般项目中,<code>viess</code> 下放的是我们各个业务模块的视图,对应这些业务模块,我们创建对应的 <code>api</code> 来封装对后台的请求,这样即使业务模块很多,但关系仍然是比较清晰的。</p>
<p>在 src 下新建 <code>api</code> 文件夹,在 <code>api</code> 文件夹下新建 <code>user.js</code>,在user.js 中我们封装了登录的后台请求:</p>
<pre><code class="language-javascript">import request from '@/utils/request'

export function userLogin(data) {
    return request({
      url: '/login',
      method: 'post',
      data
    })
}
</code></pre>
<p>当然,事实上登录用 <code>request.js</code> 不合适,因为<code>request.js</code> 拦截了token,但登录就是为了获取token——所以😅凑合着看吧,谁叫现在就这一个接口呢。</p>
<h3 id="26loginvue">2.6、login.vue</h3>
<p>之前的登录组件中,我们只是判断后端返回的状态码,如果是 200,就重定向到首页。在经过前面的配置后,我们需要修改一下登录逻辑,以最终实现登录拦截。</p>
<p>修改后的逻辑如下:</p>
<p>1.点击登录按钮,向后端发送数据<br>
2.受到后端返回的成功代码时,触发 <code>store</code> 中的 <code>mutation</code> ,存储token 和user,<br>
3.获取登录前页面的路径并跳转,如果该路径不存在,则跳转到首页</p>
<p>修改后的 <code>login()</code> 方法如下:</p>
<pre><code class="language-javascript">    login() {
      var _this = this;
      userLogin({
      loginName: this.loginForm.loginName,
      password: this.loginForm.password,
      }).then((resp) =&gt; {
      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})
      }
      });
</code></pre>
<p>完整的<code>login.vue</code>:</p>
<pre><code class="language-javascript">&lt;template&gt;
&lt;body id="login-page"&gt;
    &lt;el-form class="login-container" label-position="left" label-width="0px"&gt;
      &lt;h3 class="login_title"&gt;系统登录&lt;/h3&gt;
      &lt;el-form-item&gt;
      &lt;el-input
          type="text"
          v-model="loginForm.loginName"
          auto-complete="off"
          placeholder="账号"
      &gt;&lt;/el-input&gt;
      &lt;/el-form-item&gt;
      &lt;el-form-item&gt;
      &lt;el-input
          type="password"
          v-model="loginForm.password"
          auto-complete="off"
          placeholder="密码"
      &gt;&lt;/el-input&gt;
      &lt;/el-form-item&gt;
      &lt;el-form-item style="width: 100%"&gt;
      &lt;el-button
          type="primary"
          style="width: 100%; border: none"
          @click="login"
          &gt;登录&lt;/el-button
      &gt;
      &lt;/el-form-item&gt;
    &lt;/el-form&gt;
&lt;/body&gt;
&lt;/template&gt;

&lt;script&gt;
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) =&gt; {
      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})
      }
      });
    },
},
};
&lt;/script&gt;

&lt;style scoped&gt;
#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;
}
&lt;/style&gt;

</code></pre>
<h3 id="27helloworldvue">2.7、HelloWorld.vue</h3>
<p>大家应该还记得,到目前为止,我们 的 <code>/</code> 路径还是指向 <code>HelloWorld.vue</code> 这个组件,为了演示 <code>vuex</code> 状态的全局使用,我们做一些更改,添加一个生命周期的钩子函数,来获取 <code>store</code>中存储的用户名:</p>
<pre><code class="language-javascript">computed: {
    userName() {
      return this.$store.state.user.userName
    }
}
</code></pre>
<p>完整的 <code>HelloWorld.vue</code>:</p>
<pre><code class="language-javascript">&lt;template&gt;
&lt;div id="demo"&gt;
    {{userName}}
&lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
name: 'HelloWorld',
data () {
    return {
      msg: 'Hello Vue!'
    }
},
computed: {
    userName() {
      return this.$store.state.user.userName
    }
}
}
&lt;/script&gt;

&lt;!-- Add "scoped" attribute to limit CSS to this component only --&gt;
&lt;style scoped&gt;
#demo{
background-color: bisque;
font-size: 20pt;
color:darkcyan;
margin-left: 30%;
margin-right: 30%;
}
&lt;/style&gt;

</code></pre>
<p><b>我们看一下修改之后的整体效果:</b></p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210123000703.gif" alt="登录完善简单效果" loading="lazy"></p>
<p>访问首页会自动跳转到登录页,登录成功之后,会记录登录状态。</p>
<p><code>F12</code> 打开谷歌开发者工具:</p>
<ul>
<li>打开 <code>Application</code> ,在 <code>Session Storage</code> 中看到我们存储的信息</li>
</ul>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210123000858.png" alt="image-20210123000856893" loading="lazy"></p>
<ul>
<li>打开<code>vue</code> 开发工具,在 <code>Vuex</code> 中也能看到我们 <code>store</code>中的数据</li>
</ul>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/20210123001145.png" alt="image-20210123001144379" loading="lazy"></p>
<ul>
<li>再次登录,打开Network,可以发现异步式请求请求头里已经添加了 <code>token</code></li>
</ul>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/image-20210123103330123.png" alt="image-20210123103330123" loading="lazy"></p>
<p>再次说一下,这里偷了懒,登录用封装的公共请求方法是不合理的,毕竟登录就是为了获取token,request.js又对token进行了拦截,所以我怼我自己😂比较好的做法可以参考 <code>vue-element-admin</code> ,在 store 中写 <code>action</code> 用来登录。</p>
<h1 id="五用户管理功能">五、用户管理功能</h1>
<p>上面我们已经写了一个简单的登录功能,通过这个功能,基本可以对SpringBoot+Vue前后端分离开发有有一个初步了解,在实际工作中,一般的工作都是基于基本框架已经成型的项目,登录、鉴权、动态路由、请求封装这些基础功能可能都已经成型。所以后端的日常工作就是<code>写接口</code>、<code>写业务</code> ,前端的日常工作就是 <code>调接口</code>、<code>写界面</code>,通过接下来的用户管理功能,我们能熟悉这些日常的开发。</p>
<h2 id="1后端开发-1">1、后端开发</h2>
<p>后端开发,crud就完了。</p>
<h3 id="11自定义分页查询">1.1、自定义分页查询</h3>
<p>按照官方文档,来进行MP的分页。</p>
<h4 id="111分页配置">1.1.1、分页配置</h4>
<p>首先需要对分页进行配置,创建分页配置类</p>
<pre><code class="language-java">/**
* @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;
    }
}
</code></pre>
<h4 id="112自定义sql">1.1.2、自定义sql</h4>
<p>作为Mybatis的增强工具,MP自然是支持自定义sql的。其实在MP中,单表操作基本上是不用自己写sql。这里只是为了演示MP的自定义sql,毕竟在实际应用中,批量操作、多表操作还是更适合自定义sql实现。</p>
<ul>
<li>修改pom.xml,在 &lt;build&gt;中添加:</li>
</ul>
<pre><code class="language-java">      &lt;resources&gt;
            &lt;resource&gt;
                &lt;directory&gt;src/main/java&lt;/directory&gt;
                &lt;includes&gt;
                  &lt;include&gt;**/*.xml&lt;/include&gt;
                &lt;/includes&gt;
                &lt;filtering&gt;true&lt;/filtering&gt;
            &lt;/resource&gt;
            &lt;resource&gt;
                &lt;directory&gt;src/main/resources&lt;/directory&gt;
            &lt;/resource&gt;
      &lt;/resources&gt;
</code></pre>
<ul>
<li>配置文件:在application.properties中添加mapper扫描路径及实体类别名包</li>
</ul>
<pre><code class="language-java"># mybatis-plus
mybatis-plus.mapper-locations=classpath:cn/fighter3/mapper/*.xml
mybatis-plus.type-aliases-package=cn.fighter3.entity
</code></pre>
<ul>
<li>在UserMapper.java 中定义分页查询的方法</li>
</ul>
<pre><code class="language-java">IPage&lt;User&gt; selectUserPage(Page&lt;User&gt; page,String keyword);
</code></pre>
<ul>
<li>在UserMapper.java 同级目录下新建 UserMapper.xml文件</li>
</ul>
<pre><code class="language-java">&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"&gt;
&lt;mapper namespace="cn.fighter3.mapper.UserMapper"&gt;
    &lt;select id="selectUserPage" resultType="User"&gt;
      select * from user
      &lt;where&gt;
            &lt;if test="keyword !=null and keyword !='' "&gt;
                or login_name like CONCAT('%',#{keyword},'%')
                or user_name like CONCAT('%',#{keyword},'%')
                or email like CONCAT('%',#{keyword},'%')
                or address like CONCAT('%',#{keyword},'%')
            &lt;/if&gt;
      &lt;/where&gt;
    &lt;/select&gt;
&lt;/mapper&gt;
</code></pre>
<p>这个查询也比较简单,根据关键字查询用户。</p>
<p>OK,我们的自定义分页查询就完成了,可以写个单元测试测一下。</p>
<h3 id="12控制层">1.2、控制层</h3>
<p>新建UserControler,里面也没什么东西,增删改查的接口:</p>
<pre><code class="language-java">/**
* @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&lt;Integer&gt; ids){
      userService.batchDelete(ids);
      return new Result(200,"","");
    }
}
</code></pre>
<p>这里写的也比较简单,直接调用服务层的方法。</p>
<h3 id="13服务层">1.3、服务层</h3>
<p>接口这里就不再贴出了,实现类如下:</p>
<pre><code class="language-java">/**
* @Author 三分恶
* @Date 2021/1/23
* @Description
*/
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;

    /**
    * 分页查询
    **/
    @Override
    public IPage&lt;User&gt; selectUserPage(QueryDTO queryDTO) {
      Page&lt;User&gt; page=new Page&lt;&gt;(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&lt;Integer&gt; ids) {
      userMapper.deleteBatchIds(ids);
    }

}
</code></pre>
<p>这里也比较简单,也没什么业务逻辑。</p>
<p>实际上,业务层至少也会做一些参数校验的工作——我见过有的系统,只是在客户端进行了参数校验,实际上,服务端参数校验是必需的(如果不做,会被怼😔),因为客户端校验相比较服务端校验是不可靠的。</p>
<p>在分页查询 <code>public IPage&lt;User&gt; selectUserPage(QueryDTO queryDTO)</code> 里用了一个业务对象,这种写法,也可以用一些参数校验的插件。</p>
<h3 id="14业务实体">1.4、业务实体</h3>
<p>上面用到了一个业务实体对象,创建一个 业务实体类<code>QueryDTO</code> ,定义了一些参数,这个类主要用于前端向后端传输数据,可以可以使用一些参数校验插件添加参数校验规则。</p>
<pre><code class="language-java">/**
* @Author 三分恶
* @Date 2021/1/23
* @Description 查询业务实体
* 这里仅仅定义了三个参数,在实际应用中可以定义多个参数
*/
public class QueryDTO {
    private Integer pageNo;    //页码
    private Integer pageSize;//页面大小
    private String keyword;    //关键字
    //省略getter、setter
}
</code></pre>
<p>简单测一下,后端👌</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/image-20210126172536248.png" alt="image-20210126172536248" loading="lazy"></p>
<h2 id="2前端开发-1">2、前端开发</h2>
<h3 id="21首页">2.1、首页</h3>
<p>在前面,登录之后,跳转到HelloWorld,还是比较简陋的。本来想直接跳到用户管理的视图,觉得不太好看,所以还是写了一个首页,当然这一部分不是重点。</p>
<p>见过一些后台管理系统的都知道,后台管理系统大概都是像下面的布局:</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/%E5%90%8E%E5%8F%B0%E5%B8%83%E5%B1%80.png" alt="后台布局" loading="lazy"></p>
<p>在ElementUI中提供了这样的布局组件Container 布局容器:</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/image-20210126173415562.png" alt="image-20210126173415562" loading="lazy"></p>
<p>大家都知道根组件是 App.vue ,当然在App.vue中写整体布局是不合适的,因为还有登录页面,所以在 views 下新建 <code>home.vue</code>,采用Container 布局容器来进行布局,使用NavMenu 导航菜单来创建侧边栏。</p>
<p>当然,比较好的做法是<code>home.vue</code>里不写什么内容,将顶部和侧边栏都抽出来作为子页面(组件)。</p>
<pre><code class="language-javascript">&lt;template&gt;
&lt;el-container class="home-container"&gt;
    &lt;!--顶部--&gt;
    &lt;el-header style="margin-right: 15px; width: 100%"&gt;
      &lt;span class="nav-logo"&gt;😀&lt;/span&gt;
      &lt;span class="head-title"&gt;Just A Demo&lt;/span&gt;
      &lt;el-avatar
      icon="el-icon-user-solid"
      style="color: #222; float: right; padding: 20px"
      &gt;{{ this.$store.state.user.userName }}&lt;/el-avatar
      &gt;
    &lt;/el-header&gt;
    &lt;!-- 主体 --&gt;
    &lt;el-container&gt;
      &lt;!-- 侧边栏 --&gt;
      &lt;el-aside width="13%"&gt;
      &lt;el-menu
          :default-active="$route.path"
          router
          text-color="black"
          active-text-color="red"
      &gt;
          &lt;el-menu-item
            v-for="(item, i) in navList"
            :key="i"
            :index="item.name"
          &gt;
            &lt;i :class="item.icon"&gt;&lt;/i&gt;
            {{ item.title }}
          &lt;/el-menu-item&gt;
      &lt;/el-menu&gt;
      &lt;/el-aside&gt;
      &lt;el-main&gt;
      &lt;!--路由占位符--&gt;
      &lt;router-view&gt;&lt;/router-view&gt;
      &lt;/el-main&gt;
    &lt;/el-container&gt;
&lt;/el-container&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
name: "Home",
data() {
    return {
      navList: [
      { name: "/index", title: "首页", icon: "el-icon-s-home" },
      { name: "/user", title: "用户管理",icon:"el-icon-s-custom" },
      ],
    };
},
};
&lt;/script&gt;

&lt;style &gt;
.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;
}


&lt;/style&gt;

</code></pre>
<p>注意 <code> &lt;el-main&gt;</code> 用了路由占位符 <code>&lt;router-view&gt;&lt;/router-view&gt;</code> ,在路由<code>src\router\index.js</code>里进行配置,就可以加载我们的子路由了:</p>
<pre><code class="language-javascript">    {
      path: '/',
      name: 'Default',
      redirect: '/home',
      component: Home
    },
    {
      path: '/home',
      name: 'Home',
      component: Home,
      meta: {
      requireAuth: true
      },
      redirect: '/index',
      children:[
      {
          path:'/index',
          name:'Index',
          component:() =&gt; import('@/views/home/index'),
          meta:{
            requireAuth:true
          }
      },
      }
      ]
    },
</code></pre>
<p>首页本来不想放什么东西,后来想想,还是放了点大家爱看的——没别的意思,快过年了,各位姐夫过年好。🏮😀</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/image-20210126174723686.png" alt="image-20210126174723686" loading="lazy"></p>
<p>图片来自冰冰微博,见水印。</p>
<h3 id="22用户列表">2.2、用户列表</h3>
<p>在<code>views</code>下新建 <code>user</code> 目录,在 <code>user</code> 目录下新建 <code>index.vue</code> ,然后添加为home的子路由:</p>
<pre><code class="language-javascript">    {
      path: '/home',
      name: 'Home',
      component: Home,
      meta: {
      requireAuth: true
      },
      redirect: '/index',
      children:[
      {
          path:'/index',
          name:'Index',
          component:() =&gt; import('@/views/home/index'),
          meta:{
            requireAuth:true
          }
      },
      {
          path:'/user',
          name:'User',
          component:()=&gt;import('@/views/user/index'),
          meta:{
            requireAuth:true
          }
      }
      ]
    },
</code></pre>
<p>接下来开始用户列表功能的编写。</p>
<ul>
<li>首先封装一下api,在user.js中添加调用分页查询接口的api</li>
</ul>
<pre><code class="language-javascript">//获取用户列表
export function userList(data) {
return request({
    url: '/user/list',
    method: 'post',
    data
})
}
</code></pre>
<ul>
<li>在<code>user/index.vue</code> 中导入userList</li>
</ul>
<pre><code class="language-javascript">import { userList} from "@/api/user";
</code></pre>
<ul>
<li>为了在界面初始化的时候加载用户列表,使用了生命周期钩子来调用接口获取用户列表,代码直接一锅炖了</li>
</ul>
<pre><code class="language-javascript">export default {
data() {
    return {
      userList: [], // 用户列表
      total: 0, // 用户总数
      // 获取用户列表的参数对象
      queryInfo: {
      keyword: "", // 查询参数
      pageNo: 1, // 当前页码
      pageSize: 5, // 每页显示条数
      },
    }
created() { // 生命周期函数
    this.getUserList()
},
    methods: {
    getUserList() {
      userList(this.queryInfo)
      .then((res) =&gt; {
          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) =&gt; {
          console.log(err);
      });
    },
    }
</code></pre>
<ul>
<li>
<p>取到的数据,我们用一个表格组件来进行绑定</p>
<pre><code class="language-javascript">      &lt;!--表格--&gt;
      &lt;el-table
          :data="userList"
          border
          stripe
      &gt;
          &lt;el-table-column type="index" label="序号"&gt;&lt;/el-table-column&gt;
          &lt;el-table-column prop="userName" label="姓名"&gt;&lt;/el-table-column&gt;
          &lt;el-table-column prop="loginName" label="登录名"&gt;&lt;/el-table-column&gt;
          &lt;el-table-column prop="sex" label="性别"&gt;&lt;/el-table-column&gt;
          &lt;el-table-column prop="email" label="邮箱"&gt;&lt;/el-table-column&gt;
          &lt;el-table-column prop="address" label="地址"&gt;&lt;/el-table-column&gt;
          &lt;el-table-column label="操作"&gt;
          &lt;/el-table-column&gt;
      &lt;/el-table&gt;
</code></pre>
</li>
</ul>
<p>效果如下,点击用户管理:</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/image-20210126184434700.png" alt="image-20210126184434700" loading="lazy"></p>
<h3 id="23分页">2.3、分页</h3>
<p>在上面的图里,我们看到了在最下面有分页栏,我们接下来看看分页栏的实现。</p>
<p>我们这里使用了 Pagination 分页组件:</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/image-20210126184833582.png" alt="image-20210126184833582" loading="lazy"></p>
<pre><code class="language-javascript">      &lt;!--分页区域--&gt;
      &lt;el-pagination
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
      :current-page="queryInfo.pageNo"
      :page-sizes=""
      :page-size="queryInfo.pageSize"
      layout="total, sizes, prev, pager, next, jumper"
      :total="total"
      &gt;
      &lt;/el-pagination&gt;
</code></pre>
<p>两个监听事件:</p>
<pre><code class="language-java">    // 监听 pageSize 改变的事件
    handleSizeChange(newSize) {
      // console.log(newSize)
      this.queryInfo.pageSize = newSize;
      // 重新发起请求用户列表
      this.getUserList();
    },
    // 监听 当前页码值 改变的事件
    handleCurrentChange(newPage) {
      // console.log(newPage)
      this.queryInfo.pageNo = newPage;
      // 重新发起请求用户列表
      this.getUserList();
    },
</code></pre>
<h3 id="24检索用户">2.4、检索用户</h3>
<p>搜索框已经绑定了<code>queryInfo.keyword</code>,只需要给顶部的搜索区域添加按钮点击和清空事件——重新获取用户列表:</p>
<pre><code class="language-javascript">            &lt;!--搜索区域--&gt;
            &lt;el-input
            placeholder="请输入内容"
            v-model="queryInfo.keyword"
            clearable
            @clear="getUserList"
            &gt;
            &lt;el-button
                slot="append"
                icon="el-icon-search"
                @click="getUserList"
            &gt;&lt;/el-button&gt;
            &lt;/el-input&gt;
</code></pre>
<p>效果如下:</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/image-20210126185429397.png" alt="image-20210126185429397" loading="lazy"></p>
<h3 id="25添加用户">2.5、添加用户</h3>
<ul>
<li>还是先写api,导入后面就略过了</li>
</ul>
<pre><code class="language-javascript">//添加用户
export function userAdd(data) {
return request({
    url: '/user/add',
    method: 'post',
    data
})
}
</code></pre>
<ul>
<li>添加用户我们用到了两个组件 Dialog 对话框组件和 Form 表单组件。</li>
</ul>
<pre><code class="language-javascript">    &lt;!--添加用户的对话框--&gt;
    &lt;el-dialog
      title="添加用户"
      :visible.sync="addDialogVisible"
      width="30%"
      @close="addDialogClosed"
    &gt;
      &lt;!--内容主体区域--&gt;
      &lt;el-form :model="userForm" label-width="70px"&gt;
      &lt;el-form-item label="登录名" prop="loginName"&gt;
          &lt;el-input v-model="userForm.loginName"&gt;&lt;/el-input&gt;
      &lt;/el-form-item&gt;
      &lt;el-form-item label="用户名" prop="userName"&gt;
          &lt;el-input v-model="userForm.userName"&gt;&lt;/el-input&gt;
      &lt;/el-form-item&gt;
      &lt;el-form-item label="密码" prop="password"&gt;
          &lt;el-input v-model="userForm.password" show-password&gt;&lt;/el-input&gt;
      &lt;/el-form-item&gt;
      &lt;el-form-item label="性别" prop="sex"&gt;
          &lt;el-radio v-model="userForm.sex" label="男"&gt;男&lt;/el-radio&gt;
          &lt;el-radio v-model="userForm.sex" label="女"&gt;女&lt;/el-radio&gt;
      &lt;/el-form-item&gt;
      &lt;el-form-item label="邮箱" prop="email"&gt;
          &lt;el-input v-model="userForm.email"&gt;&lt;/el-input&gt;
      &lt;/el-form-item&gt;
      &lt;el-form-item label="地址" prop="address"&gt;
          &lt;el-input v-model="userForm.address"&gt;&lt;/el-input&gt;
      &lt;/el-form-item&gt;
      &lt;/el-form&gt;
      &lt;!--底部按钮区域--&gt;
      &lt;span slot="footer" class="dialog-footer"&gt;
      &lt;el-button @click="addDialogVisible = false"&gt;取 消&lt;/el-button&gt;
      &lt;el-button type="primary" @click="addUser"&gt;确 定&lt;/el-button&gt;
      &lt;/span&gt;
    &lt;/el-dialog&gt;
</code></pre>
<ul>
<li>使用 <code>addDialogVisible</code> 控制对话框可见性,使用<code>userForm</code> 绑定修改用户表单:</li>
</ul>
<pre><code class="language-javascript">      addDialogVisible: false, // 控制添加用户对话框是否显示
      userForm: {
      //用户
      loginName: "",
      userName: "",
      password: "",
      sex: "",
      email: "",
      address: "",
      },
</code></pre>
<ul>
<li>两个函数,<code>addUser</code> 添加用户,<code>addDialogClosed</code> 在对话框关闭时清空表单</li>
</ul>
<pre><code class="language-javascript">    //添加用户
    addUser() {
      userAdd(this.userForm)
      .then((res) =&gt; {
          if (res.data.code === 200) {
            this.addDialogVisible = false;
            this.getUserList();
            this.$message({
            message: "添加用户成功",
            type: "success",
            });
          } else {
            this.$message.error("添加用户失败");
          }
      })
      .catch((err) =&gt; {
          this.$message.error("添加用户异常");
          console.log(err);
      });
    },

    // 监听 添加用户对话框的关闭事件
    addDialogClosed() {
      // 表单内容重置为空
      this.$refs.addFormRef.resetFields();
    },
</code></pre>
<p>效果:</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/image-20210126190500082.png" alt="image-20210126190500082" loading="lazy"></p>
<p>在最后一页可以看到我们添加的用户:</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/image-20210126190528809.png" alt="image-20210126190528809" loading="lazy"></p>
<h3 id="26修改用户">2.6、修改用户</h3>
<ul>
<li>先写api</li>
</ul>
<pre><code class="language-javascript">//修改用户
export function userUpdate(data) {
return request({
    url: '/user/update',
    method: 'post',
    data
})
}
</code></pre>
<ul>
<li>在修改用户这里,我们用到一个作用域插槽,通过<code>slot-scope="scope"</code>接收了当前作用域的数据,然后通过scope.row拿到对应这一行的数据,再绑定具体的属性值就行了。</li>
</ul>
<pre><code class="language-javascript">          &lt;el-table-column label="操作"&gt;
            &lt;!-- 作用域插槽 --&gt;
            &lt;template slot-scope="scope"&gt;
            &lt;!--修改按钮--&gt;
            &lt;el-button
                type="primary"
                size="mini"
                icon="el-icon-edit"
                @click="showEditDialog(scope.row)"
            &gt;&lt;/el-button&gt;
            &lt;/template&gt;
          &lt;/el-table-column&gt;
</code></pre>
<ul>
<li>具体的修改仍然是用对话框加表单的形式</li>
</ul>
<pre><code class="language-javascript">    &lt;!--修改用户的对话框--&gt;
    &lt;el-dialog title="修改用户" :visible.sync="editDialogVisible" width="30%"&gt;
      &lt;!--内容主体区域--&gt;
      &lt;el-form :model="editForm" label-width="70px"&gt;
      &lt;el-form-item label="用户名" prop="userName"&gt;
          &lt;el-input v-model="editForm.userName" :disabled="true"&gt;&lt;/el-input&gt;
      &lt;/el-form-item&gt;
      &lt;el-form-item label="邮箱" prop="email"&gt;
          &lt;el-input v-model="editForm.email"&gt;&lt;/el-input&gt;
      &lt;/el-form-item&gt;
      &lt;el-form-item label="地址" prop="address"&gt;
          &lt;el-input v-model="editForm.address"&gt;&lt;/el-input&gt;
      &lt;/el-form-item&gt;
      &lt;/el-form&gt;
      &lt;!--底部按钮区域--&gt;
      &lt;span slot="footer" class="dialog-footer"&gt;
      &lt;el-button @click="editDialogVisible = false"&gt;取 消&lt;/el-button&gt;
      &lt;el-button type="primary" @click="editUser"&gt;确 定&lt;/el-button&gt;
      &lt;/span&gt;
    &lt;/el-dialog&gt;
</code></pre>
<ul>
<li><code>editDialogVisible</code>控制对话框显示,<code>editForm</code> 绑定修改用户表单</li>
</ul>
<pre><code class="language-javascript">      editDialogVisible: false, // 控制修改用户信息对话框是否显示
      editForm: {
      id: "",
      loginName: "",
      userName: "",
      password: "",
      sex: "",
      email: "",
      address: "",
      },
</code></pre>
<ul>
<li><code>showEditDialog</code> 除了处理对话框显示,还绑定了修改用户对象。<code>editUser</code> 修改用户。</li>
</ul>
<pre><code class="language-javascript">    // 监听 修改用户状态
    showEditDialog(userinfo) {
      this.editDialogVisible = true;
      console.log(userinfo);
      this.editForm = userinfo;
    },
    //修改用户
    editUser() {
      userUpdate(this.editForm)
      .then((res) =&gt; {
          if (res.data.code === 200) {
            this.editDialogVisible = false;
            this.getUserList();
            this.$message({
            message: "修改用户成功",
            type: "success",
            });
          } else {
            this.$message.error("修改用户失败");
          }
      })
      .catch((err) =&gt; {
          this.$message.error("修改用户异常");
          console.loge(err);
      });
    },
</code></pre>
<h3 id="27删除用户">2.7、删除用户</h3>
<ul>
<li>api</li>
</ul>
<pre><code class="language-javascript">//删除用户
export function userDelete(id) {
return request({
    url: '/user/delete',
    method: 'post',
    params: {
      id
    }
})
}
</code></pre>
<ul>
<li>
<p>在操作栏的作用域插槽里添加删除按钮,直接将作用域的id属性传递进去</p>
<pre><code class="language-javascript">          &lt;el-table-column label="操作"&gt;
            &lt;!-- 作用域插槽 --&gt;
            &lt;template slot-scope="scope"&gt;
            &lt;!--修改按钮--&gt;
            &lt;el-button
                type="primary"
                size="mini"
                icon="el-icon-edit"
                @click="showEditDialog(scope.row)"
            &gt;&lt;/el-button&gt;
            &lt;!--删除按钮--&gt;
            &lt;el-button
                type="danger"
                size="mini"
                icon="el-icon-delete"
                @click="removeUserById(scope.row.id)"
            &gt;&lt;/el-button&gt;
            &lt;/template&gt;
          &lt;/el-table-column&gt;
</code></pre>
</li>
<li>
<p><code>removeUserById</code> 根据用户id删除用户</p>
</li>
</ul>
<pre><code class="language-javascript">    // 根据ID删除对应的用户信息
    async removeUserById(id) {
      // 弹框 询问用户是否删除
      const confirmResult = await this.$confirm(
      "此操作将永久删除该用户, 是否继续?",
      "提示",
      {
          confirmButtonText: "确定",
          cancelButtonText: "取消",
          type: "warning",
      }
      ).catch((err) =&gt; err);
      // 如果用户确认删除,则返回值为字符串 confirm
      // 如果用户取消删除,则返回值为字符串 cancel
      // console.log(confirmResult)
      if (confirmResult == "confirm") {
      //删除用户
      userDelete(id)
          .then((res) =&gt; {
            if (res.data.code === 200) {
            this.getUserList();
            this.$message({
                message: "删除用户成功",
                type: "success",
            });
            } else {
            this.$message.error("删除用户失败");
            }
          })
          .catch((err) =&gt; {
            this.$message.error("删除用户异常");
            console.loge(err);
          });
      }
    },
</code></pre>
<p>效果:</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/image-20210126192208197.png" alt="image-20210126192208197" loading="lazy"></p>
<h3 id="28批量删除用户">2.8、批量删除用户</h3>
<ul>
<li>api</li>
</ul>
<pre><code class="language-javascript">//批量删除用户
export function userBatchDelete(data) {
return request({
    url: '/user/delete/batch',
    method: 'post',
    data
})
}
</code></pre>
<ul>
<li>在ElementUI表格组件中有一个多选的方式,手动添加一个<code>el-table-column</code>,设<code>type</code>属性为<code>selection</code>即可</li>
</ul>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/image-20210126192421265.png" alt="image-20210126192421265" loading="lazy"></p>
<pre><code class="language-javascript">&lt;el-table-column type="selection" width="55"&gt; &lt;/el-table-column&gt;
</code></pre>
<p>在表格里添加事件:</p>
<pre><code class="language-javascript">@selection-change="handleSelectionChange"
</code></pre>
<p>下面是官方的示例:</p>
<pre><code class="language-javascript">export default {
    data() {
      return {
      multipleSelection: []
      }
    },

    methods: {
      handleSelectionChange(val) {
      this.multipleSelection = val;
      }
    }
}
</code></pre>
<p>这个示例里取出的参数<code>multipleSelection</code>结构是这样的,我们只需要id,所以做一下处理:</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/image-20210126193018008.png" alt="image-20210126193018008" loading="lazy"></p>
<pre><code class="language-javascript">export default {
    data() {
      return {
      multipleSelection: [],
      ids: [],
      }
    },

    methods: {
      handleSelectionChange(val) {
      this.multipleSelection = val;
      //向被删除的ids赋值
      this.multipleSelection.forEach((item) =&gt; {
          this.ids.push(item.id);
          console.log(this.ids);
      });
      }
    }
}
</code></pre>
<ul>
<li>接下来就简单了,批量删除操作直接cv上面的删除,改一下api函数和参数就可以了</li>
</ul>
<pre><code class="language-javascript">   //批量删除用户
    async batchDeleteUser(){
   // 弹框 询问用户是否删除
      const confirmResult = await this.$confirm(
      "此操作将永久删除用户, 是否继续?",
      "提示",
      {
          confirmButtonText: "确定",
          cancelButtonText: "取消",
          type: "warning",
      }
      ).catch((err) =&gt; err);
      // 如果用户确认删除,则返回值为字符串 confirm
      // 如果用户取消删除,则返回值为字符串 cancel
      if (confirmResult == "confirm") {
      //批量删除用户
      userBatchDelete(this.ids)
          .then((res) =&gt; {
            if (res.data.code === 200) {
            this.$message({
                message: "批量删除用户成功",
                type: "success",
            });
            this.getUserList();
            } else {
            this.$message.error("批量删除用户失败");
            }
          })
          .catch((err) =&gt; {
            this.$message.error("批量删除用户异常");
            console.log(err);
          });
      }
</code></pre>
<p>效果:</p>
<p><img src="https://gitee.com/sanfene/picgo/raw/master/image-20210126193403139.png" alt="image-20210126193403139" loading="lazy"></p>
<p>完整代码有点长,就不贴了,请自行查看源码。</p>
<h1 id="六总结">六、总结</h1>
<p>通过这个示例,相信大家已经对 <code>SpringBoot+Vue</code> 前后端分离开发有了一个初步的掌握。</p>
<p>当然,由于这个示例并不是一个完整的项目,所以技术上和功能上都非常潦草😓</p>
<p>有兴趣的同学可以进一步地去扩展和完善这个示例。👏👏👏</p>
<blockquote>
<p><big><b>源码地址:https://gitee.com/fighter3/springboot-vue-demo.git</b></big></p>
</blockquote>
<p><br><br></p>
<p><big><b>参考:</b></big></p>
<p>【1】:Vue.js - 渐进式 JavaScript 框架</p>
<p>【2】:Element - 网站快速成型工具</p>
<p>【3】:how2j.cn</p>
<p>【4】:Vue + Spring Boot 项目实战</p>
<p>【5】:一看就懂!基于Springboot 拦截器的前后端分离式登录拦截</p>
<p>【6】:手摸手,带你用vue撸后台 系列一(基础篇</p>
<p>【7】:Vue + ElementUI的电商管理系统实例</p><br><br>
来源:https://www.cnblogs.com/three-fighter/p/14332288.html
頁: [1]
查看完整版本: 一步步使用SpringBoot结合Vue实现登录和用户管理功能