【2022.4.20更新】Vue_day04_组件_Element组件库_组件通信_PropsDown_EventsUp

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实例的生命周期:

.

生命周期钩子: 11 个
每个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’

例如: <script type=”text/x-template” id=”id_template_beyond”>
<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的通信原则: 单向数据流

正确做法是: 在事件发生时,把数据通过自定义事件的参数上交给父组件

1. 在父组件的template模板中 通过子组件标签的属性传递数组
例如:
<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 效果如下:

未完待续,下一章节,つづく