Vue组件实战

最近看了下Vue组件的相关知识,除了官网也推荐这篇博客http://www.cnblogs.com/keepfool/p/5637834.html,但是这篇博客中用的是v1.0.25,我就用最新的v2.4.4来模仿下文中的例子,也一起谈一谈下面几个方面:

  1. 组件编译作用域
  2. 父子组件传递数据
  3. 非父子组件通信
  4. 过滤器

先看下最终的实现效果:


vue_component.gif

1.整体代码

先看下整体代码,首先是html部分, 分成三部分

1.id="app"的container部分
2.id="grid-template"的表格部分
3.id="dialog-template"的对话框部分
总共涉及两个自定义组件,一个父组件<grid-template></grid-template>;一个子组件<modal-dialog></<modal-dialog>.

<body>
    <!-- container -->
    <div id="app">
        <div class="container">
            <div class="form-group">
                <label>
                    Search
                </label>
                <input type="text" v-model="searchQuery" />
            </div>
        </div>
        <div class="container">
            <grid-template :column="columns" :search-key="searchQuery" :data-list="people"></grid-template>
        </div>
    </div>

    <!-- 表格 -->
    <template id="grid-template">
        <div>
            <table>
                <thead>
                    <th v-for="headName in column">
                        {{headName.name | capitalize}}
                    </th>
                    <th>
                        Delete
                    </th>
                </thead>
                <tbody class="text-center">
                    <tr v-for="(entry, index) in filt(dataList, searchKey)">
                        <td v-for="col in column">
                            <span v-if="col.isKey">
                                <a href="javascript:void(0)" @click="openEditItemDialog(index, 'Edit item ' + entry[col.name])">{{entry[col.name]}}</a>
                            </span>
                            <span v-else>{{entry[col.name]}}</span>
                        </td>
                        <td>
                            <button @click="deleteItem(index)">Delete</button>
                        </td>
                    </tr>
                </tbody>
            </table>

            <div class="container">
                <button class="button" v-on:click="openNewItemDialog('Create new item')">Create</button>
            </div>
            <modal-dialog :show="show" :mode="mode" :title="title" :fields="column" :item="item" @on-show-change="handleShow" @add-item="addData"
                @update-item="updateData"></modal-dialog>
        </div>
    </template>

    <!-- 对话框 -->
    <template id="dialog-template">
        <div class="dialogs">
            <div class="dialog" v-bind:class="{'dialog-active': myShow}">
                <div class="dialog-content">
                    <header class="dialog-header">
                        <h1 class="dialog-title">{{title}}</h1>
                    </header>

                    <div v-for="field in fields" class="form-group">
                        <label>{{field.name}}</label>
                        <select v-if="field.dataSource" v-model="item[field.name]" :disabled="mode === 2 && field.isKey">
                            <option v-for="opt in field.dataSource" :value="opt">
                                {{opt}}
                            </option>
                        </select>
                        <input v-else type="text" v-model="item[field.name]" :disabled="mode === 2 && field.isKey" />
                    </div>

                    <footer class="dialog-footer">
                        <div class="form-group">
                            <label></label>
                            <button v-on:click="save">Save</button>
                            <button v-on:click="close">Close</button>
                        </div>
                    </footer>
                </div>
            </div>
            <div class="dialog-overlay"></div>
        </div>
    </template>
</body>

先看下第一部分的代码,

    <div id="app">
        <div class="container">
            <div class="form-group">
                <label>
                    Search
                </label>
                <input type="text" v-model="searchQuery" />
            </div>
        </div>
        <div class="container">
            <grid-template :column="columns" :search-key="searchQuery" :data-list="people"></grid-template>
        </div>
    </div>

上面代码中[column, search-key, data-list]都是组件grid-template的属性,[columns,searchQuery,people ]就是使用container传给grid-template组件的数据。
定义全局组件方式Vue.component(name, {}),在父组件grid-template中通过属性components定义子组件modal-dialog,这也是比较常用的定义全局组件和局部组件的方式。

Vue.component('grid-template', {
        template: "#grid-template",
        props: [
            'dataList', 'column', 'searchKey'
        ],        
        components: {
            'modal-dialog': {
                template: '#dialog-template',
                data: function () {
                    return {
                        myShow: this.show
                    }
                },
                props: [
                    'mode',
                    'title',
                    'fields',
                    'item',
                    'show'
                ]
            }
        }
}

html中用kebab-case (短横线分隔命名),JS中采用camelCase (驼峰式命名),这个Vue会自动进行识别,我们按各自习惯进行编码就行。

2.组件编译作用域

就跟编程语言中的函数有作用域一说,Vue中组件也有作用域,并且作用域只在组件的模板中。
看一个官方的例子:

<child-component>
    {{ message }}
</child-component>

这里message用的是父组件(父组件就是使用<child-component>这个组件的组件)的数据,还是子组件的数据?答案是父组件。
官方对作用域的定义:

父组件模板的内容在父组件作用域内编译;子组件模板的内容在子组件作用域内编译。

再看一个例子:

<!-- 无效 -->
<child-component v-show="someChildProperty"></child-component>

假定someChildProperty是子组件的属性,上面代码就会出问题,因为这里是父组件的作用域,也就是说someChildProperty应该是父组件里面定义的数据。如果要绑定子组件作用域内的指令到一个组件的根节点,需要在子组件的模板里做:

Vue.component('child-component', {
  // 有效,因为是在正确的作用域内
  template: '<div v-show="someChildProperty">Child</div>',
  data: function () {
    return {
      someChildProperty: true
    }
  }
})

来仔细看下上面代码的第二部分-表格部分,看看父组件使用子组件的情况:

    <!-- 表格 -->
    <template id="grid-template">
        <div>
            <modal-dialog :show="show" :mode="mode" :title="title" :fields="column" :item="item" @on-show-change="handleShow" @add-item="addData"
                @update-item="updateData"></modal-dialog>
        </div>
    </template>

其中,[show, mode, title, fields, item]modal-dialog组件自己定义的属性,@是v-on:的简写,method [handleShow, addData] 是父组件id="grid-template"作用域中的函数

    Vue.component('grid-template', {
        template: "#grid-template",
        methods: {
            handleShow: function (val) {
                this.show = val
            },
            addData: function (item) {
                if (this.itemExists(item)) {
                    alert('Item ' + item[this.keyColumn] + " is already exists");
                    this.show = true;
                    return;
                }
                this.dataList.push(item)
                this.item = {}
                this.show = false
            },
            updateData: function (item) {
                var keyColumn = this.keyColumn

                for (var i = 0; i < this.dataList.length; i++) {
                    if (this.dataList[i][keyColumn] === item[keyColumn]) {
                        for (var j in item) {
                            this.dataList[i][j] = item[j]
                        }
                        break;
                    }
                }

                item = {}
            }
        },
        components: {
            'modal-dialog': {
                template: '#dialog-template',
                ......
            }
        }
    })

3.父子组件传递数据

组件实例的作用域是孤立的。这意味着不能 (也不应该) 在子组件的模板内直接引用父组件的数据。父组件的数据需要通过 prop 才能下发到子组件中。
子组件要显式地用[props]声明它预期的数据:

Vue.component('child', {
  // 声明 props
  props: ['message'],
  // 就像 data 一样,prop 也可以在模板中使用
  // 同样也可以在 vm 实例中通过 this.message 来使用
  template: '<span>{{ message }}</span>'
})

然后我们可以这样向它传入一个普通字符串:
<child message="hello!"></child>
结果就是
hello!

也可以采用动态props,也就是v-bind指令。v-bind 动态地将 prop 绑定到父组件的数据。每当父组件的数据变化时,该变化也会传导给子组件:

    <div id="app">
        <div class="container">
            <div class="form-group">
                <label>
                    Search
                </label>
                <input type="text" v-model="searchQuery" />
            </div>
        </div>
        <div class="container">
            <grid-template :column="columns" :search-key="searchQuery" :data-list="people"></grid-template>
        </div>
    </div>

子组件怎么和父组件通信呢?这个时候 Vue 的自定义事件系统就派得上用场了。

使用 $on(eventName) 监听事件
使用 $emit(eventName) 触发事件

看看我们这个例子中,对话框中的添加了数据需要通知父组件<grid-template></grid-template>,可以这样操作:
在子组件对话框中<modal-dialog></<modal-dialog>

<footer class="dialog-footer">
        <div class="form-group">
               <label></label>
               <button v-on:click="save">Save</button>
         </div>
</footer>

 methods: {
     save: function () {
               this.$emit('add-item', this.item)
     }
},

Vue监听到了'add-item事件,Vue就会调用父组件<grid-template></grid-template>`中的addData方法:

<modal-dialog @add-item="addData" ></modal-dialog>

methods: {
            addData: function (item) {
                if (this.itemExists(item)) {
                    alert('Item ' + item[this.keyColumn] + " is already exists");
                    this.show = true;
                    return;
                }
                this.dataList.push(item)
                this.item = {}
                this.show = false
}

4. 非父子组件通信

有时候,非父子关系的两个组件之间也需要通信。可以使用一个空的 Vue 实例作为事件总线:

var bus = new Vue()

// 触发组件 A 中的事件
bus.$emit('id-selected', 1)

// 在组件 B 创建的钩子中监听事件
bus.$on('id-selected', function (id) {
  // ...
})

在我们这个例子中,对话框的显示需要父子组件进行通信,点击Create按钮,需要控制show的true或false,从而控制对话框的显示与否。有的小伙伴可能立马想到props的方法,但是这样就会有一个问题:

通过父组件把show的值传给子组件对话框作为属性,是可以达到控制对话框的显示或隐藏,但是在对话框中点击save或者close按钮时需要改变这个show的值,这时候就相当于子组件改变了父组件传过来的属性props了,这是比较不和谐的,Vue推荐props传到子组件的时候是只读的,也就是子组件不要改变父组件传过来的porps。

    <template id="dialog-template">
        <div class="dialogs">
            <div class="dialog" v-bind:class="{'dialog-active': show}">
                
        </div>
    </template>

这时候就可以通过事件的方式来达到这个目的,show这个状态只作为子组件对话框自己的data,父组件不知道这个属性,要控制对话框的状态,需要给子组件发送事件:

<div class="container">
     <button class="button" v-on:click="openNewItemDialog('Create new item')">Create</button>
</div>

Vue.component('grid-template', {
        template: "#grid-template",
        methods: {
            openNewItemDialog: function (title) {
                this.title = title
                this.mode = 1
                this.item = {}
                bus.$emit('dialog-show', true)    //通过总线发送dialog-show事件
            }
            addData: function (item) {
                this.dataList.push(item)
            },
        },
        components: {
            'modal-dialog': {
                template: '#dialog-template',
                mounted: function () {
                    bus.$on('dialog-show', function (show) {
                        this.show = show
                    }.bind(this))
                }
            }
        }
})

1.点击Create按钮,通过bus.$emit('dialog-show', true) //通过总线发送dialog-show事件
2.子组件在mounted生命周期钩子中监听事件
mounted: function () {
bus.$on('dialog-show', function (show) {
this.show = show
}.bind(this))
}

如果一定要通过属性props的方式呢?可以这样,

    components: {
            'modal-dialog': {
                template: '#dialog-template',
                data: function () {
                    return {
                        myShow: this.show
                    }
                },
                methods: {
                    close: function () {
                        this.myShow= false
                    }
                },
                watch: {
                    show(val) {
                        this.myShow = val;
                    },
                    myShow(val) {
                        this.$emit("on-show-change", val);
                    }
                },
            }
        }    

1.子组件中不要直接修改父组件传递过来的props,会报错:
在子组件中直接修改props-show
close: function () {
this.show = false
}
Error: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value.
2.通过计算属性,在watch中父组件show改变的时候改变子组件的myShow属性;同理,子组件myShow属性改变的时候通过emit发射事件通知父组件改变show属性
<modal-dialog @on-show-change="handleShow" ></modal-dialog>
在父组件中定义方法
handleShow: function (val) {
this.show = val
}

5.过滤器

Vue.js 允许你自定义过滤器,可被用于一些常见的文本格式化。过滤器可以用在两个地方:双花括号插值和 v-bind 表达式 (后者从 2.1.0+ 开始支持)。过滤器应该被添加在 JavaScript 表达式的尾部,由“管道”符号指示:

<!-- 在双花括号中 -->
{{ message | capitalize }}

<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>

有两种定义方式:在组件选项中通过filters定义本地的过滤器;通过Vue.filter定义全局的过滤器

// 首字母大写的过滤器
Vue.filter('capitalize', function (value) {
        if (!value) return ''
        value = value.toString()
        return value.charAt(0).toUpperCase() + value.slice(1)
})

像下面这样使用,表格标题首字母大写

 <th v-for="headName in column">
    {{headName.name | capitalize}}
</th>

6.总结

本文例子来源于http://www.cnblogs.com/keepfool/p/5637834.html,一方面是自身学习,另外一方面用V2.4.4重写了,对一些细节进行了扩展解释,希望对大家有点帮助哈,谢谢!

欢迎关注公众号:JueCode

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容