今日目标:
- 能够使用VUE中常用指令和插值表达式
- 能够使用VUE生命周期函数 mounted
- 能够进行简单的 Element 页面修改
- 能够完成查询所有功能
- 能够完成添加功能
接下来我们学习一款前端的框架,就是 VUE。
Vue 是一套前端框架,免除原生JavaScript中的DOM操作,简化书写。
我们之前也学习过后端的框架 Mybatis
,Mybatis
是用来简化 jdbc
代码编写的;而 VUE
是前端的框架,是用来简化 JavaScript
代码编写的。前一天我们做了一个综合性的案例,里面进行了大量的DOM操作,如下
学习了 VUE
后,这部分代码我们就不需要再写了。那么 VUE
是如何简化 DOM 书写呢?
基于MVVM(Model-View-ViewModel)思想,实现数据的双向绑定,将编程的关注点放在数据上。之前我们是将关注点放在了 DOM 操作上;而要了解 MVVM
思想,必须先聊聊 MVC
思想,如下图就是 MVC
思想图解
C 就是咱们 js 代码,M 就是数据,而 V 是页面上展示的内容,如下图是我们之前写的代码
MVC
思想是没法进行双向绑定的。双向绑定是指当数据模型数据发生变化时,页面展示也会随之发生变化,而如果表单数据发生变化,绑定的模型数据也随之发生变化。接下来我们聊聊 MVVM
思想,如下图是三个组件图解
图中的 Model
就是我们的数据,View
是视图,也就是页面标签,用户可以通过浏览器看到的内容;Model
和 View
是通过 ViewModel
对象进行双向绑定的,而 ViewModel
对象是 Vue
提供的。接下来让大家看一下双向绑定的效果,下图是提前准备的代码,输入框绑定了 username
模型数据,而在页面上也使用 {{}}
绑定了 username
模型数据
通过浏览器打开该页面可以看到如下页面
当我们在输入框中输入内容,而输入框后面随之实时的展示我们输入的内容,这就是双向绑定的效果。
先创建项目
Vue 使用起来是比较简单的,总共分为如下三步:
新建 HTML 页面,引入 Vue.js文件
在JS代码区域,创建Vue核心对象,进行数据绑定
new Vue({el: "#app",data() {return {username: ""}}
});
创建 Vue 对象时,需要传递一个 js 对象,而该对象中需要如下属性:
el
: 用来指定哪儿些标签受 Vue 管理。 该属性取值 #app
中的 app
需要是受管理的标签的id属性值data
:用来定义数据模型methods
:用来定义函数。这个我们在后面就会用到编写视图
{{username}}
{{}}
是 Vue 中定义的 插值表达式
,在里面写数据模型,到时候会将该模型的数据值展示在这个位置。
整体代码如下:
Title
{{username}}
指令:HTML 标签上带有 v- 前缀的特殊属性,不同指令具有不同含义。例如:v-if,v-for…
常用的指令有:
指令 | 作用 |
---|---|
v-bind | 为HTML标签绑定属性值,如设置 href , css样式等 |
v-model | 在表单元素上创建双向数据绑定 |
v-on | 为HTML标签绑定事件 |
v-if | 条件性的渲染某元素,判定为true时渲染,否则不渲染 |
v-else | |
v-else-if | |
v-show | 根据条件展示某元素,区别在于切换的是display属性的值 |
v-for | 列表渲染,遍历容器的元素或者对象的属性 |
接下来我们挨个学习这些指令
v-bind
该指令可以给标签原有属性绑定模型数据。这样模型数据发生变化,标签属性值也随之发生变化
例如:
百度一下
上面的 v-bind:"
可以简化写成 :
,如下:
百度一下
v-model
该指令可以给表单项标签绑定模型数据。这样就能实现双向绑定效果。例如:
代码演示:
Title
通过浏览器打开上面页面,并且使用检查查看超链接的路径,该路径会根据输入框输入的路径变化而变化,这是因为超链接和输入框绑定的是同一个模型数据
我们在页面定义一个按钮,并给该按钮使用 v-on
指令绑定单击事件,html代码如下
而使用 v-on
时还可以使用简化的写法,将 v-on:
替换成 @
,html代码如下
函数的括号写不写无所谓:@click="show"也行
上面代码绑定的 show()
需要在 Vue 对象中的 methods
属性中定义出来
new Vue({el: "#app",methods: {show(){alert("我被点了");}}
});
注意:
v-on:
后面的事件名称是之前原生事件属性名去掉on。例如:
- 单击事件 : 事件属性名是 onclick,而在vue中使用是
v-on:click
- 失去焦点事件:事件属性名是 onblur,而在vue中使用时
v-on:blur
整体页面代码如下:
Title
接下来通过代码演示一下。在 Vue中定义一个 count
的数据模型,如下
//1. 创建Vue核心对象
new Vue({el:"#app",data(){return {count:3}}
});
现在要实现,当 count
模型的数据是3时,在页面上展示 div1
内容;当 count
模型的数据是4时,在页面上展示 div2
内容;count
模型数据是其他值时,在页面上展示 div3
。这里为了动态改变模型数据 count
的值,再定义一个输入框绑定 count
模型数据。html 代码如下:
div1div2div3
整体页面代码如下:
Title
div1div2div3
通过浏览器打开页面并在输入框输入不同的值,效果如下
然后我们在看看 v-show
指令的效果,如果模型数据 count
的值是3时,展示 div v-show
内容,否则不展示,html页面代码如下
div v-show
浏览器打开效果如下:
通过上面的演示,发现 v-show
和 v-if
效果一样,那它们到底有什么区别呢?我们根据浏览器的检查功能查看源代码
通过上图可以看出 v-show
不展示的原理是给对应的标签添加 display
css属性,并将该属性值设置为 none
,这样就达到了隐藏的效果。而 v-if
指令是条件不满足时根本就不会渲染。
这个指令看到名字就知道是用来遍历的,该指令使用的格式如下:
<标签 v-for="变量名 in 集合模型数据">{{变量名}}
标签>
注意:需要循环那个标签,
v-for
指令就写在那个标签上。
如果在页面需要使用到集合模型数据的索引,就需要使用如下格式:
<标签 v-for="(变量名,索引变量) in 集合模型数据">{{索引变量 + 1}} {{变量名}}
标签>
代码演示:
Title
{{addr}}
{{i+1}}--{{addr}}
通过浏览器打开效果如下
v-for直接写在标签上的,直接就将标签生成了3次
生命周期的八个阶段:每触发一个生命周期事件,会自动执行一个生命周期方法,这些生命周期方法也被称为钩子方法。
下图是 Vue 官网提供的从创建 Vue 到销毁 Vue 对象的整个过程及各个阶段对应的钩子函数
看到上面的图,大家无需过多的关注这张图。这些钩子方法我们只关注 mounted
就行了。
mounted
:挂载完成,Vue初始化成功,HTML页面渲染成功。而以后我们会在该方法中发送异步请求,加载数据。
Title
一打开页面就弹出:
使用 Vue 简化我们在前一天ajax学完后做的品牌列表数据查询和添加功能
此案例只是使用 Vue 对前端代码进行优化,后端代码无需修改。
- 直接接着之前的brand-demo-ajax做也行,复制一份再导入也行
链接:https://pan.baidu.com/s/1WhHKF749aWvPNkuTJKCP0Q
提取码:78fs
这里选择复制一份重新导入为:brand-demo-ajax-vue
在 brand.html 页面引入 vue 的js文件
同时删除brand.html 里的所有JS代码
创建 Vue 对象
new Vue({el: "#app",data(){return{brands:[]}},mounted(){// 页面加载完成后,发送异步请求,查询数据var _this = this;axios({method:"get",url:"http://localhost:8080/brand-demo/selectAllServlet"}).then(function (resp) {_this.brands = resp.data;})}
})
修改视图
定义 ,指定该
div
标签受 Vue 管理
将 body
标签中所有的内容拷贝作为上面 div
标签中 (
删除表格的多余数据行,只留下一个
在表格中的数据行上使用 v-for
指令遍历
{{i + 1}} {{brand.brandName}} {{brand.companyName}} {{brand.ordered}} {{brand.description}} 启用 禁用 修改 删除
整体页面代码如下:
Title
页面操作效果如下:
整体流程如下
注意:前端代码的关键点在于使用
v-model
指令给标签项绑定模型数据,利用双向绑定特性,在发送异步请求时提交数据。
在 addBrand.html 页面引入 vue 的js文件
同时删除addBrand.html页面内里的所有JS代码
创建 Vue 对象
brand
submitForm()
函数,用于给 提交
按钮提供绑定的函数submitForm()
函数中发送 ajax 请求,并将模型数据 brand
作为参数进行传递new Vue({el: "#app",data(){return {brand:{}}},methods:{submitForm(){// 发送ajax请求,添加var _this = this;axios({method:"post",url:"http://localhost:8080/brand-demo/addServlet",data:_this.brand}).then(function (resp) {// 判断响应数据是否为 successif(resp.data == "success"){location.href = "http://localhost:8080/brand-demo/brand.html";}})}}
})
修改视图
定义 ,指定该
div
标签受 Vue 管理
将 body
标签中所有的内容拷贝作为上面 div
标签中 (
给每一个表单项标签绑定模型数据。最后这些数据要被封装到 brand
对象中
添加品牌
整体页面代码如下:
添加品牌
添加品牌
通过上面的优化,前端代码确实简化了不少。但是页面依旧是不怎么好看,那么接下来我们学习 Element,它可以美化页面。
Element:是饿了么公司前端开发团队提供的一套基于 Vue 的网站组件库,用于快速构建网页。
Element 提供了很多组件(组成网页的部件)供我们使用。例如 超链接、按钮、图片、表格等等~
如下图左边的是我们编写页面看到的按钮,上图右边的是 Element 提供的页面效果,效果一目了然。
我们学习 Element 其实就是学习怎么从官网拷贝组件到我们自己的页面并进行修改,官网网址是
https://element.eleme.cn/#/zh-CN
进入官网能看到如下页面
接下来直接点击 组件
,页面如下
接着前面创建的vue-demo项目下写
资料\02-element
下的 element-ui
文件夹直接拷贝到项目的 webapp
下。目录结构如下创建页面,并在页面引入Element 的css、js文件 和 Vue.js
css用link标签引入 rel=“stylesheet” 表示这是css样式表
创建Vue核心对象
Element 是基于 Vue 的,所以使用Element时必须要创建 Vue 对象
body 内写一个
…用于包含所有标签元素
官网复制Element组件代码
在左菜单栏找到 Button 按钮
,然后找到自己喜欢的按钮样式,点击 显示代码
,在下面就会展示出对应的代码,将这些代码拷贝到我们自己的页面即可。
整体页面代码如下:
Title
默认按钮 主要按钮 成功按钮 信息按钮 警告按钮 删除 朴素按钮 主要按钮 成功按钮 信息按钮 警告按钮 危险按钮 圆角按钮 主要按钮 成功按钮 信息按钮 警告按钮 危险按钮
要哪个留哪个即可。eg:我只想要个删除
Title
删除
Element 提供了两种布局方式,分别是:
通过基础的 24 分栏,迅速简便地创建布局。也就是默认将一行分为 24 栏,根据页面要求给每一列设置所占的栏数。
在左菜单栏找到 Layout 布局
,然后找到自己喜欢的布局样式,点击 显示代码
,在下面就会展示出对应的代码,显示出的代码中有样式,有html标签。将样式拷贝我们自己页面的 head
标签内,将html标签拷贝到 标签内。
整体页面代码如下:
Title
现在需要添加一行,要求该行显示8个格子,通过计算每个格子占 3 栏(基础24分栏,24/8=3),具体的html 代码如下
用于布局的容器组件,方便快速搭建页面的基本结构。如下图就是布局容器效果。
如下图是官网提供的 Container 布局容器实例:
该效果代码中包含了样式、页面标签、模型数据。将里面的样式 拷贝到我们自己页面的
head
标签中;将html标签拷贝到 标签中,再将数据模型拷贝到
vue
对象的 data()
中。
整体页面代码如下:
Title
导航一分组一选项1 选项2 选项3 选项4选项4-1 导航二选项1选项1-1 导航三分组一选项1 选项2 选项3 选项4选项4-1 查看 新增 删除 王小虎
其他的组件我们通过完成一个页面来学习。
我们要完成如下页面效果
要完成该页面,我们需要先对这个页面进行分析,看页面由哪几部分组成,然后到官网进行拷贝并修改。页面总共有如下组成部分
还有一个是当我们点击 新增
按钮,会在页面正中间弹出一个对话框,如下
Title
使用 Element 整体的思路就是 拷贝 + 修改。
在左菜单栏找到 Table 表格
并点击,右边主体就会定位到表格这一块,找到我们需要的表格效果(如上图),点击 显示代码
就可以看到这个表格的代码了。
将html标签拷贝到 中,如下:
将css样式拷贝到我们页面的 head
标签中,如下
将方法和模型数据拷贝到 Vue 对象指定的位置
拷贝完成后通过浏览器打开可以看到表格的效果
表格效果出来了,但是显示的表头和数据并不是我们想要的,所以接下来就需要对页面代码进行修改了。
修改为真正的隔行换色
修改表头和数据
下面是对表格代码进行分析的图解。根据下图说明修改自己的列数和列名
1、去掉了width属性,他就自适应浏览器宽度了
2、align属性就是最下面找到的
修改完页面后,还需要对绑定的模型数据进行修改,下图是对模型数据进行分析的图解
给表格添加操作列
从之前的表格拷贝一列出来并对其进行修改。按钮是从官网的 Button 按钮
组件中拷贝并修改的
给表格添加复选框列和标号列
给表格添加复选框和标号列,效果如下
此效果也是从 Element 官网进行拷贝,先找到对应的表格效果,然后将其对应代码拷贝到我们的代码中,如下是复选框列官网效果图和代码
这里需要注意在
标签上有一个事件 @selection-change="handleSelectionChange"
,这里绑定的函数也需要从官网拷贝到我们自己的页面代码中,函数代码如下:
@就是绑定事件呀,有了这个事件函数,选中复选框,对应行的数据直接就拿到了,多好呀
(属性里面不能写注释,会报错)
从该函数中又发现还需要一个模型数据 multipleSelection
,所以还需要定义出该模型数据
标号列也用同样的方式进行拷贝并修改。
方法里面绑定了数据模型viewmodel,我们就要定义该viewmodel。还可以顺便打印调试一下
在 Element 官网找到横排的表单效果,然后拷贝代码并进行修改
点击上面的 显示代码
后,就会展示出对应的代码,下面是对这部分代码进行分析的图解
然后根据我们要的效果修改代码。
(页面标签、data、method都要改)
方法和数据
表单修改
调试结果:
从 Element 官网找具有着色效果的按钮,并将代码拷贝到我们自己的页面上
在 Element 官网找对话框,如下:
下面对官网提供的代码进行分析
上图分析出来的模型数据需要在 Vue 对象中进行定义:
对话框里的表单就这里复制了:
开关也要换掉,换成值为0,1的 (默认是true/false 不是我们想要的)
在 Element 官网找到 Pagination 分页
,在页面主体部分找到我们需要的效果,如下
看着复制,其实复制这么一点+下面绑定的方法就行了
点击 显示代码
,找到 完整功能
对应的代码,接下来对该代码进行分析
上面代码属性说明:
page-size
:每页显示的条目数
page-sizes
: 每页显示个数选择器的选项设置。
:page-sizes="[100,200,300,400]"
对应的页面效果如下:
currentPage
:当前页码。我们点击那个页码,此属性值就是几。
total
:总记录数。用来设置总的数据条目数,该属性设置后, Element 会自动计算出需分多少页并给我们展示对应的页码。
事件说明:
size-change
:pageSize 改变时会触发。也就是当我们改变了每页显示的条目数后,该事件会触发。current-change
:currentPage 改变时会触发。也就是当我们点击了其他的页码后,该事件会触发。代码可以收缩的,便于看结构:
Title
查询 批量删除 新增 提交 取消 修改 删除
以上是我们在综合案例要实现的功能。除了对数据的增删改查功能外,还有一些复杂的功能,如 批量删除
、分页查询
、条件查询
等功能
批量删除
功能:每条数据前都有复选框,当我选中多条数据并点击 批量删除
按钮后,会发送请求到后端并删除数据库中指定的多条数据。分页查询
功能:当数据库中有很多数据时,我们不可能将所有的数据展示在一页里,这个时候就需要分页展示数据。条件查询
功能:数据库量大的时候,我们就需要精确的查询一些想看到的数据,这个时候就需要通过条件查询。这里的 修改品牌
和 删除品牌
功能可能不讲解,留作练习。
环境准备我们主要完成以下两件事即可
将 资料\04-初始工程
中的 brand-case
工程导入到我们自己的 idea 中。工程结构如下:
下面是创建表的语句
use db2;
-- 删除tb_brand表
drop table if exists tb_brand;
-- 创建tb_brand表
create table tb_brand (-- id 主键id int primary key auto_increment,-- 品牌名称brand_name varchar(20),-- 企业名称company_name varchar(20),-- 排序字段ordered int,-- 描述信息description varchar(100),-- 状态:0:禁用 1:启用status int
);
-- 添加数据
insert into tb_brand (brand_name, company_name, ordered, description, status)
values ('华为', '华为技术有限公司', 100, '万物互联', 1),('小米', '小米科技有限公司', 50, 'are you ok', 1),('格力', '格力电器股份有限公司', 30, '让世界爱上中国造', 1),('阿里巴巴', '阿里巴巴集团控股有限公司', 10, '买买买', 1),('腾讯', '腾讯计算机系统有限公司', 50, '玩玩玩', 0),('百度', '百度在线网络技术公司', 5, '搜搜搜', 0),('京东', '北京京东世纪贸易有限公司', 40, '就是快', 1),('小米', '小米科技有限公司', 50, 'are you ok', 1),('三只松鼠', '三只松鼠股份有限公司', 5, '好吃不上火', 0),('华为', '华为技术有限公司', 100, '万物互联', 1),('小米', '小米科技有限公司', 50, 'are you ok', 1),('格力', '格力电器股份有限公司', 30, '让世界爱上中国造', 1),('阿里巴巴', '阿里巴巴集团控股有限公司', 10, '买买买', 1),('腾讯', '腾讯计算机系统有限公司', 50, '玩玩玩', 0),('百度', '百度在线网络技术公司', 5, '搜搜搜', 0),('京东', '北京京东世纪贸易有限公司', 40, '就是快', 1),('华为', '华为技术有限公司', 100, '万物互联', 1),('小米', '小米科技有限公司', 50, 'are you ok', 1),('格力', '格力电器股份有限公司', 30, '让世界爱上中国造', 1),('阿里巴巴', '阿里巴巴集团控股有限公司', 10, '买买买', 1),('腾讯', '腾讯计算机系统有限公司', 50, '玩玩玩', 0),('百度', '百度在线网络技术公司', 5, '搜搜搜', 0),('京东', '北京京东世纪贸易有限公司', 40, '就是快', 1),('小米', '小米科技有限公司', 50, 'are you ok', 1),('三只松鼠', '三只松鼠股份有限公司', 5, '好吃不上火', 0),('华为', '华为技术有限公司', 100, '万物互联', 1),('小米', '小米科技有限公司', 50, 'are you ok', 1),('格力', '格力电器股份有限公司', 30, '让世界爱上中国造', 1),('阿里巴巴', '阿里巴巴集团控股有限公司', 10, '买买买', 1),('腾讯', '腾讯计算机系统有限公司', 50, '玩玩玩', 0),('百度', '百度在线网络技术公司', 5, '搜搜搜', 0),('京东', '北京京东世纪贸易有限公司', 40, '就是快', 1),('华为', '华为技术有限公司', 100, '万物互联', 1),('小米', '小米科技有限公司', 50, 'are you ok', 1),('格力', '格力电器股份有限公司', 30, '让世界爱上中国造', 1),('阿里巴巴', '阿里巴巴集团控股有限公司', 10, '买买买', 1),('腾讯', '腾讯计算机系统有限公司', 50, '玩玩玩', 0),('百度', '百度在线网络技术公司', 5, '搜搜搜', 0),('京东', '北京京东世纪贸易有限公司', 40, '就是快', 1),('小米', '小米科技有限公司', 50, 'are you ok', 1),('三只松鼠', '三只松鼠股份有限公司', 5, '好吃不上火', 0),('华为', '华为技术有限公司', 100, '万物互联', 1),('小米', '小米科技有限公司', 50, 'are you ok', 1),('格力', '格力电器股份有限公司', 30, '让世界爱上中国造', 1),('阿里巴巴', '阿里巴巴集团控股有限公司', 10, '买买买', 1),('腾讯', '腾讯计算机系统有限公司', 50, '玩玩玩', 0),('百度', '百度在线网络技术公司', 5, '搜搜搜', 0),('京东', '北京京东世纪贸易有限公司', 40, '就是快', 1);
如上图所示是查询所有品牌数据在页面展示的效果。要实现这个功能,要先搞明白如下问题:
什么时候发送异步请求?
页面加载完毕后就需要在页面上看到所有的品牌数据。所以在 mounted()
这个构造函数中写发送异步请求的代码。
请求需要携带参数吗?
查询所有功能不需要携带什么参数。
响应的数据格式是什么样?
后端是需要将 List
对象转换为 JSON 格式的数据并响应回给浏览器。响应数据格式如下:
整体流程如下
我们先实现后端程序,然后再实现前端程序。
在 cn.whu.mapper.BrandMapper
接口中定义抽象方法,并使用 @Select
注解编写 sql 语句
/*** 查询所有* @return*/
@Select("select * from tb_brand")
List selectAll();
由于表中有些字段名和实体类中的属性名没有对应,所以需要在 cn/whu/mapper/BrandMapper.xml
映射配置文件中定义结果映射 ,使用resultMap
标签。映射配置文件内容如下:
可别忘了字段和属性名不一致时的映射哦
定义完结果映射关系后,在接口 selectAll()
方法上引用该结构映射。使用 @ResultMap("brandResultMap")
注解
完整接口的 selectAll()
方法如下:
/*** 查询所有* @return*/
@Select("select * from tb_brand")
@ResultMap("brandResultMap")
List selectAll();
在 cn.whu.service
包下创建 BrandService
接口,在该接口中定义查询所有的抽象方法
public interface BrandService {/*** 查询所有* @return*/List selectAll();
}
并在 cn.whu.service
下再创建 impl
包;impl
表示是放 service 层接口的实现类的包。 在该包下创建名为 BrandServiceImpl
类
public class BrandServiceImpl implements BrandService {@Overridepublic List selectAll() {}
}
此处为什么要给 service 定义接口呢?因为service定义了接口后,在 servlet 中就可以使用多态的形式创建Service实现类的对象,如下:
这里使用多态是因为方便我们后期解除 Servlet
和 service
的耦合。从上面的代码我们可以看到 SelectAllServlet
类和 BrandServiceImpl
类之间是耦合在一起的,如果后期 BrandService
有其它更好的实现类(例如叫 BrandServiceImpl
),那就需要修改 SelectAllServlet
类中的代码。后面我们学习了 Spring
框架后就可以解除 SelectAllServlet
类和红色框括起来的代码耦合。而现在咱们还做不到解除耦合,在这里只需要理解为什么定义接口即可。
BrandServiceImpl
类代码如下:
public class BrandServiceImpl implements BrandService {//1. 创建SqlSessionFactory 工厂对象SqlSessionFactory factory = SqlSessionFactoryUtils.getSqlSessionFactory();@Overridepublic List selectAll() {//2. 获取SqlSession对象SqlSession sqlSession = factory.openSession();//3. 获取BrandMapperBrandMapper mapper = sqlSession.getMapper(BrandMapper.class);//4. 调用方法List brands = mapper.selectAll();//5. 释放资源sqlSession.close();return brands;}
}
在 cn.whu.web.servlet
包下定义名为 SelectAllServlet
的查询所有的 servlet
。该 servlet
逻辑如下:
selectAll()
方法查询所有的品牌数据,并接口返回结果代码如下:
@WebServlet("/selectAllServlet")
public class SelectAllServlet extends HttpServlet {private BrandService brandService = new BrandServiceImpl();@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {//1. 调用service查询数据List brands = brandService.selectAll();//2. 对象转为jsonString jsonString = JSON.toJSONString(brands);System.out.println(jsonString);//3. 返回 jsonString 到前端//小心中文乱码response.setContentType("text/json;charset=utf-8");response.getWriter().write(jsonString);//直接返回给ajax}@Overrideprotected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {doGet(request, response);}
}
在浏览器输入访问 servlet 的资源路径 http://localhost:8080/brand-case/selectAllServlet
,如果没有报错,并能看到如下信息表明后端程序没有问题
格式化之后正是vue的data里需要(返回)的样式:也就是json
前端需要在页面加载完毕后发送 ajax 请求,所以发送请求的逻辑应该放在 mounted()
钩子函数中。而响应回来的数据需要赋值给表格绑定的数据模型,从下图可以看出表格绑定的数据模型是 tableData
前端代码如下:
注意页面没有帮你引入axios,需要先引入一下对应的js文件
mounted(){//当页面加载完成后,发送异步请求,获取数据var _this = this;axios({method:"get",url:"http://localhost:8080/brand-case/selectAllServlet"}).then(function (resp) {_this.tableData = resp.data;})}
直接将返回的json字符串赋值给_this.tableData即可,完全不用遍历数据了
上图是添加数据的对话框,当点击 提交
按钮后就需要将数据提交到后端,并将数据保存到数据库中。下图是整体的流程:
页面发送请求时,需要将输入框输入的内容提交给后端程序,而这里是以 json 格式进行传递的。而具体的数据格式如下:
注意:由于是添加数据,所以上述json数据中id是没有值的。
在 BrandMapper
接口中定义 add()
添加方法,并使用 @Insert
注解编写sql语句
/*** 添加数据* @param brand*/
@Insert("insert into tb_brand values(null,#{brandName},#{companyName},#{ordered},#{description},#{status})")
void add(Brand brand);
在 BrandService
接口中定义 add()
添加数据的业务逻辑方法
/*** 添加数据* @param brand*/
void add(Brand brand);
在 BrandServiceImpl
类中重写 add()
方法,并进行业务逻辑实现
@Override
public void add(Brand brand) {//2. 获取SqlSession对象SqlSession sqlSession = factory.openSession();//3. 获取BrandMapperBrandMapper mapper = sqlSession.getMapper(BrandMapper.class);//4. 调用方法mapper.add(brand);sqlSession.commit();//提交事务//5. 释放资源sqlSession.close();
}
注意:增删改操作一定要提交事务。
在 cn.whu.web.servlet
包内新建名为 AddServlet
的 Servlet。该 Servlet 的逻辑如下:
Brand
对象add()
方法进行添加的业务逻辑处理success
字符串表示成功servlet 代码实现如下:
@WebServlet("/addServlet")
public class AddServlet extends HttpServlet {private BrandService brandService = new BrandServiceImpl();@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {//0. 处理中文乱码//request.setCharacterEncoding("utf-8");//json格式数据不需要手动处理乱码//1. 接收品牌数据BufferedReader br = request.getReader();String params = br.readLine();System.out.println(params);// 将json字符数据转java Brand对象Brand brand = JSON.parseObject(params, Brand.class);//2. 调用service添加数据brandService.add(brand);//3. 响应成功标识 (不要跳转,前端页面怎么跳,完全交给前端来做)response.getWriter().write("success");//ajax请求的 直接返回给ajax 'success'}@Overrideprotected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {doGet(request, response);}
}
响应成功后台不要跳转页面,前后端分离,后端只管完成业务后给出反馈,页面怎么跳,完全交给前端
json格式有中文也不需要处理乱码,就不会乱码。
上图左边是页面效果,里面的 提交
按钮可以通过上图右边看出绑定了一个 单击事件,而该事件绑定的是 addBrand
函数,所以添加数据功能的逻辑代码应该写在 addBrand()
函数中。在此方法中需要发送异步请求并将表单中输入的数据作为参数进行传递。如下
// 添加数据
addBrand() {var _this = this;// 发送ajax请求,添加数据axios({method:"post",url:"http://localhost:8080/brand-case/addServlet",data:_this.brand}).then(function (resp) {//响应数据的处理逻辑})
}
在 then
函数中的匿名函数是成功后的回调函数,而 resp.data
就可以获取到响应回来的数据,如果值是 success
表示数据添加成功。成功后我们需要做一下逻辑处理:
关闭新增对话框窗口
如下图所示是添加数据的对话框代码,从代码中可以看到此对话框绑定了 dialogVisible
数据模型,只需要将该数据模型的值设置为 false,就可以关闭新增对话框窗口了。
重新查询数据
数据添加成功与否,用户只要能在页面上查看到数据说明添加成功。而此处需要重新发送异步请求获取所有的品牌数据,而这段代码在 查询所有
功能中已经实现,所以我们可以将此功能代码进行抽取,抽取到一个 selectAll()
函数中
// 查询所有数据
selectAll(){var _this = this;axios({method:"get",url:"http://localhost:8080/brand-case/selectAllServlet"}).then(function (resp) {_this.tableData = resp.data;})
}
那么就需要将 mounted()
钩子函数中代码改进为
mounted(){//当页面加载完成后,发送异步请求,获取数据this.selectAll();
}
同时在新增响应的回调中调用 selectAll()
进行数据的重新查询。
注意不能直接调用 _this.mounted(); 调用了也不会执行,这不是普通方法,只会在页面加载完成后执行一次
那么刷新页面呢?不间接调用了mounted()重新查询所有吗?也不可取,1)异步请求你刷新啥子页面 2)不好弹出添加成功的提示
上图左边就是 elementUI 官网提供的成功提示代码,而上图右边是具体的效果。
注意:上面的this需要的是表示 VUE 对象的this。
和C#一样,如此简单的代码就能弹出提示信息了,真好呀~
综上所述,这部分的前端代码如下:
怎么抽取呢?
url都写成路径匹配,保证能进入对应servlet,然后后面带上方法名,就能通过反射来获取方法执行了
这样一个让BaseServlet继承HttpServlet,然后自定义servlet继承BaseServlet,最后BaseServlet的service方法里执行对应的servlet方法,配置一个/brand/* 就相当于配置了一批路径。
因为BrandServlet extends BaseServlet。到时候执行的其实是BrandServlet里的service方法,只不过这个方法继承自BaseServlet,代码写在BrandServlet里而已。this直接指的就是BrandServlet,轻易动态拿到一个实际调用类(BrandServlet)的一个对象,就能执行该类的一切资源了
自己手敲BaseServlet 练习基本功
其实就5行代码
环境(项目结构)
总共在web包下新建3个Servlet
BaseServlet继承HttpServlet是Servlet
BrandServlet和UserServlet继承自BaseServlet间接继承了HttpServlet,也是Servlet
BaseServlet.java
自己写得时候request和response方法忘记传递了
/*** 替换HttpServlet,根据请求的最后一段路径来进行方法分发*/
public class BaseServlet extends HttpServlet {//根据请求的最后一段路径来进行方法分发//重写一下service,就不根据post/get分发了@Overrideprotected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {//1. 获取请求路径 [URI短路径 URL长路径]String uri = req.getRequestURI();//System.out.println(uri);// "/brand-case/brand/selectAll"//2. 获取最后一段路径 也就是方法名了String methodName = uri.substring(uri.lastIndexOf('/')+1);//+1去掉'/'//System.out.println(methodName);// "selectAll"//3. 执行方法try {//3.1 获取Class对象//访问的是BrandServlet,BrandServlet自动调用继承的service方法,谁调用的方法this就指的谁,将来this就会是BrandServletClass extends BaseServlet> clazz = this.getClass();//3.2 创建对象(用来执行方法)// 不需要创建 this 不就是现成的对象嘛//3.3 根据方法名获取方法 (参数的字节码也要传入)Method method = clazz.getMethod(methodName,HttpServletRequest.class,HttpServletResponse.class);//3.4 执行方法method.invoke(this,req,resp);//有参数也要传入} catch (IllegalAccessException | InvocationTargetException e) {throw new RuntimeException(e);} catch (NoSuchMethodException e) {throw new RuntimeException(e);}// 其实就5行代码}
}
@WebServlet("/brand/*")
public class BrandServlet extends BaseServlet{public void selectAll(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {System.out.println("brand selectAll...");}public void add(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {System.out.println("brand add...");}
}
UserServlet
@WebServlet("/user/*")
public class UserServlet extends BaseServlet{public void selectAll(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {System.out.println("user selectAll...");}public void add(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {System.out.println("user add...");}
}
brand/selectAll
brand/add
user/selectAll
user/add
分别访问四个路径,IDEA合理输出:
宣告成功~
直接把两个servlet里的方法复制到BrandServlet里对应两个方法内即可
现在servlet其实只需要2个Servlet: BaseServlet和BrandServlet即可
BrandServlet
@WebServlet("/brand/*")
public class BrandServlet extends BaseServlet{private BrandService brandService = new BrandServiceImpl();public void selectAll(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {//1. 调用service查询数据List brands = brandService.selectAll();//2. 对象转为jsonString jsonString = JSON.toJSONString(brands);System.out.println(jsonString);//3. 返回 jsonString 到前端//小心中文乱码response.setContentType("text/json;charset=utf-8");response.getWriter().write(jsonString);//直接返回给ajax}public void add(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {//0. 处理中文乱码//request.setCharacterEncoding("utf-8");//json格式数据不需要手动处理乱码//1. 接收品牌数据BufferedReader br = request.getReader();String params = br.readLine();System.out.println(params);// 将json字符数据转java Brand对象Brand brand = JSON.parseObject(params, Brand.class);//2. 调用service添加数据brandService.add(brand);//3. 响应成功标识 (不要跳转,前端页面怎么跳,完全交给前端来做)response.getWriter().write("success");//ajax请求的 直接返回给ajax 'success'}
}
想把BrandServlet复制一份为BrandServlet0留着·,结果@WebServlet(“/brand/*”)忘记删了,两个相同的路径就冲突了,导致一直tomcat启动不了,一直报错A child container failed during start。复制文件一定记得注释路径啊
前端请求路径也改下子:
修改完成,重新访问前端,功能均正常,OK啦~
不要的Servlet放到一个单独包里,且路径都注释了,以防止不必要的影响,尤其BrandServlet0,非要复制,就必须注释掉路径
/**
* 根据ids数组批量删除数据
* @param ids
*/
void deleteByIds(@Param("ids") Integer[] ids); //需要动态sql遍历数组 就不建议写注解了
// Array 封装到map, 默认key为array或者arg0 最好自己指定 例如这里指定为: ids
delete from tb_brand where id in#{id}
/*** 批量删除* @param ids*/void deleteByIds(Integer[] ids);
@Override
public void deleteByIds(Integer[] ids) {SqlSession sqlSession = factory.openSession();BrandMapper mapper = sqlSession.getMapper(BrandMapper.class);mapper.deleteByIds(ids);sqlSession.commit();//提交事务sqlSession.close();
}
public void deleteByIds(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {//1. 接受ids参数 (ajax都是json: [1,2,3])String params = request.getReader().readLine();//转为Integer[] int数组也是个类型吗 fastJson也可以转的 (以后都spring自动帮我们转 这里好好理解原理)Integer[] ids = JSON.parseObject(params, Integer[].class);//2.调用service删除brandService.deleteByIds(ids);//3.返回给前端标识 (后端不要跳转页面)response.getWriter().write("success");
}
先给按钮绑定方法
method里写:
//批量删除
deleteByIds() {//1. 创建id数组 [1,2,3]
},
再查看数据
//批量删除deleteByIds() {//1. 创建id数组 [1,2,3]console.log(this.multipleSelection)},
发现控制台打印输出了整个对象信息(被选中的所有对象封装为json数组格式了呢),mvvm真好,获取数据太方便了呢
开始编写方法了
//批量删除
deleteByIds(){//1. 创建id数组 [1,2,3]let ids = [];//console.log(this.multipleSelection)//打印的是整个对象数组// 有数据模型真好 直接就能拿到数据 再也不用dom操作了let brands = this.multipleSelection;// 把id单独拿出来for (let i = 0; i < brands.length; i++) {ids[i] = brands[i].id;//直接赋值 动态扩张数组}//2. 发送ajax请求let _this = this;axios({method:"POST",url:"http://localhost:8080/brand-case/brand/deleteByIds",data: ids}).then(function (resp){if(resp.data=="success"){// 重新查询所有_this.selectAll();// 弹出添加成功消息提示_this.$message({message: '恭喜你,删除成功',type: 'success'});}})
}
加上提示框
//批量删除
deleteByIds() {//0. 警告框,防止误操作this.$confirm('此操作将永久删除选中的数据, 是否继续?', '提示', {confirmButtonText: '确定',cancelButtonText: '取消',type: 'warning'}).then(() => {//用户点击确认按钮//1. 创建id数组 [1,2,3]let ids = [];//console.log(this.multipleSelection)//打印的是整个对象数组// 有数据模型真好 直接就能拿到数据 再也不用dom操作了let brands = this.multipleSelection;for (let i = 0; i < brands.length; i++) {ids[i] = brands[i].id;//直接赋值 动态扩张数组}//2. 发送ajax请求let _this = this;axios({method: "POST",url: "http://localhost:8080/brand-case/brand/deleteByIds",data: ids}).then(function (resp) {if (resp.data == "success") {// 重新查询所有_this.selectAll();// 弹出添加成功消息提示_this.$message({message: '恭喜你,删除成功',type: 'success'});}})}).catch(() => {//用户点击取消按钮this.$message({type: 'info',message: '已取消删除'});});
}
SELECT 字段列表 FROM 表名 LIMIT 起始索引 , 查询条目数;
起始索引 = (当前页码 - 1) * 每页显示的条数
eg:
-- 分页查询
select * from tb_brand LIMIT 0,5;
select * from tb_brand LIMIT 5,5;
select * from tb_brand LIMIT 10,5;
// 分页查询javaBean
// 只需要传递回 2个参数
public class PageBean {//PageBean 存储对象可能各种类型 不能写死 定义时就得用泛型// 总记录数private Integer totalCount;// 当前页数据private List rows;// get/set
}
比较复杂,也显示出后端分层的作用了
/*** 分页查询* @param begin* @param size* @return*/
@Select("select * from tb_brand limit #{begin},#{size}")
@ResultMap("brandResultMap")
List selectByPage(@Param("begin") Integer begin,@Param("size") Integer size);/*** 查询总记录条数* @return*/
@Select("select count(*) from tb_brand")
Integer selectTotalCount();
/*** 分页查询* @param currentPage 当前页码* @param pageSize 每页展示条数* @return*/
PageBean selectByPage(Integer currentPage,Integer pageSize);
@Override
public PageBean selectByPage(Integer currentPage, Integer pageSize) {SqlSession sqlSession = factory.openSession();BrandMapper mapper = sqlSession.getMapper(BrandMapper.class);//查询数据// 总条数Integer totalCount = mapper.selectTotalCount();// 当前页数据 (别忘了计算开始记录序号)List rows = mapper.selectByPage((currentPage-1)*pageSize, pageSize);//封装成pageBeanPageBean pageBean = new PageBean<>();pageBean.setRows(rows);pageBean.setTotalCount(totalCount);//别忘了释放资源sqlSession.close();// 返回return pageBean;
}
PageBean也是一个对象,可以直接fastJson转换为jsonString. JSON格式真是既简单,又强大啊
public void selectByPage(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {//1. 获取参数 页码和每页条数 两个简单参数,直接这么传递: url?currentPage=1&pageSize=5String _currentPage = request.getParameter("currentPage");String _pageSize = request.getParameter("pageSize");Integer currentPage = Integer.parseInt(_currentPage);Integer pageSize = Integer.parseInt(_pageSize);//2. 根据页码和每页条数查询PageBean pageBean = brandService.selectByPage(currentPage, pageSize);//3. 转化为json串String jsonString = JSON.toJSONString(pageBean);System.out.println(jsonString);//4. 返回数据给前端response.setContentType("text/json;charset=utf-8");response.getWriter().write(jsonString);
}
测试后端: http://localhost:8080/brand-case/brand/selectByPage?currentPage=3&pageSize=5
格式化后如下:
{"rows": [{"description": "买买买","id": 20,"ordered": 10,"status": 1,"statusStr": "启用"}, {"description": "玩玩玩","id": 21,"ordered": 50,"status": 0,"statusStr": "禁用"}, {"description": "搜搜搜","id": 22,"ordered": 5,"status": 0,"statusStr": "禁用"}, {"description": "就是快","id": 23,"ordered": 40,"status": 1,"statusStr": "启用"}, {"description": "are you ok","id": 24,"ordered": 50,"status": 1,"statusStr": "启用"}],"totalCount": 43
}
JSON格式真是既简单,又强大啊
并不需要写 selectByPage(){}方法,而应该是改写selectAll方法
需要在data里新增几个viewModel
methods: {// 查询所有selectAll() {let _this = this;axios({method: "post",url: "http://localhost:8080/brand-case/brand/selectByPage?currentPage=" + _this.currentPage + "&pageSize=" + _this.pageSize}).then(function(resp) {let data = resp.data;//alert(data)// 设置表格数据_this.tableData = data.rows;// 设置总记录数_this.totalCount = data.totalCount;})},//分页handleSizeChange(val) {//console.log(`每页 ${val} 条`);// 设置页面大小this.pageSize = val;// 修改页面大小 要重新查询this.selectAll();},handleCurrentChange(val) {//console.log(`当前页: ${val}`);// 设置当前页码this.currentPage = val;// 修改页码 要重新查询this.selectAll();},
}
data() {return {// 分页默认的当前页currentPage: 1,// 每页大小pageSize: 5,// 总条数totalCount: 300,}
}
终于完成了,看看效果吧:
最后插一个Tips:代码可以收缩
/*** 分页条件查询* @param begin* @param size* @return*/
List selectByPageAndCondition(@Param("begin") Integer begin,@Param("size") Integer size,@Param("brand") Brand brand);/*** 查询符合条件的总记录条数* @return*/
Integer selectTotalCountByCondition(Brand brand);
注意不能在xml里写%号,会变成
%'华为'% and status = 1
%号跑到引号外面来了 所以%得在service里就处理好
/*** 分页条件查询* @param currentPage 当前页码* @param pageSize 每页展示条数* @return*/
PageBean selectByPageAndCondition(Integer currentPage,Integer pageSize,Brand brand);
/*** 分页条件查询* @param currentPage 当前页码* @param pageSize 每页展示条数* @param brand* @return*/
@Override
public PageBean selectByPageAndCondition(Integer currentPage, Integer pageSize, Brand brand) {// 1. 获取mapperSqlSession sqlSession = factory.openSession();BrandMapper mapper = sqlSession.getMapper(BrandMapper.class);// 数据预处理一下String brandName = brand.getBrandName().trim();if(brandName!=null&&brandName.length()>0){brandName = "%"+brandName+"%";}String companyName = brand.getCompanyName().trim();if(companyName!=null&&companyName.length()>0){companyName = "%" + companyName + "%";}brand.setBrandName(companyName);brand.setCompanyName(brandName);// 2. 查询数据// 2.1 总条数Integer totalCount = mapper.selectTotalCountByCondition(brand);// 2.2 当前页数据 (别忘了计算开始记录序号)List rows = mapper.selectByPageAndCondition((currentPage-1)*pageSize, pageSize,brand);// 3. 封装成pageBeanPageBean pageBean = new PageBean<>();pageBean.setRows(rows);pageBean.setTotalCount(totalCount);// 4. 别忘了释放资源sqlSession.close();// 5. 返回return pageBean;
}
可以既有url参数,又有data请求体参数
public void selectByPageAndCondition(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {//1. 获取参数 页码和每页条数 两个简单参数,直接这么传递: url?currentPage=1&pageSize=5//既有url的参数String _currentPage = request.getParameter("currentPage");String _pageSize = request.getParameter("pageSize");Integer currentPage = Integer.parseInt(_currentPage);Integer pageSize = Integer.parseInt(_pageSize);// 又有请求体的参数String params = request.getReader().readLine();//axios:请求体data传入的json字符串参数Brand brand = JSON.parseObject(params, Brand.class);//2. 根据页码和每页条数查询PageBean pageBean = brandService.selectByPageAndCondition(currentPage, pageSize, brand);//3. 转化为json串String jsonString = JSON.toJSONString(pageBean);System.out.println(jsonString);//4. 返回数据给前端response.setContentType("text/json;charset=utf-8");response.getWriter().write(jsonString);}
注意点:用了动态sql,完全可以复用之前的selectAll方法,因为没有条件时所有条件自动不会加上,也就成了无条件查询
页面的数据和方法都已经绑定好了:
onSubmit直接调用selectAll即可
// 提交表单的方法
onSubmit() {//console.log(this.brand);// 填了条件就是条件查询 没填条件就是无条件查询this.selectAll();
},
selectAll方法修改成动态的
// 查询所有 [填了条件就是条件查询 没填条件就是无条件查询]
selectAll() {let _this = this;axios({method: "post",//url: "http://localhost:8080/brand-case/brand/selectByPage?currentPage=1&pageSize=5"url: "http://localhost:8080/brand-case/brand/selectByPageAndCondition?currentPage="+this.currentPage+"&pageSize="+this.pageSize,data: this.brand// then外面都可以直接用this 有data参数,请求方式必须是post}).then(function (resp) {let data = resp.data;//alert(data)// 设置表格数据_this.tableData = data.rows;// 设置总记录数_this.totalCount = data.totalCount;})
},
当前状态修改成启用禁用
其实pojo里已经提供好了javaBean
直接获取对应的属性statusStr即可
tableData不需要定义格式,直接接受前端发过来的json字符串即可
selectAll() {axios({method: "post",//url: "http://localhost:8080/brand-case/brand/selectByPage?currentPage=1&pageSize=5"url: "http://localhost:8080/brand-case/brand/selectByPageAndCondition?currentPage="+this.currentPage+"&pageSize="+this.pageSize,data: this.brand// then外面都可以直接用this 有data参数,请求方式必须是post}).then(resp =>{//箭头函数里的this,就是vue对象 以后都用箭头函数 (TMD早不说)let data = resp.data;// 设置表格数据this.tableData = data.rows;// 设置总记录数this.totalCount = data.totalCount;})
},
then里面不写function函数(降低了作用域),写箭头函数,this就直接指的是vue对象。再也不用操心this域的问题了
mybatis打印sql:
mybatis-config.xml核心配置文件中添加一行配置
测试时发现一个bug
有一个bug,当前页也应该是动态的,否则当前页8,条件查询导致根本没那么多页,从而导致数据查不到
直接前端用了一个不是很好的解决方法:
selectAll后面做了一个判断
selectAll() {axios({method: "post",//url: "http://localhost:8080/brand-case/brand/selectByPage?currentPage=1&pageSize=5"url: "http://localhost:8080/brand-case/brand/selectByPageAndCondition?currentPage="+this.currentPage+"&pageSize="+this.pageSize,data: this.brand// then外面都可以直接用this 有data参数,请求方式必须是post}).then(resp =>{//箭头函数里的this,就是vue对象 以后都用箭头函数 (TMD早不说)let data = resp.data;// 设置表格数据this.tableData = data.rows;// 设置总记录数this.totalCount = data.totalCount;//不是很好的处理方式if(this.currentPage>1&&this.currentPage>this.totalCount/this.pageSize){this.currentPage = parseInt(this.totalCount/this.pageSize);if(this.currentPage<1) this.currentPage=1;this.selectAll();}})
},