第一章 Vue.js 概要介绍
1.1 Vue.js 介绍
-
Vue 是什么
- 主流的渐进式 JavaScript 框架
-
什么是渐近式
- 可以和传统的网站开发架构融合在一起,例如可以简单的把它当作一个类似 JQuery 库来使用。
- 也可以使用Vue全家桶框架来开发大型的单页面应用程序 。
- 使用它的原因
- vue.js 体积小,编码简洁优雅,运行效率高,用户体验好. 无Dom操作,它能提高网站应用程序的开发效率
- 什么场景下使用它
- 一般是需要开发单页面应用程序 (Single Page Application, 简称:SPA) 的时候去用
- 单页面应用程序,如:网易云音乐 https://music.163.com/
- 因为 Vue 是 渐进式 的,Vue 其实可以融入到不同的项目中,即插即用
1.2 学习资源
英文官网:https://vuejs.org/
中文官网(中文文档很友好):https://cn.vuejs.org/
官方教程:https://cn.vuejs.org/v2/guide/
GitHub:https://github.com/yyx990803
API文档:https://cn.vuejs.org/v2/api/
不建议买书 ,官方文档很详细,多查官方文档,因为很多书基本上都是直接抄官方文档的
1.3 发展历史
- 作者:尤雨溪(微博:尤小右),一位华裔前 Google 工程师,江苏无锡人。
个人博客:http://www.evanyou.me/
新浪微博:http://weibo.com/arttechdesign
知乎:https://www.zhihu.com/people/evanyou/activities
- 2013年12月8号在 GitHub 上发布了 0.6 版
- 2015年10月份正式发布了 1.0 版本,开始真正的火起来
- 2016年10月份正式发布了 2.0 版
- 2019.4.8号发布了 Vue 2.5.10 版本 https://github.com/vuejs/vue/releases
- 1.x 版本老项目可能还在用,新项目绝对都是选择 2.x
1.4 对比其他前端 JS 框架
- Angular
2009 年诞生的,起源于个人开发,后来被 Google 收购了。
核心技术: 模板 和 数据绑定 技术,体验越来越差,走下坡路了。 - React
2013年5月开源的,起源于 Facebook 的内部项目,对市场上所有 JS 框架都不满意,于是自已写了一套。
核心技术: 组件化 和 虚拟DOM 技术。 - Vue.js
吸收了上面两个框架的技术优点。
使用情况:
BAT 级别的企业:React 最多 > Angular > Vue.js
中小型公司:Vue.js 更多一些,有中文文档学习成本低。
Vue 不支持 IE8 及以下版本,因为 Vue 使用了 IE8 无法模拟 ECMAScript 5 特性。
推荐使用最新谷歌浏览器。
第二章 Vue 核心技术
2.1 Vue 入门开发
2.1.1 创建工程
-
在本地创建文件夹 D:\StudentProject\WebStudy
-
打开 VS Code ,点击 File > Open Folder , 找到 D:\StudentProject\WebStudy 打开
-
单击 WEBSTUDY 右侧的新建目录图标,创建目录: vue-01-core
2.1.2 创建 HTML 和 安装 vue 模块
- 在 vue-01-core 目录下新建一个页面 01-helloworld.html
- 在 vue-01-core 目录下的命令行窗口,安装2.6.10版本的 vue 模块
npm install vue@2.6.10
2.1.3 编写HTML页面
编写步骤:
1. 采用 <script> 标签引入 vue.js 库
2. 定义一个 <div>
3. new Vue() 实例化Vue应用程序
el 选项: 元素element的缩写,指定被 Vue 管理的 Dom 节点入口(值为选择器 ),必须是一个普通的
HTML 标签节点,一般是 div。
data 选项:指定初始化数据,在 Vue 所管理的 Dom 节点下,可通过模板语法来进行使用
4. 标签体显示数据: {{xxxxx}}
5. 表单元素双向数据绑定: v-model
6. 注意: el 的值不能为 html 或 body
源码实现:
<body>
<div id="app">
<p>Hello, {{ msg }}</p>
<input type="text" v-model="msg">
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el: '#app', // el选项的值不能指定html或 body
data: {
msg: 'Vue.js'
} })
</script>
</body>
2.2 分析 MVVM 模型
常见面试题:什么是 MVVM 模型?
- MVVM 是 Model-View-ViewModel 的缩写,它是一种软件架构风格
Model:模型, 数据对象(data选项当中 的)
View:视图,模板页面(用于渲染数据)
ViewModel:视图模型,其实本质上就是 Vue 实例 - 它的哲学思想是:
通过数据驱动视图 - 把需要改变视图的数据初始化到 Vue中,然后再通过修改 Vue 中的数据,
从而实现对视图的更新。 - 声明式编程
按照 Vue 的特定语法进行声明开发,就可以实现对应功能,不需要我们直接操作Dom元素, **命令式编程:**Jquery它就是,需要手动去操作Dom才能实现对应功能。
2.3 Vue Devtools 插件安装
Vue Devtools 插件让我们在一个更友好的界面中审查和调试 Vue 项目。
-
谷歌浏览器访问: chrome://extensions ,然后右上角打开 开发者模式 , 打开的效果如下,
-
将 直接拖到上面页面空白处,会自动安装
-
效果如下则安装成功
-
当你访问Vue开发的页面时,按 F12 可 Vue 标签页
2.4 模板数据绑定渲染
可生成动态的HTML页面,页面中使用嵌入 Vue.js 语法可动态生成
- {{xxxx}} 双大括号文本绑定
- v-xxxx 以 v- 开头用于标签属性绑定,称为指令
在 vue-01-core 目录下新建一个页面: 02-模板数据绑定渲染.html
2.4.1 双大括号语法 {{}}
- 格式: {{表达式}}
- 作用:
使用在标签体中,用于获取数据
可以使用 JavaScript 表达式 - 案例源码:
<body>
<div id="app">
<h3>1、双大括号输出文本内容</h3>
<!--文本内容-->
<p>普通文本:{{ message }}</p>
<!--使用JS表达式-->
<p>JS表达式:{{ number + 1 }}</p>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el: "#app",
data: {
message: "haha",
number: 1
}
});
</script>
</body>
2.4.2 一次性插值 v-once
- 通过使用 v-once 指令,你也能执行一次性地插值,当数据改变时,插值处的内容不会更新。
<h3>2、一次性插值 v-once </h3>
<span v-once> 这个将不会改变: {{ message }}</span>
2.4.3 输出HTML指令 v-html
- 格式: v-html=‘xxxx’
- 作用:
如果是HTML格式数据,双大括号会将数据解释为普通文本,为了输出真正的 HTML,你需要使用 v- html 指令。
Vue 为了防止 XSS 攻击,在此指令上做了安全处理,当发现输出内容有 script 标签时,则不渲染
- XSS 攻击主要利用 JS 脚本注入到网页中,读取 Cookie 值(Cookie一般存储了登录身份信息),读取到了发送到黑客服务器,从而黑客可以使用你的帐户做非法操作。
- XSS 攻击还可以在你进入到支付时,跳转到钓鱼网站。
案例源码:
<body>
<div id="app">
<h3>3、v-html 指令输出真正的 HTML 内容</h3>
<p>双大括号:{{ contentHtml }}</p>
<!-- 指令的值不需要使用双大括号获取,直接写获取的属性名 -->
<!-- <p>v-html指令:<span v-html="{{contentHtml}}"></span></p>-->
<p>
v-html指令:
<span v-html="contentHtml"></span>
</p>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el: "#app",
data: {
message: "haha",
number: 1, //contentHtml: '<span style="color:red">红色字体内容</span>'
contentHtml:
'<span style="color:red">红色字体内容><script>alert("hello vue")<\/script></span>'
}
});
</script>
</body>
- 效果图
2.4.4 元素绑定指令 v-bind
- 完整格式: v-bind:元素的属性名=‘xxxx’
- 缩写格式: :元素的属性名=‘xxxx’
- 作用:将数据动态绑定到指定的元素上
案例源码:
<body>
<div id="app">
<h3>4、v-bind 属性绑定指令</h3>
<!-- 红色字体是正常的 -->
<img v-bind:src="imgUrl" alt="VueLogo" />
<!-- 缩写 -->
<img :src="imgUrl" alt="VueLogo" />
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el: "#app",
data: {
message: "haha",
number: 1,
contentHtml: '<span style="color:red">红色字体内容</span>',
imgUrl: "https://cn.vuejs.org/images/logo.png"
}
});
</script>
</body>
- 效果图
2.4.5 事件绑定指令 v-on
- 完整格式: v-on:事件名称=“事件处理函数名”
- 缩写格式: @事件名称=“事件处理函数名” 注意: @ 后面没有冒号
- 作用:用于监听 DOM 事件
案例源码: 每点击1次,数据就加1
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<h3>5、v-on 事件绑定指令</h3>
<input type="text" v-model="num">
<button v-on:click="add">点击+1</button>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el: '#app',
data: {
message: 'haha',
number: 1,
contentHtml: '<span style="color:red">红色字体内容</span>',
imgUrl: 'https://cn.vuejs.org/images/logo.png',
num: 2
},
methods: { //指定事件处理方法, 在模板页面中通过 v-on:事件名 来调用
add: function () { //key为方法名
console.log('add被调用') // this 表示当前 vm 实例
this.num++ //每点击1次num加1
}
}
})
</script>
</body>
</html>
- 效果图
2.4.6 完整源码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue-模板数据绑定渲染</title>
</head>
<body>
<div id="app">
<h3>1、双大括号输出文本内容</h3>
<!--文本内容-->
<p>普通文本:{{ message }}</p>
<!--使用JS表达式-->
<p>JS表达式:{{ number + 1 }}</p>
<h3>2、一次性插值 v-once </h3> <span v-once> 这个将不会改变: {{ message }}</span>
<h3>3、v-html 指令输出真正的 HTML 内容</h3>
<p>双大括号:{{ contentHtml }}</p> <!-- 指令的值不需要使用双大括号获取 -->
<!-- <p>v-html指令:<span v-html="{{contentHtml}}"></span></p> -->
<p>v-html指令:<span v-html="contentHtml"></span></p>
<h3>4、v-bind 属性绑定指令</h3> <!-- 直接写属性名是获取不到--> <img src="imgUrl" alt="VueLogo"> <!-- 红色字体是正常的 -->
<img v-bind:src="imgUrl" alt="VueLogo"> <!-- 缩写 --> <img :src="imgUrl" alt="VueLogo">
<h3>5、v-on 事件绑定指令</h3> <input type="text" v-model="num"> <button v-on:click="add">点击+1</button>
</div> <br>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el: '#app',
data: {
message: 'haha',
number: 1,
contentHtml: '<span style="color:red">红色字体内容</span>',
imgUrl: 'https://cn.vuejs.org/images/logo.png',
num: 2
},
methods: { //指定事件处理方法, 在模板页面中通过 v-on:事件名 来调用
add: function () { //key为方法名
console.log('add被调用')
vm.num++ //每点击1次num加1
}
}
})
</script>
</body>
</html>
2.5 计算属性和监听器
在 vue-01-core 目录下新建一个页面 03-计算属性和监听器.html
2.5.1 计算属性 computed
-
computed 选项定义计算属性
-
计算属性 类似于 methods 选项中定义的函数
- 计算属性 会进行缓存,只在相关响应式依赖发生改变时它们才会重新求值。
- 函数 每次都会执行函数体进行计算。
-
需求:输入数学与英语分数,采用 methods 与 computed 分别计算出总得分
案例源码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue-模板数据绑定渲染</title>
</head>
<body>
<div id="app">
数学:<input type="text" v-model="mathScore">
英语:<input type="text" v-model="englishScore">
总分(方法-单向):<input type="text" v-model="sumScore()">
总分(计算属性-单向):<input type="text" v-model="sumScore1">
<!-- 总分(方法-单向):<input type="text" v-bind:value="sumScore()"> 上面的v-model 就是vue的一个语法糖 -->
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el: '#app',
data: {
mathScore: 80,
englishScore: 90,
},
methods: { //不要少了s
sumScore : function () { //在控制台输入 vm.sumScore() 每次都会被调用
console.info('sumScore被调用') // `this` 指向当前 vm 实例, 减 0 是为了字符串转为数字运算
return (this.mathScore - 0) + (this.englishScore - 0)
}
},
computed: { //计算属性
sumScore1: function () { //在控制台输入vm.sumScore1 不会被重新调用,说明计算属性有缓存
console.info('sumScore1被调用')
return (this.mathScore - 0) + (this.englishScore - 0)
}
}
})
</script>
</body>
</html>
computed 选项内的计算属性默认是 getter 函数,所以上面只支持单向绑定,当修改数学和英语的数据才会
更新总分,而修改总分不会更新数据和英语
2.5.2 计算属性(双向绑定)
- 计算属性默认只有 getter ,不过在需要时你也可以提供一个 setter
案例源码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue-模板数据绑定渲染</title>
</head>
<body>
<div id="app">
数学:<input type="text" v-model="mathScore"><br>
英语:<input type="text" v-model="englishScore"><br>
总分(方法-单向):<input type="text" v-model="sumScore()"><br>
总分(计算属性-单向):<input type="text" v-model="sumScore1"><br>
总分(计算属性-双向):<input type="text" v-model="sumScore2"><br>
</div>
<script src="./node_modules/vue/dist/vue.js">
</script>
<script type="text/javascript">
var vm = new Vue({
el: '#app',
data: {
mathScore: 80,
englishScore: 90,
},
methods: { //不要少了s
sumScore: function () {
//在控制台输入 vm.sumScore() 每次都会被调用
console.info('sumScore被调用')
// `this` 指向当前 vm 实例, 减 0 是为了字符串转为数字运算
return (this.mathScore - 0) + (this.englishScore - 0)
}
},
computed: { //计算属性 默认 getter 只支持单向绑定
sumScore1: function () {
//在控制台输入vm.sumScore1 不会被重新调用,说明计算属性有缓存
console.info('sumScore1被调用')
return (this.mathScore - 0) + (this.englishScore - 0)
}, //指定 getter/setter 双向绑定
sumScore2: {
get: function () {
console.info('sumScore2被调用')
return (this.mathScore - 0) + (this.englishScore - 0)
},
set: function (newValue) { //value为更新后的值
// 被调用则更新了sumScore2,然后将数学和英语更新为平均分
var avgScore = newValue / 2
this.mathScore = avgScore
this.englishScore = avgScore
}
}
}
})
</script>
</body>
</html>
2.5.3 监听器 watch
- 当属性数据发生变化时,对应属性的回调函数会自动调用, 在函数内部进行计算
- 通过 watch 选项 或者 vm 实例的 $watch() 来监听指定的属性
- 需求:
- 通过 watch 选项 监听数学分数, 当数学更新后回调函数中重新计算总分sumScore3
- 通过 vm.$watch() 选项 监听英语分数, 当英语更新后回调函数中重新计算总分sumScore3
源码实现:
注意: 在data 选择中添加一个 sumScore3 属性
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue-模板数据绑定渲染</title>
</head>
<body>
<div id="app"> 数学:<input type="text" v-model="mathScore"><br> 英语:<input type="text" v-model="englishScore"><br>
总分(方法-单向):<input type="text" v-model="sumScore()"><br> 总分(计算属性-单向):<input type="text" v-model="sumScore1"><br>
总分(计算属性-双向):<input type="text" v-model="sumScore2"><br> 总分(监听器):<input type="text" v-model="sumScore3"><br>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el: '#app',
data: {
mathScore: 80,
englishScore: 90,
sumScore3: 170
},
methods: { //不要少了s
sumScore: function () { //在控制台输入 vm.sumScore() 每次都会被调用
console.log('sumScore被调用') // `this` 指向当前 vm 实例, 减 0 是为了字符串转为数字运算
return (this.mathScore - 0) + (this.englishScore - 0)
}
}, // 计算属性
computed: { // 默认 getter 只支持单向绑定
sumScore1: function () { //在控制台输入 vm.sumScore1 不会被重新调用 ,说明计算属性有缓存
console.log('sumScore1被调用')
return (this.mathScore - 0) + (this.englishScore - 0)
},
//指定 getter/setter 双向绑定
sumScore2: {
get: function () {
console.log('sumScore2被调用')
return (this.mathScore - 0) + (this.englishScore - 0)
},
set: function (
newValue) { //value为更新后的值 // 被调用则更新了sumScore2,然后将数学和英语更新为平均分
var avgScore = newValue / 2
this.mathScore = avgScorethis.englishScore = avgScore
}
}
}, //监听器方式1:watch选项
watch: { //当数学修改后,更新总分sumScore3
mathScore: function (newValue, oldValue) {
//newValue 就是新输入的数学得分
this.sumScore3 = (newValue - 0) + (this.englishScore - 0)
}
}
})
//监听器方式2:通过vm对象调用 //第1个参数为监听的属性名,第2个回调函数
vm.$watch('englishScore', function (newValue) { //newValue 就是新输入的英语得分
this.sumScore3 = (newValue - 0) + (this.mathScore - 0)
})
</script>
</body>
</html>
2.6 Class 与 Style 绑定 v-bind
通过 class 列表和 style 指定样式是数据绑定的一个常见需求。它们都是元素的属性,都用 v-bind 处理,其中表达
式结果的类型可以是:字符串、对象或数组。
2.6.1 语法格式
- v-bind:class=‘表达式’ 或 :class=‘表达式’
- class 的表达式可以为:
- 字符串 :class=“activeClass”
- 对象 :class="{active: isActive, error: hasError}"
- 数组 :class="[‘active’, ‘error’]" 注意要加上单引号,不然是获取data中的值
v-bind:style=‘表达式’ 或 :style=‘表达式’`
- style 的表达式一般为对象
:style="{color: activeColor, fontSize: fontSize + ‘px’}"
注意:对象中的value值 activeColor 和 fontSize 是data中的属性
2.6.2 案例源码
在 vue-01-core 目录下新建一个页面 04-Class与Style绑定.html
- 效果图
源码实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue-模板数据绑定渲染</title>
</head>
<body>
<!-- 第2步:定义样式 -->
<style>
.active {
color: green;
}
.delete {
background: red;
}
.error {
font-size: 30px;
}
</style>
<div id="app">
<h2>Class绑定,v-bind:class 或 :class</h2>
<!--activeClass会从data中获取值为active,则对应样式为绿色-->
<p v-bind:class="activeClass">字符串达式</p> <!-- isDelete为 true,渲染delete样式;当 hasError为false,不取 error 样式;-->
<p :class="{delete: isDelete, error: hasError}">对象表达式</p>
<!--- 渲染 'active', 'error' 样式,注意要加上单引号,不然是获取data中的值 -->
<p :class="['active', 'error']">数组表达式</p>
<h2>Style绑定, v-bind:style 或 :class</h2>
<p :style="{color: activeColor, fontSize: fontSize + 'px'}">Style绑定</p>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">
new Vue({
el: '#app',
data: {
activeClass: 'active',
isDelete: true,
hasError: false, //演示 Style 绑定
activeColor: 'red',
fontSize: 20
}
})
</script>
</body>
</html>
2.7 条件渲染 v-if
2.7.1 条件指令
- v-if 是否渲染当前元素
- v-else
- v-else-if
- v-show 与 v-if 类似,只是元素始终会被渲染并保留在 DOM 中,只是简单切换元素的 CSS 属性 display 来显示或隐藏
2.7.2 案例源码
在 vue-01-core 目录下新建一个页面 05-条件渲染.html
效果图:
源码实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue-模板数据绑定渲染</title>
</head>
<body>
<style>
.box {
width: 200px;
height: 200px;
background: red;
}
</style>
<div id="app">
<h2>v-if 条件渲染</h2> <input v-model="seen" type="checkbox">勾选后显示红色小块
<!-- v-if 为 true则显示渲染当前元素, -->
<div v-if="seen" class="box"></div>
<p v-else="seen">红块已隐藏</p>
<h2>v-show 条件渲染</h2>
<!-- v-show 的元素始终会被渲染并保留在 DOM 中,只是简单切换元素的 CSS 属性 display 显示隐藏,而不是重新加载div-->
<div v-show="seen" class="box"></div>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el: '#app',
data: {
seen: false
}
})
</script>
</body>
</html>
2.7.3 v-if 与 v-show 比较
- 什么时候元素被渲染
v-if 如果在初始条件为假,则什么也不做,每当条件为真时,都会重新渲染条件元素
v-show 不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换 - 使用场景选择
v-if 有更高的切换开销,
v-show 有更高的初始渲染开销。
因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行后条件很少改变,则使用 v-if 较好。
2.8 列表渲染 v-for
2.8.1 列表渲染指令
- v-for 迭代数组
语法: v-for="(alias, index) in array"
说明: alias : 数组元素迭代的别名; index : 数组索引值从0开始(可选) 。
举例:
<div v-for="item in items" :key="item.id"></div>
<div v-for="(item, index) in items" :key="item.id"></div>
items
是源数据数组,item
是数组元素迭代的别名。 注意:使用key
特殊属性, 它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素
- v-for 迭代对象的属性
- 语法: v-for="(value, key, index) in Object"
- 说明: value : 每个对象的属性值; key : 属性名(可选); index : 索引值(可选) 。
举例:
<div v-for="value in object" ></div>
<div v-for="(value, key) in object"></div>
<div v-for="(value, key, index) in object"></div>
注意: 在遍历对象时,是按 Object.keys() 的结果遍历,但不能保证它的结果在不同的 JavaScript 引擎下
是顺序一致的。
3. 可用 of 替代 in 作为分隔符
2.8.2 案例源码
在 vue-01-core 目录下新建一个页面 06-列表渲染.html
效果图
源码实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue-模板数据绑定渲染</title>
</head>
<body>
<div id="app">
<h2>1. 迭代数组</h2>
<ul>
<!-- e 为当前对象别名,index 数组下标0开始-->
<li v-for="(e, index) in emps" :key="index"> 编号:{{index+1}},姓名:{{e.name}},工资:{{e.salary}} </li>
</ul> <br>
<h2>2. 迭代对象</h2>
<ul>
<!-- value是属性值,key是属性名,index索引值-->
<li v-for="(value, key, index) in emps[0]"> 第{{index+1}}个属性为:{{key}} = {{value}} </li>
</ul>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el: '#app',
data: {
emps: [
//数组
{
name: '马云',
salary: '20000'
}, {
name: '马化腾',
salary: '18000'
}, {
name: '刘强东',
salary: '13000'
}
]
}
})
</script>
</body>
</html>
2.9 事件处理 v-on
在 vue-01-core 目录下新建一个页面 07-事件处理.html
2.9.1 事件处理方法
- 完整格式: v-on:事件名=“函数名” 或 v-on:事件名=“函数名(参数……)”
- 缩写格式: @事件名=“函数名” 或 @事件名=“函数名(参数……)” 注意: @ 后面没有冒号
- event :函数中的默认形参,代表原生 DOM 事件
当调用的函数,有多个参数传入时,需要使用原生DOM事件,则通过 $event 作为实参传入 - 作用:用于监听 DOM 事件
案例源码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue-模板数据绑定渲染</title>
</head>
<body>
<div id="app">
<h2>1. 事件处理方法</h2>
<button @click="say">Say {{msg}}</button>
<button @click="warn('hello', $event)">Warn</button>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el: '#app',
data: {
msg: 'Hello, Vue.js'
},
methods: {
say: function (event) {
// `this` 在方法里指向当前 Vue 实例
alert(this.msg)
// `event` 是原生 DOM 事件
alert(event.target.innerHTML)
},
//多个参数如果需要使用原生事件,将 $event 作为实参传入
warn: function (msg, event) {
alert(msg + "," + event.target.tagName)
}
}
})
</script>
</body>
</html>
2.9.2 事件修饰符
- stop 阻止单击事件继续传播 event.stopPropagation()
- prevent 阻止事件默认行为 event.preventDefault()
- once 点击事件将只会触发一次
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue-模板数据绑定渲染</title>
</head>
<body>
<div id="app">
<h2>1. 事件处理方法</h2>
<button @click="say">Say {{msg}}</button>
<button @click="warn('hello', $event)">Warn</button> <br>
<h2>2. 事件修饰符</h2>
<!--单击事件继续传播-->
<div @click="todo">
<!--点击后会调用doThis再调用todo-->
<button @click="doThis">单击事件会继续传播</button>
</div> <br />
<!-- 阻止单击事件继续传播,-->
<div @click="todo">
<!--点击后只调用doThis-->
<button @click.stop="doThis">阻止单击事件会继续传播</button>
</div>
<!-- 阻止事件默认行为 -->
<a href="http://www.mengxuegu.com" @click.prevent="doStop">梦学谷官网</a>
<!-- 点击事件将只会触发一次 -->
<button @click.once="doOnly">点击事件将只会触发一次: {{num}}</button>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el: '#app',
data: {
msg: 'Hello, Vue.js',
num: 1
},
methods: {
say: function (
event) { // `this` 在方法里指向当前 Vue 实例 alert(this.msg) // `event` 是原生 DOM 事件
alert(event.target.innerHTML)
}, //多个参数如果需要使用原生事件,将 $event 作为实参传入
warn: function (msg, event) {
alert(msg + "," + event.target.tagName)
},
todo: function () {
alert("todo....");
},
doThis: function () {
alert("doThis....");
},
doStop: function () {
alert("href默认跳转被阻止....")
},
doOnly: function () {
this.num++
}
}
})
</script>
</body>
</html>
2.9.3 按键修饰符
- 格式: v-on:keyup.按键名 或 @keyup.按键名
- 常用按键名:
- enter
- tab
- delete (捕获“删除”和“退格”键)
- esc
- space
- up
- down
- left
- right
源码实现:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue-模板数据绑定渲染</title>
</head>
<body>
<div id="app">
<h2>1. 事件处理方法</h2>
<button @click="say">Say {{msg}}</button>
<button @click="warn('hello', $event)">Warn</button> <br>
<h2>2. 事件修饰符</h2>
<!--单击事件继续传播-->
<div @click="todo">
<!--点击后会调用doThis再调用todo-->
<button @click="doThis">单击事件会继续传播</button>
</div> <br />
<!-- 阻止单击事件继续传播,-->
<div @click="todo">
<!--点击后只调用doThis-->
<button @click.stop="doThis">阻止单击事件会继续传播</button>
</div>
<!-- 阻止事件默认行为 -->
<a href="http://www.mengxuegu.com" @click.prevent="doStop">梦学谷官网</a>
<!-- 点击事件将只会触发一次 -->
<button @click.once="doOnly">点击事件将只会触发一次: {{num}}</button>
<h2>3. 按键修饰符</h2>
<input @keyup.enter="keyEnter">
<!--进入输入框按回车时调用keyEnter-->
<input @keyup.space="keySpace">
<!--进入输入框按回车时调用keySpace-->
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el: '#app',
data: {
msg: 'Hello, Vue.js',
num: 1
},
methods: {
say: function (
event) {
// `this` 在方法里指向当前 Vue 实例
alert(this.msg)
// `event` 是原生 DOM 事件
alert(event.target.innerHTML)
},
//多个参数如果需要使用原生事件,将 $event 作为实参传入
warn: function (msg, event) {
alert(msg + "," + event.target.tagName)
},
todo: function () {
alert("todo....");
},
doThis: function () {
alert("doThis....");
},
doStop: function () {
alert("href默认跳转被阻止....")
},
doOnly: function () {
this.num++
},
keyEnter: function () {
alert("已按:回车键")
},
keySpace: function () {
alert("已按:空格键")
}
}
})
</script>
</body>
</html>
2.10 表单数据双向绑定v-model
单向绑定:数据变,视图变;视图变(浏览器控制台上更新html),数据不变;上面的都是单向绑定
双向绑定:数据变,视图变;视图变(在输入框更新),数据变;
2.10.1 基础用法
v-model 指令用于表单数据双向绑定,针对以下类型
- text 文本
- textarea 多行文本
- radio 单选按钮
- checkbox 复选框
- select 下拉框
2.10.2 案例源码
在 vue-01-core 目录下新建一个页面 08-表单数据双向绑定.html
模板页面
<body>
<div id="demo">
<form action="#">
姓名(文本):<input type="text">
<br><br>
性别(单选按钮):
<input name="sex" type="radio" value="1" />男
<input name="sex" type="radio" value="0" />女
<br><br>
技能(多选框):
<input type="checkbox" name="skills" value="java">Java开发
<input type="checkbox" name="skills" value="vue">Vue.js开发
<input type="checkbox" name="skills" value="python">Python开发
<br><br>
城市(下拉框):
<select name="citys">
<option value="bj">北京</option>
</select>
<br><br>
说明(多行文本):
<textarea cols="30" rows="5"></textarea>
<br><br>
<button type="submit">提交</button>
</form>
</div>
</body>
Vue.js源码实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vue-模板数据绑定渲染</title>
</head>
<body>
<div id="demo">
<!-- @submit.prevent 阻止事件默认提交行为 -->
<form action="#" @submit.prevent="submitForm">
姓名(文本):<input type="text" v-model="name">
<br><br>
性别(单选按钮):
<input name="sex" type="radio" value="1" v-model="sex" />男
<input name="sex" type="radio" value="0" v-model="sex" />女
<br><br>
技能(多选框):
<input type="checkbox" name="skills" value="java" v-model="skills">Java开发
<input type="checkbox" name="skills" value="vue" v-model="skills">Vue.js开发
<input type="checkbox" name="skills" value="python" v-model="skills">Python开发
<br><br>
城市(下拉框):
<select name="citys" v-model="city">
<option v-for="c in citys" :value="c.code"> {{c.name}} </option>
</select>
<br><br>
说明(多行文本):
<textarea cols="30" rows="5" v-model="desc"></textarea>
<br><br>
<button type="submit">提交</button>
</form>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">
var vm = new Vue({
el: '#demo',
data: {
name: '',
sex: '0', //默认选中:女 skills: ['vue'], //默认勾选:vue.js开发
citys: [
//初始化下拉框
{
code: 'bj',
name: '北京'
}, {
code: 'sh',
name: '上海'
}, {
code: 'sz',
name: '深圳'
}
],
city: 'sh', //默认选中:上海,
desc: ''
},
methods: {
submitForm: function () {
//发送ajax请求
alert(this.name + "," + this.sex + "," + this.skills + "," + this.city + "," + this
.desc)
}
}
})
</script>
</body>
</html>
第三章 Vue 过渡&动画和自定义指令
3.1 过渡&动画效果
3.1.1 什么是过渡&动画
元素在显示和隐藏时,实现过滤或者动画的效果。常用的过渡和动画都是使用 CSS 来实现的
- 在 CSS 中操作 trasition (过滤 )或 animation (动画)达到不同效果
- 为目标元素添加一个父元素 , 让父元素通过自动应用 class 类名来达到效果
- 过渡与动画时,会为对应元素动态添加的相关 class 类名:
- xxx-enter :定义显示前的效果。
- xxx-enter-active :定义显示过程的效果。
- xxx-enter-to : 定义显示后的效果。
- xxx-leave : 定义隐藏前的效果。
- xxx-leave-active :定义隐藏过程的效果。
- xxx-leave-to :定义隐藏后的效果。
3.1.2 过滤效果案例
- 为目标元素添加父元素
- 定义 class 过渡样式
指定过渡样式: transition
指定隐藏时的样式: opacity(持续的时间)/其它 - 功能实现
点击按钮后, 显示隐藏文本
效果1:显示和隐藏有渐变效果
效果2:显示和隐藏的有平移效果,并持续时长不同
- 在 vue-02-过渡&动画和指令 目录下创建 01-过渡效果.html
- 进入 vue-02-过渡&动画和指令 目录 ,执行 npm install vue@2.6.10 命令安装 vue模块
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>过渡效果</title>
<style>
/*显示或隐藏的过渡效果*/
.mxg-enter-active,
.mxg-leave-active {
transition: opacity 1s;
/*过渡,渐变效果 1秒*/
}
/*显示前或隐藏后的效果*/
.mxg-enter,
.mxg-leave-to {
opacity: 0
/*都是隐藏效果*/
}
/* 可以设置不同的进入和离开动画 */
/*显示过渡效果*/
.meng-enter-active {
transition: all 1s;
/*all 所有效果,持续1秒 */
}
/*隐藏过渡效果*/
.meng-leave-active {
transition: all 5s;
}
/*显示前或隐藏后的效果*/
.meng-enter,
.meng-leave-to {
opacity: 0;
/*都是隐藏效果*/
transform: translateX(10px);
/*水平方向 X 坐标移动10px*/
}
</style>
</head>
<body>
<div id="app1">
<button @click="show = !show">渐变过渡</button>
<!--在目标元素上添加此元素,结合name值来指定样式-->
<transition name="mxg">
<p v-if="show">mengxuegu</p>
</transition>
</div> <br>
<!--可以设置不同的进入和离开动画 -->
<div id="app2">
<button @click="show = !show">渐变平滑过渡</button>
<!--在目标元素上添加此元素,结合name值来指定样式-->
<transition name="meng">
<p v-if="show">mengxuegu</p>
</transition>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">
var vm1 = new Vue({
el: '#app1',
data: {
show: true
}
})
var vm2 = new Vue({
el: '#app2',
data: {
show: true
}
})
</script>
</body>
</html>
3.1.3 动画效果案例
- CSS 动画用法同 CSS 过渡,只不过采用 animation 为指定动画效果
- 功能实现:
- 点击按钮后, 文本内容有放大缩小效果
- 在 vue-02-过渡&动画和指令 目录下创建 02-动画效果.html
注意:官网上面源码有问题,要在 <p> 元素上增加样式 style="display: inline-block"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>动画效果</title>
<style>
/*显示过程中的动画效果*/
.bounce-enter-active {
animation: bounce-in 1s;
/*bounce-in引用了下面@keyframes中定义的持续3秒*/
}
/*隐藏过程中的动画效果*/
.bounce-leave-active {
animation: bounce-in 3s reverse;
/*reverse 相反的顺序*/
}
@keyframes bounce-in {
0% {
/*持续时长的百分比,如持续1s,0%表示当0秒,50%表示当0.5秒,100%表示当1秒*/
transform: scale(0);
/*缩小为0*/
}
50% {
transform: scale(1.5);
/*放大1.5倍*/
}
100% {
transform: scale(1);
/*原始大小*/
}
}
</style>
</head>
<body>
<div id="example-2">
<button @click="show = !show">放大缩小动画</button><br>
<transition name="bounce">
<p v-if="show" style="display: inline-block"> 陪你学习,伴你成长 </p>
</transition>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">
new Vue({
el: '#example-2',
data: {
show: true
}
})
</script>
</body>
</html>
3.2 Vue 内置指令总结
参考:https://cn.vuejs.org/v2/api/#指令
- v-html 内容按普通 HTML 插入,可防止 XSS 攻击
- v-show 根据表达式的真假值,切换元素的 display CSS 属性来显示隐藏元素
- v-if 根据表达式的真假值,来渲染元素
- v-else 前面必须有 v-if 或 v-else-if
- v-else-if 前面必须有 v-if 或 v-else-if
- v-for 遍历的数组或对象
- v-on 绑定事件监听器
- v-bind 用于绑定元素属性
- v-model 在表单控件或者组件上创建双向绑定
- v-once 一次性插值,当后面数据更新后视图数据不会更新
- v-pre 可以用来显示原始插入值标签 {{}} 。并跳过这个元素和它的子元素的编译过程。加快编译。
**例如:**网页中的一篇文章,文章内容不需要被 Vue 管理渲染,则可以在此元素上添加 v-pre 忽略文章编译提
高性能。
在 vue-02-过渡&动画和指令 目录下创建页面: 03-Vue内置指令.html
<span v-pre>{{ this will not be compiled }}</span>
浏览页面显示内容:并没有识别{{}}
{{ this will not be compiled }}
- v-text
- 等价于 {{}} 用于显示内容,但区别在于:
- {{}} 会造成闪烁问题, v-text 不会闪烁
如果还想用 {{}} 又不想有闪烁问题,则使用 v-cloak 来处理
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>指令</title>
<style>
/*将带有 v-clock 属性的标签隐藏*/
[v-cloak] {
display: none;
}
</style>
</head>
<body>
<!-- 在被 Vue 管理的模板入口节点上作用 v-cloak 指令-->
<div id="app" v-cloak>
<!-- 用QQ浏览器刷新页面时, {{}} 会有明显闪烁现象,
原因: 是浏览器从上往下依次解析, 会先把 {{ message }} 当作标签体直接先渲染,
然后 Vue 再进行解析 {{ message }} 变成了 message 的值: 'hello mengxuegu' -->
<h3>{{ message }}</h3>
<h3>{{ message }}</h3>
<h3 v-text="message"></h3>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">
new Vue({
el: '#app',
data: {
message: 'hello mengxuegu'
}
})
</script>
</body>
</html>
- v-cloak
如果想用 {{}} 又不想有闪烁问题,则使用 v-cloak 来处理,步骤如下:- 在被 Vue 管理的模板入口节点上作用 v-cloak 指令
- 添加一个属性选择器 [v-cloak] 的CSS 隐藏样式: [v-cloak] {display: none;}
原理:默认一开始被 Vue 管理的模板是隐藏着的,当 Vue 解析处理完 DOM 模板之后,会自动把这个样式去除,然后就显示出来。
3.3 自定义指令
3.3.1 自定义指令的作用
除了内置指令外,Vue 也允许注册自定义指令。有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候使用自定义指令更为方便。
自定义指令文档: https://cn.vuejs.org/v2/guide/custom-directive.html
3.3.2 注册与使用自定义指令方式
- 注册全局指令:
// 指令名不要带 v-
Vue.directive('指令名', {
// el 代表使用了此指令的那个 DOM 元素
// binding 可获取使用了此指令的绑定值 等
inserted: function (el, binding) {
// 逻辑代码
}
})
- 注册局部指令
directives : {
'指令名' : { // 指令名不要带 v-
inserted (el, binding) {
// 逻辑代码
}
}
}
注意:注册时,指令名不要带 v-
- 使用指令:
引用指令时,指令名前面加上 v-
直接在元素上在使用即可 : v-指令名=‘表达式’
3.3.3 案例演示
需求:
- 实现输出文本内容全部自动转为大写,字体为红色 ( 功能类型于 v-text , 但显示内容为大写)
- 当页面加载时,该元素将获得焦点 (注意: autofocus 在移动版 Safari 上不工作)
- 实现:在 vue-02-过渡&动画和指令 目录下创建页面: 04-自定义指令.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>自定义指令</title>
</head>
<body>
<div id="app">
<p v-upper-text="message"></p>
自动获取焦点:<input type="text" v-focus>
</div>
<script src="../js/vue.js" type="text/javascript"></script>
<script type="text/javascript">
// 1. 注册一个全局 v-upper-text 指令,注意指令名不要带 v-
Vue.directive('upper-text', {
// 因为是样式,所以不需要元素插入到DOM中,就好像link引入CSS文件时并不关心元素是否加载
bind: function (el) {
el.style.color = 'red'
},
// el 代表使用了此指令的那个 DOM 元素
// binding 可获取使用了引指令的绑定值 等
inserted: function (el, binding) {
// 将在 v-upper-text 指令中获取到的值,变成大写输出到标签体中
el.innerHTML = binding.value.toUpperCase()
}
})
new Vue({
el: '#app',
data: {
message: 'mengxuegu,陪你学习伴你梦想'
},
//2. 注册一个局部指令 v-focus
directives: {
'focus': {
//和js行为有关的操作,最好在inserted中执行,和样式相关的操作都可在bind中执行
inserted: function (el) { // 聚焦元素
el.focus()
}
}
}
})
</script>
</body>
</html>
第四章 经典实战项目-TodoMVC
4.1 项目介绍与演示
- TodoMVC
是一个非常经典的案例,功能非常丰富,并且针对多种不同技术分别都开发了此项目,比如React、AngularJS、JQuery等等。 - TodoMVC 案例官网:http://todomvc.com/
- 在官网首页右下角, 有 案例的模板下载 和 开发规范(需求文档),如下图:
4.2 需求说明
4.2.1 数据列表渲染
- 当任务列表(items )没有数据时, #main 和 #footer 标识的标签应该被隐藏 任务涉及字段 : id 、任务名称(name)、是否完成( completed true 为已完成)
4.2.2 添加任务
- 在最上面的文本框中添加新的任务。
- 不允许添加非空数据。
- 按 Enter 键添加任务列表中,并清空文本框。
- 当加载页面后文本框自动获得焦点,在 input 上使用 autofocus 属性可获得。
4.2.3 显示所有未完成任务数
- 左下角要显示未完成的任务数量。确保数字是由 标签包装的。
- 还要将 item 单词多元化( 1 没有 s , 其他数字均有 s ): 0 items , 1 item , 2 items
示例: 2 items left
4.2.4 切换所有任务状态
- 点击复选框 V 后,将所有任务状态标记为复选框相同的状态。
- 当 选中/取消 了单个任务时,复选框 V 也应同步更新。
4.2.5 移除任务项
- 悬停在某个任务项上显示 X 移除按钮,可点击移除当前任务项。
4.2.6 清除所有已完成任务
- 单击右下角 Clear completed 按钮时,移除所有已完成任务。
- 单击 Clear completed 按钮后,确保复选框清除了选中状态。
- 当列表中没有已完成的任务时,应该隐藏 Clear completed 按钮。
4.2.7 编辑任务项
- 双击
<label>
(某个任务项)进入编辑状态(在<li>
上通过 .editing 进行切换状态)。 - 进入编辑状态后输入框显示原内容,并获取编辑焦点。
- 输入状态按 Esc 取消编辑, editing 样式应该被移除。
- 按 Enter 键 或 失去焦点时 保存改变数据,移除 editing 样式;
4.2.8 路由状态切换(过滤不同状态数据)
根据点击的不同状态( All / Active / Completed ),进行过滤出对应的任务,并进行样式的切换。
4.2.9 数据持久化
将所有任务项数据持久化到 localStorage 中,它主要是用于本地存储数据。
4.3 下载与导入模板
在 Github 下载 TodoMVC 模板在:https://github.com/tastejs/todomvc-app-template
4.3.1 TodoMVC下载方式
- 使用 git 克隆项目
git clone https://github.com/tastejs/todomvc-app-template.git
- 直接下载 zip 压缩包
4.3.2 nmp 安装依赖
因为下载的模板没有样式 ,所以要通过 nmp 安装相关依赖,依赖配置在 package.json 文件中
- 通过 cmd 进入到 TodoMVC项目所在目录,执行命令: npm install
注意:要使用 npm 命令,需要安装 node.js 环境才可用。 - 依赖安装完成后,生成的node_modules 目录效果如下:
4.3.3 导入到 VS Code
- 将 todomvc-app-template 目录重命名为: vue-03-todomvc
- 拷贝 vue-03-todomvc 拷贝到 D:\StudentProject\WebStudy 目录下
- 在 VS Code 左侧资源管理窗口刷新一下就可以看到 vue-03-todomvc ,直接访问 index.html,效果如下:
4.4 初始化项目
4.4.1 下载Vue依赖
- 下载 vue.js 后,会在 node_modules 目录下出现 vue
npm install vue@2.6.10
4.4.2 引入 vue.js
- 引入 vue.js 到 index.html 中
注意:vue.js 的引入要在使用 Vue 操作的前面,我们在 app.js 中编写Vue代码,所以要在 app.js 前面引入
<!-- 在 app.js 前面引入-->
<script src="./node_modules/vue/dist/vue.js" type="text/javascript"></script>
<script src="js/app.js"></script>
4.4.3 找到被 Vue 管理的元素
- 找到需要被 Vue 管理的元素
,在第13行取一个id属性值为 todoapp
<section class="todoapp" id="todoapp">
- 在 app.js 中添加如下 Vue 入口
(function (Vue) { //表示依赖了全局的 Vue
var app = new Vue({
el: '#todoapp'
})
})(Vue);
4.5 数据列表渲染实战
4.5.1 功能分析
-
有数据
列表中的记录有3种状态且
- 样式不一样:
未完成(没有样式)、已完成( .completed )、编辑中( .editing )
任务字段 : id (主键) 、 content (内容)、 completed (状态 ; true 已完 成, false 未完成 ) -
无数据
main 和 .footer 标识的标签应该被隐藏 ( v-show )
4.5.2 有数据列表功能实现
- 在 app.js 声明一个存储任务数据的数组 items,并初始化一些数据。
(function (Vue) { //表示依赖了全局的 Vue, 其实不加也可以,只是更加明确点 //初始化任务
const items = [
{
id: 1,
content: 'vue.js',
completed: false //是否完成
},
{ id: 2,
content: 'java',
completed: true
},
{ id: 3,
content: 'pyhton',
completed: false
}
]
var app = new Vue({
el: '#todoapp',
data: {
items // ES6中对象属性简写,等价于items: items
}
})
})(Vue);
- 修改 list.html 列表从第25行开始
<li class="completed">
如果修改 app.js 或 html 后,刷新浏览器发现没有变,关闭当前浏览器窗口,重新打开访问。
<ul class="todo-list">
<!-- 三种状态:未完成(没有样式)、已完成(.completed )、编辑中( .editing ) -->
<!--修改:1、v-for迭代; 2、:class={key为class样式名,value为获取的数据true或false}-->
<li v-for="(item, index) in items" :key="item.id" :class="{completed: item.completed}">
<div class="view">
<!-- 修改:1、v-model 绑定状态值是否选中 -->
<input class="toggle" type="checkbox" v-model="item.completed">
<!-- 修改:1、{{ content }} 显示内容 -->
<label>{{ item.content }}</label>
<!--修改: 1、:value 绑定id删除 -->
<button class="destroy" :value="item.id"></button>
</div>
<input class="edit" value="Create a TodoMVC template">
</li>
</ul>
- 效果图
4.5.3 无数据隐藏功能实现
只要判断 items 数组 length 等于0,则表示没有数据,结合 v-show 即可
- 修改 index.html 中
<section class="main">
和<footer class="footer">
方式1(todomvc官网方法):在两个标签上处理,要添加2次麻烦
<!-- items.length 值为 0 就是 false -->
<section class="main" v-show="items.length">
....
</section>
<footer class="footer" v-show="items.length">
....
</footer>
方式2:使用 div 元素将下面包住,但是页面渲染后多出 div 元素
<div v-show="items.length">
<section class="main">
....
</section> <footer class="footer">
...
</footer>
</div>
方式3:可使用 Vue 提供的 template 元素,页面渲染后不会有 template 元素, 但是不能使用 v-show, 因为template渲染后就消失了, 而v-show是 display:none控制显示隐藏的。 需要使用 v-if 才可以
<template v-if="items.length">
<section class="main">
....
</section> <footer class="footer">
....
</footer> </template>
- 在 app.js 中注释 items 数组中的元素后,重新访问即可演示效果
4.5.4 完整源码
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Template • TodoMVC</title>
<link rel="stylesheet" href="node_modules/todomvc-common/base.css">
<link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
<!-- CSS overrides - remove if you don't need it -->
<link rel="stylesheet" href="css/app.css">
</head>
<body>
<section class="todoapp" id="todoapp">
<header class="header">
<h1>todos</h1>
<!--添加任务-->
<input class="new-todo" placeholder="What needs to be done?" autofocus>
</header>
<!-- This section should be hidden by default and shown when there are todos -->
<!--修改: items.length 值为 0 就是 false -->
<template v-if="items.length">
<section class="main">
<input id="toggle-all" class="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<!-- These are here just to show the structure of the list items -->
<!-- List items should get the class `editing` when editing and `completed` when marked as
completed -->
<!-- 三种状态:未完成(没有样式)、已完成(.completed )、编辑中( .editing ) -->
<!--修改:1、v-for迭代; 2、:class={key为class样式名,value为获取的数据true或false}-->
<li v-for="(item, index) in items" :class="{completed: item.completed}">
<div class="view">
<!-- 修改:1、v-model 绑定状态值是否选中 -->
<input class="toggle" type="checkbox" v-model="item.completed">
<!-- 修改:1、{{ content }} 显示内容 -->
<label>{{ item.content }}</label>
<!--修改: 1、:value 绑定id删除 -->
<button class="destroy" :value="item.id"></button>
</div>
<input class="edit" value="Create a TodoMVC template">
</li>
</ul>
</section>
<!-- This footer should hidden by default and shown when there are todos -->
<!--修改: items.length 值为 0 就是 false -->
<footer class="footer">
<!-- This should be `0 items left` by default -->
<span class="todo-count"><strong>0</strong> item left</span>
<!-- Remove this if you don't implement routing -->
<ul class="filters">
<li><a class="selected" href="#/">All</a> </li>
<li><a href="#/active">Active</a> </li>
<li><a href="#/completed">Completed</a> </li>
</ul>
<!-- Hidden if no completed items are left ↓ -->
<button class="clear-completed">Clear completed</button>
</footer>
</template>
</section>
<footer class="info">
<p>Double-click to edit a todo</p> <!-- Remove the below line ↓ -->
<p>Template by <a href="http://sindresorhus.com">Sindre Sorhus</a></p>
<!-- Change this out with your name and url ↓ -->
<p>Created by <a href="http://todomvc.com">you</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer> <!-- Scripts here. Don't remove ↓ -->
<script src="node_modules/todomvc-common/base.js"></script> <!-- 在 app.js 前面-->
<script src="node_modules/vue/dist/vue.js" type="text/javascript"></script>
<script src="js/app.js"></script>
</body>
</html>
4.6 添加任务实战
4.6.1 功能分析
- 在最上面的文本框中添加新的任务。
- 不允许添加非空数据。
分析:在添加到列表前,使用 .trim() 去除空格,如果去除后是空的就不添加。 - 按 Enter 键添加任务列表中,并清空文本框。
分析:在页面添加 Enter 按键监听事件,最后向文本框赋空值 ‘’
4.6.2 添加任务功能实现
- 在 hello.html 中的 文本输入框,添加 Enter 按键监听事件@keyup.enter=“addItem”
<header class="header">
<h1>todos</h1>
<!--添加任务, keyup.enter 回车键监听-->
<input @keyup.enter="addItem"
class="new-todo" placeholder="What needs to be done?" autofocus> </header>
- 在 app.js 中添加 addItem 函数,步骤如下:
1、获取文本框输入的数据
2、判断数据如果为空,则什么都不做
3、如果不为空,则添加到数组中
生成id值
添加到数组中 (默认状态为 未完成)
4、清空文本框内容
var app = new Vue({
el: '#todoapp',
data: {
items // 对象属性简写,等价于items: items
},
methods: {
addItem(event) { //对象属性函数简写,等价于addItem: function () {
console.log('addItem', event.target.value) //1. 获取文本框输入的数据
const content = event.target.value
.trim() //2. 判断数据如果为空,则什么都不做
if (!content.length) {
return
}
//3.如果不为空,则添加到数组中 // 生成id值
const id = this.items.length + 1
this.items.push({
id, //等价于 id:id
content,
completed: false
})
//4. 清空文本框内容
event.target.value = ''
}
}
})
4.6.3 完整源码
app.js
(function (Vue) { //表示依赖了全局的 Vue, 其实不加也可以,只是更加明确点 //初始化任务
const items = [
// {
// id: 1,
// content: 'vue.js',
// completed: false //是否完成
// }, {
// id: 2,
// content: 'java',
// completed: true
// }, {
// id: 3,
// content: 'pyhton',
// completed: false
// }
]
var app = new Vue({
el: '#todoapp',
data: {
items // 对象属性简写,等价于items: items
},
methods: {
addItem(event) { //对象属性函数简写,等价于addItem: function () {
console.log('addItem', event.target.value)
//1. 获取文本框输入的数据
const content = event.target.value.trim()
//2. 判断数据如果为空,则什么都不做
if (!content.length) {
return
}
//3.如果不为空,则添加到数组中
// 生成id值
const id = this.items.length + 1
// 添加到数组中
this.items.push({
id, //等价于 id:id
content,
completed: false
})
//4. 清空文本框内容
event.target.value = ''
}
}
})
})(Vue);
4.7 显示所有未完成任务数实战
4.7.1 功能分析
- 左下角要显示未完成的任务数量。数字是由 标签包装的。
分析:
当 items 数组中的元素有改变,则重新计算未完成任务数量,可通过计算属性
来获取 未完成 的任务数量
通过数组函数 filter 过滤未完成任务,然后进行汇总 - 还要将 item 单词多元化( 1 没有 s , 其他数字均有 s ): 0 items , 1 item , 2 items
分析:
当任务数据为 1 不显示 s ,否则显示
4.7.2 功能实现
- 在 list.html 页面添加剩余任务数
计算属性
{{ remaining }}
<span class="todo-count"><strong>{{ remaining }}</strong> item left</span>
- 在 app.js 的Vue实例的 methods 上一行添加一个 computed 选项,其中定义一个计算属性 remaining
注意 computed 选项的大括号最后不要少了逗号 ,
(function (Vue) { //表示依赖了全局的 Vue, 其实不加也可以,只是更加明确点
//初始化任务
const items = []
var app = new Vue({
el: '#todoapp',
data: {
items // 对象属性简写,等价于items: items
},
// 定义计算属性选项
computed: { // 过滤出所有未完成的任务项
remaining() {
/*return this.items.filter(function (item) { return !item.completed }).length*/
//ES6 箭头函数
return this.items.filter(item => !item.completed).length
}
},
// **注意** 后面不要少了逗号 ,
methods: {
addItem(event) { //对象属性函数简写,等价于addItem: function () {
console.log('addItem', event.target.value)
//1. 获取文本框输入的数据
const content = event.target.value.trim()
//2. 判断数据如果为空,则什么都不做
if (!content.length) {
return
}
//3.如果不为空,则添加到数组中
// 生成id值
const id = this.items.length + 1
// 添加到数组中
this.items.push({
id, //等价于 id:id
content,
completed: false
})
//4. 清空文本框内容
event.target.value = ''
}
}
})
})(Vue);
- 将 list.html 显示的 item 单词多元化( 1 没有 s , 其他数字均有 s )
添加 {{ remaining === 1 ? ‘’ : ‘s’ }}
<span class="todo-count">
<strong>{{ remaining }}</strong> item{{ remaining === 1 ? '' : 's' }} left
</span>
- 效果图
4.8 切换所有任务状态实战
4.8.1 功能分析
- 点击复选框 V 后,将所有任务状态标记为复选框相同的状态。
分析:
复选框状态发生变化后,就迭代出每一个任务项, 再将复选框状态值赋给每个任务项即可 。
为复选框绑定一个计算属性
,通过这个计算属性的 set 方法监听复选框更新后就更新任务项状态。 - 当 选中/取消 某个任务后,复选框 V 也应同步更新状态。
分析:
当所有未完成任务数( remaining )为 0 时,表示所有任务都完成了,就可以选中复选框。
在复选框绑定的 计算属性 的 get 方法中判断所有 remaining 是否为 0 ,从而绑定了 remaining ,
当 remaining 发生变化后, 会自动更新复选框状态(为 0 复选框会自动选中,反之不选中)。
综合上述:计算属性默认情况只有 getter 方法,而当前要用到 setter 方法,所以采用 计算属性 双向绑定。
4.8.2 切换所有任务状态功能实现
- 在 index.html 中的 id=“toggle-all” 复选框,添加计算属性 toggleAll 双向绑定
<section class="main">
<!-- 添加计算属性 toggleAll 双向绑定-->
<input v-model="toggleAll" id="toggle-all" class="toggle-all" type="checkbox">
- 在 app.js 中的步骤如下:
在 computed 选项中,添加计算属性 toggleAll 进行双向绑定 ( getter/setter )
// 定义计算属性选项
computed: {
// 过滤出所有未完成的任务项
remaining() {
/*
return this.items.filter(function (item) {
return !item.completed }).length
*/
//ES6 箭头函数
return this.items.filter(item => !item.completed).length
},
// 定义计算属性选项
//复选框计算属性(双向绑定)
toggleAll: {
get() { //等价于 get : functon () {...}
console.log(this.remaining)
//2. 当 this.remaining 发生变化后,会触发该方法运行
// 当所有未完成任务数为 0 , 表示全部完成, 则返回 true 让复选框选中 //反之就 false 不选中
return this.remaining === 0
},
set(newStatus) {
// console.log(newStatus)
//1. 当点击 checkbox 复选框后状态变化后,就会触发该方法运行,
// 迭代出数组每个元素,把当前状态值赋给每个元素的 completed
// 下面箭头函数等价于:this.items.forEach(function (item) {
this.items.forEach((item) => {
item.completed = newStatus
})
}
}
}, // **注意** 后面不要少了逗号 ,
4.8.3 完整源码
- index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Template • TodoMVC</title>
<link rel="stylesheet" href="node_modules/todomvc-common/base.css">
<link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
<!-- CSS overrides - remove if you don't need it -->
<link rel="stylesheet" href="css/app.css">
</head>
<body>
<section class="todoapp" id="todoapp">
<header class="header">
<h1>todos</h1>
<!--添加任务-->
<!--添加任务, keyup.enter 回车键监听-->
<input @keyup.enter="addItem" class="new-todo" placeholder="What needs to be done?" autofocus>
</header>
<!-- This section should be hidden by default and shown when there are todos -->
<!--修改: items.length 值为 0 就是 false -->
<template v-if="items.length">
<section class="main">
<!-- 添加计算属性 toggleAll 双向绑定-->
<input v-model="toggleAll" id="toggle-all" class="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<!-- These are here just to show the structure of the list items -->
<!-- List items should get the class `editing` when editing and `completed` when marked as
completed -->
<!-- 三种状态:未完成(没有样式)、已完成(.completed )、编辑中( .editing ) -->
<!--修改:1、v-for迭代; 2、:class={key为class样式名,value为获取的数据true或false}-->
<li v-for="(item, index) in items" :class="{completed: item.completed}">
<div class="view">
<!-- 修改:1、v-model 绑定状态值是否选中 -->
<input class="toggle" type="checkbox" v-model="item.completed">
<!-- 修改:1、{{ content }} 显示内容 -->
<label>{{ item.content }}</label>
<!--修改: 1、:value 绑定id删除 -->
<button class="destroy" :value="item.id"></button>
</div>
<input class="edit" value="Create a TodoMVC template">
</li>
</ul>
</section>
<!-- This footer should hidden by default and shown when there are todos -->
<!--修改: items.length 值为 0 就是 false -->
<footer class="footer">
<!-- This should be `0 items left` by default -->
<span class="todo-count">
<strong>{{ remaining }}</strong> item{{ remaining === 1 ? '' : 's' }} left
</span>
<!-- Remove this if you don't implement routing -->
<ul class="filters">
<li><a class="selected" href="#/">All</a> </li>
<li><a href="#/active">Active</a> </li>
<li><a href="#/completed">Completed</a> </li>
</ul> <!-- Hidden if no completed items are left ↓ -->
<button class="clear-completed">Clearcompleted</button>
</footer>
</template>
</section>
<footer class="info">
<p>Double-click to edit a todo</p> <!-- Remove the below line ↓ -->
<p>Template by <a href="http://sindresorhus.com">Sindre Sorhus</a></p>
<!-- Change this out with your name and url ↓ -->
<p>Created by <a href="http://todomvc.com">you</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer> <!-- Scripts here. Don't remove ↓ -->
<script src="node_modules/todomvc-common/base.js"></script> <!-- 在 app.js 前面-->
<script src="node_modules/vue/dist/vue.js" type="text/javascript"></script>
<script src="js/app.js"></script>
</body>
</html>
- app.js
// (function (window) {
// 'use strict';
// // Your starting point. Enjoy the ride!
// })(window);
(function (Vue) { //表示依赖了全局的 Vue, 其实不加也可以,只是更加明确点
//初始化任务
const items = []
var app = new Vue({
el: '#todoapp',
data: {
items // 对象属性简写,等价于items: items
},
methods: {
addItem(event) { //对象属性函数简写,等价于addItem: function () {
console.log('addItem', event.target.value)
//1. 获取文本框输入的数据
const content = event.target.value.trim()
//2. 判断数据如果为空,则什么都不做
if (!content.length) {
return
}
//3.如果不为空,则添加到数组中 // 生成id值
const id = this.items.length + 1
this.items.push({
id, //等价于 id:id
content,
completed: false
})
//4. 清空文本框内容
event.target.value = ''
}
},
// 定义计算属性选项
computed: {
// 过滤出所有未完成的任务项
remaining() {
/*
return this.items.filter(function (item) {
return !item.completed }).length
*/
//ES6 箭头函数
return this.items.filter(item => !item.completed).length
},
// 定义计算属性选项
//复选框计算属性(双向绑定)
toggleAll: {
get() { //等价于 get : functon () {...}
console.log(this.remaining)
//2. 当 this.remaining 发生变化后,会触发该方法运行
// 当所有未完成任务数为 0 , 表示全部完成, 则返回 true 让复选框选中 //反之就 false 不选中
return this.remaining === 0
},
set(newStatus) {
// console.log(newStatus)
//1. 当点击 checkbox 复选框后状态变化后,就会触发该方法运行,
// 迭代出数组每个元素,把当前状态值赋给每个元素的 completed
// 下面箭头函数等价于:this.items.forEach(function (item) {
this.items.forEach((item) => {
item.completed = newStatus
})
}
}
}, // **注意** 后面不要少了逗号 ,
})
})(Vue);
4.9 移除任务项
4.9.1 功能分析
- 悬停在某个任务项上显示 X 移除按钮,可点击移除当前任务项。
分析:
X 移除按钮处添加点击事件
通过数组函数 splice 移除任务项
4.9.2 移除任务项功能实现
- index.html 添加点击事件: @click="removeItem(index) "
<button class="destroy" :value="item.id" @click="removeItem(index)"></button>
- app.js 添加函数 removeItem , 通过 this.items.splice(index, 1) 移除
methods: {
// 移除任务项
removeItem(index) {
// 移除索引为index的一条记录
this.items.splice(index, 1)
},
addItem(event) { //对象属性函数简写,等价于addItem: function () {
console.log('addItem', event.target.value)
//1. 获取文本框输入的数据
const content = event.target.value.trim()
//2. 判断数据如果为空,则什么都不做
if (!content.length) {
return
}
//3.如果不为空,则添加到数组中 // 生成id值
const id = this.items.length + 1
this.items.push({
id, //等价于 id:id
content,
completed: false
})
//4. 清空文本框内容
event.target.value = ''
}
},
4.10 清除所有已完成任务
4.10.1 功能分析
- 单击右下角 Clear completed 按钮时,移除所有已完成任务。
分析: - 页面增加点击事件: @click=“removeCompleted”
- 在 Vue 中添加 removeCompleted 函数:
通过数组的 filter 函数过滤出所有未完成的任务项,将过滤出来的未完成数据赋值给 items 数组,
已完成的任务就被删除。 - 当列表中没有已完成的任务时,应该隐藏 Clear completed 按钮。
分析: - 在 Clear completed 按钮上使用 v-show ,当总任务数 ( items.length ) > 未完成数 (
remaining ) ,说明列表中还有已完成数据,则是显示按钮;反之不显示。 - 实现: v-show=“items.length > remaining”
4.10.2 清除所有已完成任务功能实现
- index.html 添加点击事件: @click=“removeCompleted”
<button @click="removeCompleted" class="clear-completed">Clear completed</button>
- app.js 添加函数 removeCompleted , 通过数组的 filter 函数过滤出所有未完成的任务项,将过滤出来的未完成数据赋值给 items 数组
methods: {
// 移除所有未完成任务项
removeCompleted() {
// 过滤出所有未完成的任务,重新赋值数组即可
this.items = this.items.filter(item => !item.completed)
},
// 移除任务项
removeItem(index) {
// 移除索引为index的一条记录
this.items.splice(index, 1)
},
addItem(event) { //对象属性函数简写,等价于addItem: function () {
console.log('addItem', event.target.value)
//1. 获取文本框输入的数据
const content = event.target.value.trim()
//2. 判断数据如果为空,则什么都不做
if (!content.length) {
return
}
//3.如果不为空,则添加到数组中 // 生成id值
const id = this.items.length + 1
this.items.push({
id, //等价于 id:id
content,
completed: false
})
//4. 清空文本框内容
event.target.value = ''
}
}
- 在 index.html 使用 v-show=“items.length > remaining” 进行切换显示/隐藏 Clear completed 按钮
<button class="clear-completed" @click="removeCompleted" v-show="items.length > remaining">Clear completed</button>
4.11 编辑任务项
4.11.1 功能分析
- 双击
-
为 <label> 绑定双击事件 @dblclick=toEdit(item)
- 当 item (任务项) === currentItem (当前点击的任务项,data中新定义的属性) 时,在
<li>
上就显
示 .editing 样式,格式 :class={editing: item === currentItem}
-
- 进入编辑状态后输入框显示原内容,并会自动获取编辑焦点。
分析:- 在
<input>
单向绑定输入框的值即可 :value=“item.content” - 通过自定义指令获取编辑焦点
- 在
- 输入状态按 Esc 取消编辑, editing 样式应该被移除。
分析为<input>
绑定 Esc 按键事件 @keyup.esc=cancelEdit ,将 currentItem 值变为 null - 按 Enter 键 或 失去焦点时 保存改变数据,移除 editing 样式;
分析:
- 添加事件 @keyup.enter=finishEdit(item, $event) 与 @blur=“finishEdit(item, $event)”
- 通过 $event 变量获取当前输入框的值,使用 .trim() 去空格后进行判断是否为空,如果为空则清除这条任务,否则修改任务项;.
- 添加数据保存任务项中
- 将 currentItem 值变为 null ,移除 editing 样式
4.11.2 进入编辑状态
4.11.2.1 修改 index.html
- 为
<label>
绑定双击事件 @dblclick=“toEdit(item)” , 将迭代出来的每个 item 传入当前行的 toEditItem 函数中 - 在
<li>
上判断是否显示 .editing 样式 :class={editing: item === currentItem}
<!--修改:1、v-for迭代; 2、:class={key为class样式名,value为获取的数据true或false}-->
<li v-for="(item, index) in items"
:class="{completed: item.completed,editing: item === currentItem}}">
<div class="view">
<!-- 修改:1、v-model 绑定状态值是否选中 -->
<input class="toggle" type="checkbox" v-model="item.completed">
<!-- 修改:1、{{ content }} 显示内容 -->
<label @dblclick="toEdit(item)">{{ item.content }}</label>
<!--修改: 1、:value 绑定id删除 -->
<button class="destroy" :value="item.id" @click="removeItem(index)"></button>
</div>
<input class="edit" value="Create a TodoMVC template">
</li>
4.11.2.2 修改 app.js
- 在 data 选项中添加属性 currentItem
- 在 methods 选项中添加函数 toEdit(item) 接收到点击的那个 item 后,将它赋值给 currentItem ,那对应的任务项就会进入编辑状态 this.currentItem = item
var app = new Vue({
el: '#todoapp',
data: {
items, // 对象属性简写,等价于items: items
currentItem: null //上面不要少了逗号, 接收当前点击的任务项
},
methods: {
// 进入编辑状态,当前点击的任务项item赋值currentItem,用于页面判断显示 .editing
toEdit(item) {
this.currentItem = item
},
....
4.11.3 编辑窗口显示原内容
在<input>
上显示当前点击的任务内容,单向绑定输入框的值即可: :value=“item.content”
<input class="edit" :value="item.content">
4.11.4 取消编辑
为 <input>
绑定 Esc 按键事件 @keyup.esc=cancelEdit ,将 currentItem 值变为 null , 就 :class={editing: item ===currentItem} 不成立了,样式就没有了
- index.html
<input class="edit" :value="item.content" @keyup.esc="cancelEdit" />
- app.js
methods: {
//取消编辑
cancelEdit() {
// 移除样式
this.currentItem = null
},
// 进入编辑状态,当前点击的任务项item赋值currentItem,用于页面判断显示 .editing
toEdit(item) {
this.currentItem = item
},
// 移除所有未完成任务项
removeCompleted() {
// 过滤出所有未完成的任务,重新赋值数组即可
this.items = this.items.filter(item => !item.completed)
},
// 移除任务项
removeItem(index) {
// 移除索引为index的一条记录
this.items.splice(index, 1)
},
addItem(event) { //对象属性函数简写,等价于addItem: function () {
console.log('addItem', event.target.value)
//1. 获取文本框输入的数据
const content = event.target.value.trim()
//2. 判断数据如果为空,则什么都不做
if (!content.length) {
return
}
//3.如果不为空,则添加到数组中 // 生成id值
const id = this.items.length + 1
this.items.push({
id, //等价于 id:id
content,
completed: false
})
//4. 清空文本框内容
event.target.value = ''
}
}
4.11.5 保存数据
-
按 Enter 键 或 失去焦点时 保存改变数据,移除 editing 样式;
分析:
- 添加事件 @keyup.enter=finishEdit(item, $event) 与 @blur=“finishEdit(item, $event)”
- 通过 $event 变量获取当前输入框的值,使用 .trim() 去空格后进行判断是否为空,如果为空则清除 这条任务,否则修改任务项;
- 添加数据保存任务项中
- 将 currentItem 值变为 null ,移除 editing 样式
index.html
<input class="edit" :value="item.content" @keyup.esc="cancelEdit"
@keyup.enter="finishEdit(item, index, $event)" @blur="finishEdit(item, index, $event)" >
app.js
methods: {
//编辑完成
finishEdit(item, index, event) {
const content = event.target.value.trim();
// 1. 如果为空, 则进行删除任务项
if (!event.target.value.trim()) {
//重用 removeItem 函数进行删除
this.removeItem(index)
return
}
// 2. 添加数据到任务项中
item.content = content
// 3. 移除 .editing 样式
this.currentItem = null
},
//取消编辑
cancelEdit() {
// 移除样式
this.currentItem = null
},
............
注意:有时候你可能按 Esc 、 Enter 、或者 失去焦点 时,发现没有被触发事件,是因为编辑框没有获取焦点,只有当编辑框有焦点时才可以触发事件。
4.11.6 获取焦点(自定义指令)
- 刷新页面后, 通过自定义全局指令 v-app-focus ,内容输入框自动获取焦点
- 当进入编辑状态后,通过自定义局部指令 v-todo-focus ,编辑窗口自动获取焦点
(function (Vue) { //表示依赖了全局的 Vue, 其实不加也可以,只是更加明确点
//初始化任务
const items = []
//自定义全局指令,用于 增加输入框
//定义时不要在前面加v-, 引用指令时要加上v-
Vue.directive('app-focus', {
//聚集元素
inserted(el, binding) {
el.focus()
}
})
var app = new Vue({
el: '#todoapp',
data: {
items, // 对象属性简写,等价于items: items
currentItem: null //上面不要少了逗号, 接收当前点击的任务项
},
//自定义局部指令,用于编辑输入框
directives: {
//定义时不要在前面加v-, 引用指令时要加上v-
'todo-focus': {
update(el, binding) { // 每当指令的值更新后,会调用此函数
if (binding.value) {
el.focus()
}
}
}
},
methods: {
//编辑完成
finishEdit(item, index, event) {
const content = event.target.value.trim();
..............
- 在 index.html 中 引入指令
- 增加输入框引用全局指令 v-app-focus
- 编辑框中引用局部指令 v-todo-focus=“item === currentItem”
<!--添加任务-->
<!--添加任务, keyup.enter 回车键监听-->
<input @keyup.enter="addItem" class="new-todo" placeholder="What needs to be done?" v-app-focus>
........
<!-- 编辑任务 -->
<input class="edit" :value="item.content" @keyup.esc="cancelEdit"
@keyup.enter="finishEdit(item, index, $event)" @blur="finishEdit(item, index, $event)" v-todo-focus="item === currentItem" >
4.11.7 完整源码
4.11.6.1 index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Template • TodoMVC</title>
<link rel="stylesheet" href="node_modules/todomvc-common/base.css">
<link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
<!-- CSS overrides - remove if you don't need it -->
<link rel="stylesheet" href="css/app.css">
</head>
<body>
<section class="todoapp" id="todoapp">
<header class="header">
<h1>todos</h1>
<!--添加任务-->
<!--添加任务, keyup.enter 回车键监听-->
<input @keyup.enter="addItem" class="new-todo" placeholder="What needs to be done?" v-app-focus>
</header>
<!-- This section should be hidden by default and shown when there are todos -->
<!--修改: items.length 值为 0 就是 false -->
<template v-if="items.length">
<section class="main">
<!-- 添加计算属性 toggleAll 双向绑定-->
<input v-model="toggleAll" id="toggle-all" class="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<!-- These are here just to show the structure of the list items -->
<!-- List items should get the class `editing` when editing and `completed` when marked as
completed -->
<!-- 三种状态:未完成(没有样式)、已完成(.completed )、编辑中( .editing ) -->
<!--修改:1、v-for迭代; 2、:class={key为class样式名,value为获取的数据true或false}-->
<li v-for="(item, index) in items"
:class="{completed: item.completed,editing: item === currentItem}">
<div class="view">
<!-- 修改:1、v-model 绑定状态值是否选中 -->
<input class="toggle" type="checkbox" v-model="item.completed">
<!-- 修改:1、{{ content }} 显示内容 -->
<label @dblclick="toEdit(item)">{{ item.content }}</label>
<!--修改: 1、:value 绑定id删除 -->
<button class="destroy" :value="item.id" @click="removeItem(index)"></button>
</div>
<!-- 编辑任务 -->
<input class="edit" :value="item.content" @keyup.esc="cancelEdit"
@keyup.enter="finishEdit(item, index, $event)" @blur="finishEdit(item, index, $event)" v-todo-focus="item === currentItem" >
</li>
</ul>
</section>
<!--修改: items.length 值为 0 就是 false -->
<footer class="footer">
<!-- This should be `0 items left` by default -->
<span class="todo-count">
<strong>{{ remaining }}</strong> item{{ remaining === 1 ? '' : 's' }} left
</span>
<!-- Remove this if you don't implement routing -->
<ul class="filters">
<li><a class="selected" href="#/">All</a> </li>
<li><a href="#/active">Active</a> </li>
<li><a href="#/completed">Completed</a> </li>
</ul> <!-- Hidden if no completed items are left ↓ -->
<button class="clear-completed" @click="removeCompleted" v-show="items.length > remaining">Clear completed</button>
</footer>
</template>
</section>
<footer class="info">
<p>Double-click to edit a todo</p> <!-- Remove the below line ↓ -->
<p>Template by <a href="http://sindresorhus.com">Sindre Sorhus</a></p>
<!-- Change this out with your name and url ↓ -->
<p>Created by <a href="http://todomvc.com">you</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer> <!-- Scripts here. Don't remove ↓ -->
<script src="node_modules/todomvc-common/base.js"></script> <!-- 在 app.js 前面-->
<script src="node_modules/vue/dist/vue.js" type="text/javascript"></script>
<script src="js/app.js"></script>
</body>
</html>
4.11.6.2 app.js
// (function (window) {
// 'use strict';
// // Your starting point. Enjoy the ride!
// })(window);
(function (Vue) { //表示依赖了全局的 Vue, 其实不加也可以,只是更加明确点
//初始化任务
const items = []
//自定义全局指令,用于 增加输入框
//定义时不要在前面加v-, 引用指令时要加上v-
Vue.directive('app-focus', {
//聚集元素
inserted(el, binding) {
el.focus()
}
})
var app = new Vue({
el: '#todoapp',
data: {
items, // 对象属性简写,等价于items: items
currentItem: null //上面不要少了逗号, 接收当前点击的任务项
},
//自定义局部指令,用于编辑输入框
directives: {
//定义时不要在前面加v-, 引用指令时要加上v-
'todo-focus': {
update(el, binding) { // 每当指令的值更新后,会调用此函数
if (binding.value) {
el.focus()
}
}
}
},
methods: {
//编辑完成
finishEdit(item, index, event) {
const content = event.target.value.trim();
// 1. 如果为空, 则进行删除任务项
if (!event.target.value.trim()) {
//重用 removeItem 函数进行删除
this.removeItem(index)
return
}
// 2. 添加数据到任务项中
item.content = content
// 3. 移除 .editing 样式
this.currentItem = null
},
//取消编辑
cancelEdit() {
// 移除样式
this.currentItem = null
},
// 进入编辑状态,当前点击的任务项item赋值currentItem,用于页面判断显示 .editing
toEdit(item) {
this.currentItem = item
},
// 移除所有未完成任务项
removeCompleted() {
// 过滤出所有未完成的任务,重新赋值数组即可
this.items = this.items.filter(item => !item.completed)
},
// 移除任务项
removeItem(index) {
// 移除索引为index的一条记录
this.items.splice(index, 1)
},
addItem(event) { //对象属性函数简写,等价于addItem: function () {
console.log('addItem', event.target.value)
//1. 获取文本框输入的数据
const content = event.target.value.trim()
//2. 判断数据如果为空,则什么都不做
if (!content.length) {
return
}
//3.如果不为空,则添加到数组中 // 生成id值
const id = this.items.length + 1
this.items.push({
id, //等价于 id:id
content,
completed: false
})
//4. 清空文本框内容
event.target.value = ''
}
},
// 定义计算属性选项
computed: {
// 过滤出所有未完成的任务项
remaining() {
/*
return this.items.filter(function (item) {
return !item.completed }).length
*/
//ES6 箭头函数
return this.items.filter(item => !item.completed).length
},
// 定义计算属性选项
//复选框计算属性(双向绑定)
toggleAll: {
get() { //等价于 get : functon () {...}
console.log(this.remaining)
//2. 当 this.remaining 发生变化后,会触发该方法运行
// 当所有未完成任务数为 0 , 表示全部完成, 则返回 true 让复选框选中 //反之就 false 不选中
return this.remaining === 0
},
set(newStatus) {
// console.log(newStatus)
//1. 当点击 checkbox 复选框后状态变化后,就会触发该方法运行,
// 迭代出数组每个元素,把当前状态值赋给每个元素的 completed
// 下面箭头函数等价于:this.items.forEach(function (item) {
this.items.forEach((item) => {
item.completed = newStatus
})
}
}
}, // **注意** 后面不要少了逗号 ,
})
})(Vue);
4.12 路由状态切换(过滤不同状态数据)
4.12.1 功能分析
- 根据点击的不同状态( All / Active / Completed ),进行过滤出对应的任务,并进行样式的切换。
- 分析 :
-
在 data 中定义变量 filterStatus , 用于接收变化的状态值
-
通过 window.onhashchange 获取点击的路由 hash (# 开头的),来获取对应的那个状态值,并将状态值赋值给 filterStatus
-
定义一个计算属性 filterItems 用于过滤出目标数据, 用于感知 filterStatus 的状态值变化,当变化后,通过 switch-case + filter 过滤出目标数据。
-
在 html 页面中,将 v-for 中之前的 items 数组替换为 filterItems 迭代出目标数据。
-
将被点击状态的 </a> 样式切换为 .select ,通过判断状态值实现,如: filterStatus === 'all'
4.12.2 功能实现
- 在 app.js 中 Vue 实例的 data 中定义变量 filterStatus , 用于接收变化的状态值
声明一个变量 app 接收 Vue 实例对象,页面要使用到这个 app 变量
var app = new Vue({
el: '#todoapp',
data: {
items, // 对象属性简写,等价于items: items
currentItem: null, //上面不要少了逗号, 接收当前点击的任务项
filterStatus: 'all' // 上面不要少了逗号,接收变化的状态值
},
- 在 app.js 通过 window.onhashchange 获取点击的路由 hash (# 开头的),来获取对应的那个状态值
注意:不是在 Vue 实例中定义,是在它同级结构下添加以下代码:
//当路由 hash 值改变后会自动调用此函数
window.onhashchange = function () {
console.log('hash改变了', window.location.hash)
// 1.获取点击的路由 hash , 当截取的 hash 不为空返回截取的,为空时返回 'all'
const hash = window.location.hash.substr(2) || 'all'
console.log('hash', hash)
// 2. 状态一旦改变,将 hash 赋值给 filterStatus
// 当计算属性 filterItems 感知到 filterStatus 变化后,就会重新过滤
// 当 filterItems 重新过滤出目标数据后,则自动同步更新到视图中
app.filterStatus = hash
}
// 第一次访问页面时,调用一次让状态生效
window.onhashchange()
- 定义一个计算属性 filterItems 用于过滤出目标数据, 用于感知 filterStatus 的状态值变化,当变化后,通过switch-case + filter 过滤出目标数据。
// 定义计算属性选项
computed: {
// 过滤出不同状态数据
filterItems() { //this.filterStatus 作为条件,变化后过滤不同数据
switch (this.filterStatus) {
case "active": // 过滤出未完成的数据
return this.items.filter(item => !item.completed)
break
case "completed": // 过滤出已完成的数据
return this.items.filter(item => item.completed)
break
default: // 其他,返回所有数据
return this.items
}
},
- 在index.html页面中,将 v-for 中之前的 items 数组替换为 filterItems 迭代出目标数据。
<!--将之前 items 替换为 filterItems --> <li v-for="(item, index) in filterItems"
:class="{completed: item.completed, editing: item === currentItem}"> 。。。省略 </li>
- 在index.html 中, 将被点击状态的
</a>
样式切换为 .select ,通过判断状态值实现
<ul class="filters">
<li><a :class="{selected: filterStatus === 'all'}" href="#/" >All</a> </li>
<li><a :class="{selected: filterStatus === 'active'}" href="#/active">Active</a></li>
<li><a :class="{selected: filterStatus === 'completed'}" href="#/completed">Completed</a></li>
</ul> <!-- Hidden if no completed items are left ↓ -->
4.12.3 完整源码
4.12.2.1 index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Template • TodoMVC</title>
<link rel="stylesheet" href="node_modules/todomvc-common/base.css">
<link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
<!-- CSS overrides - remove if you don't need it -->
<link rel="stylesheet" href="css/app.css">
</head>
<body>
<section class="todoapp" id="todoapp">
<header class="header">
<h1>todos</h1>
<!--添加任务-->
<!--添加任务, keyup.enter 回车键监听-->
<input @keyup.enter="addItem" class="new-todo" placeholder="What needs to be done?" v-app-focus>
</header>
<!-- This section should be hidden by default and shown when there are todos -->
<!--修改: items.length 值为 0 就是 false -->
<template v-if="items.length">
<section class="main">
<!-- 添加计算属性 toggleAll 双向绑定-->
<input v-model="toggleAll" id="toggle-all" class="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<!-- These are here just to show the structure of the list items -->
<!-- List items should get the class `editing` when editing and `completed` when marked as
completed -->
<!-- 三种状态:未完成(没有样式)、已完成(.completed )、编辑中( .editing ) -->
<!--修改:1、v-for迭代; 2、:class={key为class样式名,value为获取的数据true或false}-->
<li v-for="(item, index) in filterItems"
:class="{completed: item.completed,editing: item === currentItem}">
<div class="view">
<!-- 修改:1、v-model 绑定状态值是否选中 -->
<input class="toggle" type="checkbox" v-model="item.completed">
<!-- 修改:1、{{ content }} 显示内容 -->
<label @dblclick="toEdit(item)">{{ item.content }}</label>
<!--修改: 1、:value 绑定id删除 -->
<button class="destroy" :value="item.id" @click="removeItem(index)"></button>
</div>
<!-- 编辑任务 -->
<input class="edit" :value="item.content" @keyup.esc="cancelEdit"
@keyup.enter="finishEdit(item, index, $event)" @blur="finishEdit(item, index, $event)" v-todo-focus="item === currentItem" >
</li>
</ul>
</section>
<!--修改: items.length 值为 0 就是 false -->
<footer class="footer">
<!-- This should be `0 items left` by default -->
<span class="todo-count">
<strong>{{ remaining }}</strong> item{{ remaining === 1 ? '' : 's' }} left
</span>
<!-- Remove this if you don't implement routing -->
<ul class="filters">
<li><a :class="{selected: filterStatus === 'all'}" href="#/" >All</a> </li>
<li><a :class="{selected: filterStatus === 'active'}" href="#/active">Active</a></li>
<li><a :class="{selected: filterStatus === 'completed'}" href="#/completed">Completed</a></li>
</ul> <!-- Hidden if no completed items are left ↓ -->
<button class="clear-completed" @click="removeCompleted" v-show="items.length > remaining">Clear completed</button>
</footer>
</template>
</section>
<footer class="info">
<p>Double-click to edit a todo</p> <!-- Remove the below line ↓ -->
<p>Template by <a href="http://sindresorhus.com">Sindre Sorhus</a></p>
<!-- Change this out with your name and url ↓ -->
<p>Created by <a href="http://todomvc.com">you</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer> <!-- Scripts here. Don't remove ↓ -->
<script src="node_modules/todomvc-common/base.js"></script> <!-- 在 app.js 前面-->
<script src="node_modules/vue/dist/vue.js" type="text/javascript"></script>
<script src="js/app.js"></script>
</body>
</html>
4.12.2.2 app.js
// (function (window) {
// 'use strict';
// // Your starting point. Enjoy the ride!
// })(window);
(function (Vue) { //表示依赖了全局的 Vue, 其实不加也可以,只是更加明确点
//初始化任务
const items = []
//自定义全局指令,用于 增加输入框
//定义时不要在前面加v-, 引用指令时要加上v-
Vue.directive('app-focus', {
//聚集元素
inserted(el, binding) {
el.focus()
}
})
var app = new Vue({
el: '#todoapp',
data: {
items, // 对象属性简写,等价于items: items
currentItem: null, //上面不要少了逗号, 接收当前点击的任务项
filterStatus: 'all' // 上面不要少了逗号,接收变化的状态值
},
//自定义局部指令,用于编辑输入框
directives: {
//定义时不要在前面加v-, 引用指令时要加上v-
'todo-focus': {
update(el, binding) { // 每当指令的值更新后,会调用此函数
if (binding.value) {
el.focus()
}
}
}
},
methods: {
//编辑完成
finishEdit(item, index, event) {
const content = event.target.value.trim();
// 1. 如果为空, 则进行删除任务项
if (!event.target.value.trim()) {
//重用 removeItem 函数进行删除
this.removeItem(index)
return
}
// 2. 添加数据到任务项中
item.content = content
// 3. 移除 .editing 样式
this.currentItem = null
},
//取消编辑
cancelEdit() {
// 移除样式
this.currentItem = null
},
// 进入编辑状态,当前点击的任务项item赋值currentItem,用于页面判断显示 .editing
toEdit(item) {
this.currentItem = item
},
// 移除所有未完成任务项
removeCompleted() {
// 过滤出所有未完成的任务,重新赋值数组即可
this.items = this.items.filter(item => !item.completed)
},
// 移除任务项
removeItem(index) {
// 移除索引为index的一条记录
this.items.splice(index, 1)
},
addItem(event) { //对象属性函数简写,等价于addItem: function () {
console.log('addItem', event.target.value)
//1. 获取文本框输入的数据
const content = event.target.value.trim()
//2. 判断数据如果为空,则什么都不做
if (!content.length) {
return
}
//3.如果不为空,则添加到数组中 // 生成id值
const id = this.items.length + 1
this.items.push({
id, //等价于 id:id
content,
completed: false
})
//4. 清空文本框内容
event.target.value = ''
}
},
// 定义计算属性选项
computed: {
// 过滤出不同状态数据
filterItems() { //this.filterStatus 作为条件,变化后过滤不同数据
switch (this.filterStatus) {
case "active": // 过滤出未完成的数据
return this.items.filter(item => !item.completed)
break
case "completed": // 过滤出已完成的数据
return this.items.filter(item => item.completed)
break
default: // 其他,返回所有数据
return this.items
}
},
// 过滤出所有未完成的任务项
remaining() {
/*
return this.items.filter(function (item) {
return !item.completed }).length
*/
//ES6 箭头函数
return this.items.filter(item => !item.completed).length
},
// 定义计算属性选项
//复选框计算属性(双向绑定)
toggleAll: {
get() { //等价于 get : functon () {...}
console.log(this.remaining)
//2. 当 this.remaining 发生变化后,会触发该方法运行
// 当所有未完成任务数为 0 , 表示全部完成, 则返回 true 让复选框选中 //反之就 false 不选中
return this.remaining === 0
},
set(newStatus) {
// console.log(newStatus)
//1. 当点击 checkbox 复选框后状态变化后,就会触发该方法运行,
// 迭代出数组每个元素,把当前状态值赋给每个元素的 completed
// 下面箭头函数等价于:this.items.forEach(function (item) {
this.items.forEach((item) => {
item.completed = newStatus
})
}
}
}, // **注意** 后面不要少了逗号 ,
})
//当路由 hash 值改变后会自动调用此函数
window.onhashchange = function () {
console.log('hash改变了', window.location.hash)
// 1.获取点击的路由 hash , 当截取的 hash 不为空返回截取的,为空时返回 'all'
const hash = window.location.hash.substr(2) || 'all'
console.log('hash', hash)
// 2. 状态一旦改变,将 hash 赋值给 filterStatus
// 当计算属性 filterItems 感知到 filterStatus 变化后,就会重新过滤
// 当 filterItems 重新过滤出目标数据后,则自动同步更新到视图中
app.filterStatus = hash
}
// 第一次访问页面时,调用一次让状态生效
window.onhashchange()
})(Vue);
4.13 数据持久化
4.13.1 功能分析
- 将所有任务项数据持久化到 localStorage 中,它主要是用于本地存储数据。localStorage中一般浏览器支持的是5M大小,这个在不同的浏览器中 localStorage 会有所不同。
- 分析 :可以使用 Vue 中的 watch 监听器,监听任务数组 items 一旦有改变,则使用 window.localStorage 将它就重新保存到 localStorage
4.13.2 功能实现
使用 window.localStorage 实例进行保存数据与获取数据
- 定义 itemStorage 数据存储对象,里面自定义 fetch 获取本地数据 , save 存数据到本地。
- 修改 Vue 实例中 data 选项的 items 属性,通过 itemStorage.fetch() 方法初始化数据
- Vue 实例中增加一个 watch 选项,用于监听 items 的变化,一旦变化通过 itemStorage.save() 重新保存数据到本地
注意:因为items数组内部是对象,当对象的值发生变化后要被监听到,在选项参数中使用deep: true
参考 :https://cn.vuejs.org/v2/api/#vm-watch
// (function (window) {
// 'use strict';
// // Your starting point. Enjoy the ride!
// })(window);
(function (Vue) { //表示依赖了全局的 Vue, 其实不加也可以,只是更加明确点
var STORAGE_KEY = 'items-vuejs'; // 本地存储数据对象
const itemStorage = {
fetch: function () { // 获取本地数据
return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
},
save: function (items) {
// 保存数据到本地
localStorage.setItem(STORAGE_KEY, JSON.stringify(items));
}
}
//初始化任务
const items = []
//自定义全局指令,用于 增加输入框
//定义时不要在前面加v-, 引用指令时要加上v-
Vue.directive('app-focus', {
//聚集元素
inserted(el, binding) {
el.focus()
}
})
var app = new Vue({
el: '#todoapp',
data: {
items, // 对象属性简写,等价于items: items
currentItem: null, //上面不要少了逗号, 接收当前点击的任务项
filterStatus: 'all' // 上面不要少了逗号,接收变化的状态值
},
// 监听器
watch: { // 如果 items 发生改变,这个函数就会运行
items: {
deep: true, // 发现对象内部值的变化, 要在选项参数中指定 deep: true。
handler: function (newItems, oldItems) {
//本地进行存储
itemStorage.save(newItems)
}
}
},
//自定义局部指令,用于编辑输入框
directives: {
//定义时不要在前面加v-, 引用指令时要加上v-
'todo-focus': {
update(el, binding) { // 每当指令的值更新后,会调用此函数
if (binding.value) {
el.focus()
}
}
}
},
methods: {
//编辑完成
finishEdit(item, index, event) {
const content = event.target.value.trim();
// 1. 如果为空, 则进行删除任务项
if (!event.target.value.trim()) {
//重用 removeItem 函数进行删除
this.removeItem(index)
return
}
// 2. 添加数据到任务项中
item.content = content
// 3. 移除 .editing 样式
this.currentItem = null
},
//取消编辑
cancelEdit() {
// 移除样式
this.currentItem = null
},
// 进入编辑状态,当前点击的任务项item赋值currentItem,用于页面判断显示 .editing
toEdit(item) {
this.currentItem = item
},
// 移除所有未完成任务项
removeCompleted() {
// 过滤出所有未完成的任务,重新赋值数组即可
this.items = this.items.filter(item => !item.completed)
},
// 移除任务项
removeItem(index) {
// 移除索引为index的一条记录
this.items.splice(index, 1)
},
addItem(event) { //对象属性函数简写,等价于addItem: function () {
console.log('addItem', event.target.value)
//1. 获取文本框输入的数据
const content = event.target.value.trim()
//2. 判断数据如果为空,则什么都不做
if (!content.length) {
return
}
//3.如果不为空,则添加到数组中 // 生成id值
const id = this.items.length + 1
this.items.push({
id, //等价于 id:id
content,
completed: false
})
//4. 清空文本框内容
event.target.value = ''
}
},
// 定义计算属性选项
computed: {
// 过滤出不同状态数据
filterItems() { //this.filterStatus 作为条件,变化后过滤不同数据
switch (this.filterStatus) {
case "active": // 过滤出未完成的数据
return this.items.filter(item => !item.completed)
break
case "completed": // 过滤出已完成的数据
return this.items.filter(item => item.completed)
break
default: // 其他,返回所有数据
return this.items
}
},
// 过滤出所有未完成的任务项
remaining() {
/*
return this.items.filter(function (item) {
return !item.completed }).length
*/
//ES6 箭头函数
return this.items.filter(item => !item.completed).length
},
// 定义计算属性选项
//复选框计算属性(双向绑定)
toggleAll: {
get() { //等价于 get : functon () {...}
console.log(this.remaining)
//2. 当 this.remaining 发生变化后,会触发该方法运行
// 当所有未完成任务数为 0 , 表示全部完成, 则返回 true 让复选框选中 //反之就 false 不选中
return this.remaining === 0
},
set(newStatus) {
// console.log(newStatus)
//1. 当点击 checkbox 复选框后状态变化后,就会触发该方法运行,
// 迭代出数组每个元素,把当前状态值赋给每个元素的 completed
// 下面箭头函数等价于:this.items.forEach(function (item) {
this.items.forEach((item) => {
item.completed = newStatus
})
}
}
}, // **注意** 后面不要少了逗号 ,
})
//当路由 hash 值改变后会自动调用此函数
window.onhashchange = function () {
console.log('hash改变了', window.location.hash)
// 1.获取点击的路由 hash , 当截取的 hash 不为空返回截取的,为空时返回 'all'
const hash = window.location.hash.substr(2) || 'all'
console.log('hash', hash)
// 2. 状态一旦改变,将 hash 赋值给 filterStatus
// 当计算属性 filterItems 感知到 filterStatus 变化后,就会重新过滤
// 当 filterItems 重新过滤出目标数据后,则自动同步更新到视图中
app.filterStatus = hash
}
// 第一次访问页面时,调用一次让状态生效
window.onhashchange()
})(Vue);
第五章 Vue 过滤器和插件
5.1 过滤器
5.1.1 什么是过滤器
- 过滤器对将要显示的文本,先进行特定格式化处理,然后再进行显示
- 注意:过滤器并没有改变原本的数据, 只是产生新的对应的数据
5.1.2 使用方式
- 定义过滤器:
- 全局过滤器:
Vue.filter(过滤器名称, function (value1[,value2,...] ) {
// 数据处理逻辑
})
- 局部过滤器:在Vue实例中使用 filter 选项 , 当前实例范围内可用
new Vue({
filters: { 过滤器名称: function (value1[,value2,...] ) {
// 数据处理逻辑
}
}
})
- 过滤器可以用在两个地方:双花括号 {{}} 和 v-bind 表达式
<!-- 在双花括号中 -->
<div>{{数据属性名称 | 过滤器名称}}</div>
<div>{{数据属性名称 | 过滤器名称(参数值)}}</div>
<!-- 在 `v-bind` 中 -->
<div v-bind:id="数据属性名称 | 过滤器名称"></div>
<div v-bind:id="数据属性名称 | 过滤器名称(参数值)"></div>
5.1.3 案例演示
需求:
- 实现过滤敏感字符,如当文本中有 tmd、sb 都将进行过滤掉
- 过滤器传入多个参数 ,实现求和运算
实现:
3. 新建 vue-04-过滤器和插件 目录, 安装 vue.js 模块
4. 在 vue-04-过滤器和插件 目录下创建 01-过滤器.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Template • TodoMVC</title>
<link rel="stylesheet" href="node_modules/todomvc-common/base.css">
<link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
<!-- CSS overrides - remove if you don't need it -->
<link rel="stylesheet" href="css/app.css">
</head>
<body>
<div id="app">
<h3>过滤器接收多个参数:</h3>
<p>{{content | contentFilter}}</p>
<input type="text" :value="content | sensitive">
<h3>过滤器接收多个参数:</h3>
<p>{{vueScore | add(javaScore, pythonScore)}}</p>
</div>
<script src="node_modules/vue/dist/vue.js" type="text/javascript"></script>
<script type="text/javascript">
/*定义全局过滤器: 过滤敏感数据*/
/*Vue.filter('contentFilter',function (value) {
if (!value) return ''
return value.toUpperCase().replace('TMD','***').replace('SB','***') })*/
new Vue({
el: '#app',
data: {
content: '小伙子,TMD就是个SB',
vueScore: 80,
javaScore: 95,
pythonScore: 90
},
//局部过滤器
filters: { //不要少了 s
contentFilter(value) { //value是调用时 | 左边的那个属性值
if (!value) return ''
return value.toUpperCase().replace('TMD', '***').replace('SB', '***')
},
add(num1, num2, num3) {
//num1是调用时 | 左边的那个属性值
return num1 + num2 + num3
}
}
})
</script>
</body>
</html>
5.2 自定义插件
5.2.1 插件的作用
- 插件通常会为 Vue 添加全局功能,一般是添加全局方法/全局指令/过滤器等
- Vue 插件有一个公开方法 install ,通过 install 方法给 Vue 添加全局功能
- 通过全局方法 Vue.use() 使用插件,它需要在你调用 new Vue() 启动应用之前完成.
5.2.2 案例演示
- 开发插件, 在 vue-04-过滤器和插件 目录下创建 js 目录,在 js 目录建一个 plugins.js 文件
(function () {
// 声明 MyPlugin 插件对象
const MyPlugin = {}
MyPlugin.install = function (Vue, options) {
// 1. 添加全局方法
Vue.myGlobalMethod = function () {
alert('MyPlugin插件: 全局方法生效')
}
// 2. 添加全局指令
Vue.directive('my-directive', {
inserted: function (el, binding) {
el.innerHTML = "MyPlugin插件 my-directive:" + binding.value
}
})
// 3. 添加实例方法
Vue.prototype.$myMethod = function (methodOption) {
alert('Vue 实例方法生效:' + methodOption)
}
}
// 将插件添加到 window 对象中
window.MyPlugin = MyPlugin
})() // 不要少了括号(),让它立即执行
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>14-自定义插件</title>
</head>
<body>
<div id="app">
<!-- 引用指令时不要少了 v- -->
<span v-my-directive="content"></span>
</div>
<script src="./node_modules/vue/dist/vue.js" type="text/javascript"></script>
<script src="js/plugins.js" type="text/javascript"></script>
<script type="text/javascript">
// 1.引入自定义插件 MyPlugin
// 如果报错:Uncaught ReferenceError: MyPlugin is not defined
// 解决方法: 查看 plugins.js 是否引入,如果引入还是报错,检查 js 语法,特别是最后一行不要少了括号 ()
Vue.use(MyPlugin)
// 2. 创建 Vue 实例, 模板中使用引用全局指令 v-my-directive="content"
var vm = new Vue({
el: '#app',
data: {
content: 'hello'
}
})
// 3. 调用自定义的全局方法, 所以是 Vue 调用,不是 vm
Vue.myGlobalMethod()
// 4. 调用 Vue 实例方法,所以是 vm 调用,不是 Vue
vm.$myMethod('mengxuegu')
</script>
</body>
</body>
</html>
- 访问页面的效果:
- alert 弹出: MyPlugin插件: 全局方法生效
- alert 弹出: Vue 实例方法生效:
- 页面渲染出: MyPlugin插件 my-directive:hello
第六章 Vue 组件化开发
6.1 组件的概念
组件(component) 是 Vue.js 最强大的功能之一。
Vue 中的组件化开发就是把网页的重复代码抽取出来 ,封装成一个个可复用的视图组件,然后将这些视图组件拼
接到一块就构成了一个完整的系统。这种方式非常灵活,可以极大的提高我们开发和维护的效率。
通常一套系统会以一棵嵌套的组件树的形式来组织:
例如:项目可能会有头部、底部、页侧边栏、内容区等组件,每个组件又包含了其它的像导航链接、博文之类的组件。
-
组件就是对局部视图的封装,每个组件包含了:
- HTML 结构
- CSS 样式
- JavaScript 行为
- data 数据
- methods 行为
-
提高开发效率,增强可维护性,更好的去解决软件上的高耦合、低内聚、无重用的3大代码问题
-
Vue 中的组件思想借鉴于 React
-
目前主流的前端框架:Angular、React 、Vue 都是组件化开发思想
6.2 组件的基本使用
为了能在模板中使用,这些组件必须先注册以便 Vue 能够识别。
有两种组件的注册类型:全局注册和局部注册
6.2.1 全局注册
6.2.1.1 介绍
一般把网页中特殊的公共部分注册为全局组件:轮播图、分页、通用导航栏
-
全局注册之后,可以在任何新创建的 Vue 实例 (new Vue) 的模板中使用
-
简单格式:
Vue.component('组件名',{
template: '定义组件模板',
data: function(){ //data 选项在组件中必须是一个函数
return {}
}
//其他选项:methods
})
说明:
- 组件名:
可使用驼峰(camelCase)或者横线分隔(kebab-case)命名方式
但 DOM 中只能使用横线分隔方式进行引用组件
官方强烈推荐组件名字母全小写且必须包含一个连字符 - template:定义组件的视图模板
- data :在组件中必须是一个函数
6.2.1.2 示例
- 创建 vue-05-组件化 目录,安装 vue 模块, 创建 01-全局注册.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<!-- 通过组件名直接使用, 不能使用驼峰形式 -->
<component-a></component-a>
</div>
<script src="../node_modules/vue/dist/vue.js" type="text/javascript"> </script>
<script type="text/javascript">
/*1. 全局组件注册: 它们在注册之后可以用在任何新创建的 Vue 根实例 (new Vue) 的模板中
参数1:组件名 1.可使用驼峰(camelCase)或者横线分隔(kebab-case)命名方式
2.DOM 中只能使用横线分隔方式进行引用组件
*/
Vue.component('component-a', { // template 选项指定此组件的模板代码
template: '<div><h1>头部组件 - {{ name }}</h1></div>',
// data 必须是函数
data: function () {
return {
name: '全局组件'
}
}
})
new Vue({
el: '#app'
})
</script>
</body>
</html>
6.2.2 局部注册(子组件)
6.2.2.1 介绍
一般把一些非通用部分注册为局部组件,一般只适用于当前项目的。
格式:
1. JS 对象来定义组件:
var ComponentA = { data: function(){}, template: '组件模板A'}
var ComponentA = { data: function(){}, template: '组件模板A'}
2. 在Vue实例中的 components 选项中引用组件:
3. new Vue({
el: '#app',
data: {},
components: {
// 组件选项
'component-a': ComponentA // key:组件名,value: 引用的组件对象 'component-b': ComponentB
}
})
6.2.2.2 示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<!-- 通过组件名直接使用, 不能使用驼峰形式 -->
<component-b></component-b>
</div>
<script src="../node_modules/vue/dist/vue.js" type="text/javascript"> </script>
<script type="text/javascript">
// 定义局部组件对象
var ComponentB = {
template: '<div> 这是:{{ name }}</div>',
data: function () {
return {
name: '局部组件'
}
}
}
new Vue({
el: '#app',
components: {
// 局部组件
'component-b': ComponentB
}
})
</script>
</body>
</html>
6.2.3 总结
- 组件是可复用的 Vue 实例,不需要手动实例化
- 与 new Vue 接收相同的选项,例如 data 、 computed 、 watch 、 methods 等
- 组件的 data 选项必须是一个函数
6.3 多个组件示例
- 效果演示
- 可将 组件注册 抽取在一个一个的 js 文件中方便管理
- 在 vue-05-组件化 目录下创建 component 目录存放组件: Header.js Main.js Footer.js
- Header.js 文件内容
Vue.component('app-header', {
// template 选项指定此组件的模板代码
template: '<div class="header"><h1>头部组件</h1></div>'
})
- Main.js 文件内容
Vue.component('app-main', {
// template 选项指定此组件的模板代码
template: '<div class="main"><ul><li>客户管理</li><li>帐单管理</li><li>供应商管理</li></ul><h3></h3> </div>'
})
- Footer.js 文件内容
Vue.component('app-footer', {
// template 选项指定此组件的模板代码
template: '<div class="footer"><h1>底部组件</h1></div>'
})
- 页面引入组件 js 文件后, 进行使用
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<!-- 通过组件名直接使用, 不能使用驼峰形式 -->
<!-- 通过组件名直接使用 -->
<app-header></app-header>
<app-main></app-main>
<app-footer></app-footer>
</div>
<script src="../node_modules/vue/dist/vue.js" type="text/javascript"> </script>
<!-- 要在 vue.js 下面引入组件 -->
<script src="component/Footer.js" type="text/javascript"></script>
<script src="component/Header.js" type="text/javascript"></script>
<script src="component/Main.js" type="text/javascript"></script>
<script type="text/javascript">
new Vue({
el: '#app'
})
</script>
</body>
</html>
6.4 Bootstrap 首页组件化
6.4.1 分析首页
- 分析首页可拆分为多少个组件
html页面位于: 01-配套源码\bootstarp
共拆分为5个组件
6.4.2 头部导航组件注册
- 在 vue-05-组件化 目录下创建 03-bootstrap 目录
- 将网盘中 01-配套源码\bootstrap 目录的所有文件复制到 03-bootstrap 目录下
- 在 index.html 的
<body>
标签下添加一个<div id="app">
Vue管理入口 - 在 index.html 引入 vue.js 和 创建 Vue 实例
<script src="../node_modules/vue/dist/vue.js"></script>
<script>
new Vue({
el: '#app'
})
</script>
- 注册和引用头部导航组件 AppNavbar
- 左上角显示 梦学谷 ,通过 data 选项函数指定 message 属性显示
- 输入框中失去焦点后 alert(‘失去焦点’) , 通过 methods 选项
- 在 头部导航区域采用组件形式
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<meta name="description" content="">
<meta name="author" content="">
<title>Dashboard Template for Bootstrap</title>
<!-- Bootstrap core CSS -->
<link href="style/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this template -->
<link href="style/dashboard.css" rel="stylesheet">
</head>
<body>
<!--头部导航区域-->
<div id="app">
<!--头部导航区域-->
<app-navbar></app-navbar>
</div>
<!--核心区域:分左右两边-->
<div class="container-fluid">
<div class="row">
<!--左边菜单栏区域-->
<div class="col-sm-3 col-md-2 sidebar">
<ul class="nav nav-sidebar">
<li class="active"><a href="#">Overview <span class="sr-only">(current)</span></a></li>
<li><a href="#">Reports</a></li>
<li><a href="#">Analytics</a></li>
<li><a href="#">Export</a></li>
</ul>
<ul class="nav nav-sidebar">
<li><a href="">Nav item</a></li>
<li><a href="">Nav item again</a></li>
<li><a href="">One more nav</a></li>
<li><a href="">Another nav item</a></li>
<li><a href="">More navigation</a></li>
</ul>
<ul class="nav nav-sidebar">
<li><a href="">Nav item again</a></li>
<li><a href="">One more nav</a></li>
<li><a href="">Another nav item</a></li>
</ul>
</div>
<!--右边主页面区域: 分上下两个区域-->
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<!--右边上半区域-->
<h1 class="page-header">Dashboard</h1>
<div class="row placeholders">
<div class="col-xs-6 col-sm-3 placeholder">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" width="200"
height="200" class="img-responsive" alt="Generic placeholder thumbnail">
<h4>Label</h4>
<span class="text-muted">Something else</span>
</div>
<div class="col-xs-6 col-sm-3 placeholder">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" width="200"
height="200" class="img-responsive" alt="Generic placeholder thumbnail">
<h4>Label</h4>
<span class="text-muted">Something else</span>
</div>
<div class="col-xs-6 col-sm-3 placeholder">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" width="200"
height="200" class="img-responsive" alt="Generic placeholder thumbnail">
<h4>Label</h4>
<span class="text-muted">Something else</span>
</div>
<div class="col-xs-6 col-sm-3 placeholder">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" width="200"
height="200" class="img-responsive" alt="Generic placeholder thumbnail">
<h4>Label</h4>
<span class="text-muted">Something else</span>
</div>
</div>
<!--右边下半区域-->
<h2 class="sub-header">Section title</h2>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Header</th>
<th>Header</th>
<th>Header</th>
<th>Header</th>
</tr>
</thead>
<tbody>
<tr>
<td>1,001</td>
<td>Lorem</td>
<td>ipsum</td>
<td>dolor</td>
<td>sit</td>
</tr>
<tr>
<td>1,002</td>
<td>amet</td>
<td>consectetur</td>
<td>adipiscing</td>
<td>elit</td>
</tr>
<tr>
<td>1,003</td>
<td>Integer</td>
<td>nec</td>
<td>odio</td>
<td>Praesent</td>
</tr>
<tr>
<td>1,003</td>
<td>libero</td>
<td>Sed</td>
<td>cursus</td>
<td>ante</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script src="../../../node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">
//组件对象定义
const AppNavbar = {
template: `<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">{{ projectName }}</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
<li><a href="#">Dashboard</a></li>
<li><a href="#">Settings</a></li>
<li><a href="#">Profile</a></li>
<li><a href="#">Help</a></li>
</ul>
<form class="navbar-form navbar-right">
<input type="text" class="form-control" placeholder="Search..." @blur="search">
</form>
</div>
</div>
</nav>`,
data: function () {
return {
projectName: '梦学谷'
}
},
methods: {
search(){
alert('失去焦点')
}
}
}
new Vue({
el: '#app',
components:{
'app-navbar': AppNavbar
}
})
</script>
</body>
</html>
- 浏览器访问 index.html
如果如果控制台报如下错,则<app-navbar></app-navbar>
标签名与组件名不相同
7. 抽取定义的 AppNavbar 对象到 AppNavbar.js 中
- 把 const AppNavbar= {…} 部分剪切进 AppNavbar.js 文件中, AppNavbar对象添加到 window 域
;(function () {
//粘贴到这里
window.AppNavbar = {
//
}
})()
- 将 template 提取出来AppNavbar.js
;
(function () {
//组件对象定义
const template = `<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">{{ projectName }}</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
<li><a href="#">Dashboard</a></li>
<li><a href="#">Settings</a></li>
<li><a href="#">Profile</a></li>
<li><a href="#">Help</a></li>
</ul>
<form class="navbar-form navbar-right">
<input type="text" class="form-control" placeholder="Search..." @blur="search">
</form>
</div>
</div>
</nav>`
window.AppNavbar = { // 添加 window 域中,html 页面才可以进行获取
template, // 等价于template: template,
data: function () {
return {
projectName: '梦学谷'
}
},
methods: {
search() {
alert('失去焦点')
}
}
}
})()
- 在 index.html 中引入 AppNavbar.js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<meta name="description" content="">
<meta name="author" content="">
<title>Dashboard Template for Bootstrap</title>
<!-- Bootstrap core CSS -->
<link href="style/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this template -->
<link href="style/dashboard.css" rel="stylesheet">
</head>
<body>
<!--头部导航区域-->
<div id="app">
<!--头部导航区域-->
<app-navbar></app-navbar>
</div>
<!--核心区域:分左右两边-->
<div class="container-fluid">
<div class="row">
<!--左边菜单栏区域-->
<div class="col-sm-3 col-md-2 sidebar">
<ul class="nav nav-sidebar">
<li class="active"><a href="#">Overview <span class="sr-only">(current)</span></a></li>
<li><a href="#">Reports</a></li>
<li><a href="#">Analytics</a></li>
<li><a href="#">Export</a></li>
</ul>
<ul class="nav nav-sidebar">
<li><a href="">Nav item</a></li>
<li><a href="">Nav item again</a></li>
<li><a href="">One more nav</a></li>
<li><a href="">Another nav item</a></li>
<li><a href="">More navigation</a></li>
</ul>
<ul class="nav nav-sidebar">
<li><a href="">Nav item again</a></li>
<li><a href="">One more nav</a></li>
<li><a href="">Another nav item</a></li>
</ul>
</div>
<!--右边主页面区域: 分上下两个区域-->
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<!--右边上半区域-->
<h1 class="page-header">Dashboard</h1>
<div class="row placeholders">
<div class="col-xs-6 col-sm-3 placeholder">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" width="200"
height="200" class="img-responsive" alt="Generic placeholder thumbnail">
<h4>Label</h4>
<span class="text-muted">Something else</span>
</div>
<div class="col-xs-6 col-sm-3 placeholder">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" width="200"
height="200" class="img-responsive" alt="Generic placeholder thumbnail">
<h4>Label</h4>
<span class="text-muted">Something else</span>
</div>
<div class="col-xs-6 col-sm-3 placeholder">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" width="200"
height="200" class="img-responsive" alt="Generic placeholder thumbnail">
<h4>Label</h4>
<span class="text-muted">Something else</span>
</div>
<div class="col-xs-6 col-sm-3 placeholder">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" width="200"
height="200" class="img-responsive" alt="Generic placeholder thumbnail">
<h4>Label</h4>
<span class="text-muted">Something else</span>
</div>
</div>
<!--右边下半区域-->
<h2 class="sub-header">Section title</h2>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Header</th>
<th>Header</th>
<th>Header</th>
<th>Header</th>
</tr>
</thead>
<tbody>
<tr>
<td>1,001</td>
<td>Lorem</td>
<td>ipsum</td>
<td>dolor</td>
<td>sit</td>
</tr>
<tr>
<td>1,002</td>
<td>amet</td>
<td>consectetur</td>
<td>adipiscing</td>
<td>elit</td>
</tr>
<tr>
<td>1,003</td>
<td>Integer</td>
<td>nec</td>
<td>odio</td>
<td>Praesent</td>
</tr>
<tr>
<td>1,003</td>
<td>libero</td>
<td>Sed</td>
<td>cursus</td>
<td>ante</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script src="../../../node_modules/vue/dist/vue.js"></script>
<!-- 注意要在 vue.js 下面引入 -->
<script src="components/AppNavbar.js"></script>
<script type="text/javascript">
new Vue({
el: '#app',
components: {
AppNavbar // 等价于 AppNavbar: AppNavbar
}
})
</script>
</body>
</html>
- 效果
- 如果报错解决方式 ,
在 index.html 中要引入 AppNavbar.js
AppNavbar.js 要将 AppNavbar 放到 window 域中
6.4.3 左侧导航组件注册
- AppLeaf.js 文件中定义 AppLeaf 组件对象
;
(function() {
const template = `<div class="col-sm-3 col-md-2 sidebar">
<ul class="nav nav-sidebar">
<li class="active"><a href="#">{{ message }}<span class="sr-only">(current)</span></a></li>
<li><a href="#">Reports</a></li>
<li><a href="#">Analytics</a></li>
<li><a href="#">Export</a></li>
</ul>
<ul class="nav nav-sidebar">
<li><a href="">Nav item</a></li>
<li><a href="">Nav item again</a></li>
<li><a href="">One more nav</a></li>
<li><a href="">Another nav item</a></li>
<li><a href="">More navigation</a></li>
</ul>
<ul class="nav nav-sidebar">
<li><a href="">Nav item again</a></li>
<li><a href="">One more nav</a></li>
<li><a href="">Another nav item</a></li>
</ul>
</div>`
window.AppLeaf = {
template,
data: function () {
return {
message: '学员管理'
}
}
}
})()
- 在 index.html 中引入 AppLeaf.js , Vue实例中添加 AppLeaf 组件, 主页面通过
<app-leaf/>
组件渲染
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<meta name="description" content="">
<meta name="author" content="">
<title>Dashboard Template for Bootstrap</title>
<!-- Bootstrap core CSS -->
<link href="style/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this template -->
<link href="style/dashboard.css" rel="stylesheet">
</head>
<body>
<!--头部导航区域-->
<div id="app">
<!--头部导航区域-->
<app-navbar></app-navbar>
<!--核心区域:分左右两边-->
<div class="container-fluid">
<div class="row">
<!--左边菜单栏区域-->
<app-leaf></app-leaf>
<!--右边主页面区域: 分上下两个区域-->
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<!--右边上半区域-->
<h1 class="page-header">Dashboard</h1>
<div class="row placeholders">
<div class="col-xs-6 col-sm-3 placeholder">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" width="200"
height="200" class="img-responsive" alt="Generic placeholder thumbnail">
<h4>Label</h4>
<span class="text-muted">Something else</span>
</div>
<div class="col-xs-6 col-sm-3 placeholder">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" width="200"
height="200" class="img-responsive" alt="Generic placeholder thumbnail">
<h4>Label</h4>
<span class="text-muted">Something else</span>
</div>
<div class="col-xs-6 col-sm-3 placeholder">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" width="200"
height="200" class="img-responsive" alt="Generic placeholder thumbnail">
<h4>Label</h4>
<span class="text-muted">Something else</span>
</div>
<div class="col-xs-6 col-sm-3 placeholder">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" width="200"
height="200" class="img-responsive" alt="Generic placeholder thumbnail">
<h4>Label</h4>
<span class="text-muted">Something else</span>
</div>
</div>
<!--右边下半区域-->
<h2 class="sub-header">Section title</h2>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Header</th>
<th>Header</th>
<th>Header</th>
<th>Header</th>
</tr>
</thead>
<tbody>
<tr>
<td>1,001</td>
<td>Lorem</td>
<td>ipsum</td>
<td>dolor</td>
<td>sit</td>
</tr>
<tr>
<td>1,002</td>
<td>amet</td>
<td>consectetur</td>
<td>adipiscing</td>
<td>elit</td>
</tr>
<tr>
<td>1,003</td>
<td>Integer</td>
<td>nec</td>
<td>odio</td>
<td>Praesent</td>
</tr>
<tr>
<td>1,003</td>
<td>libero</td>
<td>Sed</td>
<td>cursus</td>
<td>ante</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script src="../../../node_modules/vue/dist/vue.js"></script>
<!-- 注意要在 vue.js 下面引入 -->
<script src="components/AppNavbar.js"></script>
<script src="components/AppLeaf.js"></script>
<script type="text/javascript">
new Vue({
el: '#app',
components: {
AppNavbar, // 等价于 AppNavbar: AppNavbar
AppLeaf
}
})
</script>
</body>
</html>
- 效果
6.4.4 右侧主页面组件注册
6.4.4.1 主页面组件化
- 在 home\AppHome.js 中定义 AppHome 组件对象
;
(function () {
const template = `<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<!--右边上半区域-->
<h1 class="page-header">Dashboard</h1>
<div class="row placeholders">
<div class="col-xs-6 col-sm-3 placeholder">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" width="200"
height="200" class="img-responsive" alt="Generic placeholder thumbnail">
<h4>Label</h4>
<span class="text-muted">Something else</span>
</div>
<div class="col-xs-6 col-sm-3 placeholder">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" width="200"
height="200" class="img-responsive" alt="Generic placeholder thumbnail">
<h4>Label</h4>
<span class="text-muted">Something else</span>
</div>
<div class="col-xs-6 col-sm-3 placeholder">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" width="200"
height="200" class="img-responsive" alt="Generic placeholder thumbnail">
<h4>Label</h4>
<span class="text-muted">Something else</span>
</div>
<div class="col-xs-6 col-sm-3 placeholder">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" width="200"
height="200" class="img-responsive" alt="Generic placeholder thumbnail">
<h4>Label</h4>
<span class="text-muted">Something else</span>
</div>
</div>
<!--右边下半区域-->
<h2 class="sub-header">Section title</h2>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Header</th>
<th>Header</th>
<th>Header</th>
<th>Header</th>
</tr>
</thead>
<tbody>
<tr>
<td>1,001</td>
<td>Lorem</td>
<td>ipsum</td>
<td>dolor</td>
<td>sit</td>
</tr>
<tr>
<td>1,002</td>
<td>amet</td>
<td>consectetur</td>
<td>adipiscing</td>
<td>elit</td>
</tr>
<tr>
<td>1,003</td>
<td>Integer</td>
<td>nec</td>
<td>odio</td>
<td>Praesent</td>
</tr>
</tbody>
</table>
</div>
</div>`
window.AppHome = {
template
}
})()
- 在 index.html 中引入 components/home/AppHome.js , Vue实例添加 AppHome 组件,主页面通过
<app-home/>
组件渲染
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<meta name="description" content="">
<meta name="author" content="">
<title>Dashboard Template for Bootstrap</title>
<!-- Bootstrap core CSS -->
<link href="style/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this template -->
<link href="style/dashboard.css" rel="stylesheet">
</head>
<body>
<!--头部导航区域-->
<div id="app">
<!--头部导航区域-->
<app-navbar></app-navbar>
<!--核心区域:分左右两边-->
<div class="container-fluid">
<div class="row">
<!--左边菜单栏区域-->
<app-leaf></app-leaf>
<!--右边主页面区域: 分上下两个区域-->
<app-home></app-home>
</div>
</div>
</div>
<script src="../../../node_modules/vue/dist/vue.js"></script>
<!-- 注意要在 vue.js 下面引入 -->
<script src="components/AppNavbar.js"></script>
<script src="components/AppLeaf.js"></script>
<script src="components/home/AppHome.js"></script>
<script type="text/javascript">
new Vue({
el: '#app',
components: {
AppNavbar, // 等价于 AppNavbar: AppNavbar
AppLeaf,
AppHome
}
})
</script>
</body>
</html>
6.4.4.2 Dashboard 子组件化
- 将 AppHome.js 中的 右边上半区域 中的
<div>
部分剪切到 Dashboard 组件中,Dashboar.js 内容如下:
;
(function () {
const template = `
<!--右边上半区域-->
<div class="row placeholders">
<div class="col-xs-6 col-sm-3 placeholder">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" width="200"
height="200" class="img-responsive" alt="Generic placeholder thumbnail">
<h4>Label</h4>
<span class="text-muted">Something else</span>
</div>
<div class="col-xs-6 col-sm-3 placeholder">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" width="200"
height="200" class="img-responsive" alt="Generic placeholder thumbnail">
<h4>Label</h4>
<span class="text-muted">Something else</span>
</div>
<div class="col-xs-6 col-sm-3 placeholder">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" width="200"
height="200" class="img-responsive" alt="Generic placeholder thumbnail">
<h4>Label</h4>
<span class="text-muted">Something else</span>
</div>
<div class="col-xs-6 col-sm-3 placeholder">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" width="200"
height="200" class="img-responsive" alt="Generic placeholder thumbnail">
<h4>Label</h4>
<span class="text-muted">Something else</span>
</div>
</div>`
window.Dashboard = {
template
}
})()
- 在 AppHome.js 中引入 Dashboard 组件作为 AppHome 的子组件
在 AppHome 对象的 components 选项中注册 Dashboard 组件
在 AppHome 对象的 template 模板中引入组件<dashboard/>
;
(function () {
const template = `<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<!--右边上半区域-->
<h1 class="page-header">Dashboard</h1>
<dashboard></dashboard>
<!--右边下半区域-->
<h2 class="sub-header">Section title</h2>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Header</th>
<th>Header</th>
<th>Header</th>
<th>Header</th>
</tr>
</thead>
<tbody>
<tr>
<td>1,001</td>
<td>Lorem</td>
<td>ipsum</td>
<td>dolor</td>
<td>sit</td>
</tr>
<tr>
<td>1,002</td>
<td>amet</td>
<td>consectetur</td>
<td>adipiscing</td>
<td>elit</td>
</tr>
<tr>
<td>1,003</td>
<td>Integer</td>
<td>nec</td>
<td>odio</td>
<td>Praesent</td>
</tr>
</tbody>
</table>
</div>
</div>`
window.AppHome = {
template,
components: {
// 子组件
Dashboard
}
}
})()
- 在 index.html 中引入 Dashboard.js , 注意:Dashboard.js 要在 AppHome.js 前面引入
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<meta name="description" content="">
<meta name="author" content="">
<title>Dashboard Template for Bootstrap</title>
<!-- Bootstrap core CSS -->
<link href="style/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this template -->
<link href="style/dashboard.css" rel="stylesheet">
</head>
<body>
<!--头部导航区域-->
<div id="app">
<!--头部导航区域-->
<app-navbar></app-navbar>
<!--核心区域:分左右两边-->
<div class="container-fluid">
<div class="row">
<!--左边菜单栏区域-->
<app-leaf></app-leaf>
<!--右边主页面区域: 分上下两个区域-->
<app-home></app-home>
</div>
</div>
</div>
<script src="../../../node_modules/vue/dist/vue.js"></script>
<!-- 注意要在 vue.js 下面引入 -->
<script src="components/AppNavbar.js"></script>
<script src="components/AppLeaf.js"></script>
<!-- 注意:Dashboard.js 要在 Home.js 前面引入 -->
<script src="components/Home/Dashboard.js"></script>
<script src="components/home/AppHome.js"></script>
<script type="text/javascript">
new Vue({
el: '#app',
components: {
AppNavbar, // 等价于 AppNavbar: AppNavbar
AppLeaf,
AppHome
}
})
</script>
</body>
</html>
6.4.4.3 查询列表子组件化
- 将 AppHome.js 中的 右边下半区域 中的
<div>
部分剪切到 HomeList.js 组件中, HomeList.js 内容如下:
;
(function () {
const template = `<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Header</th>
<th>Header</th>
<th>Header</th>
<th>Header</th>
</tr>
</thead>
<tbody>
<tr>
<td>1,001</td>
<td>Lorem</td>
<td>ipsum</td>
<td>dolor</td>
<td>sit</td>
</tr>
</tbody>
</table>
</div>`
window.HomeList = {
template,
data() {
return {
name: '梦学谷'
}
}
}
})()
- 在 AppHome.js 中引入 HomeList 组件作为 Home 的子组件
在 AppHome 对象的 components 选项中注册 HomeList 组件
在 AppHome 对象的 template 模板中引入组件<home-list/>
, 注意是横线分隔方式引用组件
;
(function () {
const template = `<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<!--右边上半区域-->
<h1 class="page-header">Dashboard</h1>
<dashboard></dashboard>
<!--右边下半区域-->
<h2 class="sub-header">Section title</h2>
<home-list></home-list>
</div>`
window.AppHome = {
template,
components: {
// 子组件
Dashboard,
HomeList
}
}
})()
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<meta name="description" content="">
<meta name="author" content="">
<title>Dashboard Template for Bootstrap</title>
<!-- Bootstrap core CSS -->
<link href="style/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this template -->
<link href="style/dashboard.css" rel="stylesheet">
</head>
<body>
<!--头部导航区域-->
<div id="app">
<!--头部导航区域-->
<app-navbar></app-navbar>
<!--核心区域:分左右两边-->
<div class="container-fluid">
<div class="row">
<!--左边菜单栏区域-->
<app-leaf></app-leaf>
<!--右边主页面区域: 分上下两个区域-->
<app-home></app-home>
</div>
</div>
</div>
<script src="../../../node_modules/vue/dist/vue.js"></script>
<!-- 注意要在 vue.js 下面引入 -->
<script src="components/AppNavbar.js"></script>
<script src="components/AppLeaf.js"></script>
<!-- 注意:Dashboard.js 要在 Home.js 前面引入 -->
<script src="components/Home/Dashboard.js"></script>
<script src="components/Home/HomeList.js"></script>
<script src="components/home/AppHome.js"></script>
<script type="text/javascript">
new Vue({
el: '#app',
components: {
AppNavbar, // 等价于 AppNavbar: AppNavbar
AppLeaf,
AppHome
}
})
</script>
</body>
</html>
- 效果
6.4.5 极致组件化
6.4.5.1 根组件提取 App.js
- 将 index.html 中
<div id="#app">
标签体中的代码提取出来,变成一个根组件存入 App.js
中,提取的内容如下:
<app-navbar></app-navbar>
<!--核心区域:分左右两边-->
<div class="container-fluid">
<div class="row">
<!--左边菜单栏区域-->
<app-leaf></app-leaf>
<app-home></app-home>
</div>
</div>
- 在 03-bootstrap 目录下创建 App.js 文件
- 将
<div id="#app">
标签体中的代码剪切到 App.js 文件中作为模板页面
注意:template 模板中必须要的根元素,所以要在提取的内容外层加上<div></div>
, 一定要不要少了,不然报以下错误:
App.js
;
(function () {
// 不要少了最外层的根元素 div
const template = `<div>
<app-navbar></app-navbar>
<!--核心区域:分左右两边-->
<div class="container-fluid">
<div class="row">
<!--左边菜单栏区域-->
<app-leaf></app-leaf>
<app-home></app-home>
</div>
</div>
</div>`
window.App = {
template,
components: {
AppNavbar, // 等价于 Navbar: Navbar
AppLeaf,
AppHome
}
}
})()
6.4.5.2 剪切组件对象
- 当前 App.js 的 template 中引用了 AppNavbar 、 AppLeaf 和 AppHome 组件,所以我们要将index.html 中的 components 选项中的组件对象剪切到 App 组件对象中。
App.js 代码如下:
;
(function () {
// 不要少了最外层的根元素 div
const template = `<div>
<app-navbar></app-navbar>
<!--核心区域:分左右两边-->
<div class="container-fluid">
<div class="row">
<!--左边菜单栏区域-->
<app-leaf></app-leaf>
<app-home></app-home>
</div>
</div>
</div>`
window.App = {
template,
components: {
AppNavbar, // 等价于 Navbar: Navbar
AppLeaf,
AppHome
}
}
})()
6.4.5.3 index.html 引入 App.js
- 在 index.html 的引入 App.js 文件,并在 Vue 实例中的 components 选项中引入 App 组件,
然后在<div id="app">
下引用</app>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<meta name="description" content="">
<meta name="author" content="">
<title>Dashboard Template for Bootstrap</title>
<!-- Bootstrap core CSS -->
<link href="style/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this template -->
<link href="style/dashboard.css" rel="stylesheet">
</head>
<body>
<!-- 留一个组件的出口,此处要被子组件替换 -->
<div id="app">
<app></app>
</div>
<script src="../../../node_modules/vue/dist/vue.js"></script>
<!-- 注意要在 vue.js 下面引入 -->
<script src="components/AppNavbar.js"></script>
<script src="components/AppLeaf.js"></script>
<!-- 注意:Dashboard.js 要在 Home.js 前面引入 -->
<script src="components/Home/Dashboard.js"></script>
<script src="components/Home/HomeList.js"></script>
<script src="components/home/AppHome.js"></script>
<script src="App.js"></script>
<script type="text/javascript">
new Vue({
el: '#app',
components: {
App //等价于 App: App
}
})
</script>
</body>
</html>
- 在
<div id="app">
下通过</app>
引用 App 组件不是很好, 因为页面代码中会多出一个 div 。更好的方式是,在<div id="app">
下无须使用</app>
引用 App 组件,可以通过 Vue 根实例的 template 选项引用组件<app></app>
后,然后会把 template 中的渲染结果替换掉#app 标签。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<meta name="description" content="">
<meta name="author" content="">
<title>Dashboard Template for Bootstrap</title>
<!-- Bootstrap core CSS -->
<link href="style/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this template -->
<link href="style/dashboard.css" rel="stylesheet">
</head>
<body>
<!-- 留一个组件的出口,此处要被子组件替换 -->
<div id="app">
</div>
<script src="../../../node_modules/vue/dist/vue.js"></script>
<!-- 注意要在 vue.js 下面引入 -->
<script src="components/AppNavbar.js"></script>
<script src="components/AppLeaf.js"></script>
<!-- 注意:Dashboard.js 要在 Home.js 前面引入 -->
<script src="components/Home/Dashboard.js"></script>
<script src="components/Home/HomeList.js"></script>
<script src="components/home/AppHome.js"></script>
<script src="App.js"></script>
<script type="text/javascript">
new Vue({
el: '#app',
components: {
App //等价于 App: App
}
})
</script>
</body>
</html>
- 当前 index.html 文件中还有 JS 代码,可以将这些 JS 代码提取出来放到 main.js 中
在 03-bootstrap 目录下创建一个 main.js 文件
剪切 Vue 实例化代码到 main.js 中
main.js
new Vue({
el: '#app', // Vue 根实例中有 template 选项引用了组件后,然后会把 template 中的渲染结果替换掉 #app 标签
template: '<app></app>',
components: {
App //等价于 App: App
}
})
index.html 文件中引入 main.js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags -->
<meta name="description" content="">
<meta name="author" content="">
<title>Dashboard Template for Bootstrap</title>
<!-- Bootstrap core CSS -->
<link href="style/bootstrap.min.css" rel="stylesheet">
<!-- Custom styles for this template -->
<link href="style/dashboard.css" rel="stylesheet">
</head>
<body>
<!-- 留一个组件的出口,此处要被子组件替换 -->
<div id="app">
</div>
<script src="../../../node_modules/vue/dist/vue.js"></script>
<!-- 注意要在 vue.js 下面引入 -->
<script src="components/AppNavbar.js"></script>
<script src="components/AppLeaf.js"></script>
<!-- 注意:Dashboard.js 要在 Home.js 前面引入 -->
<script src="components/Home/Dashboard.js"></script>
<script src="components/Home/HomeList.js"></script>
<script src="components/home/AppHome.js"></script>
<script src="App.js"></script>
<script src="main.js"></script>
</body>
</html>
6.5 组件化注意事项
- 组件可以理解为特殊的 Vue 实例,不需要手动实例化,管理自己的 template 模板
- 组件的 template 必须有且只有一个根节点
- 组件的 data 选项必须是函数,且函数体返回一个对象
- 组件与组件之间是相互独立的,可以配置自己的一些选项资源 data、methods、computed 等等
- 思想:组件自己管理自己,不影响别人
6.6 Vue 父子组件间通信
6.6.1 组件间通信方式
1.props
父组件向子组件传递数据
2. $emit
自定义事件
3. slot
插槽分发内容
6.6.2 组件间通信规则
- 不要在子组件中直接修改父组件传递的数据
- 数据初始化时,应当看初始化的数据是否用于多个组件中,如果需要被用于多个组件中,则初始化在父组件中;如果只在一个组件中使用,那就初始化在这个要使用的组件中。
- 数据初始化在哪个组件, 更新数据的方法(函数)就应该定义在哪个组件。
6.6.3 props 向子组件传递数据
6.6.3.1 声明组件对象中定义 props
- 在声明组件对象中使用 props 选项指定
const MyComponent = {
template: '<div></div>',
props: 此处值有以下3种方式,
components: {}
}
方式1:指定传递属性名,注意是 数组形式
props: ['id','name','salary', 'isPublished', 'commentIds', 'author', 'getEmp']
方式2:指定传递属性名和数据类型,注意是 对象形式
props: {
id: Number,
name: String,
salary: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
getEmp: Function
}
方式3:指定属性名、数据类型、必要性、默认值
props: {
name: {
type: String,
required: true,
default: 'mxg'
}
}
6.6.3.2 引用组件时动态赋值
在引用组件时,通过 v-bind 动态赋值
<my-component v-bind:id="2" :name="meng" :salary="9999" :is-published="true" :comment-ids="[1, 2]" :author="{name: 'alan'}" :get-emp="getEmp" > </my-component>
6.6.3.3 传递数据注意
- props 只用于父组件向子组件传递数据
- 所有标签属性都会成为组件对象的属性, 模板页面可以直接引用
- 问题:
a. 如果需要向非子后代传递数据,必须多层逐层传递
b. 兄弟组件间也不能直接 props 通信, 必须借助父组件才可以
6.6.4 props 案例-列表渲染
需求:实现 DashBoard 和 HomeList 组件中渲染数据功能
6.6.4.1 复制 bootstrp 案例
- 复制 vue-05-组件化\03-bootstrap 项目为 vue-06-组件间通信
- NPM 安装 vue 模块
- 更改 vue-06-组件间通信\index.html 中 vue.js 的路径
<script src="node_modules/vue/dist/vue.js"></script>
- 测试访问 index.html 正常,再进行下面操作
6.6.4.2 AppHome.js
;
(function () {
const template = `<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<!--右边上半区域-->
<h1 class="page-header">Dashboard</h1>
<dashboard :hobbies="hobbies"></dashboard>
<!--右边下半区域-->
<h2 class="sub-header">Section title</h2>
<home-list :emp-list="empList"></home-list>
</div>`
window.AppHome = {
template,
data() {
return {
hobbies: ['看书', '台球', '睡觉', '撸代码'],
empList: [{
id: 1,
name: '放生1',
salary: 80001
}, {
id: 2,
name: '放生2',
salary: 80002
}, {
id: 3,
name: '放生3',
salary: 80003
}, {
id: 4,
name: '放生4',
salary: 80004
}]
}
},
components: {
// 子组件
Dashboard,
HomeList
}
}
})()
6.6.4.3 Dashboard.js
;
(function () {
const template = `
<!--右边上半区域-->
<div class="row placeholders">
<div v-for="(hobby,index) in hobbies" :key="index" class="col-xs-6 col-sm-3 placeholder">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" width="200"
height="200" class="img-responsive" alt="Generic placeholder thumbnail">
<h4>{{hobby}}</h4>
<span class="text-muted">Something else</span>
</div>
</div>`
window.Dashboard = {
template,
// 声明当前子组件接收父组件的属性
props: ['hobbies']
}
})()
6.6.4.4 HomeList.js
将 HomeList.js 中列表<tr></tr>
抽取到一个新的 Item 组件中
;
(function () {
const template = `<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>工资</th>
</tr>
</thead>
<tbody>
<Item v-for="(emp, index) in empList" :key="emp.id" :emp="emp"/>
</tbody>
</table>
</div>`
window.HomeList = {
template,
//声明组件接收父组件传递的属性
props: {
empList: Array
},
data() {
return {
name: '梦学谷'
}
},
components: {
Item
}
}
})()
6.6.4.5 Item.js
新增的 Item.js 位于 vue-06-组件化通信\components\home 目录下
;
(function () {
const template = ` <tr><td>{{emp.id}}</td>
<td>{{emp.name}}</td>
<td>{{emp.salary}}</td>
</tr> `
window.Item = {
props: {
emp: { // 指定属性名/数据类型/是否必须
type: Object,
required: true
}
},
template
}
})()
6.6.4.6 index.js 引入 item.js
注意: 引入时的顺序和位置
<body>
<!-- 留一个组件的出口,此处要被子组件替换 -->
<div id="app">
</div>
<script src="../../node_modules/vue/dist/vue.js"></script>
<!-- 注意要在 vue.js 下面引入 -->
<script src="components/AppNavbar.js"></script>
<script src="components/AppLeaf.js"></script>
<!-- 注意:Dashboard.js 要在 Home.js 前面引入 -->
<script src="components/home/Item.js"></script>
<script src="components/Home/Dashboard.js"></script>
<script src="components/Home/HomeList.js"></script>
<script src="components/home/AppHome.js"></script>
<script src="App.js"></script>
<script src="main.js"></script>
</body>
6.6.5 props 案例-删除员工功能实现
6.6.5.1 AppHome.js
- 因为删除 emp 是对 empList 做更新操作,而 empList 是初始化在当前这个组件里,所以删除的函数要定义在 AppHome.js 这个组件中
- 向子组件传递 deleteEmp 函数 :deleteEmp=“deleteEmp”
;
(function () {
const template = `<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<!--右边上半区域-->
<h1 class="page-header">Dashboard</h1>
<dashboard :hobbies="hobbies"></dashboard>
<!--右边下半区域-->
<h2 class="sub-header">Section title</h2>
<home-list :emp-list="empList" :deleteEmp="deleteEmp"></home-list>
</div>`
window.AppHome = {
template,
data() {
return {
hobbies: ['看书', '台球', '睡觉', '撸代码'],
empList: [{
id: 1,
name: '放生1',
salary: 80001
}, {
id: 2,
name: '放生2',
salary: 80002
}, {
id: 3,
name: '放生3',
salary: 80003
}, {
id: 4,
name: '放生4',
salary: 80004
}]
}
},
methods: {
// 删除指定下标的数据
// 因为删除 emp 会对 empList 做更新操作,
// 而 empList 是初始化在当前这个组件里,所以删除的函数要定义在这个组件中
deleteEmp(index) {
this.empList.splice(index, 1)
}
},
components: {
// 子组件
Dashboard,
HomeList
}
}
})()
6.6.5.2 HomeList.js
- 模板中添加
<th>
操作</th>
- 父组件传递的 deleteEmp 函数在 Item.js 才会使用,所以需要通过 HomeList.js 逐层传递给 Item.js
- 将 index 索引值 传递给 Item.js 中的组件
;
(function () {
const template = `<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>工资</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<Item v-for="(emp, index) in empList" :key="emp.id" :emp="emp" :deleteEmp="deleteEmp" :index="index"/>
</tbody>
</table>
</div>`
window.HomeList = {
template,
//声明组件接收父组件传递的属性
props: {
empList: Array,
deleteEmp: Function // 逐层传递
},
data() {
return {
name: '梦学谷'
}
},
components: {
Item
}
}
})()
6.6.5.4 Item.js
- props 声明接收父组件传递的 deleteEmp 和 index 属性值
- 为 删除 按钮添加点击事件
<a href="#" @click="deleteItem">
删除</a>
- 定义 deleteItem 处理函数,其中调用 AppHome.js 传递过来 deleteEmp 函数。
;
(function () {
const template = ` <tr><td>{{emp.id}}</td>
<td>{{emp.name}}</td>
<td>{{emp.salary}}</td>
<td><a href="#" @click="deleteItem">删除</a> </td>
</tr> `
window.Item = {
props: {
emp: { // 指定属性名/数据类型/是否必须
type: Object,
required: true
},
deleteEmp: Function,
index: Number
},
template,
methods: {
//删除员工
deleteItem() {
if (window.confirm(`确定删除${this.emp.name}的记录吗?`)) {
// 移除索引为index的一条记录,
// 注意:不要少了 this
this.deleteEmp(this.index)
}
}
},
}
})()
6.6.6 自定义事件
作用:通过 自定义事件 来代替 props 传入函数形式
6.6.6.1 绑定自定义事件
在父组件中定义事件监听函数,并引用子组件标签上 v-on 绑定事件监听。
// 通过 v-on 绑定
// @自定义事件名=事件监听函数
// 在子组件 dashboard 中触发 delete_hobby 事件来调用 deleteHobby 函数
<dashboard @delete_hobby="deleteHobby"></dashboard>
6.6.6.2 触发监听事件函数执行
在子组件中触发父组件的监听事件函数调用
// 子组件触发事件函数调用
// this.$emit(自定义事件名, data)
this.$emit('delete_emp', index)
6.6.6.3 自定义事件注意
- 自定义事件只用于子组件向父组件发送消息(数据)
- 隔代组件或兄弟组件间通信此种方式不合适
6.6.7 自定义事件案例-删除仪表盘
6.6.7.1 Dashboard.js
在子组件中触发父组件的监听事件函数执行删除操作
;
(function () {
const template = `
<!--右边上半区域-->
<div class="row placeholders">
<div v-for="(hobby,index) in hobbies" :key="index" class="col-xs-6 col-sm-3 placeholder">
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAHd3dwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==" width="200"
height="200" class="img-responsive" alt="Generic placeholder thumbnail">
<h4>{{hobby}}</h4>
<span class="text-muted">
<a href="#" @click="deleteHobby">删除</a>
</span>
</div>
</div>`
window.Dashboard = {
template,
// 声明当前子组件接收父组件的属性
props: ['hobbies'],
methods: {
// 删除爱好
deleteHobby(index) {
// 触发父组件中 delete_hobby 事件进行删除操作
this.$emit('delete_hobby', index)
}
},
}
})()
6.6.7.2 AppHome.js
自定义事件删除函数deleteHobby
在引用 dashboard 组件标签上自定义事件, 绑定事件监听函数@delete_hobby=“deleteHobby”
;
(function () {
const template = `<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<!--右边上半区域-->
<h1 class="page-header">Dashboard</h1>
<!--通过自定义事件实现删除功能: @自定义事件名=事件监听函数 在子组件 dashboard 中触发 delete_hobby 事件来调用 deleteHobby 函数 -->
<dashboard :hobbies="hobbies" @delete_hobby="deleteHobby"></dashboard>
<!--右边下半区域-->
<h2 class="sub-header">Section title</h2>
<home-list :emp-list="empList" :deleteEmp="deleteEmp"></home-list>
</div>`
window.AppHome = {
template,
data() {
return {
hobbies: ['看书', '台球', '睡觉', '撸代码'],
empList: [{
id: 1,
name: '放生1',
salary: 80001
}, {
id: 2,
name: '放生2',
salary: 80002
}, {
id: 3,
name: '放生3',
salary: 80003
}, {
id: 4,
name: '放生4',
salary: 80004
}]
}
},
methods: {
// 删除指定下标的数据
// 因为删除 emp 会对 empList 做更新操作,
// 而 empList 是初始化在当前这个组件里,所以删除的函数要定义在这个组件中
deleteEmp(index) {
this.empList.splice(index, 1)
},
//删除爱好
deleteHobby(index) {
this.hobbies.splice(index, 1)
}
},
components: {
// 子组件
Dashboard,
HomeList
}
}
})()
注意: 删除员工有垮组件传递函数,不推荐使用自定义事件来实现删除
6.6.8 slot 插槽
作用: 主要用于父组件向子组件传递 标签+数据 , (而上面prop和自定事件只是传递数据)
场景:一般是某个位置需要经常动态切换显示效果(如饿了么)
6.6.8.1 子组件定义插槽
在子组件中定义插槽, 当父组件向指定插槽传递标签数据后, 插槽处就被渲染,否则插槽处不会被渲染.
<div>
<!-- name属性值指定唯一插槽名,父组件通过此名指定标签数据-->
<slot name="aaa">不确定的标签结构 1</slot>
<div>组件确定的标签结构</div>
<slot name="bbb">不确定的标签结构 2</slot>
</div>
6.6.8.2 父组件传递标签数据
<child>
<!--slot属性值对应子组件中的插槽的name属性值-->
<div slot="aaa">向 name=aaa 的插槽处插入此标签数据</div>
<div slot="bbb">向 name=bbb 的插槽处插入此标签数据</div>
</child>
6.6.8.3 插槽注意事项
- 只能用于父组件向子组件传递 标签+数据
- 传递的插槽标签中的数据处理都需要定义所在父组件中
6.6.9 slot 插槽案例
需求: 将Dashboard 的标题通过插槽显示
6.6.9.1 AppHome.js
将 Dashboard 的标题标签定义为插槽
;
(function () {
const template = `<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">
<!--右边上半区域-->
<h1 class="page-header">Dashboard</h1>
<!--定义插槽-->
<slot name="dashboard"></slot>
<!--通过自定义事件实现删除功能: @自定义事件名=事件监听函数 在子组件 dashboard 中触发 delete_hobby 事件来调用 deleteHobby 函数 -->
<dashboard :hobbies="hobbies" @delete_hobby="deleteHobby"></dashboard>
<!--右边下半区域-->
<h2 class="sub-header">Section title</h2>
<home-list :emp-list="empList" :deleteEmp="deleteEmp"></home-list>
</div>`
window.AppHome = {
template,
data() {
return {
hobbies: ['看书', '台球', '睡觉', '撸代码'],
empList: [{
id: 1,
name: '放生1',
salary: 80001
}, {
id: 2,
name: '放生2',
salary: 80002
}, {
id: 3,
name: '放生3',
salary: 80003
}, {
id: 4,
name: '放生4',
salary: 80004
}]
}
},
methods: {
// 删除指定下标的数据
// 因为删除 emp 会对 empList 做更新操作,
// 而 empList 是初始化在当前这个组件里,所以删除的函数要定义在这个组件中
deleteEmp(index) {
this.empList.splice(index, 1)
},
//删除爱好
deleteHobby(index) {
this.hobbies.splice(index, 1)
}
},
components: {
// 子组件
Dashboard,
HomeList
}
}
})()
6.6.9.2 App.js
向子组件中定义的插槽处传递标签数据。其中标题使用 title 数据绑定
;
(function () {
// 不要少了最外层的根元素 div
const template = `<div>
<app-navbar></app-navbar>
<!--核心区域:分左右两边-->
<div class="container-fluid">
<div class="row">
<!--左边菜单栏区域-->
<app-leaf></app-leaf>
<!--右边主页面区域: 分上下两个区域-->
<app-home>
<h1 slot="dashboard" class="page-header">{{title}}</h1>
</app-home>
</div>
</div>
</div>`
window.App = {
template,
components: {
AppNavbar, // 等价于 Navbar: Navbar
AppLeaf,
AppHome
},
data() {
return {
title: '仪表盘'
}
},
}
})()
6.7 非父子组件间通信 PubSubJS (补充知识)
6.7.1 介绍
Vue.js 可通过 PubSubJS 库来实现非父子组件之间的通信 ,使用 PubSubJS 的消息发布与订阅模式,来进行数据的传递。
理解:订阅信息 ==== 绑定事件监听 ,发布消息 ==== 触发事件。
注意: 但是必须先执行订阅事件 subscribe ,然后才能 publish 发布事件。
6.7.2 订阅消息(绑定事件监听)
先在 created 钩子函数中订阅消息
// event接收的是消息名称,data是接收发布时传递的数据
PubSub.subscribe('消息名称(相当于事件名)', function(event, data) {
// 事件回调处理
})
6.7.3 发布消息(触发事件)
PubSub.publish('消息名称(相当于事件名)', data)
6.7.4 案例
复制 vue-06-组件间通信 为 vue-06-组件间通信2
需求:在右侧删除爱好数据成功后,在左侧导航栏上显示已删除的总数,没有删除则不显示数量。
分析 :左侧导航与右侧爱好仪表盘是非父子组件,采用 PubSubJS 消息订阅发布实现功能
6.7.4.2 安装 pubsub-js
npm install pubsub-js
6.7.4.2 index.html 引入 pubsub-js 库
<script src="../../../node_modules/vue/dist/vue.js"></script>
<script src="./node_modules/pubsub-js/src/pubsub.js"></script>
6.7.4.3 AppLeaf.js 订阅消息
初始化数量 ,在 created 钩子中订阅消息,监听当删除爱好后,进行统计已删除总数
;
(function () {
const template = `<div class="col-sm-3 col-md-2 sidebar">
<ul class="nav nav-sidebar">
<li class="active">
<a href="#">Overview
<!--是否显示和显示总删除数-->
<span v-show="delNum">( {{ delNum }} )</span>
</a> </li> <li><a href="#">Reports</a>
</li> <li><a href="#">Analytics</a></li>
<li><a href="#">Export</a></li>
</ul>
</div>`
window.AppLeaf = {
template,
data: function () {
return {
message: '学员管理',
delNum: 0 //初始化数量
}
},
created() {
//一开始初始化实例时,就进行订阅消息
PubSub.subscribe('changeNum', (event, num) => {
// 箭头函数
// 删除成功
this.delNum = this.delNum + num
})
},
}
})()
6.7.4.4 AppHome.js 发布消息
methods: {
。。。
。。。
// 删除爱好
deleteHobby (index) {
this.hobbies.splice(index, 1) // 删除成功,发布消息,导航统计数据
PubSub.publish('changeNum', 1)
}
},
6.7.5 优点
不管是父子之间还是非父子之间通信 PubSubJS 都可以实现
6.8 单文件组件
6.8.1 当前存在的问题
上面定义组件时存在的问题:
- 全局定义 (Global definitions) 强制要求每个 component 中的命名不得重复
- 字符串模板 (String templates) 缺乏语法高亮,在 HTML 有多行的时候,需要用到丑陋的 \
- 不支持 CSS (No CSS support) 意味着当 HTML 和 JavaScript 组件化时,CSS 明显被遗漏
- 没有构建步骤 (No build step) 限制只能使用 HTML 和 ES5 JavaScript, 而不能使用预处理器,如 Pug (formerly Jade) 和 Babel
文件扩展名为 .vue 的 single-file components(单文件组件) 为以上所有问题提供了解决方法,并且还可以使用
webpack 或 Browserify 等构建工具。
6.8.2 单文件组件模板格式
<template>
// 组件的模块
</template>
<script>
// 组件的JS
</script>
<style>
// 组件的样式
</style>