站点图标 久久日记本

深入理解VueJS组件间数据传递

深入理解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-uiinput会生成这样的dom

<div class="el-input">
  <input type="text" autocomplete="off" placeholder="keyword" class="el-input__inner">
</div>

所以单纯使用:keyup并不能达到监听事件的目的。

监听原生事件,我们可以使用

v-on:keyup.native
@keyup.native

关于这个,可以参考这两篇文章:

el-input如何响应v-on:keyup.enter

用 v-on 监听原生事件

5.关键字高亮:使用filter过滤器

我们考虑在子组件中监视keyword的变化,来达到过滤当前一条数据的目的。

首先, 父组件keyword输入框不需要绑定任何事件,直接需要传递 articlekeyword给子组件,在子组件中实现监视。

子组件中添加一个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.keywordthis.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.参考文档:

原生事件监听:

el-input如何响应v-on:keyup.enter

用 v-on 监听原生事件

组件刷新

Vue 父组件ajax异步更新数据,子组件获取不到

Object.assign() 浅拷贝与深拷贝

模板语法

计算属性和侦听器

filter绑定demo

Vue系列之computed使用详解(附demo,不定期更新)

Filter in PROPS in vue.js

v-html does not work with filters

模板样式

vue 中控制v-html 中的样式,但不影响全局的小技巧

vue中慎用style的scoped属性

Vue scoped CSS 与深度作用选择器 /deep/

Deep Selectors: 深度作用选择器

退出移动版