近期工作小结

最近在做一个UI模板解析的vue组件,说是小结,但后期在布局和业务逻辑还需要做很多工作,这里仅总结一下现有的数据展示和简单的事件钩子部分

Quick Start

主要功能

UI模板解析组件的作用是通过后台传递的json数据解析成表单页面,json数据包含表单结构数据和表单值。使用饿了么前端组件开发,包装成vue单文件组件。

基本思路

表单页面中的业务对象种类很多,但很多只是业务逻辑上的区别,所以大体可以抽象成以下几个组件:文本、input组件(包括input输入框和文本域)、选择框、日期选择器、数据表格、参照。由于涉及行列布局,加入了form和table组件用于布局。

基本输入组件

input组件为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<template>
<el-input
:placeholder="data.describe"
:type="type"
v-model="value"
v-on:keyup.native="handleInput"
:id="data.fieldId">
</el-input>
</template>
<script>
export default {
data() {
return {
type: this.data.componentKey==='TextArea'?'textarea':'text',
value: this.templateValue[this.data.fieldId]?this.templateValue[this.data.fieldId]:''
}
},
props:['data','templateValue'],
methods:{
handleInput:function(){
if(this.data.componentKey==='NumberComponent'){
this.value=this.value.split('').filter(v => !(v>'9'||v<'0')).join('');
}
this.$emit('change',this.data.fieldId,this.value);
}
},
created: function(){
}
}
</script>

这里实际上是根据json结构数据对饿了么组件进行二次包装,组件props接收父组件数据,data为结构数据,templateValue为表单值,我们不会对结构数据进行修改,所以这里父子组件直接传递了引用类型的结构数据对象。templateValue需要修改,但父组件数据不能在子组件中修改,这里通过vue实例方法$emit触发父组件change事件,一级级传到UI模板组件,修改对应templateValue数据使子组件触发视图更新,handleInput方法可针对具体业务组件类型加入新的过滤规则。

布局组件

form为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<template>
<el-row>
<el-form :model="data" :label-width="labelWidth" :inline="true">
<el-form-item v-for="layout in data.layoutDetail" :label="layout.title">
<div @change="change" :is="map[layout.componentKey]" :data="layout" :templateValue="templateValue"></div>
</el-form-item>
</el-form>
</el-row>
</template>
<script>
export default {
data(){
return {
labelWidth: 200px,
isInline: true,
map: {} //组件map
}
},
methods:{
change:function(index,value){
this.$emit('change',index,value);
}
},
props:['data','templateValue'],
components:{
'test-select':select,
'test-input':input,
//'yon-label':label,
'test-datepicker':datepicker,
'test-list':list,
'test-table':table,
'test-image':image,
'test-ref':ref
}
}
</script>

form组件本身没有业务逻辑,仅通过定义一个map映射,配合vue动态组件来根据json结构数据加载对应的基本输入组件,同时监听子组件触发的change方法,转发给父组件(UI模板组件)。

除了UI模板,还做了一个首页导航菜单组件

主要功能

这个组件功能点很简单,根据后台提供的json数据生成对应多级菜单,支持路由、跳转和标签式浏览组件。

基本思路

数据节点可大致分为两种,叶子节点和非叶子节点,其区别是有无children属性(Array类型),拿到json数据生成单级菜单,事件(click/hover)触发时叶子结点直接触发对应路由或跳转,非叶子节点则直接拿children属性渲染下一级菜单即可。
跳路由分两种,一种是一个路由对应一个组件,直接使用vue-router中的router-view可实现;另一种是多个路由对应一个组件,这里可以用类似/:id形式的动态路由,先把满足某一类形式的路由映射到同一组件,在组件中获取路由数据进行对应操作。

示例代码

一对一

1
2
3
4
5
<div class="main-right" >
<transition name="fade">
<router-view class="view" :menuList="menuList.data"></router-view>
</transition>
</div>

多对一

1
2
3
4
5
<el-tabs>
<el-tab-pane v-for="(tab,index) in routeCards" :name="tab.path" :closable="true" :label="tab.label">
<div :is="tab.component"></div>
</el-tab-pane>
</el-tabs>

导航菜单每一项都是有选中状态的,所以这里有一个功能点,路由组件多对一的情况下,菜单的选中状态和组件内部状态应该是对应的,选中一级菜单下的任一选项,导航菜单上选中的应该(也只能)是其祖先节点中的那个一级菜单节点,目前通过在组件内部监听$route对象,路由变化的同时更新对应菜单或组件的内部状态,很简单可以实现。

关于vue使用上的一些问题和总结:

1.关于组建件通信:
整体遵循props down、events up,不要在子组件中改变父组件的数据,这一点但凡组件或者数据规模稍大,应该很容易体会到这么做的好处:组件职责明确、数据流清晰不会互相影响错综复杂。
组件之间尽量传递简单值,如果传递引用,需确保不要在子组件修改其数据(有时可用JSON.parse+JSON.stringify拷贝数据对象避免这一点)。
事件通信分父子组件通信和非父子组件通信,前者通过v-on为子组件绑定自定义事件,子组件触发事件时调用父组件对应方法实现通信,后者需要通过event bus,因为vue实现了事件接口,每个vue实例都可以作为事件代理使用,可以直接在根节点注册一个vue实例作为bus使用,此外还有更专业的状态管理方案vuex。

2.关于数组更新检测
官方文档关于数据更新检测的描述分为变异和非变异方法,一开始看到这里直觉性地认为变异方法改变了数组本身所以能触发更新,非变异方法并没有改变数组本身所以无法触发更新。但数组如何检测到本身发生变化?如果能检测到变化,为何字面量赋值以及修改length的形式无法触发视图更新(这两种都改变了数组)?查阅资料得知vue对变异方法进行了包装,具体逻辑类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const aryMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
const arrayAugmentations = [];
aryMethods.forEach((method)=> {
// 这里是原生Array的原型方法
let original = Array.prototype[method];
// 将push, pop等封装好的方法定义在对象arrayAugmentations的属性上
// 注意:是属性而非原型属性
arrayAugmentations[method] = function () {
console.log('我被改变啦!');
// 调用对应的原生方法并返回结果
return original.apply(this, arguments);
};
});
let list = ['a', 'b', 'c'];
// 将我们要监听的数组的原型指针指向上面定义的空数组对象
// 别忘了这个空数组的属性上定义了我们封装好的push等方法
list.__proto__ = arrayAugmentations;
list.push('d'); // 我被改变啦! 4
// 这里的list2没有被重新定义原型指针,所以就正常输出
let list2 = ['a', 'b', 'c'];
list2.push('d'); // 4

所以这里说到底还是个设计问题,要在方法调用时通知watcher数组被改变,当然是包装变异方法。