Vue.js 四天课程学习笔记_第4天
课程内容概要:
1. 前情回顾
2. 通过demo再次复习一下 计算属性 是如何生成 过滤后的数组
3. Vue实例的生命周期(11个钩子)
4. 介绍组件化思想
5. 介绍 Element组件库 的安装和使用
6. 全局组件 与 局部组件
7. 组件模板的几种写法
8. 以.vue为后缀的单文件组件(需结合webpack)
9. 父子组件
10. 父子组件通信之Props Down
11. prop 单向数据流 与 prop 验证
11. 父子组件通信之Events Up
12. 非父子组件通信
13. 将上次完整功能的todoMVC拆分为组件组织形式(父子组件通信形式)
插入一句: 提到了 开发中没有银弹(出自<人月神话>)
前情回顾
1. 在Vue中如何解决表达式闪烁
1.1 v-text
1.2 v-cloak 与 [cloak]{display:none;}
当Vue解析处理完后,会把v-clock属性移除,则么display就不再是none了
2. v-show 与 v-if的区别
2.1 v-show 无论如何都会渲染, 再根据条件 显示,适合于 频繁 显示和隐藏时使用
2.2 v-if 真正的根据条件 再进行渲染
3. 计算属性
3.1 计算属性的使用方式是 属性, 不能当方法用,不能作为事件处理函数,虽然它本质上是gett/setter方法
3.2 计算属性本身不存储任何值, 计算属性 缓存计算结果
3.3 计算属性 与 v-model 同时使用,可发挥巨大力量
4. 观察者watch选项
4.1 可定向观察data选项中数据的改变,从而执行相应的业务逻辑
4.2 引用类型(如数组/对象)只能监视第1层,无法监视内部子成员的改变
4.3 当数据改变时,自动调用handler(value,oldValue)方法
4.4 deep属性可以尝试监视,immediate属性可以立即执行一次(无论有无数据的改变)
5. 在Vue中,使用计算属性(过滤数组) 具体见下面的demo vue_30.html
6. 自定义指令
6.1 当需要操作DOM的时候,使用自定义指令
6.2 Vue.directive('beyond-show',{})
6.3 参数2是一个对象 有5个可选的钩子函数
bind(el,binding)
bind钩子中,拿不到父元素
inserted(el,binding)
inserted钩子中,可以拿到父元素,focus必须写在inserted中
update(el,binding)
DOM里是 模板更新前的内容
componentUpdated(el,binding)
DOM里是 模板更新后的内容
unbind(el,binding)
收尾工作,例如,清除定时器
下面通过一个简单的demo再次复习一下, 如何通过 计算属性 来过滤数组的
vue_30.html效果如下:
vue_30.html代码如下:
<!-- 第3步, vue最终渲染结果的容器 -->
<div v-cloak id="id_div_container" style="color:#666;padding:10px 10px;border:1px solid Cadetblue;margin:4px;border-radius:8px;">
<!-- 双向绑定 -->
<input type="text" v-model="inputString"/>
<button class="class_btn class_btn_green"
v-on:click="queryBtnClicked">查询</button>
<br/>
<input type="radio" value="0" v-model="filterLoliType"/> 全部
<input type="radio" value="1" v-model="filterLoliType" /> loli
<input type="radio" value="2" v-model="filterLoliType" /> ロリじゃない
<hr/>
<table id="id_table" style="margin:0 auto;" class="sgcontentcolor">
<tr>
<th>女主角</th>
<th>芳龄</th>
<th>动漫名称</th>
<th>上映年代</th>
</tr>
<tr v-for="girl in filtedGirlArr">
<td>{{ girl.girlName }}</td>
<td>{{ girl.girlAge }} 岁</td>
<td>{{ girl.girlAnime }}</td>
<td>{{ girl.girlShowTime }}</td>
</tr>
</table>
</div>
<!-- 第1步,导入vue.js -->
<script type="text/javascript" src="/node_modules/vue/dist/vue.js"></script>
<!-- 第2步,生成一个全新的vue对象实例 -->
<script type="text/javascript">
var girlArr = [
{
"id": 1,
"girlName": "面码",
"girlAge": 15,
"girlAnime": "未闻花名",
"girlShowTime": "2011.04"
},
{
"id": 2,
"girlName": "逢坂大河",
"girlAge": 16,
"girlAnime": "龙与虎",
"girlShowTime": "2008.10"
},
{
"id": 3,
"girlName": "艾拉",
"girlAge": 9,
"girlAnime": "可塑性记忆",
"girlShowTime": "2015.04"
},
{
"id": 4,
"girlName": "平泽唯",
"girlAge": 14,
"girlAnime": "轻音少女",
"girlShowTime": "2009.04"
},
{
"id": 5,
"girlName": "mathilda",
"girlAge": 12,
"girlAnime": "这个杀手不太冷",
"girlShowTime": "1994.09"
},
]
var appVue = new Vue({
data: {
girlArr: girlArr,
// 用户输入的查询 条件 include方法中的参数 要用到
inputString: '',
// 用于计算属性的 过滤字符串
filterName: '',
// 0 全部; 1 loli; 2 非loli
filterLoliType: 0
},
methods: {
queryBtnClicked(){
NSLog('点击了按钮')
this.filterName = this.inputString
}
},
// 计算属性 过滤数组
computed: {
// 过滤后的数组
filtedGirlArr: {
get: function () {
var tempGirlArr = this.girlArr.filter(girl => girl.girlName.includes(this.filterName))
if(this.filterLoliType === '1'){
return tempGirlArr.filter(girl =>
girl.girlAge < 14 && girl.girlAge > 8
)
}else if(this.filterLoliType === '2'){
return tempGirlArr.filter(girl =>
( girl.girlAge < 8 || girl.girlAge >= 14)
)
}else{
return tempGirlArr
}
}
}
}
}).$mount('#id_div_container')
</script>
<p class="sgcenter sgcontentcolor">
<b>注意: 计算属性过滤数组 演示
</p>
Vue生命周期图示:
Vue实例的生命周期:
.
每个Vue实例在被创建之前fjtb要经过一系列的初始化过程
例如:
1. 需要设置数据监听
2. 编译模板
3. 挂载实例到DOM
4. 在数据变化时更新DOM等
因此提供了11个 生命周期钩子函数,供用户添加自己的业务代码
例如:
1. created钩子可以用来在一个实例被创建之后执行业务代码
new Vue({
data: {
girlName: '面码'
},
// 生命周期钩子
created: function(){
NSLog('hello ' + this.girlName)
}
})
// 输出: hello 面码
也有一些其他的钩子, 在实例生命周期不同的场景下调用
例如:
beforeCreate、created、
beforeMount、mounted、
beforeUpdate、updated、
activated、deactivated、
beforeDestroy、destroyed
errorCaptured 共11个钩子
注意: 不要在选项属性或回调上使用 箭头函数,因为 this 是和上级作用域绑定
例如: created: ()=> NSLog(this)
或者: appVue.$watch('girlName',newName => this.updateDB())
因为箭头函数是和父级上下文绑定的, this 不会如你所愿
常常导致 Cannot Read Property of undefined 或
this.updateDB is not a function之类的错误
下面演示一下Vue实例的生命周期(11个钩子)
vue_44.html代码如下
<!-- 第3步, vue最终渲染结果的容器 -->
<!-- 因Vue实例中,如果有更高优先级的 template选项,所以el挂载点将被取代 -->
<div id="id_div_container" style="color:#666;padding:10px 10px;border:1px solid Cadetblue;margin:4px;border-radius:8px;">
<div v-cloak id="id_div_vue">
<button class="class_btn class_btn_green" v-on:click="btnClicked">御坂{{ girlCount }}号</button>
</div>
<footer id="copyright" style="margin-top:16px;">
<p class="sgcenter sgcontentcolor" style="font-size:14px;">
<b>注意: Vue实例的生命周期
</p>
<p style="font-size:14px;text-align:center;font-style:italic;">
Copyright © <a id="author">2018</a> Powered by <a id="author">beyond</a>
</p>
</footer>
</div>
<!-- 第1步,导入vue.js -->
<script type="text/javascript" src="/node_modules/vue/dist/vue.js"></script>
<script type="text/javascript">
var appVue = new Vue({
el: '#id_div_vue',
data: {
// 御坂9982号
girlCount: 9982
},
methods: {
btnClicked(){
this.girlCount += 1
}
},
// 11个生命周期函数
beforeCreate(){
NSLog('beforeCreate: ' + this.girlCount)
},
created(){
NSLog('created: ' + this.girlCount)
},
beforeMount(){
NSLog('beforeMount: ' + this.girlCount)
},
mounted(){
NSLog('mounted: ' + this.girlCount)
},
beforeUpdate(){
NSLog('beforeUpdate: ' + this.girlCount)
},
updated(){
NSLog('updated: ' + this.girlCount)
},
activated(){
NSLog('activated: ' + this.girlCount)
},
deactivated(){
NSLog('deactivated: ' + this.girlCount)
},
beforeDestroy(){
NSLog('beforeDestroy: ' + this.girlCount)
},
destroyed(){
NSLog('destroyed: ' + this.girlCount)
},
errorCaptured(){
NSLog('errorCaptured: ' + this.girlCount)
}
})
</script>
第一次运行时,效果如下:
注意: beforeCreated里面 还没有data数据
单击按钮一次, 效果如下:
下面开始隆重介绍 组件 Component
组件(Component)是Vue.js最强大的功能之一,也是最核心的技能之一
组件可以扩展HTML元素,封装可重用的HTML/CSS/JS代码
组件的封装,就是封装一个自定义的HTML标签
例如<beyond-rate> </beyond-rate>
组件示意图如下:
先来一下总览:
在Vue.js中,一个组件 就是一个拥有预定义选项的 Vue 实例
在Vue中,定义一个组件:
Vue.compent('beyond-item',{
template: '<li> 未闻花名 </li>'
})
使用一个组件
<ul>
<beyond-item > </beyond-item>
</ul>
渲染结果:
<ul>
<li> 未闻花名 </li>
</ul>
在Vue中, 组件 是用来封装视图的,即HTML
组件化思想,就是把复杂的Web页面,拆分成一个个 组件视图
包括: HTML 结构
CSS样式
JS行为
优点:
开发效率高
可维护性好
可重用性好
下面介绍 一下基于Vue.js 2.0的一个桌面端的组件库:
element ui
官网: element-cn.eleme.io
第1步, 安装 (使用 npm ):
npm install element-ui --save
版本: v2.3.7
第2步, 配置 (引入css和js):
<!-- 使用element-ui 第1步,引入css 和 js -->
<link rel="stylesheet" href="/node_modules/element-ui/lib/theme-chalk/index.css" />
<script type="text/javascript" src="/node_modules/element-ui/lib/index.js"></script>
第3步, 使用: (参照官方文档)
vue_31.html效果如下:
vue_31.html代码如下:
<!-- 第3步, vue最终渲染结果的容器 -->
<div v-cloak id="id_div_container" style="color:#666;padding:10px 10px;border:1px solid Cadetblue;margin:4px;border-radius:8px;">
<el-rate v-model="value" show-text v-bind:texts="rateTextArr">
</el-rate>
</div>
<!-- 第1步,导入vue.js -->
<script type="text/javascript" src="/node_modules/vue/dist/vue.js"></script>
<!-- 使用element-ui 第2步,引入css 和 js -->
<script type="text/javascript" src="/node_modules/element-ui/lib/index.js"></script>
<!-- 第2步,生成一个全新的vue对象实例 -->
<script type="text/javascript">
var appVue = new Vue({
data: {
value: 0,
rateTextArr: ['名作','一般','良作','佳作','神作']
},
methods: {
queryBtnClicked(){
NSLog('点击了按钮')
this.filterName = this.inputString
}
}
}).$mount('#id_div_container')
</script>
<p class="sgcenter sgcontentcolor">
<b>注意: element-uiのrate组件 演示
</p>
下面是另一个element-ui的组件示例: 日期选择组件
vue_32.html效果如下:
vue_32.html代码如下:
<!-- 第3步, vue最终渲染结果的容器 -->
<div v-cloak id="id_div_container" style="color:#666;padding:10px 10px;border:1px solid Cadetblue;margin:4px;border-radius:8px;">
<el-date-picker v-model="value" type="date" placeholder="放送日期" >
</el-date-picker>
</div>
<!-- 第1步,导入vue.js -->
<script type="text/javascript" src="/node_modules/vue/dist/vue.js"></script>
<!-- 使用element-ui 第2步,引入css 和 js -->
<script type="text/javascript" src="/node_modules/element-ui/lib/index.js"></script>
<!-- 第2步,生成一个全新的vue对象实例 -->
<script type="text/javascript">
var appVue = new Vue({
data: {
value: ''
},
methods: {
queryBtnClicked(){
NSLog('点击了按钮')
this.filterName = this.inputString
}
}
}).$mount('#id_div_container')
</script>
<p class="sgcenter sgcontentcolor">
<b>注意: element-ui の date-picker组件 演示
</p>
下面再介绍一下 antd 的Web UI组件库的使用
http://ant.design/components/upload-cn/
https://mobile.ant.design/components/popover-cn/ (移动端Mobile组件库)
下面介绍一下 radon UI组件库的使用
下面介绍一下 iView组件库的使用
插入一个,与组件无关的效果:
组件的定义
1. 全局定义
1.1 全局定义的组件,在任意组件中都可以使用
1.2 注册全局组件
Vue. component('beyond-component',{template: '<div> 未闻花名 </div>'
})
1.3 使用组件
<div id="id_div_container">
<beyond-component> </beyond-component>
</div>
1.4 渲染结果:
<div id="id_div_container">
<div> 未闻花名 </div>
</div>
vue_33.html代码如下:
<!-- 第3步, vue最终渲染结果的容器 -->
<div v-cloak id="id_div_container" style="color:#666;padding:10px 10px;border:1px solid Cadetblue;margin:4px;border-radius:8px;">
<!-- 组件 第2步: 使用一个全局组件 -->
<beyond-component> </beyond-component>
</div>
<!-- 第1步,导入vue.js -->
<script type="text/javascript" src="/node_modules/vue/dist/vue.js"></script>
<!-- 第2步,生成一个全新的vue对象实例 -->
<script type="text/javascript">
// 组件使用 第1步: 定义一个全局组件
Vue.component('beyond-component',{
template: '<div> 我们仍未知道那年夏天所见到的花的名字 </div>'
})
var appVue = new Vue({
data: {},
methods: {}
}).$mount('#id_div_container')
</script>
<p class="sgcenter sgcontentcolor">
<b>注意: 全局组件 演示
</p>
vue_33.html效果如下:
vue_34.html代码如下:
<!-- 第3步, vue最终渲染结果的容器 -->
<div v-cloak id="id_div_container" style="color:#666;padding:10px 10px;border:1px solid Cadetblue;margin:4px;border-radius:8px;">
<!-- 组件 第2步: 使用一个局部组件 -->
<beyond-component> </beyond-component>
</div>
<!-- 第1步,导入vue.js -->
<script type="text/javascript" src="/node_modules/vue/dist/vue.js"></script>
<!-- 第2步,生成一个全新的vue对象实例 -->
<script type="text/javascript">
var appVue = new Vue({
data: {},
methods: {},
// 组件使用 第1步: 定义一个局部组件
components: {
'beyond-component': {
template: '<div> 我们仍未知道那年夏天所见到的花的名字 </div>'
}
}
}).$mount('#id_div_container')
</script>
<p class="sgcenter sgcontentcolor">
<b>注意: 局部组件 演示
</p>
2. 局部定义
2.1 局部定义的组件,只能在 当前组件中使用
2.2 定义局部组件
var appVue = new Vue({
data: {},
methods: {},
// 组件使用 第1步: 定义一个局部组件
components: {
'beyond-component': {
template: '<div> 我们仍未知道那年夏天所见到的花的名字 </div>'
}
}
}).$mount('#id_div_container')
2.3 使用局部组件
<!-- 组件 第2步: 使用一个局部组件 -->
<beyond-component> </beyond-component>
vue_34.html效果如下:
注意: 1. 组件的data必须是 函数
2. 构造Vue实例时的各选项, 在组件里也一样可以有
3. 唯一例外的是 data选项, 必须是一个函数
例如:
// 全局组件
Vue.component('beyond-counter',{
template: '<button v-on:click="number++">{{ number }} </button>',
// data选项必须是函数
data: function(){
var obj = {
number: 0
}
// 组件中的data 函数, 必须返回一个对象
return obj
}
})
// 使用全局组件
<div id="id_div_container">
<beyond-counter></beyond-counter>
</div>
使用js中的字符串字面量来定义template模板
vue_35.html代码如下:
<!-- 第3步, vue最终渲染结果的容器 -->
<div v-cloak id="id_div_container" style="color:#666;padding:10px 10px;border:1px solid Cadetblue;margin:4px;border-radius:8px;">
<!-- 组件 第2步: 使用一个全局组件 -->
<beyond-counter> </beyond-counter>
</div>
<!-- 第1步,导入vue.js -->
<script type="text/javascript" src="/node_modules/vue/dist/vue.js"></script>
<!-- 使用element-ui 第2步,引入css 和 js -->
<!-- <script type="text/javascript" src="/node_modules/element-ui/lib/index.js"></script> -->
<!-- 第2步,生成一个全新的vue对象实例 -->
<script type="text/javascript">
// 组件使用 第1步: 定义一个全局组件
Vue.component('beyond-counter',{
template: '<button v-on:click="number++" class="class_btn class_btn_green">{{ number }}个萝莉 </button>',
// data选项必须是函数
data: function(){
var obj = {
number: 0
}
// 组件中的data 函数, 必须返回一个对象
return obj
}
})
var appVue = new Vue({
data: {},
methods: {
queryBtnClicked(){
NSLog('点击了按钮')
this.filterName = this.inputString
}
}
}).$mount('#id_div_container')
</script>
<p class="sgcenter sgcontentcolor">
<b>注意: 组件中的data选项必须是一个函数,返回一个对象
</p>
使用js中的字符串字面量来定义template模板
vue_35.html效果如下:
使用script标签来创建组件的模板(template)
vue_36.html代码如下:
<!-- 使用script标签来创建模板 -->
<script type="text/x-template" id="id_template_beyond-counter">
<button v-on:click="number++" class="class_btn class_btn_green">{{ number }}个萝莉 </button>
</script>
<!-- 第2步,生成一个全新的vue对象实例 -->
<script type="text/javascript">
// 组件使用 第1步: 定义一个全局组件
Vue.component('beyond-counter',{
// 使用的是 script作为template
template: '#id_template_beyond-counter',
// data选项必须是函数
data: function(){
var obj = {
number: 0
}
// 组件中的data 函数, 必须返回一个对象
return obj
}
})
var appVue = new Vue({
data: {},
methods: {
queryBtnClicked(){
NSLog('点击了按钮')
this.filterName = this.inputString
}
}
}).$mount('#id_div_container')
</script>
<p class="sgcenter sgcontentcolor">
<b>注意: 使用script标签来创建模板<br/>组件中的data选项必须是一个函数,返回一个对象
</p>
使用script标签来创建组件的模板(template)
vue_36.html效果如下:
对低开销的静态组件(包含大量静态内容) 请使用v-once,将其缓存起来
组件的模板template 只能有一个根节点
1. DOM模板
2. JS内联字符串模板,缺点是: 没有高亮,与js代码揉杂在一起,不利于开发与维护
例如: template: '<h1>未闻花名</h1>'
3. 写在特定的script标签中, 可以进行高亮
但是,它将组件的模板 与 组件的其他选项分离开了
除非是在演示应用 或 特别小的应用中,否则不推荐这样写
具体做法是:
3.1 type 指定为 x-template
3.2 起一个id
3.3 在component定义中使用
template: '#id'
<h2>那朵花</h2>
</script>
在组件定义中是这样:
Vue.component('beyond-component',{
// 使用srcipt来创建模板
template: '#id_template_beyond',
// data必须是一个返回对象的函数
data: function(){
var obj = {girlName: ''}
return obj
}
})
4. 以.vue作为后缀名的单文件中的 template模板(强烈推荐)
为啥推荐单独一个.vue作为后缀的单文件 为作为组件的template呢?
首先,我们来看一看传统的定义的缺点:
4.1 全局定义 强制要求每一个组件的 名字不能重复
4.2 字符串模板 没有语法高亮,特别在有多行的时候
4.3 不支持CSS样式
4.4 ???Excuse Me??? 没有构建步骤
只能使用HTML及ES5,不能使用Babel 及 Pug(曾经的Jade)
而扩展名为.vue的single-file component则完全没有上面的缺点
并且还有如下优点
4.1 完整的语法高亮
4.2 组件作用域的CSS
4.3 CommonJS模块
下面是一个componet_beyond.vue的简单示例:
<template>
<h1> {{ girlName }} </h1>
</template>
<script>
// export default {
// 或者
module.exports = {
data: function(){
var obj = {
girlName: '面码'
}
return obj
}
}
</script>
<style scoped>
h1 {
color:white;
text-shadow:2px 2px 4px #000;
letter-spacing:5px;
}
</style>
再次强调:
1. 组件 是独立的作用域
2. 组件 无法访问外部 作用域的成员
3. 外部作用域也无法访问组件内部成员
组件 最常见的就是形成父子组件的关系,
即: 组件A 在其template模板 中,使用了 组件B
组件通信:
父组件 下发数组给 子组件
子组件 将其内部发生的事 告知父组件
总结就是:
Prop向下传递
事件向上传递
如图所示:
组件设计初衷就是为了配合使用
最常见的就是形成父子组件的关系
父组件给子组件下发数据
子组件则将其内部发生的事件告诉父组件
总结来说: Props向下传递数据
Event向上传递事件
组件其实就是一个特殊的Vue实例,可以有自己的选项
(例如选项 data,methods,computed,watch等, 其中data是返回一个对象的函数)
组件的 data必须是一个方法, 返回一个对象,作为组件的数据源
组件一般分为两大类:
1. 全局组件
通用的组件,例如:评分,日期选择器,轮播图等
2. 局部组件
与具体业务绑定的组件,不通用
下面用一个demo来演示一下, 组件的作用域是独立的
vue_37.html代码如下:
vue_37.html效果如下:
<!-- 第3步, vue最终渲染结果的容器 -->
<div v-cloak id="id_div_container" style="color:#666;padding:10px 10px;border:1px solid Cadetblue;margin:4px;border-radius:8px;">
<!-- 组件 第2步: 使用一个全局组件 -->
<beyond-component> </beyond-component>
<br/>
<hr/>
<br/>
<!-- 演示一下 组件 独立的作用域 -->
<beyond-component> </beyond-component>
</div>
<!-- 第1步,导入vue.js -->
<script type="text/javascript" src="/node_modules/vue/dist/vue.js"></script>
<!-- 第2步,生成一个全新的vue对象实例 -->
<script type="text/javascript">
// 组件使用 第1步: 定义一个全局组件
Vue.component('beyond-component',{
// 使用的是 js中的字符串 作为template
// template必须有且只有一个根节点
template: `<div>
<h3>{{ girlName }}</h3>
<input type="text" v-model="girlName" />
<button v-on:click="showGirlBtnClicked" class="clas_btn class_btn_green">弹出组件自己的girl</button>
<input type="checkbox" v-model="isVisible" />
<div v-show="isVisible"
class="class_div_cube class_div_center"></div>
</div>
`,
// data选项必须是函数
data: function(){
var obj = {
girlName: '',
isVisible: true
}
// 组件中的data 函数, 必须返回一个对象
return obj
},
methods: {
showGirlBtnClicked(){
window.alert(this.girlName)
}
}
})
var appVue = new Vue({
data: {},
methods: {}
}).$mount('#id_div_container')
</script>
<p class="sgcenter sgcontentcolor">
<b>注意: 组件的作用域是独立的<br/>组件中的data选项必须是一个函数,返回一个对象
</p>
下面演示一下 全局组件的嵌套
vue_38.html代码如下:
<!-- 第3步, vue最终渲染结果的容器 -->
<div v-cloak id="id_div_container" style="color:#666;padding:10px 10px;border:1px solid Cadetblue;margin:4px;border-radius:8px;">
<!-- 组件 第2步: 使用一个全局组件 -->
<!-- 演示一下 全局组件 嵌套 -->
<beyond-component-1> </beyond-component-1>
</div>
<!-- 第1步,导入vue.js -->
<script type="text/javascript" src="/node_modules/vue/dist/vue.js"></script>
<!-- 第2步,生成一个全新的vue对象实例 -->
<script type="text/javascript">
// -----------------全局组件1---------------------
// 组件使用 第1步: 定义一个全局组件
Vue.component('beyond-component-1',{
// 使用的是 js中的字符串 作为template
// template必须有且只有一个根节点
template: `<div>
<h2>{{ girlName }}</h2>
<beyond-component-2></beyond-component-2>
</div>`,
// data选项必须是函数
data: function(){
var obj = {
girlName: '面码',
}
// 组件中的data 函数, 必须返回一个对象
return obj
},
methods: {}
})
// -----------------全局组件2---------------------
Vue.component('beyond-component-2',{
template: `<div>
<h4>{{ girlName }}</h4>
</div>`,
// data选项必须是一个返回对象的函数
data: function(){
var obj = {
girlName: 'めんま'
}
return obj
},
methods: {}
})
// -----------------Vue实例---------------------
var appVue = new Vue({
data: {},
methods: {}
}).$mount('#id_div_container')
</script>
<p class="sgcenter sgcontentcolor">
<b>注意: 全局组件嵌套<br/>组件中的data选项必须是一个函数,返回一个对象
</p>
全局组件的嵌套
vue_38.html效果如下:
下面再演示一下 全局组件 嵌套 局部组件
核心代码:
// -----------------全局组件1---------------------
// 组件使用 第1步: 定义一个全局组件
Vue.component('beyond-component-1',{
// 使用的是 js中的字符串 作为template
// template必须有且只有一个根节点
template: `<div>
<h2>{{ girlName }}</h2>
<beyond-component-2></beyond-component-2>
</div>`,
// components 选项, 局部组件,只能在当前组件中使用
components: {
// 属性名,就是 局部组件名; 值是一个对象,在对象中配置局部组件的 各选项
// 属性名,必须是驼峰形式,不能用连接符-
beyondComponent2: {
// 局部组件模板
template: `<div>
<h4>{{ girlName }}</h4>
</div>`,
// 组件中data是一个函数,必须返回一个对象
data: function(){
var obj = {
girlName: 'めんま'
}
return obj
}
}
},
// data选项必须是函数
data: function(){
var obj = {
girlName: '面码',
}
// 组件中的data 函数, 必须返回一个对象
return obj
},
methods: {}
})
全局组件 嵌套 局部组件
vue_39.html 完整代码如下:
<!-- 第3步, vue最终渲染结果的容器 -->
<div v-cloak id="id_div_container" style="color:#666;padding:10px 10px;border:1px solid Cadetblue;margin:4px;border-radius:8px;">
<!-- 组件 第2步: 使用一个全局组件 -->
<!-- 演示一下 全局组件 嵌套 局部组件-->
<beyond-component-1> </beyond-component-1>
</div>
<!-- 第1步,导入vue.js -->
<script type="text/javascript" src="/node_modules/vue/dist/vue.js"></script>
<!-- 第2步,生成一个全新的vue对象实例 -->
<script type="text/javascript">
// -----------------全局组件1---------------------
// 组件使用 第1步: 定义一个全局组件
Vue.component('beyond-component-1',{
// 使用的是 js中的字符串 作为template
// template必须有且只有一个根节点
template: `<div>
<h2>{{ girlName }}</h2>
<beyond-component-2></beyond-component-2>
</div>`,
// components 选项
components: {
// 属性名,就是 局部组件名; 值是一个对象,在对象中配置局部组件的 各选项
// 属性名,必须是驼峰形式,不能用连接符-
beyondComponent2: {
// 局部组件模板
template: `<div>
<h4>{{ girlName }}</h4>
</div>`,
// 组件中data是一个函数,必须返回一个对象
data: function(){
var obj = {
girlName: 'めんま'
}
return obj
}
}
},
// data选项必须是函数
data: function(){
var obj = {
girlName: '面码',
}
// 组件中的data 函数, 必须返回一个对象
return obj
},
methods: {}
})
// -----------------Vue实例---------------------
var appVue = new Vue({
data: {},
methods: {}
}).$mount('#id_div_container')
</script>
<p class="sgcenter sgcontentcolor">
<b>注意: 全局组件 嵌套 局部组件<br/>
局部组件名,必须用小驼峰,不能用连接符-
<br/>组件中的data选项必须是一个函数,返回一个对象
</p>
全局组件 嵌套 局部组件
vue_39.html 效果如下:
补充一下:
在Vue实例中,如果也声明了模板template选项(高优先级),那么它将覆盖掉el选项挂载的html内的节点
vue_40.html代码如下:
<!-- 第3步, vue最终渲染结果的容器 -->
<!-- 因Vue实例中,有更高优先级的 template选项,所以el挂载点将被取代 -->
<div v-cloak id="id_div_container" style="color:#666;padding:10px 10px;border:1px solid Cadetblue;margin:4px;border-radius:8px;">
</div>
<!-- 第1步,导入vue.js -->
<script type="text/javascript" src="/node_modules/vue/dist/vue.js"></script>
<!-- 第2步,生成一个全新的vue对象实例 -->
<script type="text/javascript">
// -----------------Vue实例---------------------
var appVue = new Vue({
// Vue实例的template选项,将替换掉el挂载点
template: '<beyond-component></beyond-component>',
// 定义一个局部组件
components: {
// 组件名
beyondComponent: {
// 绑定组件自己的girlName
template: "<h3 style='text-align:center;color:teal;'>{{ girlName }}</h3>",
// 组件data选项是一个函数,返回一个对象
data: function () {
var obj = {
// 供组件自己的template用
girlName: '面码'
}
return obj
}
}
},
data: {
// 混淆视听
girlName: 'menma'
},
methods: {}
}).$mount('#id_div_container')
</script>
<p class="sgcenter sgcontentcolor">
<b>注意: 组件中的data选项必须是一个函数,返回一个对象<br/>Vue实例中的template选项优先级更高,将取代el挂载点
</p>
vue_40.html效果如下:
下面介绍 父组件 如何 向 子组件 传递数组
父子组件通信: Props Down
注意: 父组件中的数据发生变化时,所有子组件都会同步更新
注意: 子组件能修改父组件的引用数据类型里的成员的值,但不建议这样做
注意: 子组件不能对父组件中的任何数据进行重新赋值
注意: 子组件 只能使用 来自父组件的数据, 万万不可重新赋值!!!
否则报错:
因为违背了Vue的通信原则: 单向数据流
正确做法是: 在事件发生时,把数据通过自定义事件的参数上交给父组件
例如:
<beyond-footer key_author="面码"></beyond-footer>
或者 通过v-bind 传递 reactive data
<beyond-footer v-bind:key_author="girlName"></beyond-footer>
2. 在子组件中 显式地用 props选项 声明它预期的数据,并使用
Vue.component('beyond-footer',{
// 2.1 声明props
props: ['key_author']
// 2.2 像data方法返回的对象一样 使用
template: '<span> {{ key_author }} </span>'
})
总结起来 父组件 传递数据给子组件 就是分两步走:
从父组件 传递数据给子组件, 只要两步
第1步: 在父组件的template中, 使用v-bind在子组件的属性上传值
注: girlName来自父组件的data方法返回的对象
<div>
<h3>这是父组件</h3>
<!-- 父组件通过 v-bind 子组件的 标签属性, 给子组件传递数据-->
<beyond-footer v-bind:key_author="girlName"></beyond-footer>
</div>
第2步: 在子组件中的 props选项里声明,然后就可以像数组一样使用了
// 把小组件对象 添加到window上,供大组件 beyond-component.js使用
window.beyondFooter = {
// 声明来自父组件的数据
// 这样就可以在模板template中使用 父组件的数据了
props: ['key_author'],
// 简写属性
template
}
然后, 在子组件的template中,像使用其data方法返回的对象中的数据一样,使用来自父组件的数据
<a id="author">2018</a> Powered by <a id="author">「{{ key_author }}」</a>
注意: 由于 html 是不区分大小写的,
所以当使用的不是字符串模板时,
camelCase(驼峰式命名)的props需要 转换成 相对应的kebab-case(短横线分隔式命名)
注意: 如果使用的是字符串模板,则没这些限制
父组件如下:
// 在父组件的template中,使用子组件
// 因html不区分大小写,所以 给子组件的属性bind数据时,要将属性改成 - 连接
Vue.component('beyond-component',{
// 在模板中,使用子组件
template: '<beyond-footer v-bind:key-author="{{ girlName }}"></beyond-footer>',
// 组件中的data 必须是返回一个对象的函数
data: function(){
var obj = {
girlName: '面码'
}
return obj
}
})
子组件如下:
// 子组件如下:
Vue.component('beyond-footer',{
// 在JS中的props使用 camelCase
// 声明接收来自父类的数据,然后就可以像data一样使用了
props: ['keyAuthor'],
template: '<span> {{keyAuthor}} </span>'
})
.
项目组织如下:
app入口是vue_41.js
父组件是vue_41_beyond_component.js
子组件是vue_41_beyond_footer.js
vue_41.html代码如下:
<!-- 第3步, vue最终渲染结果的容器 -->
<!-- 因Vue实例中,有更高优先级的 template选项,所以el挂载点将被取代 -->
<div v-cloak id="id_div_container" style="color:#666;padding:10px 10px;border:1px solid Cadetblue;margin:4px;border-radius:8px;">
<beyond-component > </beyond-component>
</div>
<!-- 第1步,导入vue.js -->
<script type="text/javascript" src="/node_modules/vue/dist/vue.js"></script>
<!-- 第2步,导入 子组件beyond-footer.js -->
<script type="text/javascript" src="/public/js/vue_41_beyond_footer.js"></script>
<!-- 第2步,导入 父组件beyond-component.js -->
<script type="text/javascript" src="/public/js/vue_41_beyond_component.js"></script>
<!-- 第4步,导入程序入口js -->
<script type="text/javascript" src="/public/js/vue_41.js"></script>
app入口 vue_41.js如下:
new Vue({
el: '#id_div_container',
components: {
// 简写属性,局部组件,同时也是父组件
beyondComponent
}
})
父组件vue_41_beyond-component.js代码如下:
;(
function () {
// 组件 必须有且仅一个根节点
const template = `
<div>
<h3>这是父组件</h3>
<!-- 父组件通过 v-bind 子组件的 标签属性, 给子组件传递数据-->
<beyond-footer v-bind:key_author="girlName"></beyond-footer>
</div>
`
// 把大组件对象 添加到window上,供vue_41.js使用
window.beyondComponent = {
// 简写属性
template,
// 子组件(局部组件,为了演示传值)
components:{
// 简写属性
beyondFooter
},
// 父组件 传递数据 给子组件
// 组件中 data 是一个返回对象的方法
data: function () {
var obj = {
girlName: '面码'
}
return obj
}
}
}
)()
子组件vue_41_beyond_footer.js代码如下:
;( function () {
// 这个girlName 来自父组件(beyond-component)
// 组件 必须有且仅一个根节点
const template = `
<footer id="copyright">
<p style="font-size:14px;text-align:center;font-style:italic;"> <br/><br/>注意: 这是子组件, 下面的数据 「{{key_author}} 」来自父组件<br/>
Copyright © <a id="author">2018</a> Powered by <a id="author">「{{ key_author }}」</a>
</p>
</footer>
`
// 把小组件对象 添加到window上,供大组件 beyond-component.js使用
window.beyondFooter = {
// 声明来自父组件的数据
// 这样就可以在模板template中使用 父组件的数据了
props: ['key_author'],
// 简写属性
template
}
}
)()
vue_41.html效果如下:
单向数据流
Prop是单向绑定的: 当父组件的属性变化时,将传给子组件
但是,反过来不会
这是为了防止 子组件无意间修改了父组件的状态造成混乱
每次父组件更新时, 子组件的所有props都会同步更新
你不应该在子组件内部改变props的值
1. 针对我们想把传递进来的值 作为局部变量使用的情况
我们可以在data返回的对象中,定义一个局部变量, 并用props的值初始化它
然后我们就可以一直使用这个局部变量了
例如:
props: ['key_number'],
data: function(){
var obj = {
tempNumber: this.key_number
}
return obj
}
2. 针对于props传递进来的值需要处理才能使用的情况
我们可以定义一个计算属性处理props数据,并返回
例如:
props: ['key_name'],
computed: {
tempName: function{
return this.key_name.trim().toLowerCase()
}
}
注意:
在JS中,对象和数组是 引用类型, 指向同一个内存空间
如果props是一个对象或数组,那么一旦在子组件中改变它的话,
将会影响到父组件中的状态
Props验证
我们可以为组件的props指定一个验证规则
如果传入的数据不符合要求, Vue就会发出警告
要指定验证规则 , 需要用对象的形式来定义prop(不能用字符串数组)
例如:
Vue.component('beyond-btn',{
props: {
// 基础类型检测(null表示允许任何值)
propA: Number,
// 可以是多种类型
propB: [String,Number],
// 必须是字符串
propC: {
type: String,
required: true
},
// 带有默认值的数字
propD: {
type: Number,
default: 67
},
// 带有默认值的对象
propE: {
type: Object,
default: function(){
var obj = {
girlName: '面码',
girlAge: 13
}
return obj
}
},
// 自定义验证函数
propF: {
validator: function(value){
// 这个值必须匹配下列字符串中的一个
return ['面码','mathilda','逢坂大河'].indexOf(value) !== -1
}
}
}
})
注意: 当prop 验证失败时, Dev环境下 Vue 会报警告
注意: 上面那些prop 会在一个 组件实例 创建之前 进行验证,
所以实例的属性在default或validator函数中是不可用的
下面开始介绍 父子组件通信: Events Up
步骤1. 在子组件中,当事件发生时(如按钮被点击) 调用this.$emit()方法 发送一个事件,
参数是: 一个自定义事件,例如: beyond-click (注意: 这儿必须用 - 连接,不能用小驼峰)
Vue.component('beyond-button',{
// 子组件中有一个按钮
template: '<button v-on:click=" btnClicked">{{ girlNumber }}个萝莉</button>',
// 组件data选项必须是一个返回对象的函数
data: function(){
var obj = {
girlNumber
}
return obj
},
// 响应按钮点击事件
methods: {
btnClicked: function(){
// 1.先处理自己的逻辑
this.girlNumber += 1
// 2.再告诉父组件(手动发布一个自定义事件)
// 注意: 这儿必须是 - 连接, 不能是小驼峰
this.$emit(' beyond-click')}
}
})
步骤2: 在父组件的template中的子组件上 注册监听 自定义事件(beyond-click)
在父组件(注册)监听子组件的自定义事件(beyond-click),
当子组件内部手动发出自定义事件(beyond-click)的消息,父组件就能监听到
<div id="#id_div_vue">
<p>{{ girlCount }}</p><!--
监听 自定义事件 beyond-click
当子组件内部手动发出了beyond-click自定义事件时,
父组件将执行相应的事件处理函数
注意: 这儿必须是 - 连接, 不能是小驼峰
-->
<beyond-button v-on: beyond-click="btnClickedCallback"></beyond-button></div>
步骤3:
new Vue({el: '#id_div_vue',
data: {
girlCount: 0
},
methods: {
// 子组件内部事件发生时,回调处理函数
btnClickedCallback: function(){
this.girlCount += 1
}
}
})
下面演示一下子组件如何 向上传递事件 给父组件
vue_42.html效果如下:
vue_42.html代码如下:
<!-- 第3步, vue最终渲染结果的容器 -->
<!-- 因Vue实例中,如果有更高优先级的 template选项,所以el挂载点将被取代 -->
<div id="id_div_container" style="color:#666;padding:10px 10px;border:1px solid Cadetblue;margin:4px;border-radius:8px;">
<div v-cloak id="id_div_vue">
<p>父组件: {{ girlCount }}</p>
<!--
监听 自定义事件 beyondClick
当子组件内部手动发出了beyondClick自定义事件时,
父组件将执行相应的事件处理函数
这儿必须是 - 连接???Excuse Me???
-->
<beyond-button v-on:beyond-click="btnClickedCallback"></beyond-button>
</div>
<footer id="copyright">
<br/>
<p class="sgcenter sgcontentcolor" style="font-size:14px;">
<b>注意: 子组件emit自定义事件
</p>
<p style="font-size:14px;text-align:center;font-style:italic;">
Copyright © <a id="author">2018</a> Powered by <a id="author">beyond</a>
</p>
</footer>
</div>
<!-- 第1步,导入vue.js -->
<script type="text/javascript" src="/node_modules/vue/dist/vue.js"></script>
<!-- 第2步,导入 子组件beyond-btn.js -->
<script type="text/javascript" src="/public/js/vue_42_beyond_btn.js"></script>
<!-- 第4步,导入程序入口js -->
<script type="text/javascript" src="/public/js/vue_42.js"></script>
app入口vue_42.js代码如下:
new Vue({
el: '#id_div_vue',
data: {
girlCount: 0
},
methods: {
// 子组件内部事件发生时,回调处理函数
btnClickedCallback: function(){
this.girlCount += 1
}
}
})
全局子组件vue_42_beyond_btn.js代码如下:
const template = `<button class="class_btn class_btn_green" v-on:click="btnClicked">子组件: {{ girlNumber }}个萝莉</button>`
Vue.component('beyond-button',{
// 子组件模板中有一个按钮
template,
// 组件data选项必须是一个返回对象的函数
data: function(){
var obj = {
girlNumber: 0
}
return obj
},
// 响应按钮点击事件
methods: {
btnClicked: function(){
// 1.先处理自己的逻辑
this.girlNumber += 1
// 2.再告诉父组件(手动发布一个自定义事件)
// 这儿必须是 - 连接???Excuse Me???
this.$emit('beyond-click')
}
}
})
再来一个实例, 演示的是父组件props传递数据给子组件,
子组件发送自定义事件给父组件
效果如下:
vue_43.html代码如下:
<!-- 第3步, vue最终渲染结果的容器 -->
<!-- 因Vue实例中,如果有更高优先级的 template选项,所以el挂载点将被取代 -->
<div id="id_div_container" style="color:#666;padding:10px 10px;border:1px solid Cadetblue;margin:4px;border-radius:8px;">
<div v-cloak id="id_div_vue">
<p>父组件: {{ girlCount }}</p>
<!--
监听 自定义事件 beyondClick
当子组件内部手动发出了beyondClick自定义事件时,
父组件将执行相应的事件处理函数
这儿必须是 - 连接???Excuse Me???
-->
<beyond-button
v-bind:key_count="girlCount"
v-on:beyond-click="addCountCallback"></beyond-button>
</div>
<footer id="copyright">
<br/>
<p class="sgcenter sgcontentcolor" style="font-size:14px;">
<b>注意: props传递数据给子组件<br/>子组件emit自定义事件
</p>
<p style="font-size:14px;text-align:center;font-style:italic;">
Copyright © <a id="author">2018</a> Powered by <a id="author">beyond</a>
</p>
</footer>
</div>
<!-- 第1步,导入vue.js -->
<script type="text/javascript" src="/node_modules/vue/dist/vue.js"></script>
<!-- 第2步,导入 子组件beyond-btn.js -->
<script type="text/javascript" src="/public/js/vue_43_beyond_btn.js"></script>
<!-- 第4步,导入程序入口js -->
<script type="text/javascript" src="/public/js/vue_43.js"></script>
vue_43.js代码如下:
new Vue({
el: '#id_div_vue',
data: {
// 御坂9982号
girlCount: 9982
},
methods: {
// 子组件内部事件发生时,回调处理函数
// 纯业务方法
addCountCallback: function(){
this.girlCount += 1
}
}
})
vue_43_beyond_btn.js代码如下:
const template = `<button class="class_btn class_btn_green" v-on:click="btnClicked">子组件: 御坂{{ tempGirlNumber }}号</button>`
Vue.component('beyond-button',{
// 子组件模板中有一个按钮
template,
// 声明来自父组件的数据
// 这样就可以在模板template中使用 父组件的数据了
props: ['key_count'],
// 组件data选项必须是一个返回对象的函数
data: function(){
var obj = {
// 由于是单向数据流,我们不能改父类传递过来的数据
// 所以,只能 1.定义一个局部变量并用父组件传递过来的数据初始化
// 或者 2. 定义一个计算属性,处理props过来的数据,并返回
tempGirlNumber: this.key_count
}
return obj
},
// 响应按钮点击事件
methods: {
btnClicked: function(){
// 1.先处理自己的逻辑,将临时变量 + 1
this.tempGirlNumber += 1
// 2.再告诉父组件(手动发布一个自定义事件)
// 这儿必须是 - 连接???Excuse Me???
// 由于 子组件不能改, 只能告诉父组件,让它自己改
this.$emit('beyond-click')
}
}
})
下面介绍非父子组件之间的通信
非父子组件通信: Event Bus
在简单场景下,可以使用一个空的Vue实例,作为事件总线
例如:
var vueBus = new Vue()
// 手动触发组件A中的自定义事件
vueBus.$emit('component-a-click', '面码')
// 在组件B中 创建的钩子中 监听 组件A的自定义事件
busVue.$on('component-a-click',function(girlName){
console.log('来自组件A自定义事件中的数据: ' + girlName)
})
最后,隆重介绍 专业 组件通信 大杀器: Vuex
下面新写一个todoMVC, 以组件的形式进行组织起来
第1步,从官网上git clone下来 todoMVC的 样本 template
官网:todomvc.com, 右下方, 点击 template, 跳转到git去下载
github:
https://github.com/tastejs/todomvc-app-template
clone下来并重全名为todomvc-component: ( --depth=1 表示 只下载最后一次更新 )
git clone https://github.com/tastejs/todomvc-app-template.git --depth=1 todomvc-component
第2步, 因为下载下来的的样本template的文件夹里已经有了package.json, 所以直接安装
npm install
第3步, 安装 browser-sync, 方便保存时,页面立即刷新
3.1
npm install browser-sync --save-dev
或者使用简写(注意大写D)
npm i browser-sync -D
3.2 打开package.json, 配置script命令,方便快速启动
// 注意,package.json中不能写注释
"scripts": {
// dev代表的就是后面的长长的命令, 后面代表的是要监视的文件路径和类型,public目录下的所有html是我手动加的
"dev": "browser-sync start --server --files \"*.html, css/*.css, js/*.js\"",
// 使用npm start命令时,就会执行后面的 npm run dev命令
// 一般都要写npm run start,但只有这个start比较特殊,可以省掉中间的run,从而简写写npm start
"start": "npm run dev"
},
如图:
3.3 启动 (全自动打开浏览器,并且监视本地的html、css、js文件的变动,实时更新)
npm start
效果如下:
最后再把vue安装一下
在index.html中引用vue
<!-- 第1步,导入vue.js -->
<script type="text/javascript" src="/node_modules/vue/dist/vue.js"></script>
<!-- 第2步,生成一个全新的vue对象实例 -->
<script type="text/javascript">
var appVue = new Vue({
data: {},
methods: {}
}).$mount('#id_div_container')
</script>
<!-- 第3步, vue最终渲染结果的容器 -->
<div id="id_div_container"></div>
到此todoMVC组件化开发的准备工作已经全部就绪了
现在, 我们右键, 审查元素, 先来将视图进行 分块(组件化)
如图所示:
.
如图所示:
组件分块 示意图:
最外层大的组件是: beyond-component, 它包含4个子组件
其中:
子组件beyond-header.js包括一个<h1>, 一个输入框
子组件beyond-main.js包括一个checkbox(选择全部/取消全部), 一个ul列表
子组件beyond-footer.js包括一个<span>(剩余数),一个ul(3种状态切换),一个button(消除已完成)
子组件beyond-copyright.js包括一个p标签
项目组织如下:
1. index.html中 引入 appVue.js
2. appVue.js 生成 Vue实例, 有自己的template选项,在components选项中,有一个名字叫beyondComponent的大局部组件
3. beyond-component.js 就是那个beyondComponent大局部组件, 它也有自己的template, 同时,在自己的components选项中,又定义了4个局部组件(就是上面4个,即beyondHeader,beyondMain,beyondFooter,beyondCopyright)
如图所示:
最终效果如下:
Vue 组件版 todoMVC
index.html代码如下:
<!doctype html>
<html lang="en">
<head>
<link rel="icon" href="/public/img/beyond2.jpg" type="image/x-icon"/>
<meta charset="UTF-8">
<meta name="author" content="beyond">
<meta http-equiv="refresh" content="520">
<meta name="description" content="未闻花名-免费零基础教程-beyond">
<meta name="viewport" content="width=device-width,
initial-scale=1.0, maximum-scale=1.0,minimum-scale=1.0,user-scalable=0" />
<meta name="keywords" content="HTML,CSS,JAVASCRIPT,JQUERY,XML,JSON,C,C++,C#,OC,PHP,JAVA,JSP,PYTHON,RUBY,PERL,LUA,SQL,LINUX,SHELL,汇编,日语,英语,泰语,韩语,俄语,粤语,阿语,魔方,乐理,动漫,PR,PS,AI,AE">
<title>beyond心中の动漫神作</title>
<link rel="stylesheet" href="node_modules/todomvc-common/base.css">
<link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
<link rel="stylesheet" href="css/beyondbasestylewhite5.css">
<style type="text/css">
/*解决Mustache 闪烁问题
起初div是隐藏的
当Vue数据绑定完成后,重新渲染时,会自动将v-cloak属性移除
*/
[v-cloak] {
display: none;
}
</style>
</head>
<body>
<div v-cloak id="id_div_container">
<!-- 使用大组件 -->
<beyond-component v-bind:key_hash='hashString' />
</div>
<!-- 第1步,导入vue.js -->
<script type="text/javascript" src="node_modules/vue/dist/vue.js"></script>
<script type="text/javascript" src="js/nslog.js"></script>
<!-- 第2步,导入自己的代码 四个小组件.js -->
<script type="text/javascript" src="js/beyond-header.js"></script>
<script type="text/javascript" src="js/beyond-main.js"></script>
<script type="text/javascript" src="js/beyond-footer.js"></script>
<script type="text/javascript" src="js/beyond-copyright.js"></script>
<!-- 第3步,导入自己的代码 大组件.js -->
<script type="text/javascript" src="js/beyond-component.js"></script>
<!-- 第4步,导入自己的代码appVue.js -->
<script type="text/javascript" src="js/appVue.js"></script>
</body>
</html>
Vue 组件版 todoMVC
程序入口appVue.js代码如下:
// ------------------全局自定义组件(简写形式)------------
Vue.directive('beyond-focus',function (el,binding) {
el.focus()
})
var appVue = new Vue({
el: '#id_div_container',
// 定义局部组件
components: {
// 大组件的beyondComponent
beyondComponent
},
// data对象,要传递hash给 大组件
data: {
// 根据 window.location.hash 进行同步改变; 而hashString又作为 过滤数组的重要的判断条件;
// 从而实现了地址栏 与 列表数组 的联动
hashString: ''
},
// 可以把监听window的hash change事件,写到Vue实例的生命周期方法里
created: function () {
NSLog('created')
// 只有hansh 发生 改变时, 才会调用本方法
// 之所以这儿要使用箭头函数,目的就是为了利用箭头函数的特性,拿到外层(父作用域)的this (即vue实例)
window.onhashchange = ()=>{
// 拿到 window.location.hash
// 赋值给 data的 中间变量 hashString(即 过滤数组的条件)
// #/active => active #/completed => completed
this.hashString = window.location.hash.substr(2)
NSLog(this.hashString)
}
// 因为我们要一上来,就改一次hashString属性,从而根据路由自动刷新一下列表(保持住路由状态)
// 页面初始化时,需要手动调用一次,会自动根据当前路由状态进行回显
window.onhashchange()
}
})
window.appVue = appVue
Vue 组件版 todoMVC
大组件(父组件)beyond-component.js代码如下:
;(
function () {
var animeArr = [
{animeID: 1,animeName: "未闻花名",animeHaveSeen: false},
{animeID: 2,animeName: "龙与虎",animeHaveSeen: true},
{animeID: 3,animeName: "轻音少女",animeHaveSeen: false}]
// 初始值来自 localStorage (默认初次 没有值, 便以空数组代替)
var animeArrInitString = window.localStorage.getItem('animeArr')
var animeArr = JSON.parse(animeArrInitString || '[]')
// 大组件的模板代码
const template = `
<div>
<section class="todoapp">
<!-- 子组件发出的自定义事件 -->
<beyond-header v-on:beyond-keydown="addAnimeCallback"></beyond-header>
<!-- props第2步, 将数据 传递给子组件
其中 key_arr是子组件的props选项中预先声明的
-->
<!-- 使用虚拟元素template将这个元素两个包起来
1是为了 避免代码冗余
2是为了 避免套上额外的div
-->
<template v-if="animeArr.length">
<!-- 数组没有东西时,不要渲染 -->
<beyond-main v-bind:key_hashString="key_hash" v-bind:key_arr="animeArr" v-on:beyond-update="updateAnimeCallback" v-on:beyond-delete="deleteAnimeCallback" v-on:beyond-update-all="chooseAllOrNotCallback"></beyond-main>
<beyond-footer v-bind:key_hashString="key_hash" v-bind:key_arr="animeArr" v-on:beyond-click="clearCompletedAnimeBtnClickedCallback"></beyond-footer>
</template>
</section>
<beyond-copyright></beyond-copyright>
</div>
`
// 把大组件对象 添加到window上,供appVue.js使用
window.beyondComponent = {
// 属性简写
template,
// 接收来自父组件的数据(只是中转,要传递给子组件)
props: ['key_hash'],
// 定义自己的4个子组件
components: {
// 同样的 属性简写
beyondHeader,
beyondMain,
beyondFooter,
beyondCopyright
},
// data是返回一个对象的函数
data: function () {
var obj = {
animeArr: animeArr
}
return obj
},
// 观察者 选项
// 注意: 不能使用 箭头函数来定义 watch选项中的函数
// 原因是: 箭头函数 绑定的是父级作用域的上下文
watch: {
// 观察data选项中的animeArr属性的一举一动,并随之执行相应的业务处理逻辑
animeArr: {
// 数组 和 对象 等引用类型,默认只能监视第1层,为了能够监视其子成员的改变, 必须 深度观察
deep: true,
// 监听开始时,立即调用(不管改没改变)
immediate: true,
// 发现改变时,随之 调用的业务逻辑 (持久化)
handler: function (newValue,oldValue) {
// NSLog('animeArr发生改变,写入本地存储')
window.localStorage.setItem('animeArr',JSON.stringify(newValue))
}
}
},
// 事件处理
methods: {
// 纯业务方法,类似于CRUD
addAnimeCallback(newAnimeName){
NSLog('父组件: ' + newAnimeName)
var animeArr = this.animeArr
// 索引健壮性
var newAnimeID = -1
if(animeArr.length === 0){
newAnimeID = 1
}else{
newAnimeID = animeArr[animeArr.length -1 ].animeID + 1
}
var newAnime = {
animeID: newAnimeID,
animeHaveSeen: false,
animeName: newAnimeName
}
animeArr.push(newAnime)
},
// 纯业务方法
clearCompletedAnimeBtnClickedCallback(){
// 方式二: 推荐使用filter 过滤出新数组
this.animeArr = this.animeArr.filter( anime =>
!anime.animeHaveSeen
)
},
// 纯业务方法
updateAnimeCallback(arr){
var index = arr[0]
var newAnimeName = arr[1]
this.animeArr[index].animeName = newAnimeName
},
// 纯业务方法
deleteAnimeCallback(index){
NSLog(this)
this.animeArr.splice(index,1)
},
// 纯业务方法
chooseAllOrNotCallback(isChecked) {
this.animeArr.forEach(anime =>
anime.animeHaveSeen = isChecked
)
}
}
}
}
)()
Vue 组件版 todoMVC
四个子组件之一 beyond-header.js代码如下:
;(function () {
// 小组件模板
// 因为组件只能有一个根节点,所以注释要放到里面
const template = `
<header class="header">
<h1 style="color:white;text-shadow:2px 2px 4px #e5cdcf;letter-spacing:5px;font-family:inherit;font-weight:380;" class="sgcontentcolor sgcenter">
あの花
</h1>
<input @keydown.enter="addAnimeFunction" class="new-todo" placeholder="请输入动漫名,按回车键录入" autofocus>
</header>
`
// 把小组件对象 添加到window上,供大组件 beyond-component.js使用
window.beyondHeader = {
// 属性简写
template,
// 事件
methods: {
addAnimeFunction(event){
NSLog('按下了回车: ' + event)
// 获取input节点
var inputNode = event.target
var newAnimeName = inputNode.value.trim()
// 过滤非空数据
if(!newAnimeName.length){
return
}
// 清空输入框
inputNode.value = ''
// 发送自定义事件, 将数据传递给父组件
this.$emit('beyond-keydown',newAnimeName)
}
}
}
}
)()
Vue 组件版 todoMVC
四个子组件之一 beyond-main.js代码如下:
;(function () {
// 小组件模板
// 因为组件只能有一个根节点,所以注释要放到里面
const template = `
<section class="main">
<!-- 全选/全不选
方式1: 在这儿监听 change 事件
v-on:change="selectAllOrNotFunction"
方式2: MVVM 高级玩法
使用计算属性chooseAllOrNot作为v-model 实现双向绑定 到checkbox
因为checkbox既有初始值, 又要勾选设置值,所以要双向绑定
所以,当初始化checkbox时, 会调用计算属性的getter方法
而当你勾选checkbox的时候,就会自动调用计算属性的 setter方法
-->
<input id="toggle-all" class="toggle-all" type="checkbox"
v-model="chooseAllOrNot"
>
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<!-- 三种样式
editing when editing
completed when marked as completed
这儿实行的是单向绑定class
-->
<!-- 注意: 下面这儿关于当前编辑的样式editing 有点儿巧妙
data新增一个属性 currentEditAnime, 保存的是那一个被双击的 anime对象
如果 两个对象一样时, 自动进入 正在编辑样式
-->
<li v-for="(anime,index) in filtedAnimeArr" v-bind:class="{completed: anime.animeHaveSeen,
editing: currentEditAnime === anime
}">
<div class="view">
<!-- 根据状态选择是否选中
这儿实现的双向绑定
-->
<input class="toggle" type="checkbox" v-model="anime.animeHaveSeen" >
<!-- 上面的双击 要传递3个参数
第1是: 双击事件
第2是: 索引
第3是: anime对象
-->
<label v-on:dblclick="doubleClickFunction($event,index,anime)">{{ anime.animeName }}</label>
<!-- 点击按钮删除item
注意:
第1个参数是索引,
第2个参数是手动传递的事件 $event 对象
(固定写法,死记硬背)
-->
<button class="destroy"
v-on:click="deleteAnimeBtnClicked($event,index)"
></button>
</div>
<!-- 编辑框 必须是单向绑定,因为
1. 还有esc撤销编辑的功能
2. 去除editing样式,即把currentEditAnime置空即可
3. 敲回车 或 失去焦点自动保存
如果没有内容,则自动删除该item
使用自定义指令 自动聚焦,条件是 当前编辑的对象是自己
-->
<input class="edit" v-bind:value="anime.animeName"
v-on:keydown.enter="editCompleteFunction($event,index,anime)"
v-on:blur="editCompleteFunction($event,index,anime)"
v-on:keydown.esc="cancleEditFunction"
v-beyond-focus="currentEditAnime === anime"
>
</li>
</ul>
</section>
`
// 把小组件对象 添加到window上,供大组件 beyond-component.js使用
window.beyondMain = {
// 属性简写
template,
// 声明来自父组件的数组,然后就可以像data选项中返回的对象里的数据那样使用了
// 注意: 子组件 只能使用 来自父组件的数据, 万万不可修改!!!
// 因为违背了Vue的通信原则: 单向数据流
// 正确做法是: 在事件发生时,把数据通过自定义事件的参数上交给父组件
props: ['key_arr','key_hashString'],
// 组件的data必须是一个返回对象的函数
data: function(){
var obj = {
// 记录 被人双击时的那一个 anime对象
currentEditAnime: null,
}
return obj
},
// 计算属性
computed: {
// 根据条件 过滤出来的 供列表显示的 数组
filtedAnimeArr: {
get: function () {
// 如果 根据路由得出的 中间变量 为 completed 或者 active ,则 过滤相应的数组 展示到界面上
if(this.key_hashString === 'active'){
return this.key_arr.filter(anime =>
anime.animeHaveSeen === false
)
}else if(this.key_hashString === 'completed'){
return this.key_arr.filter(anime =>
anime.animeHaveSeen === true
)
}else{
return this.key_arr
}
}
},
// 选择全部 / 取消选择
chooseAllOrNot: {
get: function () {
// 当数组中每一个 anime 都 have seen的时候, 自动勾选 checkbox
// 当数组中 只要有一个anime 不是have seen时, 取消勾选 checkbox
// 注意: 计算属性知道自己依赖了 数组, 所以,当 数组 变化时, 计算属性也会重新计算
var b = this.key_arr.every(anime =>
anime.animeHaveSeen === true
)
NSLog('get: ' + b)
return b
},
// 只要用户 勾选或取消 绑定了此计算属性的checkbox, 就会来到setter方法 (因为有 设置值)
// 数组animeArr中的所有animeHaveSeen的状态也就都会跟着联动
set: function () {
// 1. 非常巧妙! 先通过计算属性的getter方法,获取当前的 勾选 状态
var isChecked = this.chooseAllOrNot
// 2. 然后选择相反的状态
var newIsChecked = !isChecked
// NSLog('set: ' + newIsChecked)
// 3. 发送自定义事件,通知父组件,同步更新全部数组中的成员状态
this.$emit('beyond-update-all',newIsChecked)
}
}
},
// 事件处理
methods: {
// 双击事件发生时调用
// 参数1: 双击事件
// 参数2: 索引
// 参数3: anime对象
doubleClickFunction(event,index,anime){
// 非常巧妙的1步,使用中间变量 保存被双击的对象(其实用索引也行)
this.currentEditAnime = anime
},
// 按esc时, 结束编辑 不保存
cancleEditFunction() {
// 1. 退出editing样式(让中间变量currentEditAnime 置null)
this.currentEditAnime = null
},
// 按回车 或 失去焦点时, 结束编辑
editCompleteFunction(event,index,anime){
// 1. 退出editing样式(让中间变量currentEditAnime 置null)
this.currentEditAnime = null
// 2. 获取并验证 新输入的 animeName
var editNode = event.target
var newAnimeName = editNode.value
NSLog(newAnimeName)
// 2.1 如果为空,则删除该anime,
if (newAnimeName.length === 0) {
// 发送自定义事件,告诉父组件 删除 它
this.$emit('beyond-delete',index)
return
}
// 2.2 如果有内容,则更新
// anime.animeName = newAnimeName
// 发送自定义事件,告诉父组件 删除 它
this.$emit('beyond-update',[index,newAnimeName])
},
// 当事件处理函数,没有传递参数时,第一个参数就是event
// 当事件处理函数传递了参数时,就没有办法再获取到event对象了
// 因此,我们在传递参数时,就要手动传递事件对象 $event
deleteAnimeBtnClicked(event,index){
// 发送自定义事件给父组件
this.$emit('beyond-delete',index)
}
}
}
}
)()
Vue 组件版 todoMVC
四个子组件之一 beyond-footer.js代码如下:
;(function () {
// 小组件模板
// 因为组件只能有一个根节点,所以注释要放到里面
const template = `
<footer class="footer">
<!-- 数组没有东西时, footer不用渲染
剩余未未完成的数量
这儿有3种写法:
第1种: 直接在Mustache语法中 进行 数组的 filter
第2种: 使用方法包装
第3种: 使用计算属性
-->
<span class="todo-count">
<strong>
<!-- 第3种, 计算属性
好处1: 有缓存 (会缓存计算结果,提高性能)
好处2: 模板没有 厚重的逻辑
好处3: 易维护
注意: 计算属性 必须也只能按属性使用 (不能用于事件处理函数,虽然它本质是getter和setter方法)
-->
{{ unSeenAnimeCount }}
</strong>item left</span>
<!-- 1. 根据 路由routing 的切换, 导致window.location.hash变化
2. 而监听到地址栏的hash的变化时 , 又人为导致 data中的一个 中间变量(hashString)的变化
3. 而条件(hashString)的变化 , 又改变 计算属性 返回的结果数组 filtedAnimeArr
4. 从而实现了对 列表的过滤与切换 -->
<ul class="filters">
<li>
<a v-bind:class="{selected: key_hashString === ''}" href="#/">All</a>
</li>
<li>
<a v-bind:class="{selected: key_hashString === 'active'}" href="#/active">Active</a>
</li>
<li>
<a v-bind:class="{selected: key_hashString === 'completed'}" href="#/completed">Completed</a>
</li>
</ul>
<!-- 如果没有已经完成的item, 那么不渲染这个按钮
这儿非常巧妙地使用 array的some函数
-->
<button v-if="key_arr.some(anime => anime.animeHaveSeen)" class="clear-completed"
v-on:click="clearCompletedAnimeBtnClicked"
>Clear completed</button>
</footer>
`
// 把小组件对象 添加到window上,供大组件 beyond-component.js使用
window.beyondFooter = {
// 属性简写
template,
// 来自父组件的数组(然后就可以使用了,注意不能改喔)
props: ['key_arr','key_hashString'],
// 事件处理
methods: {
// 点击 清除完成 按钮 事件
clearCompletedAnimeBtnClicked() {
// 把发生的点击事件 通知父组件
this.$emit('beyond-click')
}
},
// 计算属性 本质是一个getter/setter方法, 但是必须也只能按属性使用
computed: {
// 第2种 完整写法 对象形式
unSeenAnimeCount: {
// 默认 只有一个 getter
get: function () {
var leftCount = this.key_arr.filter(anime =>
anime.animeHaveSeen === false
).length
// NSLog(leftCount)
return leftCount
}
}
}
}
}
)()
Vue 组件版 todoMVC
四个子组件之一 beyond-copyright.js代码如下:
;(
function () {
// 小组件模板
// 因为组件只能有一个根节点,所以注释要放到里面
const template = `
<footer id="copyright">
<p style="font-size:14px;"> 注意: <b>VUE 组件版</b></p>
<p style="font-size:14px;text-align:center;font-style:italic;">
Copyright © <a id="author">2018</a> Powered by <a id="author">beyond</a>
</p>
</footer>
`
// 把小组件对象 添加到window上,供大组件 beyond-component.js使用
window.beyondCopyright = {
// 属性简写
template
}
}
)()
最终Vue 组件版 todoMVC 效果如下:
未完待续,下一章节,つづく