VUE+ElementUI+SpringBoot实现前后端分离

环境配置

安装node与npm

sudo apt-get install -y nodejs

配置运行路径

sudo ln -s /usr/bin/nodejs /usr/bin/node

但是npm用同样的方法处理,报错:

bash: /usr/bin/npm: 符号连接的层数过多升级node到指定版本,后面接版本号

先寻找路径:whereis npm 然后查看该目录下路径,删掉有误的

然而再安装npm时却没有对应的包(我还以为跟windows一样npm会跟node一起安装呢..)

sudo apt remove nodejs 先删除了看看有没有别的包可用

看了下BBS,好像是官方apt源有点问题一直安装不了npm

尝试安装nodejs-bin, 查询npm时仍然

bash: /usr/bin/npm: 权限不够

寻找npm sudo find / -name npm

看到可以用nvm来解决这个问题

nvm官方文档

curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.2/install.sh | bash

nvm install node

果真方便

nvm的详细使用(Linux)

稳妥起见还是安装了一个LTS版本

安装vue-cli

开始下载不动,配置一下国内源

npm config set registry https://registry.npm.taobao.org

npm install vue

npm install -g @vue/cli

速度果然很快了

vue -V 4.2.3版本

使用 VUE UI创建项目

vue ui 启动8000端口,界面如下:

该GUI界面只支持3.0以上版本,深色控改成了夜间模式…

手动完成相关配置,项目创建完成后自动打开一个仪表盘

运行服务器, 打开8080端口,出现前端页面,VUE牛皮!

在IDEA中开发

前端VUE

在IDEA中导入刚才创建的项目,并安装vue.js插件

在IDE的终端 npm run serve 也可以直接启动8080端口

用假数据测试

新建Book.vue文件,写好页面内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<template>
<div>
<table>
<tr>
<td>编号</td>
<td>书名</td>
<td>作者</td>
</tr>
<tr v-for="item in books">
<td>{{item.id}}</td>
<td>{{item.name}}</td>
<td>{{item.author}}</td>

</tr>
</table>
</div>
</template>

<script>
export default {
name: "Book",
data(){
return{
books:[
{
id:1,
name: 'java_learn',
author: 'wallen'
},
{
id:2,
name: 'java_learn30day',
author: 'wallen'
},
{
id:3,
name:'java',
author: 'wallen'
},
]
}
}
}
</script>

router的index.js中引入该组件

前端页面/book热更新

后端Spring Boot

创建一个后端项目,选择所需依赖

配置application.perporties

连接到Mysql

1
2
3
4
5
6
7
8
9
10
11
12
spring.datasource.url = jdbc:mysql://localhost:3306/bookstore
spring.datasource.username = ...
spring.datasource.password = ...
spring.datasource.driverClassName = com.mysql.jdbc.Driver

# Specify the DBMS
spring.jpa.database = MYSQL
# Show or not log for each sql query
spring.jpa.show-sql = true
spring.jpa.properties.hibernate.format_sql = true

server.port=8181
JPA绑定数据

创建实体类BookList

1
2
3
4
5
6
7
8
@Entity
@Data
public class BookList {
@Id
private Integer id;
private String name;
private String author;
}

创建BookListRepository 接口

1
2
public interface BookListRepository extends JpaRepository<BookList,Integer> {
}

对该接口进行单元测试

接口右键go to test

添加测试代码

1
2
3
4
5
6
7
8
9
10
11
@SpringBootTest
class BookListRepositoryTest {

@Autowired
private BookListRepository bookListRepository;

@Test
void findAll(){
System.out.println(bookListRepository.findAll());
}
}

运行findAll()方法,报错

Failed to resolve org.junit.platform:junit-platform-launcher:1.5.2

在maven的setting.xml文件中添加阿里云镜像后依赖导入成功,运行报错

Caused by: java.sql.SQLSyntaxErrorException: Table ‘bookstore.book_list’ doesn’t exist

难道不能用驼峰?把之前的类和接口中的List都改成小写,再次运行,成功。可能的原因是JPA与数据库表名对应时只有首字母的大小写可以忽略

因为配置了show-sql 和 hibernate.format_sql 才显示上面的部分

根据提示:Loading class com.mysql.jdbc.Driver'. This is deprecated. The new driver class iscom.mysql.cj.jdbc.Driver`. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.

更改application中的配置类为com.mysql.cj.jdbc.Driver

对外接口

创建BooklistHandler作为controller

1
2
3
4
5
6
7
8
9
10
11
@RestController
@RequestMapping("/book")
public class BooklistHandler {
@Autowired
private BooklistRepository booklistRepository;

@GetMapping("/findAll")
public List<Booklist> findAll(){
return booklistRepository.findAll();
}
}

运行项目application的main方法, 访问 http://localhost:8181/book/findAll ,成功读出数据

前后端Axios对接

在前端页面发送请求后端8181端口

vue add axios 在IDEA终端中安装插件axios,安装完毕在src中自动添加了一个plugins文件夹

Vue2.0之后,尤雨溪推荐大家用axios替换JQuery ajax,axios 是一个基于Promise 用于浏览器和 nodejs 的 HTTP 客户端,本质上也是对原生XHR的封装,只不过它是Promise的实现版本,符合最新的ES规范,它本身具有以下特征:

  1. 从浏览器中创建 XMLHttpRequest
  2. 支持 Promise API
  3. 客户端支持防止CSRF
  4. 提供了一些并发请求的接口(重要,方便了很多的操作)
  5. 从 node.js 创建 http 请求
  6. 拦截请求和响应
  7. 转换请求和响应数据
  8. 取消请求
  9. 自动转换JSON数据

axios是通过promise实现对ajax技术的一种封装,就像jQuery实现ajax封装一样。简单来说: ajax技术实现了网页的局部数据刷新,axios实现了对ajax的封装。

Axios中文说明

在Book.vue中添加初始化方法create()

1
2
3
4
5
created() {
axios.get('http://localhost:8181/book/findAll').then(function(resp){
console.log(resp)
})
}

请求类型为get , then 是回调函数,传入reponse结果

CROS跨域问题

可以在前端或者后端进行解决,这里选择后端。

创建CrosConfig配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
public class CrosConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
//设置允许跨域的路径
registry.addMapping("/**")
//设置允许跨域请求的域名
.allowedOrigins("*")
//是否允许证书 不再默认开启
.allowCredentials(true)
//设置允许的方法
.allowedMethods("GET", "POST", "DELETE", "PUT","HEAD","OPTIONS")
//跨域允许时间
.maxAge(3600)
.allowedHeaders("*");
}
}

然后刷新8080,就可以在控制台获取到后台的data了

后台数据展示到页面
1
2
3
4
5
6
created() {
const _this = this
axios.get('http://localhost:8181/book/findAll').then(function(resp){
_this.books = resp.data
})
}

这里需要注意的是function原本的this 不能访问到外面的变量,所以要用_this把当前页面传进去

数据成功加载出来了

集成ElementUI

npm install --save element-ui 项目命令行安装或者vue ui 查找element插件安装

重启项目,打开8080端口,主页会多出一个Button

Element官方文档

CV布局容器中代码到App.vue , 注意html部分的el-container要放到div中

主页已经与官方示例一致,接下来分析el-xxx 了解其构成

  • el-menu
    • :default-openeds 默认展开的子菜单,通过子菜单的index 值来关联
    • :default-active 默认选中的子菜单
  • el-submenu 可展开的菜单
    • index 必须是加引号的文本类型
    • template 菜单名
    • i 通过class 设置菜单图标
      • el-icon-message
      • el-icon-menu
      • el-icon-setting
      • 更多样式参考icon组件
  • el-menu-item 菜单不可展开的子节点
    • index 必须是加引号的文本类型
  • el-main 一般用来存放页面跳转时的刷新内容
    • router-view 放在这里实现路由

导航动态加载

App.vue中设置左侧菜单

1
2
3
4
5
6
7
8
<el-aside width="200px" style="background-color: rgb(238, 241, 246)">
<el-menu>
<el-submenu v-for="(item,index) in $router.options.routes" :index="index+''">
<template slot="title"><i class="el-icon-setting"></i>{{item.name}}</template>
<el-menu-item v-for="(item2,index2) in item.children" :index="index+'-'+index2">{{item2.name}}</el-menu-item>
</el-submenu>
</el-menu>
</el-aside>

v-for循环可以遍历index.js中配置的routes,加载相应个数的el-menu 这样的好处是方便以后新增菜单页面

读取出页面名称,:index = “index+’-‘“ 将遍历的下标转为文本类型的菜单index, 方便控制

index.js路由内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const routes = [
{
path: '/',
name: 'nav1',
component: App,
children:[
{
path: '/pageone',
name:"pageone",
component: PageOne
},
{
path: '/pagetwo',
name:"pagetwo",
component: PageTwo
},
]
},
{
path:"/navigation",
name:"nav2",
component:App,
children:[
{
path: '/pagethree',
name:"pagthree",
component: PageThree
},
{
path: '/pagefour',
name:"pagefour",
component: PageFour
},
]
}
]

导航效果

左边的导航可以点击展开,下面的页面也可以点击切换了

确定驻留区和更新区

可以看到上面的页面存在着页面嵌套的问题,接下来要对切换区域进行划分

App.vue只加载路由

1
2
3
4
5
<template>
<div id="app">
<router-view></router-view>
</div>
</template>

菜单栏放在Index.vue中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<el-container style="height: 500px; border: 1px solid #eee">
<el-aside width="200px" style="background-color: rgb(238, 241, 246)">
<el-menu>
<el-submenu v-for="(item,index) in $router.options.routes" :index="index+''">
<template slot="title"><i class="el-icon-setting"></i>{{item.name}}</template>
<el-menu-item v-for="(item2,index2) in item.children" :index="index+'-'+index2">{{item2.name}}</el-menu-item>
</el-submenu>
</el-menu>
</el-aside>

<el-main>
<router-view></router-view>
</el-main>

</el-container>
</template>

然后把index.js中的app组件改为index,即可去掉嵌套

菜单切换与页面绑定

  1. el-menu 标签添加router属性

    Index.vue中el-menu 标签添加 router

  2. 在页面添加router-view 标签,这个容器可以动态渲染router

  3. el-menu-item 标签index 值就是要跳转的router

    el-menu-item 标签index 值改为`:index=”item2.path”

  4. 默认初始页面 redirect:跳转路径

    路由数组中添加 redirect:"/pageone"

  5. 当前显示的页面被选中(特殊颜色)

    :class="$route.path==item2.path?'is-active':''" 即地址栏路径与菜单栏item的index路径一致时,将该item标记特殊颜色

  6. 默认展开菜单 el-menu 添加 :default-openeds="['0','1']" 表示默认展开菜单1和菜单2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<el-container style="height: 500px; border: 1px solid #eee">
<el-aside width="200px" style="background-color: rgb(238, 241, 246)">
<el-menu router :default-openeds="['0','1']">
<el-submenu v-for="(item,index) in $router.options.routes" :index="index+''">
<template slot="title"><i class="el-icon-setting"></i>{{item.name}}</template>
<el-menu-item v-for="(item2,index2) in item.children" :index="item2.path" :class="$route.path==item2.path?'is-active':''">{{item2.name}}</el-menu-item>
</el-submenu>
</el-menu>
</el-aside>

<el-main>
<router-view></router-view>
</el-main>

</el-container>
</template>

基于具体页面的数据对接

选择一个所需的表样式和分页样式,注意要放在一个容器中,故可以新建一个div

绑定本页面script 中的数据

el-table-column 标签的label 表示表头的名称,prop 用于跟script 中的tabelData中对应键名称进行绑定,实现数据导入

el-pagination分页标签

  • 设置每页展示的记录条数:page-size="5"
  • 点击切换页面记录 @current-change="page" 绑定page切换方法,方法要在script中实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
<template>
<div>
<el-table
:data="tableData"
border
style="width: 100%">
<el-table-column
prop="id"
label="编号"
width="150">
</el-table-column>
<el-table-column
prop="name"
label="书名"
width="150">
</el-table-column>
<el-table-column
prop="author"
label="标签"
width="150">
</el-table-column>
<el-table-column
label="操作"
width="150">
<template slot-scope="scope">
<el-button @click="handleClick(scope.row)" type="text" size="small">查看</el-button>
<el-button type="text" size="small">编辑</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
background
layout="prev, pager, next"
:page-size="5"
:total="50"
@current-change="page">
</el-pagination>
</div>

</template>

<script>
export default {
methods: {
handleClick(row) {
console.log(row);
},
page(currentPage){
switch(currentPage){
case 1:
this.tableData = [{
id: 1,
name: '前端',
author: 'f',
}, ]
break;
case 2:
this.tableData = [{
id: 2,
name: '后端',
author: 'b',
},]
break;
}
}
},

data() {
return {
tableData: [{
id: 1,
name: '前端',
author: 'f',
}, {
id: 2,
name: '后端',
author: 'b',
},]
}
}
}
</script>

页面效果

绑定后端数据

首先在后端也要实现数据分页,JPA已经实现了这个功能, 进行测试,注意由于有多个重载,要选对参数类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootTest
class VueBackendtestApplicationTests {

@Autowired
private BooklistRepository repository;

@Test
void contextLoads() {
PageRequest pageRequest = PageRequest.of(0,2);
Page<Booklist> page = repository.findAll(pageRequest);
int i = 0;
}

}

取第0页的两条记录,bug测试结果

BooklistHandler 中进行分页展示

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequestMapping("/book")
public class BooklistHandler {
@Autowired
private BooklistRepository booklistRepository;

@GetMapping("/findAll/{page}/{size}")
public Page<Booklist> findAll(@PathVariable("page") Integer page, @PathVariable("size") Integer size){
PageRequest request = PageRequest.of(page,size);
return booklistRepository.findAll(request);
}
}

打开http://localhost:8181/book/findAll/0/2 查看分页内容

对接要做的就是用实际的数据去替换tableData, 注意resp获取的数据格式, 数据在resp.data.content

script 初始化

1
2
3
4
5
6
7
created() {
const _this = this
axios.get('http://localhost:8181/book/findAll/0/2').then(function(resp){
//console.log(resp)
_this.tableData = resp.data.content
})
}

前端内容已更新为数据库内容

还要动态获取page和size, 在data中定义pageSize和total 变量,从created()中获取值

然后绑定给el-pagination的对应属性 :page-size="pageSize" :total="total"

同时更改跳转函数 page(), 只需在created()基础上传入当前页面参数即可

完整script代码(每页显示两条记录)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<script>
export default {
methods: {
handleClick(row) {
console.log(row);
},
page(currentPage){
const _this = this
axios.get('http://localhost:8181/book/findAll/'+(currentPage-1)+'/2').then(function(resp){
_this.tableData = resp.data.content
_this.pageSize = resp.data.size
_this.total = resp.data.totalElements
})
}
},

data() {
return {
pageSize:'',
total:'',
tableData: []
}
},
created() {
const _this = this
axios.get('http://localhost:8181/book/findAll/0/2').then(function(resp){
_this.tableData = resp.data.content
_this.pageSize = resp.data.size
_this.total = resp.data.totalElements
})
}
}
</script>