还在写最简单的vue功能组件么?

5,100 阅读7分钟

在日常项目开发过程中,往往离不开封装组件的过程。比如a页面有一个功能恰巧b页面和c页面也使用。这时候我们考虑到代码复用,会考虑将这个功能做成组件。这篇文章我们就来讲一讲开发vue组件时候的一些小技巧和一些黑()科()技()。

我们会通过大量例子来一个个讲解
所有本文例子代码已经上传到github

目录

  • 常规功能组件小技巧
  • 函数式组件
  • 内联模板
  • 递归组件
  • v-once低开销静态组件
  • 全局函数调用组件

常规功能组件小技巧

首先我们用脚手架搭建一个项目,用来写我们的组件,我们用到的UI框架是Iview。功能组件的编写,离不开组件之间的通信,这个相关的文章很多,本文就不详细阐述了,这里推荐一篇掘金的关于组件通信的文章,写的十分全面感兴趣的小伙伴可以去看看。

slot插槽

slot插槽在我们平常写组件的时候出场率应该很高。将 <slot> 元素作为承载分发内容的出口,我们可以在插槽内包含任何模板代码,这使得我们的组件更加的灵活。我们来讲一个比较经典的例子对话框组件,先看代码。

<template>
  <div>
    <div>头部</div>
    <div>
      <slot>默认内容</slot>
    </div>
    <div>
      <slot name="footer"> <Button>确认</Button> </slot>
    </div>
  </div>
</template>
<script>
export default { name: "model" };
</script>

model标签中除slot为footer的代码,其他代码都将作为对话框的主体内容显示,这就是作用域插槽的用法。而slot为footer的代码将在底部操作按钮部分显示,对应<slot name="footer">这就是具名插槽的用法。

属性透传

属性透传的出现的场景不多,我们以UI组件的二次封装为例。假如你的项目中频繁使用一个UI组件,而且这个UI组件每次都要设置很多类似的属性,那么我们就可以进行二次封装,调用起来更加的方便,甚至可以在原有组件上扩展功能。我们来看一段二次封装Iview的Table组件的代码。

<script>
import { Table } from "iview";
export default {
  name: "customizeTable",
  props: { ...Table.props, test: { type: [String], default: "扩展属性" } },
  data() {
    return { defaultProps: { border: true, stripe: true } };
  },
  render(h) {
    var props = Object.assign({ ...this._props }, this.defaultProps);
    return h(Table, { props: props });
  }
};
</script>

//方法二

<template>
  <div>
    <Table v-bind="{...mergeProps}"></Table>
  </div>
</template>
<script>
import { Table } from "iview";
export default {
  name: "customizeTable",
  props: { 
      ...Table.props,
      test: { 
          type: [String],
          default: "扩展属性" 
      } 
  },
  computed: {
    mergeProps: function() {
      return Object.assign({ ...this._props }, this.defaultProps);
    }
  },
  data() {
    return { 
        defaultProps: { 
            border: true, 
            stripe: true 
        } 
    };
  }
};
</script>

首先我们通过Table组件的props属性获取的Table所有默认参数,然后通过this._props实现属性透传,同时我们还可以在props中扩展你需要的属性如test。方法一中绑定属性我们使用了render方法,这个我们在函数式组件中再细说。方法二中我们通过v-bind实现绑定对象中所有的属性。


函数式组件

我们先引用一下官网对于函数式组件的介绍

函数式组件,没有管理任何状态,也没有监听任何传递给它的状态,也没有生命周期方法。实际上,它只是一个接受一些 prop 的函数。在这样的场景下,我们可以将组件标记为 functional,这意味它无状态 (没有响应式数据),也没有实例 (没有 this 上下文)。

组件需要的一切都是通过 context 参数传递,它是一个包括如下字段的对象:

  • props:提供所有 prop 的对象
  • children: VNode 子节点的数组
  • slots: 一个函数,返回了包含所有插槽的对象
  • scopedSlots: (2.6.0+) 一个暴露传入的作用域插槽的对象。也以函数形式暴露普通插槽。
  • data:传递给组件的整个数据对象,作为 createElement 的第二个参数传入组件
  • parent:对父组件的引用
  • listeners: (2.3.0+) 一个包含了所有父组件为当前组件注册的事件监听器的对象。这是 data.on 的一个别名。
  • injections: (2.3.0+) 如果使用了 inject 选项,则该对象包含了应当被注入的属性。

在添加 functional: true 之后,需要更新我们的锚点标题组件的渲染函数,为其增加 context 参数,并将 this.$slots.default 更新为 context.children,然后将 this.level 更新为 context.props.level

为什么要用函数式组件?

由于函数式组件没有生命周期、无状态、没有实例,那就意味着它渲染速度更快,开销更低,同时因为函数式组件只是函数,我们可以在函数中实现一些我们需要的逻辑。

什么时候使用函数式组件?

在我看来函数式组件更像是一个中间件的角色,所以它比较常见的应用场景就是作为包装组件。举例来说我们有一个组件可能渲染多个组件中的一个,需要根据传递的参数来决定渲染那个组件,我们来看一段代码。

export default { 
  functional: true, 
  render: function (createElement, context) { 
    return createElement(context.props.domType, context.data, context.children); 
  } 
}

<template>
  <div>
    <functionnalScript domType="model" style="width:100%"></functionnalScript>
  </div>
</template>

<script>
import customizeTable from "../components/customizeTable.vue";
import model from "../components/model.vue";
import functionnalScript from "../components/functionnal.js";
export default {
  name: "pageA",
  components: { 
      customizeTable, 
      model, 
      functionnalScript: functionnalScript 
  }
};
</script>
//模板的函数式组件 functional.vue

<template functional>
  <div>
    <button v-bind="data.attrs" v-on="listeners">
      <slot />
    </button>
  </div>
</template>

正常调用的时候父页面我们有customizeTable组件和model组件我们可以通过在一个属性domType指定functionnalScript显示对应的组件,而且可以在多处复用。

还记得我们前面是不是讲了属性透传,其实通过函数式组件也可以实现属性透传,我们来看模板的函数式组件functional组件就实现了完全透传任何 attribute事件监听器子节点等。

内联模板

组件的模板一般都是在template选项内定义,而内联模板在使用组件时,给组件标签使用inline-template特性,组件就会把它的内容当作模板,而不是把它当内容分发,这让模板更灵活。但是这也导致了一些问题,我们先看代码。

<script>
export default {
  name: "inlineChildren",
  data() {
    return { tip: "我是子组件的数据" };
  }
};
</script>

<template>
  <div>
    <inlineChildren inline-template>
      <div>
        <h3>{{tip}}</h3>
      </div>
    </inlineChildren>
  </div>
</template>
<script>
import inlineChildren from "../components/inlineChildren.vue";
export default {
  name: "pageA",
  components: { inlineChildren },
  data() {
    return { tip: "我是父组件的数据" };
  }
};
</script>

inline-template的存在让inlineChildren标签内部内容不再作为slot(分发内容),而是作为inlineChildren组件的template,并且这部分内容所在的上下文,是子组件的,并不是父组件的。所以除非特殊场景(我是没遇到过~~),其它适合正如官网所说的,inline-template 会让模板的作用域变得更加难以理解。所以作为最佳实践,请在组件内优先选择 template 选项或 .vue 文件里的一个 <template> 元素来定义模板。

递归组件

什么是递归组件?

在组件自己的模板中调用自身就是我们所谓的递归组件。(注意需要通过组件的name调用)

使用场景有哪些?

递归组件在我们日常开发中的发挥空间还是很大的,比如我们的菜单,评论等有层级的功能都可以使用到它。我们来看一个菜单的例子:

<template>
  <div>
    <div v-for="item in menuList">
      <div class="hasChildren">{{item.name}}
        <Icon v-if="item.children" type="ios-arrow-down" />
      </div>
      <side-menu v-if="item.children" :menuList="item.children"></side-menu>
    </div>
  </div>
</template>

<script>
export default {
  name: "SideMenu",
  props: {
    menuList: {
      type: Array,
      default() {
        return [];
      }
    }
  }
};
</script>

<style>
.hasChildren {
  background: #999;
  color: #fff;
  height: 40px;
  line-height: 40px;
}
</style>

<template>
  <div>
    <sideMenu :menuList="menuList"></sideMenu>
  </div>
</template>
<script>
import sideMenu from "../components//side-menu.vue";
export default {
  name: "pageA",
  components: { sideMenu },
  data() {
    return {
      menuList: [
        { name: "side1", children: [{ name: "side1-1" }, { name: "side1-2" }] },
        {
          name: "side2",
          children: [
            { name: "side2-1" },
            { name: "side2-2", children: [{ name: "side2-2-1" }] }
          ]
        }
      ]
    };
  }
};
</script>

我们可以看到在sideMenu组件内部调用了它本身,这就是递归组件的基本使用方式。在更加复杂一点的情况就是有2个组件A和B,他们之间互有调用关系。这时会出现一个问题,到底是先有鸡还是先有蛋。


模块系统发现它需要 A,但是首先 A 依赖 B,但是 B 又依赖 A,但是 A 又依赖 B,如此往复。这变成了一个循环,不知道如何不经过其中一个组件而完全解析出另一个组件,解决方法有2个。

//方法一 在A组件
beforeCreate: function () {
  this.$options.components.TreeFolderContents = require('./B.vue').default
}
//方法二 异步加载B组件
components: {
  B: () => import('./B.vue')
}

v-once低开销静态组件

如果你的组件中出现了大量静态内容,那么它们可能导致你的组件渲染速度变慢,这时候你可以在根元素上添加 v-once attribute 以确保这些内容只计算一次然后缓存起来。

<template>
  <div v-once> {{hugeData}} </div>
</template>

<script>
export default {
  name: "vOnce",
  data() {
    return { hugeData: "huge Data..." };
  },
  mounted() {
    setTimeout(() => {
      this.hugeData = "change Data...";
    }, 100);
  }
};
</script>

你会发现hugeData一直是huge Data...没有变,因为v-once导致这个模板渲染后数据发生变化无法正常更新,所以考虑到其他开发者对于这个属性并不一定了解,如果不是特别需要尽量不要使用。

全局函数调用组件

正常的组件调用的过程:

<testModule></testModule>
import TestModule from "./TestModule "

export default {  components: { testModule  }};

如果你需要在多处使用这个组件,那么意味着每次调用你都需要重复上面的步骤,那么有没有更加方便的调用方式呢?console.logalert不陌生吧,有时候我们也希望能通过这种方式调用一个组件,不在需要把组件写入到节点中。例如一个提示框组件,可能需要在各种时候唤醒它,并且提示对应内容,我们通过代码来实现这个tip组件。

//extend.js
import Vue from 'vue'import Tip from './tip.vue';
//使用基础 Vue 构造器,创建一个包含tip组件的“子类”    
Tip.newInstance = (props) => { 
  const tip = Vue.extend(Tip); 
  const messageInstance = new tip({ propsData: props }); 
  const div = document.createElement('div'); 
  document.body.appendChild(div); messageInstance.$mount(div) 
}
export default Tip.newInstance

//main.js
import tip from 'extend.js'Vue.prototype.$tip=tip;
//调用
this.$tip({});

这样我们就能实现通过调用一个函数this.$tip({})来调用Tip组件了。你可以看到我们的核心思想就是创建一个包含我们需要组件的vue实例,然后将这个实例挂载到一个新创建的div上。如果我们可能多次调用这个函数,那么页面中将会被添加入多个div,这并不是我们所希望,所以我们还可以再优化一下。

<template>
  <div>
    <div v-for="item in tips">
      <tip v-bind="item"></tip>
    </div>
  </div>
</template>
<script>
let i = 0;
const now = new Date().getTime();
function getUid() {
  return "tip" + now + "-" + i++;
}
import tip from "./tip.vue";
export default {
  name: "tipList",
  data() {
    return { tips: [] };
  },
  components: { tip },
  methods: {
    add(options) {
      let name = getUid;
      let _options = Object.assign({ name: name }, options);
      this.tips.push(_options);
    }
  }
};
</script>

import Vue from 'vue'import Tip from './tipList.vue';
let tip, initialization;
//使用基础 Vue 构造器,创建一个包含tip组件的“子类”
Tip.newInstance = () => {  
  tip = Vue.extend(Tip);  
  const messageInstance = new tip();  
  const div = document.createElement('div'); 
  document.body.appendChild(div); 
  messageInstance.$mount(div)      
  return {          
    prompt(props) {              
      messageInstance.add(props);          
    }      
  }
}
initialization=function(){   
  if(!tip){        
    tip=Tip.newInstance()    
  }
  return tip
}
export default initialization
//main.js
import tip from 'extend.js'
Vue.prototype.$tip=tip;
//调用
this.$tip.prompt({});

我们通过一个组件tipList.vueprompt方法实现多次调用的tip组件的需求,每次调用tip组件等于向tips数组中push一个对象这样就会多渲染出一个tip组件。

总结

希望看完之后,你也能在你的组件中使用上它们,让你的组件可以减少一些不必要的逻辑。作者可能有一些理解不对的地方希望大佬可以再评论里提出来,我们一起学习一起进步。

其他文章传送门:

  1. 基于vue实现web端超大数据量表格
  2. 百度地图-大数据量点实时更新