VUE Render 学习 与 树组件封装

733 阅读4分钟

一切的一切开始于UI的那个按钮。。。

前言

公司的上个项目是一个在线云盘项目。有一个需求是选择文件点击移动按钮会弹出一个资源树列表的对话框,选择文件夹进行文件移动的操作。原本对话框产品的原图是没有新建文件夹的按钮的。结果UI按照百度网盘的页面给我设计了一个。。。

我嘴上向他们抱怨了几句,又开始吹起了牛批 No Problem !!!

想着element UI 的 tree 组件已经提供了动态操作节点的api,我操起键盘就是一梭子~~

刚写到一半就遇到问题了,el-tree 组件在懒加载的情况下调用insertBeforeinsertAfter方法会报错

而且el-tree没有包装根节点,所以树第一层的节点也无法通过append方法来动态新增。

在网上一顿搜索,网友也都遇到这样的问题,有个处理方法是记录展开的节点,动态新增后刷新tree组件,再展开记录的节点。但是这个实现的方法再懒加载的情况体验实在不好。

实在没有办法了,决定让UI砍掉这个按钮~~~

但牛批已经吹出去了,岂能跌这个份儿。我要自己撸!!!

Render 函数

在vue的项目中,我们平常写页面或封装组件都是在template标签内编写HTML代码。众所周知,代码但凡与树扯上关系就一定离不开递归。但我又该如何使用递归去渲染页面呢?这时就用到了一个我听过却从没用过的vue渲染函数 render ---- 传送门

Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。

上一个官方的栗子:假设我们要生成一些带锚点的标题,你可能很快想到这样实现

<script type="text/x-template" id="anchored-heading-template">
  <h1 v-if="level === 1">
    <slot></slot>
  </h1>
  <h2 v-else-if="level === 2">
    <slot></slot>
  </h2>
  <h3 v-else-if="level === 3">
    <slot></slot>
  </h3>
  <h4 v-else-if="level === 4">
    <slot></slot>
  </h4>
  <h5 v-else-if="level === 5">
    <slot></slot>
  </h5>
  <h6 v-else-if="level === 6">
    <slot></slot>
  </h6>
</script>
Vue.component('anchored-heading', {
  template: '#anchored-heading-template',
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

这里用模板并不是最好的选择:不但代码冗长,而且在每一个级别的标题中重复书写了 ,在要插入锚点元素时还要再次重复。 虽然模板在大多数组件中都非常好用,但是显然在这里它就不合适了。那么,我们来尝试使用 render 函数重写上面的例子:

Vue.component('anchored-heading', {
  render: function (createElement) {
    return createElement(
      'h' + this.level,   // 标签名称
      this.$slots.default // 子节点数组
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

createElement 参数

接下来你需要熟悉的是如何在 createElement 函数中使用模板中的那些功能。这里是 createElement 接受的参数:

// @returns {VNode}
createElement(
  // {String | Object | Function}
  // 一个 HTML 标签名、组件选项对象,或者
  // resolve 了上述任何一种的一个 async 函数。必填项。
  'div',

  // {Object}
  // 一个与模板中 attribute 对应的数据对象。可选。
  {
    // (详情见下一节)
  },

  // {String | Array}
  // 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
  // 也可以使用字符串来生成“文本虚拟节点”。可选。
  [
    '先写一些文字',
    createElement('h1', '一则头条'),
    createElement(MyComponent, {
      props: {
        someProp: 'foobar'
      }
    })
  ]
)

深入数据对象

有一点要注意:正如 v-bind:class 和 v-bind:style 在模板语法中会被特别对待一样,它们在 VNode 数据对象中也有对应的顶层字段。该对象也允许你绑定普通的 HTML attribute,也允许绑定如 innerHTML 这样的 DOM property (这会覆盖 v-html 指令)。

{
  // 与 `v-bind:class` 的 API 相同,
  // 接受一个字符串、对象或字符串和对象组成的数组
  'class': {
    foo: true,
    bar: false
  },
  // 与 `v-bind:style` 的 API 相同,
  // 接受一个字符串、对象,或对象组成的数组
  style: {
    color: 'red',
    fontSize: '14px'
  },
  // 普通的 HTML attribute
  attrs: {
    id: 'foo'
  },
  // 组件 prop
  props: {
    myProp: 'bar'
  },
  // DOM property
  domProps: {
    innerHTML: 'baz'
  },
  // 事件监听器在 `on` 内,
  // 但不再支持如 `v-on:keyup.enter` 这样的修饰器。
  // 需要在处理函数中手动检查 keyCode。
  on: {
    click: this.clickHandler
  },
  // 仅用于组件,用于监听原生事件,而不是组件内部使用
  // `vm.$emit` 触发的事件。
  nativeOn: {
    click: this.nativeClickHandler
  },
  // 自定义指令。注意,你无法对 `binding` 中的 `oldValue`
  // 赋值,因为 Vue 已经自动为你进行了同步。
  directives: [
    {
      name: 'my-custom-directive',
      value: '2',
      expression: '1 + 1',
      arg: 'foo',
      modifiers: {
        bar: true
      }
    }
  ],
  // 作用域插槽的格式为
  // { name: props => VNode | Array<VNode> }
  scopedSlots: {
    default: props => createElement('span', props.text)
  },
  // 如果组件是其它组件的子组件,需为插槽指定名称
  slot: 'name-of-slot',
  // 其它特殊顶层 property
  key: 'myKey',
  ref: 'myRef',
  // 如果你在渲染函数中给多个元素都应用了相同的 ref 名,
  // 那么 `$refs.myRef` 会变成一个数组。
  refInFor: true
}

了解这些就可以轻松的在render里面写递归了。

主要逻辑通了,树组件的开发也就只是工作量的问题了。

dl-tree

总结

这一次开发组件主要是学习了render函数,还有vue插槽的使用。也加深了对vue的了解。这篇文章大部分都是官方的文档,我在组件开发中遇到的一些问题也都没有记录,如果你们想要批阅的我代码 --- Github 传送门,欢迎指导学习(前端菜鸟在线卑微)