4.Vue
1. Vue.js 是什么
Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。
如果你想在深入学习 Vue 之前对它有更多了解,我们制作了一个视频,带您了解其核心概念和一个示例工程。
如果你已经是有经验的前端开发者,想知道 Vue 与其它库/框架有哪些区别,请查看对比其它框架。
2. 入门
官方指南假设你已了解关于 HTML、CSS 和 JavaScript 的中级知识。
2.1 安装
尝试 Vue.js 最简单的方法是使用 Hello World 例子。你可以在浏览器新标签页中打开它,跟着例子学习一些基础用法。或者你也可以创建一个 .html
文件,然后通过如下方式引入 Vue:
在Resources里的template下创建index.html,在head标签中用script标签引入vue.js,或者用cdn的方式引入,即在head标签中写下:<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<!-- 开发环境版本,包含了有帮助的命令行警告 -->
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<!-- Thymeleaf引入vue.js -->
<script type="text/javascript" th:src="@{https://cdn.jsdelivr.net/npm/vue/dist/vue.js}" charset="UTF-8"></script>
或者:
<!-- 生产环境版本,优化了尺寸和速度 -->
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/vue"></script>
安装教程给出了更多安装 Vue 的方式。请注意我们不推荐新手直接使用 vue-cli
,尤其是在你还不熟悉基于 Node.js 的构建工具时。
当vue.js引入成功后,在
2.2 Vue实例
-
helloworld
每个 Vue 应用都是通过用
Vue
函数创建一个新的 Vue 实例开始的:var vm = new Vue({ // 选项 })
虽然没有完全遵循 MVVM 模型,但是 Vue 的设计也受到了它的启发。因此在文档中经常会使用
vm
(ViewModel 的缩写) 这个变量名表示 Vue 实例。当创建一个 Vue 实例时,你可以传入一个选项对象。这篇教程主要描述的就是如何使用这些选项来创建你想要的行为。作为参考,你也可以在 API 文档中浏览完整的选项列表。
一个 Vue 应用由一个通过
new Vue
创建的根 Vue 实例,以及可选的嵌套的、可复用的组件树组成。举个例子,一个 todo 应用的组件树可以是这样的:根实例 └─ TodoList ├─ TodoItem │ ├─ TodoButtonDelete │ └─ TodoButtonEdit └─ TodoListFooter ├─ TodosButtonClear └─ TodoListStatistics
我们会在稍后的组件系统章节具体展开。不过现在,你只需要明白所有的 Vue 组件都是 Vue 实例,并且接受相同的选项对象 (一些根实例特有的选项除外)。
vue的应用主要是两个部分:视图和脚本
脚本中新建Vue对象,来注册view中的变量的信息,对象中el表示element,用id选择器的方式来选中div,data用来保存数据。view中的变量需要用 {{}} 括起来
<body> <!--view--> <div id="app"> {{message}} </div> <!--脚本--> <script type="text/javascript"> var app = new Vue({ el: '#app', data:{ message: 'Hello,Vue!' } }) </script> </body>
-
数据与方法
当一个 Vue 实例被创建时,它将
data
对象中的所有的 property 加入到 Vue 的响应式系统中。当这些 property 的值发生改变时,视图将会产生“响应”,即匹配更新为新的值。<div th:id="app"> {{a}} </div> <script type="text/javascript"> // 数据对象 var data = {a:1} // 该对象被加入到一个 Vue 实例中 var vm = new Vue({ el: '#app', data: data }); data.a = 11 // 设置 property 也会影响到原始数据 // vm.a = 2 </script>
当这些数据改变时,视图会进行重渲染。值得注意的是只有当实例被创建时就已经存在于
data
中的 property 才是响应式的。也就是说如果你添加一个新的 property,比如:vm.b = 'hi'
那么对
b
的改动将不会触发任何视图的更新。如果你知道你会在晚些时候需要一个 property,但是一开始它为空或不存在,那么你仅需要设置一些初始值。如:data: { newTodoText: '', visitCount: 0, hideCompletedTodos: false, todos: [], error: null }
这里唯一的例外是使用 Object.freeze(),这会阻止修改现有的 property,也意味着响应系统无法再追踪变化。
<div id="app"> <p>{{ foo }}</p> <!-- 这里的 `foo` 不会更新! --> <button v-on:click="foo = 'baz'">Change it</button> </div> <script> var obj = { foo: 'bar' } Object.freeze(obj) new Vue({ el: '#app', data: obj }) </script>
除了数据 property,Vue 实例还暴露了一些有用的实例 property 与方法。它们都有前缀 $,以便与用户定义的 property 区分开来。例如:
<script> var data = { a: 1 } var vm = new Vue({ el: '#example', data: data }) vm.$data === data // => true vm.$el === document.getElementById('example') // => true // $watch 是一个实例方法 vm.$watch('a', function (newValue, oldValue) { // 这个回调将在 `vm.a` 改变后调用 }) </script>
以后可以在 API 参考中查阅到完整的实例 property 和方法的列表。
-
每个 Vue 实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。
下图展示了实例的生命周期。你不需要立马弄明白所有的东西,不过随着你的不断学习和使用,它的参考价值会越来越高。
3. 模版语法
3.1 插值
-
文本
数据绑定最常见的形式就是使用“Mustache”语法 (双大括号) 的文本插值:
<span>Message: {{ msg }}</span>
Mustache 标签将会被替代为对应数据对象上
msg
property 的值。无论何时,绑定的数据对象上msg
property 发生了改变,插值处的内容都会更新。通过使用 v-once 指令,你也能执行一次性地插值,当数据改变时,插值处的内容不会更新。但请留心这会影响到该节点上的其它数据绑定:
<span v-once>这个将不会改变: {{ msg }}</span>
-
html
双大括号会将数据解释为普通文本,而非 HTML 代码。为了输出真正的 HTML,你需要使用
v-html
指令:<div th:id="app"> {{msg}} <!-- 文本方式显示--> <p>Using mustaches: {{ rawHtml }}</p> <!-- html方式显示 --> <p v-html="rawHtml"></p> </div> <script type="text/javascript"> // 数据对象 var data = {a:1} // 该对象被加入到一个 Vue 实例中 var vm = new Vue({ el: '#app', data: { msg:'Hi,Vue', rawHtml:'<span style="color: red">This should be red</span>' } }); </script>
这个
span
的内容将会被替换成为 property 值rawHtml
,直接作为 HTML——会忽略解析 property 值中的数据绑定。注意,你不能使用v-html
来复合局部模板,因为 Vue 不是基于字符串的模板引擎。反之,对于用户界面 (UI),组件更适合作为可重用和可组合的基本单位。你的站点上动态渲染的任意 HTML 可能会非常危险,因为它很容易导致 XSS 攻击。请只对可信内容使用 HTML 插值,绝不要对用户提供的内容使用插值。
-
Mustache 语法不能作用在 HTML attribute 上,遇到这种情况应该使用
v-bind
指令:<div v-bind:id="dynamicId"></div>
对于布尔 attribute (它们只要存在就意味着值为
true
),v-bind
工作起来略有不同,在这个例子中:<button v-bind:disabled="isButtonDisabled">Button</button>
如果
isButtonDisabled
的值是null
、undefined
或false
,则disabled
attribute 甚至不会被包含在渲染出来的<button>
元素中。<!--插值:属性--> <div th:id="app"> <div th:v-bind:class="color">test...</div> </div> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { color:'blue' } }); </script> <style type="text/css"> .red{color: red} .blue{color: blue} </style>
-
迄今为止,在我们的模板中,我们一直都只绑定简单的 property 键值。但实际上,对于所有的数据绑定,Vue.js 都提供了完全的 JavaScript 表达式支持。
<!--插值:使用 JavaScript 表达式--> <div th:id="app"> {{ number + 1 }} {{ ok ? 'YES' : 'NO' }} {{ message.split('').reverse().join('') }} <div th:v-bind:id="'list-' + id"></div> </div> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { number: 10, ok: true, message:'vue' } }); </script>
这些表达式会在所属 Vue 实例的数据作用域下作为 JavaScript 被解析。有个限制就是,每个绑定都只能包含单个表达式,所以下面的例子都不会生效。
<!-- 这是语句,不是表达式 --> {{ var a = 1 }} <!-- 流控制也不会生效,请使用三元表达式 --> {{ if (ok) { return message } }}
模板表达式都被放在沙盒中,只能访问全局变量的一个白名单,如
Math
和Date
。你不应该在模板表达式中试图访问用户定义的全局变量。
3.2 指令
指令 (Directives) 是带有 v-
前缀的特殊 attribute。指令 attribute 的值预期是单个 JavaScript 表达式 (v-for
是例外情况,稍后我们再讨论)。指令的职责是,当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM。回顾我们在介绍中看到的例子:
<!--************** 模版语法之----指令 ***************-->
<div th:id="app">
<div th:v-if="seen">你看见我了</div>
</div>
<script type="text/javascript">
var vm = new Vue({
el: '#app',
data: {
seen: true
}
});
</script>
这里,v-if
指令将根据表达式 seen
的值的真假来插入/移除 <p>
元素。
-
一些指令能够接收一个“参数”,在指令名称之后以冒号表示。例如,
v-bind
指令可以用于响应式地更新 HTML attribute:<!--指令--参数--> <div th:id="app"> <a th:v-bind:href="url">...</a> </div> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { url:'https://www.google.com/' } }); </script>
在这里
href
是参数,告知v-bind
指令将该元素的href
attribute 与表达式url
的值绑定。另一个例子是 v-on 指令,它用于监听 DOM 事件:
<a v-on:click="doSomething">...</a>
在这里参数是监听的事件名。我们也会更详细地讨论事件处理。
-
从 2.6.0 开始,可以用方括号括起来的 JavaScript 表达式作为一个指令的参数:
<!-- 注意,参数表达式的写法存在一些约束,如之后的“对动态参数表达式的约束”章节所述。 --> <a v-bind:[attributeName]="url"> ... </a>
这里的
attributeName
会被作为一个 JavaScript 表达式进行动态求值,求得的值将会作为最终的参数来使用。例如,如果你的 Vue 实例有一个data
propertyattributeName
,其值为"href"
,那么这个绑定将等价于v-bind:href
。同样地,你可以使用动态参数为一个动态的事件名绑定处理函数:
<a v-on:[eventName]="doSomething"> ... </a>
在这个示例中,当
eventName
的值为"focus"
时,v-on:[eventName]
将等价于v-on:focus
。对动态参数的值的约束
动态参数预期会求出一个字符串,异常情况下值为
null
。这个特殊的null
值可以被显性地用于移除绑定。任何其它非字符串类型的值都将会触发一个警告。对动态参数表达式的约束
动态参数表达式有一些语法约束,因为某些字符,如空格和引号,放在 HTML attribute 名里是无效的。例如:
<!-- 这会触发一个编译警告 --> <a v-bind:['foo' + bar]="value"> ... </a>
变通的办法是使用没有空格或引号的表达式,或用计算属性替代这种复杂表达式。
在 DOM 中使用模板时 (直接在一个 HTML 文件里撰写模板),还需要避免使用大写字符来命名键名,因为浏览器会把 attribute 名全部强制转为小写:
<!-- 在 DOM 中使用模板时这段代码会被转换为 `v-bind:[someattr]`。 除非在实例中有一个名为“someattr”的 property,否则代码不会工作。 --> <a v-bind:[someAttr]="value"> ... </a>
-
修饰符 (modifier) 是以半角句号
.
指明的特殊后缀,用于指出一个指令应该以特殊方式绑定。例如,.prevent
修饰符告诉v-on
指令对于触发的事件调用event.preventDefault()
:<!--指令--修饰符--> <!--这里的.stop就是修饰符,如果不加它,就先后记录click2,click1,加上了.stop就不会记录click1了--> <div th:id="app"> <div @click="click1"> <div @click.stop="click2"> click me </div> </div> </div> <script type="text/javascript"> var vm = new Vue({ el:'#app', methods:{ click1:function(){ console.log('click1....') }, click2:function(){ console.log('click2....') } } }); </script>
4. Class 与 Style 绑定
操作元素的 class 列表和内联样式是数据绑定的一个常见需求。因为它们都是 attribute,所以我们可以用 v-bind
处理它们:只需要通过表达式计算出字符串结果即可。不过,字符串拼接麻烦且易错。因此,在将 v-bind
用于 class
和 style
时,Vue.js 做了专门的增强。表达式结果的类型除了字符串之外,还可以是对象或数组。
4.1 绑定 HTML Class
-
v-bind:style
的数组语法可以将多个样式对象应用到同一个元素上:<div th:id="app"> <div class="test" th:v-bind:class="[isActive?'active':'',isGreen?'green':'']" style="height: 200px;width: 200px;text-align: center;line-height: 200px"> Hi,vue </div> </div> <script type="text/javascript"> var vm = new Vue({ el:'#app', data:{ isActive:true, isGreen:true } }); </script> <style type="text/css"> .test{font-size: 30px} .active{background-color: honeydew} .green{color: green} </style>
4.2 绑定内联样式
5. 条件渲染
v-if
指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 truthy 值的时候被渲染。
<h1 v-if="awesome">Vue is awesome!</h1>
也可以用 v-else
添加一个“else 块”:
<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no 😢</h1>
5.1 在 template 元素上使用 v-if 条件渲染分组
因为 v-if
是一个指令,所以必须将它添加到一个元素上。但是如果想切换多个元素呢?此时可以把一个 <template>
元素当做不可见的包裹元素,并在上面使用 v-if
。最终的渲染结果将不包含 <template>
元素。
<template v-if="ok">
<h1>Title</h1>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</template>
5.2 v-else
指令
可以使用 v-else
指令来表示 v-if
的“else 块”:
<div v-if="Math.random() > 0.5">
Now you see me
</div>
<div v-else>
Now you don't
</div>
v-else
元素必须紧跟在带 v-if
或者 v-else-if
的元素的后面,否则它将不会被识别。
5.3 v-else-if
v-else-if
,顾名思义,充当 v-if
的“else-if 块”,可以连续使用:
<div v-if="type === 'A'">
A
</div>
<div v-else-if="type === 'B'">
B
</div>
<div v-else-if="type === 'C'">
C
</div>
<div v-else>
Not A/B/C
</div>
类似于 v-else
,v-else-if
也必须紧跟在带 v-if
或者 v-else-if
的元素之后。
5.4 用 key
管理可复用的元素
Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。这么做除了使 Vue 变得非常快之外,还有其它一些好处。例如,如果你允许用户在不同的登录方式之间切换:
<template v-if="loginType === 'username'">
<label>Username</label>
<input placeholder="Enter your username">
</template>
<template v-else>
<label>Email</label>
<input placeholder="Enter your email address">
</template>
那么在上面的代码中切换 loginType
将不会清除用户已经输入的内容。因为两个模板使用了相同的元素,<input>
不会被替换掉——仅仅是替换了它的 placeholder
。
自己动手试一试,在输入框中输入一些文本,然后按下切换按钮:
这样也不总是符合实际需求,所以 Vue 为你提供了一种方式来表达“这两个元素是完全独立的,不要复用它们”。只需添加一个具有唯一值的 key
attribute 即可。使用key,使得切换loginType后,清空前一次的输入,不会复用前一个输入框内的内容
<!--************** 条件渲染 ***************-->
<div th:id="app">
<template v-if="loginType === 'username'">
<label>Username</label>
<input placeholder="Enter your username" key="username-input">
</template>
<template v-else>
<label>Email</label>
<input placeholder="Enter your email address" key="email-input">
</template>
<br>
<button @click="toggleLoginType">Toggle login type</button>
</div>
<script type="text/javascript">
var vm = new Vue({
el:'#app',
data:{
loginType: 'username',
},
methods:{
toggleLoginType:function () {
return this.loginType = this.loginType === 'username' ? 'email' : 'username'
}
}
});
</script>
现在,每次切换时,输入框都将被重新渲染。请看:
注意,<label>
元素仍然会被高效地复用,因为它们没有添加 key
attribute。
5.5 v-show
另一个根据条件展示元素的选项是 v-show
指令。用法大体上一样:
<div th:id="app">
<h1 v-show="ok">Hello!</h1>
</div>
<script type="text/javascript">
var vm = new Vue({
el:'#app',
data:{
ok: true
}
});
</script>
不同的是有v-show 的元素会始终渲染并保持在 DOM 中。v-show
是简单的切换元素的 CSS 属性 display
。即:v-show只是判断值是否为真,如果为真就显示,反之就不显示。但是查看源代码是能看得见的
注意 v-show
不支持 <template>
语法。
5.6 v-if
vs v-show
-
v-if
是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。 -
v-if
也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。 -
相比之下,
v-show
就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。 -
一般来说,
v-if
有更高的切换开销,而v-show
有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用v-show
较好;如果在运行时条件很少改变,则使用v-if
较好。
5.7 v-if
与 v-for
一起使用
不推荐同时使用 v-if
和 v-for
。请查阅风格指南以获取更多信息。
当 v-if
与 v-for
一起使用时,v-for
具有比 v-if
更高的优先级。请查阅列表渲染指南以获取详细信息。
6. 列表渲染
6.1 用 v-for
把一个数组对应为一组元素
我们可以用 v-for
指令基于一个数组来渲染一个列表。v-for
指令需要使用 item in items
形式的特殊语法,其中 items
是源数据数组,而 item
则是被迭代的数组元素的别名。
<div th:id="app">
<ul>
<li v-for="item in items" :key="item.message">
{{ item.message }}
</li>
</ul>
</div>
<script type="text/javascript">
var vm = new Vue({
el:'#app',
data:{
items:[
{message:"Foo"},
{message:"Bar"}
]
}
});
</script>
注意:如果使用thymeleaf,v-for前面不要加th:
在 v-for
块中,我们可以访问所有父作用域的 property。v-for
还支持一个可选的第二个参数,即当前项的索引index。
<div th:id="app">
<ul>
<li v-for="item,index in items" :key="item.message">
{{ parentMessage }} - {{ index }} - {{ item.message }}
</li>
</ul>
</div>
<script type="text/javascript">
var vm = new Vue({
el:'#app',
data:{
parentMessage: 'Parent',
items:[
{message:"Foo"},
{message:"Bar"}
]
}
});
</script>
你也可以用 of
替代 in
作为分隔符,因为它更接近 JavaScript 迭代器的语法:
<div v-for="item of items"></div>
6.2 在 v-for
里使用对象
你也可以用 v-for
来遍历一个对象的 property。
<ul th:id="app">
<li v-for="value,name in object">
{{name}} : {{ value }}
</li>
</ul>
<script type="text/javascript">
var vm = new Vue({
el:'#app',
data:{
object:{
title:'How to list the element of a object',
author:'cxy',
publish_time:'2020.11.04'
}
}
});
</script>
还可以用第三个参数作为索引:
<div v-for="(value, name, index) in object">
{{ index }}. {{ name }}: {{ value }}
</div>
在遍历对象时,会按 Object.keys()
的结果遍历,但是不能保证它的结果在不同的 JavaScript 引擎下都一致。
6.3 维护状态
当 Vue 正在更新使用 v-for
渲染的元素列表时,它默认使用“就地更新”的策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是就地更新每个元素,并且确保它们在每个索引位置正确渲染。这个类似 Vue 1.x 的 track-by="$index"
。
这个默认的模式是高效的,但是只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出。
为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一 key
attribute:
<ul th:id="app">
<li v-for="value,name in object" th:v-bind:key="name">
{{name}} : {{ value }}
</li>
</ul>
<script type="text/javascript">
var vm = new Vue({
el:'#app',
data:{
object:{
title:'How to list the element of a object',
author:'cxy',
publish_time:'2020.11.04'
}
}
});
</script>
建议尽可能在使用 v-for
时提供 key
attribute,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。
因为它是 Vue 识别节点的一个通用机制,key
并不仅与 v-for
特别关联。后面我们将在指南中看到,它还具有其它用途。
不要使用对象或数组之类的非基本类型值作为 v-for
的 key
。请用字符串或数值类型的值。
更多 key
attribute 的细节用法请移步至 key
的 API 文档。
7. 事件处理
特别注意:thymeleaf 使用 vue 的 v-on 指令时,前面不加 th: ,需要在html标签中加上 xmlns:v-on="http://www.w3.org/1999/xhtml
7.1 监听事件
可以用 v-on
指令监听 DOM 事件,并在触发时运行一些 JavaScript 代码。示例:
<html lang="en" xmlns:th="https://www.thymeleaf.org" xmlns:v-on="http://www.w3.org/1999/xhtml">
<div th:id="app">
<div>
<button v-on:click="counter+=1">Add 1</button>
</div>
</div>
<script type="text/javascript">
var vm = new Vue({
el:'#app',
data: {
counter: 0,
}
})
</script>
</html>
7.2 事件处理方法
然而许多事件处理逻辑会更为复杂,所以直接把 JavaScript 代码写在 v-on
指令中是不可行的。因此 v-on
还可以接收一个需要调用的方法名称。
<html lang="en" xmlns:th="https://www.thymeleaf.org" xmlns:v-on="http://www.w3.org/1999/xhtml">
<div th:id="app">
<!-- `greet` 是在下面定义的方法名 -->
<button v-on:click="greet">Greet</button>
</div>
<script type="text/javascript">
var vm = new Vue({
el:'#app',
data: {
name:'Vue.js'
},
methods:{
greet:function (str) {
// `this` 在方法里指向当前 Vue 实例
alert('Hello' + this.name + '!')
alert(str)
// `event` 是原生 DOM 事件
// if (event) {
// alert(event.target.tagName)
// }
}
}
});
vm.greet('abc');
</script>
</html>
7.3 内联处理器中的方法
除了直接绑定到一个方法,也可以在内联 JavaScript 语句中调用方法,即调用方法的时候传参
<html lang="en" xmlns:th="https://www.thymeleaf.org" xmlns:v-on="http://www.w3.org/1999/xhtml">
<div th:id="app">
<button v-on:click="say('hi')">Say hi</button>
<button v-on:click="say('what')">Say what</button>
</div>
<script type="text/javascript">
var vm = new Vue({
el:'#app',
methods:{
say: function (message) {
alert(message)
}
}
})
</script>
</html>
有时也需要在内联语句处理器中访问原始的 DOM 事件。可以用特殊变量 $event
把它传入方法:
7.4 事件修饰符
在事件处理程序中调用 event.preventDefault()
或 event.stopPropagation()
是非常常见的需求。尽管我们可以在方法中轻松实现这点,但更好的方式是:方法只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。
为了解决这个问题,Vue.js 为 v-on
提供了事件修饰符。之前提过,修饰符是由点开头的指令后缀来表示的。
.stop
.prevent
.capture
.self
.once
.passive
<!-- 阻止单击事件继续传播 -->
<a v-on:click.stop="doThis"></a>
<!-- 提交事件不再重载页面 -->
<form v-on:submit.prevent="onSubmit"></form>
<!-- 修饰符可以串联 -->
<a v-on:click.stop.prevent="doThat"></a>
<!-- 只有修饰符 -->
<form v-on:submit.prevent></form>
<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 -->
<div v-on:click.capture="doThis">...</div>
<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div v-on:click.self="doThat">...</div>
使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 v-on:click.prevent.self
会阻止所有的点击,而 v-on:click.self.prevent
只会阻止对元素自身的点击。
2.1.4 新增
<!-- 点击事件将只会触发一次 -->
<a v-on:click.once="doThis"></a>
不像其它只能对原生的 DOM 事件起作用的修饰符,.once
修饰符还能被用到自定义的组件事件上。如果你还没有阅读关于组件的文档,现在大可不必担心。
2.3.0 新增
Vue 还对应 addEventListener
中的 passive
选项提供了 .passive
修饰符。
<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 -->
<!-- 而不会等待 `onScroll` 完成 -->
<!-- 这其中包含 `event.preventDefault()` 的情况 -->
<div v-on:scroll.passive="onScroll">...</div>
这个 .passive
修饰符尤其能够提升移动端的性能。
不要把 .passive
和 .prevent
一起使用,因为 .prevent
将会被忽略,同时浏览器可能会向你展示一个警告。请记住,.passive
会告诉浏览器你不想阻止事件的默认行为。
8. 表单输入绑定
8.1 基础用法
你可以用 v-model
指令在表单 <input>
、<textarea>
及 <select>
元素上创建双向数据绑定。它会根据控件类型自动选取正确的方法来更新元素。尽管有些神奇,但 v-model
本质上不过是语法糖。它负责监听用户的输入事件以更新数据,并对一些极端场景进行一些特殊处理。
v-model
会忽略所有表单元素的 value
、checked
、selected
attribute 的初始值而总是将 Vue 实例的数据作为数据来源。你应该通过 JavaScript 在组件的 data
选项中声明初始值。v-model
在内部为不同的输入元素使用不同的 property 并抛出不同的事件:
- text 和 textarea 元素使用
value
property 和input
事件; - checkbox 和 radio 使用
checked
property 和change
事件; - select 字段将
value
作为 prop 并将change
作为事件。
对于需要使用输入法 (如中文、日文、韩文等) 的语言,你会发现 v-model
不会在输入法组合文字过程中得到更新。如果你也想处理这个过程,请使用 input
事件。
8.2 文本
双向绑定(在输入框和message变量中都绑定为输入值), 如:
<!--单行文本的双向绑定(在输入框和message变量中都绑定为输入值)-->
<div th:id="app">
<input v-model="message" placeholder="edit me">
<p>Message is: {{ message }}</p>
</div>
<script type="text/javascript">
var vm = new Vue({
el:'#app',
data:{
message:""
}
})
</script>
8.3 多行文本
<!--多行文本的双向绑定(在输入框和message变量中都绑定为输入值)-->
<div th:id="app">
<span>Multiline message is:</span>
<p style="white-space: pre-line;">{{ message }}</p>
<br>
<textarea v-model="message" placeholder="add multiple lines"></textarea>
</div>
<script type="text/javascript">
var vm = new Vue({
el:'#app',
data:{
message:""
}
})
</script>
8.4 复选框
- 单个复选框,绑定到布尔值
<div th:id="app">
<input type="checkbox" id="checkbox" v-model="checked">
<label for="checkbox">{{ checked }}</label>
</div>
<script type="text/javascript">
var vm = new Vue({
el:'#app',
data:{
checked:true
}
})
</script>
- 多个复选框,绑定到同一个数组
<div th:id="app">
<input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
<label for="jack">Jack</label>
<input type="checkbox" id="john" value="John" v-model="checkedNames">
<label for="john">John</label>
<input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
<label for="mike">Mike</label>
<br>
<span>Checked names: {{ checkedNames }}</span>
</div>
<script type="text/javascript">
var vm = new Vue({
el:'#app',
data:{
checkedNames:[]
}
})
</script>
8.5 单选按钮
<div th:id="app">
<div id="example-4">
<input type="radio" id="one" value="One" v-model="picked">
<label for="one">One</label>
<br>
<input type="radio" id="two" value="Two" v-model="picked">
<label for="two">Two</label>
<br>
<span>Picked: {{ picked }}</span>
</div>
</div>
<script type="text/javascript">
var vm = new Vue({
el:'#app',
data:{
picked:''
}
})
</script>
8.6 选择框
5. 功能实现
5.1 用户注册和登录功能
解决办法就是将生成后的mapper.xml文件中的namespace与你的mapper接口路径比对一下,不同就改为mapper接口的路径。
原理:在mybatis中,映射文件中的namespace是用于绑定mapper接口的,即面向接口编程。 当你的namespace绑定接口后,你可以不用写接口实现类,mybatis会通过该绑定自动帮你找到对应需要执行的SQL语句。Spring在动态代理时,就是需要一个mapper接口的一个实现类,所以当你的mapper.xml与mapper接口类未关联到一块的时候,就会出现上述异常。
前端页面
-
登录页面login.html
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <link rel="stylesheet" type="text/css" th:href="@{/css/mystyle.css}"/> <script type="text/javascript" th:src="@{/js/jquery-1.7.2.js}"></script> <script type="text/javascript" th:src="@{https://cdn.jsdelivr.net/npm/vue/dist/vue.js}" charset="UTF-8"></script><!-- 开发环境版本,包含了有帮助的命令行警告 --> <title>Login</title> </head> <body> <div id="container" style="width: 100%;"> <div id="header" style="background-color88: azure;"> <ul> <li><a class="active" href="#home">微微博</a></li> <li><a href="#found">发现</a></li> <li><a href="#about">关于</a></li> <li text-align="right"><a href="#about me">cxy</a></li> </ul> </div> </div> <div th:id="login" style="background-color: honeydew"> <!-- 表单: 收集用户的信息,提交到后台服务器 --> <br> <h3 align="center">登录</h3> <form th:id="login_form" action="#" th:action="@{/user/login}" method="post"> <p>用户名: <input type="text" name="username"> <!--/*@thymesVar id="login_user" type="java.lang.String"*/--> <span th:text="${login_user}"></span></p> <p>密 码: <input type="password" name="password"></p> <p><input type="submit" value="登录"></p> <p th:if="${login_user eq 'user has not registered'}" th:v-html="acount_uncreated"></p> <p><input type="checkbox" name="remember" value="1"/>一周内免登陆</p> </form> <br> </div> <div id="footer" style="background-color: darksalmon;clear:both;text-align:center;"> 版权 © cxy.com</div> </div> <script type="text/javascript"> new Vue({ el:'#login', data: { acount_uncreated: '<span style="color: gray">没有账号?</span><span><a href="/user/toRegister">创建账号</a></span>' } }); </script> </body> </html>
-
登录成功页面welcome.html
<!DOCTYPE html> <html> <html lang="en" xmlns:th="https://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <script type="text/javascript" th:src="@{/js/jquery-1.7.2.js}"></script> <title>Title</title> </head> <script> alert('欢迎您:<span th:text="${user.name}">确定</span>') </script> <body> 注册或登录后才能看到的界面 <h1 th:text="${session.msg}"></h1> <a href="/admin/logout">退出登录</a> <!--<a href="#" th:href="@{/admin/logout}" class="item">注销</a>--> </body> </html>
-
注册页面register.html
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:v-on="http://www.w3.org/1999/xhtml"> <head> <meta charset="UTF-8"> <link rel="stylesheet" type="text/css" th:href="@{/css/mystyle.css}"/> <script type="text/javascript" th:src="@{/js/jquery-1.7.2.js}"></script> <script type="text/javascript" th:src="@{http://static.runoob.com/assets/jquery-validation-1.14.0/lib/jquery.js}"></script> <script type="text/javascript" th:src="@{http://static.runoob.com/assets/jquery-validation-1.14.0/dist/jquery.validate.min.js}"></script> <script type="text/javascript" th:src="@{https://cdn.jsdelivr.net/npm/vue/dist/vue.js}" charset="UTF-8"></script><!-- 开发环境版本,包含了有帮助的命令行警告 --> <title>Register</title> </head> <body> <div id="container" style="width: 100%;"> <div id="header" style="background-color88: azure;"> <ul> <li><a class="active" href="#home">微微博</a></li> <li><a href="#found">发现</a></li> <li><a href="#about">关于</a></li> <li text-align="right"><a href="#about me">cxy</a></li> </ul> </div> </div> <div th:id="register" style="background-color: honeydew"> <br> <!-- 表单: 收集用户的信息,提交到后台服务器 --> <form id="register_form" action="/user/register" method="post"> <p>用户名: <input type="text" name="username" placeholder="username"><span th:text="${register_user}"></span></p> <p>密 码: <input type="password" th:id="password" name="password" placeholder="password"></p> <p>确认密码: <input type="password" th:id="confirm_password" name="confirm_password" placeholder="confirm password"></p> <p>年 龄: <input type="text" name="age" placeholder=""></p> <p>性 别: <label for="man">男</label> <input type="radio" id="man" value="man"> <label for="woman">女</label> <input type="radio" id="woman" value="woman"> </p> <p>邮 箱:<input type="text" name="email" placeholder=""></p> <p>电 话:<input type="text" name="tel" placeholder=""></p> <button v-on:click="submit">submit</button> <!--/*@thymesVar id="register_user" type=""*/--> <p th:if="${register_user eq 'user already registered'}" th:v-html="acount_already_created"></p> </form> <br> <br> <br> </div> <div id="footer" style="background-color: darksalmon;clear:both;text-align:center"> 版权 © cxy.com</div> </div> <script> jQuery.validator.setDefaults({ debug: false, success: "valid" }); $("#register_form").validate({ rules:{ username:"required", password: "required", confirm_password:{ equalTo: "#password" } } }); </script> <script type="text/javascript"> new Vue({ el:'#register', data:{ acount_already_created:'<span style="color: gray">已有账号?</span><span><a href="/user/toLogin">登录</a></span>' } }) </script> </body> </html>
后台登录和注册
-
UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.cxy.blog.dao.UserMapper"> <select id="getUserByUsername" resultType="User"> select id,username,password from t_user where username = #{username} </select> <select id="login" resultType="User"> SELECT username,password FROM t_user where username = #{username} and password = #{password} </select> <insert id="register" parameterType="User" useGeneratedKeys="true" keyProperty="id"> INSERT INTO t_user(username, password,gender,email) VALUES(#{username},#{password},#{gender},#{email}) </insert> </mapper>
-
UserMapper.java
如果没有在启动类上加@MapperScan注解则需要在每一个Mapper类上加上@Mapper注解
@Repository public interface UserMapper { User getUserByUsername(String username); User login(String username,String password); int register(User user); }
-
UserService
@Service public class UserService { @Autowired private UserMapper userMapper; public User getUserByUsername(String username) { return userMapper.getUserByUsername(username); } public User login(String username, String password) { //MD5加密 //return userMapper.login(username, MD5Utils.code(password)); //SHA256加密 return userMapper.login(username, Sha256Util.getSHA256(password)); } public int register(User user) { //MD5加密 //user.setPassword(MD5Utils.code(user.getPassword())); //SHA256加密 user.setPassword(Sha256Util.getSHA256(user.getPassword())); return userMapper.register(user); } }
-
UserController
package com.cxy.blog.controller; import com.cxy.blog.beans.User; import com.cxy.blog.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Controller @RequestMapping("/user") public class UserController { @Autowired private UserService userService; /** * 跳转注册页 */ @RequestMapping("/toRegister") public String toRegister(){ return "user/register"; } /** * 注册操作 * @param user * @param model * @return */ @RequestMapping(value = "/register",method = RequestMethod.POST) public String register(User user, Model model){ String username = user.getUsername(); if (userService.getUserByUsername(username) != null){ System.out.println("该用户已经注册"); model.addAttribute("register_user","user already registered"); return "forward:/user/toRegister"; } int res = userService.register(user); if (res == 0){ System.out.println("注册失败"); return "forward:/user/toRegister"; } return "redirect:/user/toLogin"; } /** * 跳转首页(登录页) */ @RequestMapping("/toLogin") public String show(){ return "user/login"; } /** * 登录操作 */ @RequestMapping("/login") public String login(User user, HttpServletRequest request,Model model){ String username = user.getUsername(); String password = user.getPassword(); if (userService.getUserByUsername(username) == null){ model.addAttribute("login_user","user has not registered"); return "forward:/user/toLogin"; } User user1 = userService.login(username, password); if (user1==null){ model.addAttribute("login_user","username or password is not true"); return "forward:/user/toLogin"; }else{ //登录成功后将用户放入session中,用于拦截 request.getSession().setAttribute("session_user",user); return "user/welcome"; } } /** * 测试未登陆拦截页面 */ @RequestMapping("/welcome") public String welcome(){ return "user/welcome"; } /** * 退出登录 */ @RequestMapping("/logout") public void outUser(HttpServletRequest request, HttpServletResponse response) throws IOException { request.getSession().removeAttribute("session_user"); response.sendRedirect("/user/toLogin"); } }
5.2 未登录拦截
-
定义拦截器
package com.cxy.blog.intercepter; import com.cxy.blog.beans.User; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class UserInterceptor implements HandlerInterceptor { /* * 进入controller层之前拦截请求 * 返回值:表示是否将当前的请求拦截下来 false:拦截请求,请求终止。true:请求不被拦截,继续执行 * Object obj:表示被拦的请求的目标对象(controller中方法) */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { System.out.println("执行到了preHandle方法"); System.out.println(handler); User user = (User) request.getSession().getAttribute("session_user"); if (user == null) { response.sendRedirect(request.getContextPath() + "/user/toLogin");//拦截后跳转的方法 System.out.println("已成功拦截并转发跳转"); return false; } System.out.println("合格不需要拦截,放行"); return true; } /* * 处理请求完成后视图渲染之前的处理操作 * 通过ModelAndView参数改变显示的视图,或发往视图的方法 */ @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { System.out.println("执行了postHandle方法"); } /* * 视图渲染之后的操作 */ @Override public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3) throws Exception { System.out.println("执行到了afterCompletion方法"); } }
-
注册拦截器
package com.cxy.blog.intercepter; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.ArrayList; import java.util.List; /** * @author cxy * @create 2020-10-30-1:23 AM */ @Configuration public class SessionInterceptor implements WebMvcConfigurer { /** * 注册拦截器,添加拦截路径和排除拦截路径 * addPathPatterns():添加需要拦截的路径 * excludePathPatterns():添加不需要拦截的路径 */ @Override public void addInterceptors(InterceptorRegistry registry) { List list = new ArrayList(); list.add("/user/toLogin"); list.add("/user/login"); list.add("/user/toRegister"); list.add("/user/register"); registry.addInterceptor(new UserInterceptor()) .addPathPatterns("/user/**") .excludePathPatterns(list) .excludePathPatterns("/static"); } }
5.3 七天内免登录
Cookie是由服务器端生成并储存在浏览器客户端上的数据。
在javaweb开发中Cookie被当做java对象在web服务器端创建,并由web服务器通过Http协议,Header,body,line响应发送给特定浏览器客户端,并且WEB服务器可以向同一个浏览器客户端上同时发送多个Cookie,每一个Cookie对象都由name和value组成,name(cookie的name)和value(放入cookie的值)只能是字符串类型,浏览器接收到来自服务器的Cookie数据之后默认将其保存在浏览器缓存中(如果浏览器关闭,缓存消失,Cookie数据消失),只要浏览器不关闭,当我们下一次发送“特定”请求的时候,浏览器负责将Cookie数据发送给WEB服务器。我们还可以使用特殊的方法,将Cookie保存在客户端的硬盘上,永久性保存。这样关闭浏览器Cookie还是存在的,不会消失,比如:实现十天内自动登录。
Cookie 和 Session
-
Cookie
-
Cookie是浏览器(User Agent)访问一些网站后,这些网站存放在客户端的一组数据,用于使网站等跟踪用户,实现用户自定义功能。
-
Cookie的Domain和Path属性标识了这个Cookie是哪一个网站发送给浏览器的;Cookie的Expires属性标识了Cookie的有 效时间,当Cookie的有效时间过了之后,这些数据就被自动删除了。
-
如果不设置过期时间,则表示这个Cookie生命周期为浏览器会话期间,只要关闭浏览器窗口,Cookie就消失了。这种生命期为浏览会话期的 Cookie被称为会话Cookie。会话Cookie一般不保存在硬盘上而是保存在内存里。如果设置了过期时间,浏览器就会把Cookie保存到硬盘 上,关闭后再次打开浏览器,这些Cookie依然有效直到超过设定的过期时间。存储在硬盘上的Cookie可以在不同的浏览器进程间共享,比如两个IE窗 口。而对于保存在内存的Cookie,不同的浏览器有不同的处理方式。
-
-
Session
Session是存放在服务器端的类似于HashTable结构(每一种Web开发技术的实现可能不一样,下文直接称为HashTable)来存放用户 数据。
当浏览器第一次发送请求时,服务器自动生成了一个HashTable和一个Session ID用来唯一标识这个HashTable,并将其通过响应发送到浏览器。当浏览器第二次发送请求,会将前一次服务器响应中的Session ID放在请求中一并发送到服务器上,服务器从请求中提取出Session ID,并和保存的所有Session ID进行对比,找到这个用户对应的HashTable。
一般情况下,服务器会在一定时间内(默认20分钟)保存这个HashTable,过了时间限制,就会销毁这个HashTable。在销毁之前,程序员可以 将用户的一些数据以Key和Value的形式暂时存放在这个HashTable中。当然,也有使用数据库将这个HashTable序列化后保存起来的,这 样的好处是没了时间的限制,坏处是随着时间的增加,这个数据库会急速膨胀,特别是访问量增加的时候。一般还是采取前一种方式,以减轻服务器压力。
-
区别
存储位置 浏览器携带的数据量 存储的数据类型 安全性 默认的有效路径 数据的传输量 Cookie 浏览器 多 只能是字符串 较低 当前目录及其子目录 有限制4K,不能超过20个 Session 服务器 少(只携带session-id) 任意类型 较高 整站有效 无限制
Cookie简介
-
Cookie是什么? Cookie作用? Cookie保存在哪里?
- 翻译过来:曲奇饼干
- Cookie可以保存会话状态,但是这个会话状态是保留在客户端上。
- 只要Cookie清除,或者Cookie失效,这个会话状态就没有了。
- Cookie可以保存在浏览器的缓存中,浏览器关闭Cookie消失。
- Cookie也可以保存在客户端的硬盘文件中,浏览器关闭Cookie还在,除非Cookie失效。
-
Cookie只有在javaweb中有吗?
- Cookie不止是在javaweb中存在。
- 只要是web开发,只要是B/S架构的系统,只要是基于HTTP协议,就有Cookie的存在。
- Cookie这种机制是HTTP协议规定的。
-
Cookie实现的功能,常见的有哪些?
- 保留购物车商品的状态在客户端上
- 十天内免登录
- …
-
Cookie在现实生活中对应的场景图
-
在java程序中怎么创建Cookie?
Cookie cookie = new Cookie(String cookieName,String cookieValue);
-
Cookie在客户端的保存形式和有效时间
-
服务器端默认创建的Cookie,发送到浏览器之后,浏览器默认将其保存在缓存中,当浏览器关闭之后Cookie消失。
-
可以通过设置Cookie的有效时长,以保证Cookie保存在硬盘文件当中。但是这个有效时长必须是 >0 的。换句话说,只要设置Cookie的有效时长大于0,则该Cookie会被保存在客户端硬盘文件当中。有效时长过去之后,则硬盘文件当中的Cookie失效。
-
服务器创建Cookie对象之后,调用setMaxAge方法设置Cookie的有效时间。
cookie.setMaxAge(60 * 60 * 24 * 10); //10天内有效
如果这个有效时间 >0,则该Cookie对象发送给浏览器之后浏览器将其保存到硬盘文件中。
如果这个有效时间 <0,则该Cookie对象也是被保存在浏览器缓存中,待浏览器关闭Cookie消失。
如果这个有效时间 =0,则该Cookie从服务器端发过来的时候就已经是一个已过时的Cookie。 -
-
在浏览器客户端无论是硬盘文件中还是缓存中保存的Cookie,什么时候会再次发送给服务器呢?
- 浏览器会不会提交发送这些Cookie给服务器,是和请求路径有关系的。
- 请求路径和Cookie是紧密关联的
- 不同的请求路径会发送提交不同的Cookie
-
Cookie和请求路径之间的关系
-
每一个Cookie和请求路径是绑定在一起的,只有特定的路径才可以发送特定的Cookie。
实际上浏览器是这样做的:浏览器在向web服务器发送请求的时候先去对应的请求路径下搜索是否有对应的Cookie,如果有Cookie,并且Cookie没有失效,则发送该Cookie或者多个Cookie到服务器端。请求路径和Cookie的关系是这样对应的:
- 假如获取Cookie时的路径是 :http://localhost:8080/cxy/getCookie
这时浏览器请求服务器,服务器生成Cookie,并将Cookie发送给浏览器客户端。这个浏览器中的Cookie会默认和“cxy/”这个路径绑定在一起。也就是说,以后只要发送“cxy/”请求,Cookie一定会提交给服务器。
将来发送Cookie的路径包括如下路径 :
http://localhost:8080/cxy/getCookie(相同路径)
http://localhost:8080/cxy/xxxx(同目录)
http://localhost:8080/cxy/xxxx/xxxx/xxx(子目录)- 假如获取Cookie时的路径是 :http://localhost:8080/cxy/servlet/getCookie
这时浏览器请求服务器,服务器生成Cookie,并将Cookie发送给浏览器客户端。这个浏览器中的Cookie会默认和“servlet/”这个路径绑定在一起。也就是说,以后只要发送“servlet/”请求,Cookie一定会提交给服务器。
将来发送Cookie的路径包括如下路径 :
http://localhost:8080/cxy/servlet/getCookie(相同路径)
http://localhost:8080/cxy/servlet/xxxxx(同目录)
http://localhost:8080/cxy/servlet/xxxxx/xxxx(子目录) -
其实路径是可以指定的,可以通过java程序进行设置,保证Cookie和某个特定的路径绑定在一起,例如:
cookie.setPath("/cxy/king");
- 那么Cookie将和 “/cxy/king” 路径绑定在一起,只有发送 “http://localhost:8080/cxy/king” 请求路径,浏览器才会提交Cookie给服务器。
-
-
浏览器是可以禁用 Cookie 什么意思?
当浏览器禁用Cookie之后,服务器还是仍然会将Cookie发送给浏览器,只不过这次浏览器选择了不保存
-
浏览器提交Cookie给服务器,服务器怎么接收Cookie?
```java
//从request对象中获取所有提交的Cookie
Cookie[] cookies = request.getCookies();
//遍历cookie数组取出所有的cookie对象
if(cookies != null){
for(Cookie cookie : cookies){
String cookieName = cookie.getName();
String cookieValue = cookie.getValue();
System.out.println(cookieName + "=" + cookieValue);
}
}
```
Cookie 的使用
1、创建 Cookie
// Cookie 为键值对数据格式
Cookie cookie_username = new Cookie("cookie_username", username);
12
2、设置 Cookie 持久时间
// 即:过期时间,单位是:秒(s)
cookie_username.setMaxAge(30 * 24 * 60 * 60);
12
3、设置 Cookie 共享路径
// 表示当前项目下都携带这个cookie
cookie_username.setPath(request.getContextPath());
12
4、向客户端发送 Cookie
// 使用 HttpServletResponse 对象向客户端发送 Cookie
response.addCookie(cookie_username);
12
5、销毁 Cookie
// 根据 key 将 value 置空
Cookie cookie_username = new Cookie("cookie_username", "");
// 设置持久时间为0
cookie_username.setMaxAge(0);
// 设置共享路径
cookie_username.setPath(request.getContextPath());
// 向客户端发送 Cookie
response.addCookie(cookie_username);
12345678
利用Cookie机制实现7天内免登录功能
方式一:
首先想到的是使用cookie保存用户登录信息,设置有效期,在用户下次访问时免去登录环节,直接通过cookie获取用户信息。
方式二:
直接将session会话保存,用户下次访问时,继续使用这个session。相比之下session显得更加安全,但是,大家知道,session会随着浏览器的关闭而消失(确切的说,是在客户端消失,服务器端的session存活周期取决于相应配置),当用户下次启动浏览器,访问网站时,又会得到由网站自动分配的新的session。那么,问题来了:如何做到关闭浏览器后到下次登录时session仍然有效?
思路:
- 在用户登录成功时,创建session对象,保存用户信息
- 将此session的sessionid保存到cookie中
- 同时将sessionid于session对应关系存储到应用域中,以便后面可以根据sessionid来获取到session
- 在用户关闭浏览器,重新打开浏览器访问网站时,读取用户的cookie,得到sessionid
- 根据sessionid获取到第3步存储到应用域中的session对象
- 从session中读取用户信息
方式二不知道session存在哪里,且session有保存时间限制,所以采用第一种方式
因为就算是加密后到cookie,有可能被攻击,获取了用户的密码。不安全,可能被监听,然后被猜出密码的加密方式之类的
下图是用Cookie机制来实现十天内免登录功能的流程图:
-
Controller
package com.cxy.blog.controller;
import com.cxy.blog.beans.User;
import com.cxy.blog.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;
/**
* @author cxy
* @create 2020-10-29-5:09 PM
*/
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
/**
* 登录操作
*/
@RequestMapping("/login")
public String login(User user, Integer remenber,HttpServletRequest request,HttpServletResponse response,Model model){
String username = user.getUsername();
String password = user.getPassword();
if (userService.getUserByUsername(username) == null){
model.addAttribute("login_massage","user has not registered");
System.out.println("user has not registered");
return "forward:/user/toLogin";
}
if(remenber == null){
Cookie cookie = new Cookie("token","");
cookie.setMaxAge(0); //Cookie从服务器端发过来的时候就已经是一个已过时的Cookie
cookie.setPath(request.getContextPath());
response.addCookie(cookie);
}
User user1 = userService.login(username, password);
if(remenber != null){
//如果该用户没有token 添加token到数据库。否则更新token
String tokenUUID = UUID.randomUUID().toString();
user1.setToken(tokenUUID);
if (user1.getToken() == null){
int i = userService.updateToken(user1);
System.out.println(i+"账号或者密码不正确");
}
//添加刚才生成的token到网页Cookie中
Cookie cookie = new Cookie("token",tokenUUID);
cookie.setMaxAge(7*24*60*60); //设置7天的过期时间
cookie.setPath(request.getContextPath());//cookie在该路径下的网页起作用
//添加cookie 到响应头,真正回到浏览器的时候才会被添加到浏览器的cookie
response.addCookie(cookie);
}
if (user1==null){
model.addAttribute("login_massage","username or password is not true");
System.out.println("账号或者密码不正确");
return "forward:/user/toLogin";
}else{
//登录成功后将用户放入session中,用于拦截
request.getSession().setAttribute("login_user",user);
System.out.println("首次登录,查询数据库用户名和密码无误,登录成功,设置cookie成功");
return "user/welcome";
}
}
/**
* 退出登录
*/
@RequestMapping("/logout")
public void outUser(HttpServletRequest request, HttpServletResponse response) throws IOException {
request.getSession().removeAttribute("session_user");
Cookie[] cookies = request.getCookies();
for(Cookie cookie:cookies){
if("login_user".equals(cookie.getName())){
System.out.println("退出登录时,cookie还没过期,清空cookie");
cookie.setMaxAge(0);
cookie.setValue(null);
cookie.setPath(request.getContextPath());
response.addCookie(cookie);
break;
}
}
//重定向到登录页面
response.sendRedirect("/user/toLogin");
}
}
注:在拦截器中已对携带Cookie的登录过的用户进行拦截和处理,所以Controller中处理的是初次登录的用户和未注册的用户
-
Interceptor
-
HandlerInterceptor
package com.cxy.blog.intercepter; import com.cxy.blog.beans.User; import com.cxy.blog.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * @author cxy * @create 2020-11-11-2:49 PM * 对登录过,携带Cookie的用户进行拦截,在Controller中就是初次登录用户的处理 */ public class CookiendSessionInterceptor implements HandlerInterceptor { @Autowired private UserService userService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object loginUser = request.getSession().getAttribute("login_user"); if (loginUser != null){ System.out.println("session域中获取到user,放行"); return true; } System.out.println("进入拦截器"); Cookie[] cookies = request.getCookies(); if (cookies != null && cookies.length > 0) { for (Cookie cookie : cookies) { System.out.println("cookie 遍历" + cookie.getName()); if ("token".equals(cookie.getName())) { System.out.println("有名为token的cookie,并且cookie还没过期..."); //遍历cookie如果找到登录状态则返回true 继续执行原来请求url到controller中的方法 String token = cookie.getValue(); //是否存在对应的token对象 User user = userService.getToken(token); System.out.println(user); //存在表示cookie未失效 不拦截 否则拦截 if (user != null) { request.getSession().setAttribute("login_user", user); } return true; } } } request.setAttribute("msg", "没有权限,请先登录"); System.out.println("没有权限,请先登录"); response.sendRedirect("/user/toLogin"); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }
-
注册HandlerInterceptor:WebMvcConfigurer
```java
package com.cxy.blog.intercepter;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author cxy
* @create 2020-10-30-1:23 AM
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 注册拦截器,添加拦截路径和排除拦截路径
* addPathPatterns():添加需要拦截的路径
* excludePathPatterns():添加不需要拦截的路径
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new CookiendSessionInterceptor())
.addPathPatterns("/user/**")
.excludePathPatterns("/", "/**/toLogin","/**/toRegister")
.excludePathPatterns("/static/**");
}
}
```
-
Service
package com.cxy.blog.service;
import com.cxy.blog.beans.User;
import com.cxy.blog.dao.UserMapper;
import com.cxy.blog.utils.Sha256Util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author cxy
* @create 2020-10-29-5:09 PM
*/
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
//查询用户名是否存在
public User getUserByUsername(String username) {
return userMapper.getUserByUsername(username);
}
//登录
public User login(String username, String password) {
//SHA256加密
return userMapper.login(username, Sha256Util.getSHA256(password));
}
//读取数据库中的token
public User getToken(String token){
return userMapper.getToken(token);
}
//更新数据库中的token
public int updateToken(User user){
return userMapper.updateToken(user);
}
//注册
public int register(User user) {
//SHA256加密
user.setPassword(Sha256Util.getSHA256(user.getPassword()));
return userMapper.register(user);
}
}
-
Dao
package com.cxy.blog.dao;
import com.cxy.blog.beans.User;
import org.springframework.stereotype.Repository;
@Repository
public interface UserMapper {
User getUserByUsername(String username);
User login(String username,String password);
int register(User user);
User getToken(String token);
int updateToken(User user);
}
-
UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cxy.blog.dao.UserMapper">
<select id="getUserByUsername" resultType="User">
select id,username,password from t_user WHERE username = #{username}
</select>
<select id="login" resultType="User">
SELECT username,password FROM t_user WHERE username = #{username} and password = #{password}
</select>
<select id="getToken" resultType="User">
SELECT username,token FROM t_user WHERE token = #{token}
</select>
<select id="updateToken">
UPDATE t_user SET token = #{token},updated_time = CURRENT_TIMESTAMP WHERE username = #{username}
</select>
<insert id="register" parameterType="User" useGeneratedKeys="true" keyProperty="id">
INSERT INTO t_user(username, password,gender,email,updated_time) VALUES(#{username},#{password},#{gender},#{email},CURRENT_TIMESTAMP)
</insert>
</mapper>
-
前端界面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" type="text/css" th:href="@{/css/mystyle.css}"/>
<script type="text/javascript" th:src="@{/js/jquery-1.7.2.js}"></script>
<script type="text/javascript" th:src="@{https://cdn.jsdelivr.net/npm/vue/dist/vue.js}" charset="UTF-8"></script><!-- 开发环境版本,包含了有帮助的命令行警告 -->
<title>Login</title>
</head>
<body>
<div id="container" style="width: 100%;background-color: azure">
<div id="header" style="background-color88: azure;">
<ul>
<li><a class="active" href="#home">微微博</a></li>
<li><a href="#found">发现</a></li>
<li><a href="#about">关于</a></li>
<li text-align="right"><a href="#about me">cxy</a></li>
</ul>
</div>
</div>
<div th:id="login" style="background-color: honeydew">
<!-- 表单: 收集用户的信息,提交到后台服务器 -->
<br>
<h3 align="center">登录</h3>
<form th:id="login_form" action="#" th:action="@{/user/login}" method="post">
<p>用户名: <input type="text" name="username"><span th:text="${login_massage}"></span></p>
<p>密 码: <input type="password" name="password"></p>
<p><input type="submit" value="登录"></p>
<!--/*@thymesVar id="login_massage" type="java.lang.String"*/-->
<p><span th:if="${login_massage eq 'user has not registered'}" th:v-html="acount_uncreated"></span></p>
<p><input type="checkbox" name="remember" value="1"/>一周内免登陆</p>
</form>
<br>
</div>
<div id="footer" style="background-color: darksalmon;clear:both;text-align:center;">
版权 © cxy.com</div>
</div>
<script type="text/javascript">
new Vue({
el:'#login',
data: {
acount_uncreated: '<span style="color: gray">没有账号?</span><span><a href="/user/toRegister">创建账号</a></span>'
}
});
</script>
</body>
</html>