深圳幻海软件技术有限公司 欢迎您!

基于HBuilderX+UniApp+ThorUI的手机端前端的页面组件化开发经验

2023-03-02

现在的很多程序应用,基本上都是需要多端覆盖,因此基于一个WebAPI的后端接口,来构建多端应用,如微信、H5、APP、WInForm、BS的Web管理端等都是常见的应用。本篇随笔继续分析总结一下项目开发的经验,针对页面组件化开发经验方面进行一些梳理总结,内容包括组件的概念介绍,简单页面组件的抽取开发

现在的很多程序应用,基本上都是需要多端覆盖,因此基于一个Web API的后端接口,来构建多端应用,如微信、H5、APP、WInForm、BS的Web管理端等都是常见的应用。本篇随笔继续分析总结一下项目开发的经验,针对页面组件化开发经验方面进行一些梳理总结,内容包括组件的概念介绍,简单页面组件的抽取开发,以及对控件值进行更改的页面组件的处理等,希望能够对大家有所启发。

1、Vue组件的概念

组件是Vue中的一个重要概念,是一个可以重复使用的Vue实例,它拥有独一无二的组件名称,它可以扩展HTML元素,以组件名称的方式作为自定义的HTML标签。因为组件是可复用的Vue实例,所以它们与new Vue()接收相同的选项,例如data,computed、watch、methods以及生命周期钩子等。 

组件是可复用的 Vue 实例, 把一些公共的模块抽取出来,然后写成单独的的工具组件或者页面,在需要的页面中就直接引入即可那么我们可以将其抽出为一个组件进行复用。例如 页面头部、侧边、内容区,尾部,上传图片,等多个页面要用到一样的就可以做成组件,提高了代码的复用率。

Vue的前端界面,对界面内容部分可以根据需要进行适当的拆分,也可以把通用的部分封装为组件进行使用。如果整体界面内容比较多,可以进行拆分,根据内容的展示不同,拆分为各自的组件模块,然后合并使用即可,如下所示。

在对象UML的图例中,应该是如下所示的效果图,组织机构包含组织成员和角色的内容。

在界面上,组织成员还需要添加成员的功能,同理角色也需要添加角色的处理,如下图示。

角色界面模块的内容划分图如下所示。 

实际页面组件的开发,也可以按照内容的划分方式进行组件的开发,然后将它们组合起来。

如果我们开发了很多不同业务场景的组件,那么在实际页面中,就可以对它们进行组合使用,提高页面的整洁,同时也便于代码的维护。

组件的拆分和封装,是我们前端开发中非常重要的部分,也是我们快速构建复杂页面功能的,又能轻松应对的必杀技之一。 

 例如对于一个异常信息的处理,我们整合了多个模块的内容进行展示,采用自定义组件的方式,可以减少很多繁杂的前端代码。

 上面页面的大部分都是自定义组件的整合使用,如下代码截图所示。

 需要使用的组件,在Vue的JS代码中导入组件即可

2、简单页面组件的抽取开发

例如在一些新录入内容的页面中,我们往往有一些类似人员和时间的信息需要展示,可以把它做成一个简单的页面组件模块

如果用户是创建新记录的,那么显示当前登录的用户名称和当前日期,如果是已有记录,则显示记录中的用户和时间即可,一个很简单的例子。

<!--通用填报信息展示,用于创建新记录或者明细的展示-->
<template>
    <view class="">
        <view class="tui-order-info">
            <tui-list-cell :hover="false" :arrow="showArrow" @click="showDetail">
                <view class="tui-order-title">
                    <view>{{title}}</view>
                    <view class="tui-order-status" :style="{color:subTitleColor}">
                        {{subTitle}}
                    </view>
                </view>
            </tui-list-cell>
            <view class="tui-order-content" v-show="visible">
                <view class="tui-order-flex">
                    <view class="tui-item-title">填报时间:</view>
                    <view class="tui-item-content">{{currentDate}}</view>
                    <view style="padding-right: 50rpx;"></view>
                    <view class="tui-item-title">填报人:</view>
                    <view class="tui-item-content">
                        {{currentUser}}
                    </view>
                </view>
            </view>
        </view>
    </view>
</template>

信息填报的标题中,单击可以切换折叠或者展示模式,通过事件showDetail 进行触发的,而内容这里通过visible属性进行控制,同时接收来时Props属性的detailVisible的初始化设置。

在Props的父传子属性中,我们定义了一些通用数据属性,如标题、副标题、当前时间和填报人等。

<script>
    export default {
        emits: ['click', 'cancel'],
        props: {
            title: {
                type: String,
                default: "信息填报"
            },
            subTitle: {
                type: String,
                default: ""
            },
            subTitleColor: {
                type: String,
                default: ""
            },
            showArrow: { //默认是否展示箭头
                type: Boolean,
                default: false
            },
            creator: {
                type: String,
                default: ""
            },
            createTime: {
                type: String,
                default: ""
            },
            detailVisible: { //默认是否展示详情内容
                type: Boolean,
                default: true
            },
        },

另外,我们在data中添加一个组件属性visbile属性,

data() {
    return {
        visible: true
    }
},

这是因为传入的detailVisible不能在组件中中修改其属性值,因此接收它的值,并让她赋值给局部的属性,这种做法是常见的处理方式。为了同时保持两个属性的一致,我们通过Created事件中初始化局部变量,并通过Watch监控数据的变化。

created() {
    this.visible = this.detailVisible;
},
watch: {
    detailVisible(val) {
        this.visible = val
    }
},

当前用户名称和日期,我们判断传递的属性是否为空值,非空则使用传递的值,否则使用当前用户身份和当前日期值,因此我们通过一个计算属性来判断,如下所示。

computed: {
    currentUser() {
        if (uni.$u.test.isEmpty(this.creator)) {
            return this.vuex_user?.info?.fullName;
        } else {
            return this.creator;
        }
    },
    currentDate() {
        if (uni.$u.test.isEmpty(this.createTime)) {
            return uni.$u.time.timeFormat();
        } else {
            return uni.$u.time.timeFormat(this.createTime);
        }
    }
},

这里面的 this.vuex_user?.info?.fullName 是我们通过Vuex的方式存储的一个当前用户身份的信息,通过?来判断是否非空。另外,我们对list-cell的事件进行处理,切换折叠和展开的处理,因此只需要设置局部变量visible的属性即可。

methods: {
    showDetail() {
        this.visible = !this.visible;
    }
}

这样定义好的页面内容后,我们只需要把它加入到页面组件中就可以使用它了

<script>
    import createinfo from '@/pages/components/createinfo.vue';
    export default {
        components: {
            createinfo
        },

HTML代码和其他简单的HTML常规组件类似。

<createinfo></createinfo>

或者传入数据处理

<createinfo :createTime="entity.createTime" :creator="entity.deliverName"></createinfo>

另外,如果我们需要在内部保留一块区域给父页面定义的模板,可以使用Slot进行处理,这样除了添加了人员和日期信息,我们还可以嵌入一个更多内容信息作为区块给组件进行展示了。

<template>
    <view class="">
        <view class="tui-order-info">
            <tui-list-cell :hover="false" :arrow="showArrow" @click="showDetail">
                <view class="tui-order-title">
                    <view>{{title}}</view>
                    <view class="tui-order-status" :style="{color:subTitleColor}">
                        {{subTitle}}
                    </view>
                </view>
            </tui-list-cell>
            <view class="tui-order-content" v-show="visible">
                <slot #default>
                    <!--                 
                    <view class="tui-order-flex">
                        <view class="tui-item-title">角色编码</view>
                        <view class="tui-item-content">123456</view>    
                    </view>
                    -->
                </slot>
            </view>
        </view>
    </view>
</template>

 

3、对控件值进行更改的页面组件的处理

除了上面的闭合组件,我们有时候也需要一个传递信息和变更v-model的值的处理,或者抛出一些事件给父页面进行通知。

例如,我们在选择一些系统数据字典项目的时候,以及在选择时间的时候,这些是比较常见的操作,我们可以结合我们的组件库,做一些简单的组件封装,从而达到简化页面使用的目的。

如果没有自定义组件的情况,我们使用字典模块的内容,需要请求对应的字典列表,然后绑定在控件Picker中,如下代码所示。

<tui-list-cell arrow padding="0" @click="showPicker = true">
    <tui-input required backgroundColor="transparent" :borderBottom="false" label="送货区域"
        placeholder="请选择送货区域" v-model="formData.deliveryArea" disabled></tui-input>
</tui-list-cell>

<tui-picker :show="showPicker" :pickerData="areas" @hide="showPicker = false" @change="changePicker">
</tui-picker>

在页面的JS脚本处理中,还需要对字典类型进行取值、以及选择后修改属性值等操作处理

<script>
    import dict from '@/api/dictdata.js'
    
    export default {
    data() {
        return {
            showPicker: false,
            areas: [
                // {text: "中国", value: "1001"},
            ]
        }    
    },
    created() {
        dict.GetListItemByDictType("送货区域").then(res => {
            this.areas = res;
        })
        dict.GetListItemByDictType("客户等级").then(res => {
            this.grades = res;
        })
    },
    methods: {
        changePicker(e) {
            this.formData.deliveryArea = e.value;
        }
    }
  }
  </script>

一顿操作下来还是比较麻烦的,但是如果你使用自定义用户组件来操作,那么代码量迅速降低,如下所示。

<createinfo>
    <view class="tui-bg-img"></view>
    <dict-items-picker v-model="formData.deliveryArea" dictTypeName="送货区域" label="送货区域"></dict-items-picker>
    <dict-items-picker v-model="formData.grade" dictTypeName="供货档级" label="供货档级"></dict-items-picker>
</createinfo>

获得的界面效果如下所示,是不是感觉更好,代码更整洁了。

 

 而其他部分的代码,则只是包括了引入组件的代码即可。

import dictItemsPicker from '@/pages/components/dict-items-picker.vue'
export default {
    components: {
        dictItemsPicker
    },

上面组件的dictTypeName 对应的是数据库后台的字典类型名称,根据名称,从组件中调用api类获得数据即可,页面则不需要干涉这些逻辑了。其中我们自定义组件中使用 v-model 来绑定选择的值到页面的data属性中。

我们来看看整个页面组件的全部代码(内容不多,就一次性贴出来)

<!--通用字典下拉框展示-->
<template>
    <view class="">
        <tui-list-cell arrow padding="0" @click="showPicker = true">
            <tui-input :required="required" backgroundColor="transparent" :borderBottom="false" :label="labelName"
                :placeholder="placeholder" v-model="value" disabled></tui-input>
        </tui-list-cell>
        <tui-picker :show="showPicker" :pickerData="dictItems" @hide="showPicker = false" @change="changePicker">
        </tui-picker>
    </view>
</template>

<script>
    import dict from '@/api/dictdata.js'
    export default {
        emits: ['click', 'cancel', 'update:value', 'change'],
        props: {
            required: { //是否必选
                type: Boolean,
                default: false
            },
            dictTypeName: {
                type: String,
                default: ""
            },
            options: {
                type: Array,
                default () {
                    return []
                }
            },
            value: {
                type: String,
                default: ""
            },
            label: {
                type: String,
                default: ""
            }
        },
        components: {},
        data() {
            return {
                showPicker: false,
                dictItems: []
            }
        },
        mounted() {
            if (!uni.$u.test.isEmpty(this.dictTypeName)) {
                dict.GetListItemByDictType(this.dictTypeName).then(res => {
                    this.dictItems = res;
                })
            } else {
                this.dictItems = this.options;
            }
        },
        watch: {},
        computed: {
            placeholder() {
                return "请选择" + this.dictTypeName;
            },
            labelName() {
                if (this.label) {
                    return this.label;
                } else if (!uni.$u.test.isEmpty(this.dictTypeName)) {
                    return this.dictTypeName
                } else {
                    return ""
                }
            }
        },
        methods: {
            changePicker(e) {
                this.$emit('change', e)
                this.$emit("input", e.value)
                this.$emit("update:value", e.value);
            }
        }
    }
</script>

这里在内部组件的值变化的时候,通过事件 this.$emit("update:value", e.value); 进行通知更新父组件的绑定值,而 this.$emit("input", e.value)事件则是更新内部包含的input组件的值,同时提供一个change的事件进行完整的通知处理。

由于我们传递的是DIctTypeName,组件内部在Mounted的事件后进行获取字典的内容,并将返回值更新到内部组件中去。,如果是静态的字典集合,可以通过options 进行赋值即可。

if (!uni.$u.test.isEmpty(this.dictTypeName)) {
    dict.GetListItemByDictType(this.dictTypeName).then(res => {
        this.dictItems = res;
    })
} else {
    this.dictItems = this.options;
}

因此该组件可以接受系统数据字典的字典类型名称,或者接受静态的字典列表作为数据源供选择需要。

整个组件内部封装了读取数据的细节以及展示控件的处理,并将新值通过事件this.$emit("update:value", e.value); 的方式更新父页面的v-modal的绑定值,因此看起来和一个简单的Input的组件使用类似了。

以上就是一些简单组件的封装介绍,我们可以根据实际的需要,把我们 项目中遇到的可以封装的内容提出取出来,然后进行封装为组件的方式,会发现页面维护起来更加方便整洁了。

在组件逐步增多的情况下,我们同步完善一个简单的页面用来测试查看组件的效果,否则组件一多,记起来某个组件的效果就比较困难,我们的一个测试页面例子如下所示。