深入理解VueJS组件间数据传递
1.引言
最近在用nodejs+mysql+vuejs
做一个网站,期间因为需要再次看了不少关于vuejs
的文档,所以这是一篇关于vuejs
的文章。
首先来看这样一个界面:
这是 google 搜索结果页面,这个页面展示了一个简单的列表过滤效果,那么这篇文章的例子将围绕这幅图。
我们将实现如下功能:
1.搜索关键字输入框;
2.搜索结果列表;
3.搜索结果跟随"搜索关键字"高亮关键字
2.父子布局与数据绑定
(searchbox.vue)
|--keyword
父组件--|
|--子组件--|->article list
(article.vue)
设计出以上父子组件嵌套的效果:
父组件包含一个搜索框和一个子组件,该子组件是v-for
的渲染列表。
父组件
<template>
<div id="searchbox">
<div>
<el-input></el-input>
</div>
<ul>
<ui-article v-for="(article,index) in list" :key="index"></ui-article>
</ul>
</div>
</template>
<script>
import Article from './Article.vue';
export default {
name: 'SearchBox',
components: {
'ui-article': Article,
},
data() {
return {
list:[
{
title: 'The Allegations Against R. Kelly: An Abridged History',
date: '2019-01-17 12:07'
},
{
title: 'Jawbox Announces Reunion Tour After A Decade Away',
date: '2019-01-17 16:34'
},
{
title: 'A Song Called \'Quiet\' Struck A Chord With Women. Two Years Later, It is Still Ringing',
date: '2019-01-16 01:21'
}
]
};
}
};
</script>
<style lang="scss" scoped>
.searchbox {
}
</style>
子组件
<template>
<li class="article">
<span class="title">title</span>
<span class="user">author</span>
<span class="el-tag el-tag--info">date</span>
</li>
</template>
<script>
export default {
name: 'Article',
components: {
},
props: [],
data () {
return {
};
},
mounted: function () {
},
computed: {
},
filters: {
},
methods: {
}
};
</script>
<style lang="scss" scoped>
.article {
}
</style>
一个简单的父子嵌套模式就完成了,我们填充上需要传递的数据:
父组件
<el-input v-model="keyword" placeholder="keyword"></el-input>
<ui-article v-for="(article,index) in list" :key="index" :initarticle="article" :initkeyword="keyword" ></ui-article>
子组件
<template>
<li class="article">
<span class="title">{{artilce.title}}</span>
<span class="user">author</span>
<span class="el-tag el-tag--info">{{article.date}}</span>
</li>
</template>
props: [
'initarticle',
'initkeyword'
],
data () {
return {
article: this.initarticle,
keyword: this.initkeyword
};
}
3.过滤效果
根据关键字过滤列表:
<ui-intro v-for="(article,index) in list" :key="index" :initarticle="article" :initkeyword="keyword" v-show="keyword.length==0 || (keyword.length>0 && article.title.indexOf(keyword.trim())>=0)"></ui-intro>
很简单,类似AngularJS
,我们只需要使用v-if
或者v-show
来达到前端过滤的效果。
4.关键字高亮:父组件传值给子组件
达到了搜索效果,那么还有最后一步,如果对搜索的关键字
在搜索结果中高亮
显示呢。
需要给每个文章标题中
的关键字
加上HTML
样式就可以解决问题了。
在父组件
中将输入框
绑定键盘事件
来触发高亮事件:
<el-input v-model="keyword" placeholder="keyword" @keyup.native="filterArticle()"></el-input>
filterArticle: function () {
let keyword = this.keyword;
let temp = this.list; // Object.assign({}, this.post.list);
temp.map(item => {
if (item.title.indexOf(keyword) >= 0) {
item.selected = true;
item.titlehtml = item.title.replace(keyword, '<span class="colored">' + keyword + '</span>');
} else {
item.selected = false;
item.titlehtml = item.title;
}
});
this.post.list = temp;
}
在子组件中修改绑定为:
<span class="title">{{artilce.titlehtml}}</span>
更新完数据后,子组件的dom并未重新渲染。
用log
标记,发现数据确实已经更新,但是dom
尚未重新得到渲染。
我们可以在这篇文章中找到原因:Vue 父组件ajax异步更新数据,子组件获取不到
子组件钩子mounted应该在刚开始页面渲染时候执行一次,执行的时候如果你的父组件异步请求还未执行回调,子组件mounted获取不到值的,正确的做法是在子组件里面使用watch监听lineone,如果lineone改变后即可更新renderChart
看起来加一个watch
可以解决问题,但是一切看起来复杂起来。
(注:未尝试)
插一个题外的知识:@keyup.native
由于element-ui
的input
会生成这样的dom
<div class="el-input">
<input type="text" autocomplete="off" placeholder="keyword" class="el-input__inner">
</div>
所以单纯使用:keyup
并不能达到监听事件的目的。
监听原生事件,我们可以使用
v-on:keyup.native
@keyup.native
关于这个,可以参考这两篇文章:
5.关键字高亮:使用filter过滤器
我们考虑在子组件
中监视keyword
的变化,来达到过滤当前一条数据的目的。
首先, 父组件keyword
输入框不需要绑定任何事件,直接需要传递 article
和keyword
给子组件,在子组件中实现监视。
在子组件
中添加一个watch来监视数据的变化。
watch: {
initarticle () {
this.article = this.initarticle;
},
initkeyword () {
this.keyword = this.initkeyword;
}
}
实际上这里主要是监视keyword
的变化,更新dom
使用filter
过滤器,这是在写AngularJS
种常用的一种方式,查了查,VueJS
有同样的方式。
先注释上一步[4]的filterArticle
解决方法。
根据文档,带参数的filter
用法如下:
{{param1 | function(param2)}}
function(param1,param2){}
在子组件
中添加filter
:
{{article.title | colorTitle(keyword)}}
filters: {
colorTitle: function (title, keyword) {
return keyword.length === 0 ? title : title.replace(keyword, '<span class="colored">' + keyword + '</span>');
}
}
(注:如果不使用参数,直接使用this.keyword
和this.title
是不是也可以获取到这两个值呢?经过实验,发现filter
方法中无法拿到props
的值)
通过官方文档计算属性和侦听器,我们可以使用computed
来实现模板内的表达式,这里不再探讨。
刷新页面,发现随着keyword
的变化,title
中的关键字成功被加入了<span class="colored"></span>
但是有个问题,如何让HTML
的title友好的渲染呢。
6.使用 v-html渲染
使用v-html
可以渲染带标签的字符串,
<span class="title" v-html="article.title"></span>
但是有个问题,v-html
中无法使用filter
,通过这个issue: v-html does not work with filters
我们可以直接在v-html
中调用方法来解决问题。
<span class="titles" v-html="colorTitle(article.title,keyword)"></span>
methods: {
colorTitle: function (title, keyword) {
return keyword.length === 0 ? title : title.replace(keyword, '<span class="colored">' + keyword + '</span>');
}
}
刷新页面,title
输出的HTML
被正确的展示了出来。
7.高亮colored样式
为colored
样式添加高亮效果
.title {
.colored {
color: black;
background-color: #FFFF00;
}
}
然而样式并未正确展示在页面上。
参考文档 Deep Selectors: 深度作用选择器 ,知道有个叫做深度作用选择器
的东西,感觉雷同上面的监听原生事件
。
文档这样描述 如果你希望 scoped 样式中的一个选择器能够作用得“更深”,例如影响子组件,你可以使用 >>> 操作符:
(然而经过实验>>>
操作符号并不能解决,这里不知为何?)
在.colored
前标记/deep/
:
.title {
/deep/ .colored {
color: black;
background-color: #FFFF00;
}
}
刷新页面,完美解决。
8.展开文章详情:子组件触发父组件
上面列表实现,如何实现展开详情呢。
有这样一个思路:点击子组件
文章列表某一条
,触发点击事件,该事件触发父组件
获取该文章详情的请求更新数据。
所以可以这样来:
子组件:
<template>
<!-- $emit('one-article') 中的 click 事件 触发父组件文章详情事件 -->
<li class="article" v-on:click="$emit('one-article')">
<!-- todo html -->
</li>
</template>
父组件:
<ul>
<ui-intro v-for="(article,index) in list" :key="index" :initarticle="article" :initkeyword="keyword" v-show="keyword.length==0 || (keyword.length>0 && article.title.indexOf(keyword.trim())>=0)" v-on:one-article="oneArticle(article)"></ui-intro>
<!-- oneArticle 为后面点击列表查看详情使用 -->
</ul>
<script>
export default {
name: 'SearchBox',
components: {
},
data () {
return {
};
},
mounted: function () {
},
methods: {
oneArticle: function (article) {
// todo 获取文章详情
}
}
};
</script>
接下来,我们可以继续做Detail
组件来实现上面的文章内容的渲染。
通过这篇简单的文章,可以深入了解一下VueJS
中父子组件传值
可能的坑。
如果想完美避开各种传值的难点,在单页网页中,可以定义一个全局的PageModel
来渲染页面并绑定渲染,事件触发只更新数据,并做到及时更新和清理数据,这样也不是不可以解决问题的。
9.完整Demo
懒得传git了,附上本次手写简单的demo,仅供参考:
父组件
<template>
<div id="searchbox">
<div>
<el-input></el-input>
</div>
<ul>
<ui-article v-for="(article,index) in list" :key="index"></ui-article>
</ul>
</div>
</template>
<template>
<div id="searchbox">
<div>
<el-input v-model="keyword" placeholder="keyword" @keyup.native="filtertitle()"></el-input>
</div>
<ul>
<ui-intro v-for="(article,index) in list" :key="index" :initarticle="article" :initkeyword="keyword" v-show="keyword.length==0 || (keyword.length>0 && article.title.indexOf(keyword.trim())>=0)" v-on:one-article="oneArticle(article)"></ui-intro>
<!-- oneArticle 为后面点击列表查看详情使用 -->
</ul>
</div>
</template>
<script>
import Article from './Article.vue';
export default {
name: 'SearchBox',
components: {
'ui-article': Article,
},
data() {
return {
list:[
{
title: 'The Allegations Against R. Kelly: An Abridged History',
date: '2019-01-17 12:07'
},
{
title: 'Jawbox Announces Reunion Tour After A Decade Away',
date: '2019-01-17 16:34'
},
{
title: 'A Song Called \'Quiet\' Struck A Chord With Women. Two Years Later, It is Still Ringing',
date: '2019-01-16 01:21'
}
]
};
},
mounted: function () {
},
methods: {
oneArticle: function (article) {
// todo 获取文章详情
}
}
};
</script>
<style lang="scss" scoped>
.searchbox {
}
</style>
子组件
<template>
<!-- $emit('one-article') 触发父组件文章详情事件 -->
<li class="article" v-on:click="$emit('one-article')">
<!-- {{article.title}} -->
<!-- {{article.title | colortitle(keyword)}} -->
<span class="title" v-html="colortitle(article.title,keyword)"></span>
<span class="user">author</span>
<span class="el-tag el-tag--info">{{article.date}}</span>
</li>
</template>
<script>
export default {
name: 'Article',
components: {
},
props: [
'initarticle',
'initkeyword'
],
data () {
return {
article: this.initarticle,
keyword: this.initkeyword
};
},
watch: {
initarticle () {
this.article = this.initarticle;
},
initkeyword () {
this.keyword = this.initkeyword;
}
},
// mounted: function () {
// },
computed: {
// colortitle: function () {
// return this.article.title.replace(this.keyword, '<span class="colored">' + this.keyword + '</span>');
// }
},
filters: {
// colortitle: function (title, keyword) {
// return keyword.length === 0 ? title : title.replace(keyword, '<span class="colored">' + keyword + '</span>');
// }
},
methods: {
colortitle: function (title, keyword) {
return keyword.length === 0 ? title : title.replace(keyword, '<span class="colored">' + keyword + '</span>');
}
}
};
</script>
<style lang="scss" scoped>
.article {
.title {
/deep/ .colored {
color: black;
background-color: #FFFF00;
}
}
}
</style>
10.参考文档:
原生事件监听:
组件刷新
模板语法
Vue系列之computed使用详解(附demo,不定期更新)
v-html does not work with filters
模板样式