转载一篇比较全面的文章
前后端分离开发是当今开发的主流。本篇文章从零开始,一步步使用SpringBoot结合Vue来实现日常开发中最常见的登录功能,以及登录之后对用户的管理功能。通过这个例子,可以快速入门SpringBoot+Vue前后端分离的开发。
在这里首先简单说明一下什么是前后端分离和单页式应用:前后端分离 的核心思想是前端页面通过 ajax 调用后端的 restuful api 进行数据交互,而 单页面应用(single page web application,SPA),就是只有一个页面,并在用户与应用程序交互时动态更新该页面的 Web 应用程序。
简单说明以下本示例中所用到的技术,如图所示:
后端
官网地址:https://spring.io/projects/sp...
官网地址:https://mybatis.plus/
前端:
官网地址:https://cn.vuejs.org/
官网地址:https://element.eleme.cn/#/zh-CN
数据库:
官网地址:https://www.mysql.com/
上面已经简单介绍了本实例用到的技术,在开始本实例之前,最好能对以上技术具备一定程度的掌握。
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
下载对应的版本,一步步安装即可。安装之后,初始界面如下:
PS:自己喜欢哪种软件用哪种都可
VS Code安装后,我们一般还需要搜索安装一些所需要的插件辅助开发。安装插件很简单,在搜索面板中查找到后,直接安装即可。
一般会安装这些插件:
1.5、Chrome
Chrome 是比较流行的浏览器,也是我们前端开发的常用工具。
Chrome 下载途径很多,请自行搜索下载安装。
Chrome下载安装完成之后,建议安装一个插件 Vue.js devtools
,是非常好用的 vue 调试工具。
谷歌商店下载地址:https://chrome.google.com/web...
数据库采用的是MySQL5.7,安装可以参考: Win10配置免安装版MySQL5.7
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
首页文件的初始代码如下:
demo-vue
需要注意的是 这一行带代码,下面有一行注释,构建的文件将会被自动注入,也就是说我们编写的其它的内容都将在这个 div 中展示。
所谓单页面应用,就是整个项目只有这一个 html 文件,当我们打开这个应用,表面上可以有很多页面,实际上它们都是动态地加载在一个 div
中。
1.3.2、App.vue
这个文件称为“根组件”,因为其它的组件又都包含在这个组件中。
.vue
文件是一种自定义文件类型,在结构上类似 html,一个 .vue 文件即是一个 vue 组件。先看它的初始代码:
![](./assets/logo.png)
这里也有一句 这个文件最关键的一点其实是第四行, 1.3.3、main.js App.vue 和 index.html是怎么联系的?关键点就在于这个文件: 最上面 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是单式应用的关键,这里我们来看一下 最上面 import 了几个组件,在 vue-cli会我们的更改进行热更新,再次打开 http://localhost:8080/,界面发生改变: 2.1、后端项目创建 后端项目创建如下: 2.3、引入MybatisPlus 如果对MybatisPlus不熟悉,入门可以参考 SpringBoot学习笔记(十七:MyBatis-Plus ) 想了解更多可以直接查看官网。 2.3.1、引入MP依赖 由于本实例的数据库表非常简单,只有一个单表,所以这里我们直接将基本的增删改查写出来 2.3.2、数据库创建 数据库设计非常简单,只有一张表。 建表语句如下: 2.3.3、配置 在 在启动类里添加 2.3.3、相关代码 MP提供了代码生成器的功能,可以按模块生成Controller、Service、Mapper、实体类的代码。在数据库表比较多的情况下,能提升开发效率。官网给出了一个Demo,有兴趣的可以自行查看。 OK,到此单表的增删改查功能已经完成了,是不是很简单。 可以写一个单元测试测一下。 2.3.4、单元测试 至此前后端项目基本搭建完成,接下来开始进行功能开发。 1.1、登录界面 在前面访问页面的时候,有一个 V logo,看起来比较奇怪,我们先把它去掉,这个图片的引入是在根组件中—— 在src目录下新建文件夹views,在views下新建文件 1.2、添加路由 在 router OK,现在在浏览器里输入 页面有点粗糙简陋对不对,没关系,我们可以引入 1.3、引入ElementUI美化界面 Element 的官方地址为 http://element-cn.eleme.io/#/... ,官方文档比较好懂,大部分组件复制粘贴即可。 1.3.1、安装Element UI 在vscode 中打开终端,运行命令 1.3.2、引入 Element 引入分为完整引入和按需引入两种模式,按需引入可以缩小项目的体积,这里我们选择完整引入。 根据文档,我们需要修改 main.js 为如下内容: 1.3.3、使用ElementUI美化登录页面 现在开始使用 ElementUI和 css美化我们的登录界面,修改后的 需要注意: 好了,看看我们修改之后的登录界面效果: OK,登录界面的面子已经做好了,但是里子还是空的,没法和后台交互。 1.4、引入axios发起请求 相信大家都对 ajax 有所了解,前后端分离情况下,前后端交互的模式是前端发出异步式请求,后端返回 json 。 axios 是一个基于Promise 用于浏览器和 nodejs 的 HTTP 客户端,本质上也是对原生XHR的封装,只不过它是Promise的实现版本,符合最新的ES规范。在这里我们只需要知道它是非常强大的网络请求处理库,且得到广泛应用即可。 在项目目录下运行命令 在 那么怎么使用 在 这个方法里通过 axios 向后台发起了请求,如果返回成功的结果就跳转到 在登录按钮里触发这个方法: 那么现在就能向后台发起请求了吗?还没完。 1.5、前端相关配置 反向代理 修改 这么一来,我们在前面写的登录请求,访问的后台地址实际就是 跨域配置 前后端分离会带来一个问题—跨域,关于跨域,这里就不展开讲解。在 2.1、统一结果封装 这里我们创建了一个 Result 类,用于异步统一返回的结果封装。一般来说,结果里面有几个要素必要的 实际上由于响应码是固定的, 2.2、登录业务实体类 为了接收前端登录的数据,我们这里创建了一个登录用的业务实体类: 2.3、控制层 LoginController,进行业务响应: 2.4、业务层 业务层进行实际的业务处理。 启动后端项目: 访问登录界面,效果如下: 这样一个简答的登录就完成了,接下来,我们会对这个登录进一步完善。 前面虽然实现了登录,但只是一个简单的登录跳转,实际上并不能对用户的登录状态进行判别,接下来我们进一步完善登录功能。 首先开始后端的开发。 1.1、拦截器 在前后端分离的情况下,比较流行的认证方案是 1.2、拦截器配置 拦截器创建完成之后,还需要进行配置。 1.3、跨域配置 细致的同学可能会发现,在之前的后台接口,有一个注解 1.3、登录service 这样一来,后端就需要生成一个 其中对返回的 最后,测试一下登录接口: PS:软件为Postman,不知道的可以百度查询使用方法 OK,没有问题。 前面我们使用了后端拦截器,接下来我们尝试用前端实现相似的功能。 实现前端登录器,需要在前端判断用户的登录状态。我们可以像之前那样在组件的 data 中设置一个状态标志,但登录状态应该被视为一个全局属性,而不应该只写在某一组件中。所以我们需要引入一个新的工具——Vuex,它是专门为 Vue 开发的状态管理方案,我们可以把需要在各个组件中传递使用的变量、方法定义在这里。 2.1引入Vuex 首先在终端里使用命令 在 src 目录下新建一个文件夹 store,并在该目录下新建 index.js 文件,在该文件中引入 vue 和 vuex,代码如下: 接下来,在 这里我们还用到了 2.2、修改路由配置 为了能够区分哪些路由需要被拦截,我们在路由里添上一个元数据 完整的 2.3、使用钩子函数判断是否拦截 上面我们添加了 钩子函数及在某些时机会被调用的函数。这里我们使用 打开 并修改vue对象里的内容,使 store 能全局使用: 解下来,我们写 完整的 main.js 代码如下: 2.4、请求封装 我们前面写的后端拦截器,对请求进行了拦截,要求请求头里携带token,这个怎么处理呢? 答案是封装 在 src 目录下新建目录 utils ,在uitls 目录下新建文件 request.js 。 首先导入 接下来在请求拦截器中,给请求头添加 完整的request.js: 注意创建axios实例里用到了 baseUrl ,在 这样一封装,我们就不用每个请求都手动来塞 token,或者来做一些统一的异常处理,一劳永逸。 而且我们的 api 可以根据 2.5、封装api request.js 既然已经封装了,那么接下来就要开始用它。 我们可以像上面的 一般项目中, 在 src 下新建 当然,事实上登录用 2.6、login.vue 之前的登录组件中,我们只是判断后端返回的状态码,如果是 200,就重定向到首页。在经过前面的配置后,我们需要修改一下登录逻辑,以最终实现登录拦截。 修改后的逻辑如下: 1.点击登录按钮,向后端发送数据 修改后的 完整的 2.7、HelloWorld.vue 大家应该还记得,到目前为止,我们 的 完整的 我们看一下修改之后的整体效果: 访问首页会自动跳转到登录页,登录成功之后,会记录登录状态。 再次说一下,这里偷了懒,登录用封装的公共请求方法是不合理的,毕竟登录就是为了获取token,request.js又对token进行了拦截,所以我怼我自己 比较好的做法可以参考 上面我们已经写了一个简单的登录功能,通过这个功能,基本可以对SpringBoot+Vue前后端分离开发有有一个初步了解,在实际工作中,一般的工作都是基于基本框架已经成型的项目,登录、鉴权、动态路由、请求封装这些基础功能可能都已经成型。所以后端的日常工作就是 后端开发,crud就完了。 1.1、自定义分页查询 按照官方文档,来进行MP的分页。 1.1.1、分页配置 首先需要对分页进行配置,创建分页配置类 1.1.2、自定义sql 作为Mybatis的增强工具,MP自然是支持自定义sql的。其实在MP中,单表操作基本上是不用自己写sql。这里只是为了演示MP的自定义sql,毕竟在实际应用中,批量操作、多表操作还是更适合自定义sql实现。 这个查询也比较简单,根据关键字查询用户。 OK,我们的自定义分页查询就完成了,可以写个单元测试测一下。 1.2、控制层 新建UserControler,里面也没什么东西,增删改查的接口: 这里写的也比较简单,直接调用服务层的方法。 1.3、服务层 接口这里就不再贴出了,实现类如下: 这里也比较简单,也没什么业务逻辑。 实际上,业务层至少也会做一些参数校验的工作——我见过有的系统,只是在客户端进行了参数校验,实际上,服务端参数校验是必需的(如果不做,会被怼),因为客户端校验相比较服务端校验是不可靠的。 在分页查询 1.4、业务实体 上面用到了一个业务实体对象,创建一个 业务实体类 简单测一下,后端 2.1、首页 在前面,登录之后,跳转到HelloWorld,还是比较简陋的。本来想直接跳到用户管理的视图,觉得不太好看,所以还是写了一个首页,当然这一部分不是重点。 见过一些后台管理系统的都知道,后台管理系统大概都是像下面的布局: 在ElementUI中提供了这样的布局组件Container 布局容器: 大家都知道根组件是 App.vue ,当然在App.vue中写整体布局是不合适的,因为还有登录页面,所以在 views 下新建 当然,比较好的做法是 注意 首页本来不想放什么东西,后来想想,还是放了点大家爱看的——没别的意思,快过年了,各位姐夫过年好。 图片来自冰冰微博,见水印。 2.2、用户列表 在 接下来开始用户列表功能的编写。 取到的数据,我们用一个表格组件来进行绑定 效果如下,点击用户管理 2.3、分页 在上面的图里,我们看到了在最下面有分页栏,我们接下来看看分页栏的实现。 我们这里使用了 Pagination 分页组件: 两个监听事件: 2.4、检索用户 搜索框已经绑定了 效果如下: 2.5、添加用户 效果: 在最后一页可以看到我们添加的用户: 2.6、修改用户 2.7、删除用户 在操作栏的作用域插槽里添加删除按钮,直接将作用域的id属性传递进去 2.8、批量删除用户 在表格里添加事件: 下面是官方的示例: 这个示例里取出的参数 完整代码有点长,就不贴了,请自行查看源码。 通过这个示例,相信大家已经对 当然,由于这个示例并不是一个完整的项目,所以技术上和功能上都非常潦草 有兴趣的同学可以进一步地去扩展和完善这个示例。 源码地址:https://gitee.com/fighter3/sp... 参考: 【1】:Vue.js - 渐进式 JavaScript 框架 【2】:Element - 网站快速成型工具 【3】:how2j.cn 【4】:Vue + Spring Boot 项目实战 【5】:一看就懂!基于Springboot 拦截器的前后端分离式登录拦截 【6】:手摸手,带你用vue撸后台 系列一(基础篇 【7】:Vue + ElementUI的电商管理系统实例 原文-三分恶:一步步使用SpringBoot结合Vue实现登录和用户管理功能 - SegmentFault 思否标签里的内容即该组件的脚本,也就是 js 代码,export default 是 ES6 的语法,意思是将这个组件整体导出,之后就可以使用 import 导入组件了。大括号里的内容是这个组件的相关属性。
,是一个容器,名字叫“路由视图”,意思是当前路由( URL)指向的内容将显示在这个容器中。也就是说,其它的组件即使拥有自己的路由(URL,需要在 router 文件夹的 index.js 文件里定义),也只不过表面上是一个单独的页面,实际上只是在根组件 App.vue 中。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: '
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
}
]
})
routes
这个数组里定义了路由,可以看到 /
路径路由到了 HelloWorld
这个组件,所以访问 http://localhost:8080/ 会看到上面的界面。为了更直观的理解,这里可以对 src\components\HelloWorld.vue
组件进行修改,修改如下:
2、后端项目搭建
New Project
,选择 Spring Intializr
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;
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);
}
}
/**
* @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等
}
public interface UserMapper extends BaseMapper
@SpringBootTest
class UserMapperTest {
@Autowired
UserMapper userMapper;
@Test
@DisplayName("插入数据")
public void testInsert(){
User user=new User("test1","test","t123","男","[email protected]","满都镇");
Integer id=userMapper.insert(user);
System.out.printf(id.toString());
}
@Test
@DisplayName("根据id查找")
public void testSelectById(){
User user=userMapper.selectById(1);
System.out.println(user.toString());
}
@Test
@DisplayName("查找所有")
public void testSelectAll(){
List userList=userMapper.selectObjs(null);
System.out.println(userList.size());
}
@Test
@DisplayName("更新")
public void testUpdate(){
User user=new User();
user.setId(1);
user.setAddress("金葫芦镇");
Integer id=userMapper.updateById(user);
System.out.println(id);
}
@Test
@DisplayName("删除")
public void testDelete(){
userMapper.deleteById(1);
}
}
三、登录功能开发
1、前端开发
src\App.vue
,把下面一行注释或者去掉。login.vue
登录
用户名:
密码:
\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
}
]
})
http://localhost:8080/#/login
,就可以访问登录页面:ElmentUI
,使用ElementUI中已经成型的组件。 npm i element-ui -S
,就安装了 element ui 最新版本—当前是 2.15.0// 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: '
login.vue
代码如下:
系统登录
src\assets
路径下新建一个一个文件夹 img
,在 img 里放了一张网上找到的无版权图片作为背景图App.vue
里删了一行代码,不然会有空白:margin-top: 60px;margin-top: 60px;
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 => {
})
}
},
/
路由下。
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、后端开发
/**
* @Description: 统一结果封装
**/
public class Result {
//相应码
private Integer code;
//信息
private String message;
//返回数据
private Object data;
//省略getter、setter、构造方法
}
code
属性应该是一个枚举值,这里作了一些简化。public class LoginDTO {
private String loginName;
private String password;
//省略getter、setter
}
@RestController
public class LoginController {
@Autowired
LoginService loginService;
@PostMapping(value = "/api/login")
@CrossOrigin //后端跨域
public Result login(@RequestBody LoginDTO loginDTO){
return loginService.login(loginDTO);
}
}
public interface LoginService {
public Result login(LoginDTO loginDTO);
}
/**
* @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
四、登录功能完善
1、后端开发
JWT认证
认证,和传统的session认证不同,jwt是一种无状态的认证方法,也就是服务端不再保存任何认证信息。出于篇幅考虑,我们这里不再引入 JWT
,只是简单地判断一下前端的请求头里是否存有 token
。对JWT 认证感兴趣的可以查看文章:SpringBoot学习笔记(十三:JWT ) 。
interceptor
包,包下新建拦截器 LoginInterceptor
/**
* @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);
}
}
util
包,util 包下新建工具类 HttpContextUtil
/**
* @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");
}
}
/**
* @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");
}
}
@CrossOrigin
,这个注解是用来跨域的,每个接口都写一遍肯定是不太方便的,这里我们 创建跨域配置类并添加统一的跨域配置:/**
* @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);
}
}
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
data
封装了一个VO:/**
* @Description: 登录VO
**/
public class LoginVO implements Serializable {
private Integer id;
private String token;
private User user;
//省略getter、setter
}
2、前端开发
npm install vuex --save
来安装 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
相比,比较利于保证实时性。 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
}
]
})
requireAuth
, 接下来就要用到它了。router.beforeEach()
,意思是在访问每一个路由前调用。src\main.js
,首先添加对 store
的引用import store from './store'
new Vue({
el: '#app',
router,
// 注意这里
store,
components: { App },
template: '
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()
}
}
)
// 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: '
axios
。axios
和 store
:import axios from 'axios'
import store from '@/store'
token
:// request 请求拦截
service.interceptors.request.use(
config => {
if (store.state.token) {
config.headers['token'] = window.sessionStorage.getItem("token")
}
return config
},
error => {
// do something with request error
console.log(error) // for debug
return Promise.reject(error)
}
)
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
config\dev.env.js
里修改配置:module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
BASE_API: '"http://localhost:8088/api"',
})
env
环境变量动态切换。axios
添加到 main.js 中,这样就能被全局调用。但是有更好的用法。viess
下放的是我们各个业务模块的视图,对应这些业务模块,我们创建对应的 api
来封装对后台的请求,这样即使业务模块很多,但关系仍然是比较清晰的。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.受到后端返回的成功代码时,触发 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
:
系统登录
/
路径还是指向 HelloWorld.vue
这个组件,为了演示 vuex
状态的全局使用,我们做一些更改,添加一个生命周期的钩子函数,来获取 store
中存储的用户名: computed: {
userName() {
return this.$store.state.user.userName
}
}
HelloWorld.vue
:
F12
打开谷歌开发者工具:
Application
,在 Session Storage
中看到我们存储的信息
vue
开发工具,在 Vuex
中也能看到我们 store
中的数据
token
vue-element-admin
,在 store 中写 action
用来登录。五、用户管理功能
写接口
、写业务
,前端的日常工作就是 调接口
、写界面
,通过接下来的用户管理功能,我们能熟悉这些日常的开发。1、后端开发
/**
* @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;
}
}
# mybatis-plus
mybatis-plus.mapper-locations=classpath:cn/fighter3/mapper/*.xml
mybatis-plus.type-aliases-package=cn.fighter3.entity
IPage
/**
* @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
/**
* @Author 三分恶
* @Date 2021/1/23
* @Description
*/
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
/**
* 分页查询
**/
@Override
public IPage
public IPage
里用了一个业务对象,这种写法,也可以用一些参数校验的插件。QueryDTO
,定义了一些参数,这个类主要用于前端向后端传输数据,可以可以使用一些参数校验插件添加参数校验规则。/**
* @Description 查询业务实体
* 这里仅仅定义了三个参数,在实际应用中可以定义多个参数
*/
public class QueryDTO {
private Integer pageNo; //页码
private Integer pageSize; //页面大小
private String keyword; //关键字
//省略getter、setter
}
2、前端开发
home.vue
,采用Container 布局容器来进行布局,使用NavMenu 导航菜单来创建侧边栏。home.vue
里不写什么内容,将顶部和侧边栏都抽出来作为子页面(组件)。
用了路由占位符
,在路由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
}
},
}
]
},
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
}
}
]
},
//获取用户列表
export function userList(data) {
return request({
url: '/user/list',
method: 'post',
data
})
}
user/index.vue
中导入userListimport { 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);
});
},
}
// 监听 pageSize 改变的事件
handleSizeChange(newSize) {
// console.log(newSize)
this.queryInfo.pageSize = newSize;
// 重新发起请求用户列表
this.getUserList();
},
// 监听 当前页码值 改变的事件
handleCurrentChange(newPage) {
// console.log(newPage)
this.queryInfo.pageNo = newPage;
// 重新发起请求用户列表
this.getUserList();
},
queryInfo.keyword
,只需要给顶部的搜索区域添加按钮点击和清空事件——重新获取用户列表
//添加用户
export function userAdd(data) {
return request({
url: '/user/add',
method: 'post',
data
})
}
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();
},
//修改用户
export function userUpdate(data) {
return request({
url: '/user/update',
method: 'post',
data
})
}
slot-scope="scope"
接收了当前作用域的数据,然后通过scope.row拿到对应这一行的数据,再绑定具体的属性值就行了。
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);
});
},
//删除用户
export function userDelete(id) {
return request({
url: '/user/delete',
method: 'post',
params: {
id
}
})
}
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);
});
}
},
//批量删除用户
export function userBatchDelete(data) {
return request({
url: '/user/delete/batch',
method: 'post',
data
})
}
el-table-column
,设type
属性为selection
即可@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);
});
}
}
}
//批量删除用户
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
前后端分离开发有了一个初步的掌握。