本文涉及到的知识点:
- Vue函数式组件
- 递归函数
- 深拷贝对象
- 正则匹配
近期在开发一个vue组织架构图组件时,为了实现高性能渲染和一些特殊用法,使用了函数式组件,要实现的效果是这样:
写一个组织架构图组件来深入认识vue函数式高阶组件
要求实现的效果有:
- 可以点击节点来展开/收缩其下面的子级节点;
- 可以很轻松地自定义每个节点HTML结构和样式,本人的想法是能够直接使用高亮显示的vue模板语法,而不是简单的拼接html字符串,类似于组件插槽的方式;
- 支持展开/收缩事件、能够一键展开收缩全部节点;
- 使用标准json格式数据;
下面进入正题。
什么是函数式高阶组件以及它的优缺点在vue官网中已经介绍的非常详细,这里只说两点:
- 相对于普通组件,函数式组件多了一个
render
函数,用于生成整个组件的虚拟dom树,render
函数的参数是createElement
和context
,createElement
的返回结果是虚拟节点(vnode);context
是从父级组件传入的一切数据,例如props
、插槽、作用域插槽、监听事件等等,下面会说如何去用它们; - 函数式组件没有自己的状态,也就是没有自己的
data
(响应式数据),只能被动地从父级接收props
,也没有this
上下文;
可以像这样声明并引用一个函数式组件:
其中vueFtNode
就是我们的函数式组件了,它的render
函数比较庞大,所以将它单独写在了render.js中。
当然在单文件组件中也可以在template
标签上面加functional
声明一个函数式组件,但是这样就体会不到render
函数那样纯粹的函数式的编程体验了。
然后我们返回到插件实现中,具体的HTML结构是这样:
总的来说就是用一个class
为vue-ftree
的div包裹住整个组件结构,用class为vue-ftree-node
的div渲染每一个节点,用vue-ftree-node-content
渲染节点内容,然后每个节点下又包含一个vue-ftree-children
,这里面又包含了当前节点的子节点,子节点依然用vue-ftree-node
渲染。
这样就形成了一个简单的递归过程。
生成虚拟节点的方式是使用createElement
函数,看完官网对createElement
函数的介绍再来看我们的组件HTML结构,你会觉得很头大,用createElement
创建一个vnode基本形式是这样:
其中children是vue-ftree下的子节点组成的数组,每个子节点同样也需要用createElement函数生成。
也就是说,要想生成上面截图中的复杂HTML结构岂不是要createElement嵌套createElement,一层又一层,像剥洋葱一样辣眼睛,等写完了满眼都是泪啊。
其实你想的一点也没错,事实就是如此残酷,不过好在有jsx这个东西,它能像写普通HTML一样生成虚拟节点,具体可以到vue官网里查看,但是需要引入一系列依赖,本着公用组件尽量少用依赖的原则,只能硬着头皮一层一层写了。
首先我们要在render函数中生成一个基本框架,一个class为vue-ftree的div作为容器节点,此节点下面包含了所有组织架构节点:
h是啥?createElement换个马甲你就不认识啦?
其中renderTree函数用于生成每个组织架构节点,renderTree中又有renderNode函数等等,这其中的弯弯绕绕我这个写插件的人都不忍再回顾,里面不光涉及到createElement的各种嵌套还有递归函数和遍历,感兴趣的朋友可以进GitHub上看源码。
GitHub:https://github.com/weitamingting/vue-furcate-tree
总之看完会掉不少头发。
讲重点:
在render函数里,要给vnode添加class,与普通组件差不多,支持字符串、数组和对象形式。所以上面生成一个class为vue-ftree的节点可以有以下几种写法:
如何给vnode添加事件监听?
我们希望给每个组织架构节点添加点击事件,而且这个点击事件需要暴露在组件外面以方便别人自由定义事件发生的事情,用法像这样:
对应的method:
vueFurcateTree组件实际上是在函数式组件vueFtNode外面又包裹的一层外壳,主要作用是隐藏函数式组件实现细节,让它能像普通组件一样被引用。
普通组件要加事件监听需要用到自定义事件并在组件内部合适的地方使用this.$emit触发,但是函数式组件没有this上下文,所以在函数式组件中这一方式行不通。
这里就要用到render函数的context参数了。
上面说了,context是从父级组件传入的一切数据,例如props、插槽、作用域插槽、监听事件等等,打印一下看看都有什么:
可以看到我们在vueFurcateTree上面监听的click事件函数就在listeners对象了,然后通过查阅createElement函数文档,我们只需要把这个函数传入它的数据对象中即可,例如我要在class是vue-ftree-node-content-inner的节点上监听这个点击事件,那么就可以像这样实现:
但是我们希望click事件中可以暴露出一些有用的参数,例如当前点击的节点数据,所以我们在context.listeners.click外面再包裹一层函数,像这样:
这样就可以利用闭包原理将函数体内的变量暴露到外面喽,什么是闭包?嘿嘿。
按照这个方法可以添加所有你能想到的原生事件和你自己天马行空命名的自定义事件。
最后,在开头我们说到,组件希望使用一种极其简单的方式,像写普通的html一样来制作每个组织架构节点里面显示的内容,有编辑器的高亮提示,易于阅读和编写,总之使用起来就像这样:
渲染出来的效果是这样:
传入组件的数据格式,也就是截图里的ft-data格式是这样的:
在render函数中如何拿到ft-data?很简单,context.props.ftData
。然后遍历、递归、巴拉巴拉…
继续说按照模板编译的事情:
我的想法是把原来组件插槽的位置变成节点渲染模板,每个节点都按照插槽的格式来生成,模板里可以使用#{变量}的形式来访问内部变量。
例如在上面的架构图中,第一个节点里面的#{label}
代表的就是'节点1'
这个值,#{test.a}
代表的就是'a'
这个值。好吧,我知道作用域插槽也能实现,这不是为了高大上一些么,哈哈。
#{...}
这是个啥?是我自己规定的一个模板标签,后面可以利用正则匹配得到变量名称从而进一步解析出变量来,跟vue的{{...}}
本质上是一个东东。
context参数中已经包含了组件传入的插槽数据,看文档会发现有两个可以用的属性,一个是children
,一个是slots
,这两个属性乍一看返回的都是插槽内容,那么平时如此人性化的尤大大为什么会突然整出这么两个迷惑众生的属性呢?
大家都知道,插槽是可以有命名的,也就是具名插槽,使用context.slots
方法取到的是一个对象,里面包含了所有具名插槽的信息,例如context.slots().test
,返回的就是命名为test的插槽数据;
而children取到的是一个数组,数组包含了插槽位置的所有虚拟节点,它不分命名,只要出现在插槽位置的节点都会返回。
因为我们这里只需要拿到插槽里的节点数据不用区分命名,所以用了children属性,接着遍历children所有节点,使用正则表达式匹配到#{}里面的变量,将变量转换为真正的值,然后把转换后的children传给vnode。
有一点需要注意,因为context的children属性是一个对象数组,属于引用类型,所以每次转换children时需要深拷贝一下,否则最终会导致所有组织架构节点内容都一样。
了解了这几个属性的运用,高阶函数组件基本上就没什么难点了,因为主要讲函数式组件,所以里面的正则匹配、递归函数等知识就不说了,进项目里看代码吧。