likes
comments
collection
share

兴趣爱好者的下拉和专业的下拉也是有区别的讲解在同名B/D上都有,主要介绍一些跟业务无关的代码技巧 注: 部分内容主观性较

作者站长头像
站长
· 阅读数 25

讲解在同名B/D上都有,主要介绍一些跟业务无关的代码技巧

注: 部分内容主观性较大,一家之言姑且听之

本文主要介绍select组件的二次封装

爱好者的下拉组件使用 -- 又不是不能用

<template>
    <div id="app">
        <div>正常 {{ v }}
            <el-select v-model="v">
                <el-option v-for="item in data" :key="item.value" :label="item.label" :value="item.value"></el-option>
            </el-select>
        </div>
        <div>空数据 {{ v1 }}
            <el-select v-model="v1">
                <el-option v-for="item in data1" :key="item.value" :label="item.label" :value="item.value"></el-option>
            </el-select>
        </div>
        <div>通讯中 {{ v2 }}
            <el-select v-model="v2">
                <el-option v-for="item in data2" :key="item.value" :label="item.label" :value="item.value"></el-option>
            </el-select>
        </div>
        <div>接口挂了 {{ v3 }}
            <el-select v-model="v3">
                <el-option v-for="item in data3" :key="item.value" :label="item.label" :value="item.value"></el-option>
            </el-select>
        </div>
    </div>
</template>
<script>
import { api, api1, api2, api3 } from './api'
import xxSelect from './xxSeelct.vue'
export default {
    comments: {
        xxSelect
    },
    data() {
        return {
            v: "",
            v1: "",
            v2: "",
            v3: "",
            data: [],
            data1: [],
            data2: [],
            data3: [],
        }
    },
    created() {
        api().then(res => this.data = res)
        api1().then(res => this.data1 = res)
        api2().then(res => this.data2 = res)
        api3().then(res => this.data3 = res)
    }

}
</script>

问题

export function api() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve([
                { value: '选项1', label: '黄金糕' },
                { value: '选项2', label: '双皮奶' },
                { value: '选项3', label: '蚵仔煎' },
                { value: '选项4', label: '龙须面' },
                { value: '选项5', label: '北京烤鸭' }
            ]);
        }, 1000);
    });
}

// 空数据
export function api1() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve([]);
        }, 1000);
    });
}

// 通讯中
export function api2() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve([
                { value: '选项1', label: '黄金糕' },
                { value: '选项2', label: '双皮奶' },
                { value: '选项3', label: '蚵仔煎' },
                { value: '选项4', label: '龙须面' },
                { value: '选项5', label: '北京烤鸭' }
            ]);
        }, 100000);
    });
}

// 接口挂了
export function api3() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            reject()
        }, 1000);
    });
}

这四个组件,描述的四种业务通讯状态,而通讯状态对应的用户反应预期是不一样的

  • 有数据: 正常使用
  • 无数据:找后台录入
  • 通讯中:等
  • 挂了:刷新

对于后三种都显示为无数据,会产生与预期不符的场景

为什么二次

ui/pm 在描述组件细节时有详细的交互要求,包括如下内容

  • 如果下拉的数据源请求处于loading状态,显示loading的界面[转圈圈]
  • 如果下拉的数据源请求处于error状态,此时需要显示错误文案,并提供ReTry按钮
  • 如果没有数据,显示无数据界面
  • 可能[会显示更多的信息]
  • 正确的管理下拉z-index级别
  • overflow 异常时,不要出问题
  • url的生命周期 abort/loading/error/success,接口的单例 [技术侧]
    • url 是否专门为某个组件服务
    • 再次请求时加载策略
    • ...

而通常的UI组件提供的属性管理的方式,提供每一帧的样式,至于通讯这种状态,需要业务人员自行处理

上面的需求,是每个下拉都要有的需求,显然我们需要进行二次封装

封装

调用页

<template>
    <div id="app">
        <div>正常 {{ v }}
            <xxSelect v-model="v" :api="api" clearable>
                <template v-slot:option="{ item }">
                    <div style="display: flex;">
                        <div style="flex:1">
                            {{ item.label }}
                        </div>
                        <div>
                            <el-tag size="mini">{{ item.value }}</el-tag>
                        </div>
                    </div>
                </template>
            </xxSelect>
        </div>
        <div>空数据 {{ v1 }}
            <xxSelect v-model="v1" :api="api1">

            </xxSelect>
        </div>
        <div>通讯中 {{ v2 }}
            <xxSelect v-model="v2" :api="api2">

            </xxSelect>
        </div>
        <div>接口挂了 {{ v3 }}
            <xxSelect v-model="v3" :api="api3">

            </xxSelect>
        </div>
    </div>
</template>
<script>
import { api, api1, api2, api3 } from './api'
import xxSelect from './xxSeelct.vue'
export default {
    components: {
        xxSelect
    },
    data() {
        return {
            v: "",
            v1: "",
            v2: "",
            v3: ""
        }
    },
    methods: {
        api,
        api1,
        api2,
        api3
    }
}
</script>

封装内容

<template>
    <el-select v-bind="$attrs" v-on="$listeners" @visible-change="visibleChangeHanlder">
        <el-option v-for="item in data" :key="item[props.key]" :label="item[props.label]" :value="item[props.value]">
            <slot name="option" :item="item"></slot>
        </el-option>
        <template slot="empty">
            <div v-if="status === 1">
                loading...
            </div>
            <div v-else-if="status === 2">
                没有数据
            </div>
            <div v-else-if="status === 3">
                error
                <el-button @click="loadData">
                    reTry
                </el-button>
            </div>
        </template>
    </el-select>
</template>
<script>
/**
 * 静态组件
 * ui复用: 每一帧的状态
 * 业务组件: 产品逻辑
 * - 下拉没有数据 -- 无数据ui
 * - 下拉对应的url正在通讯 - loadingui
 * - 下拉接口挂了 - 错误ui
 * 
 * 预加载: 组件生命周期 === 接口生命周期
 * 惰性加载: 组件展示是进行请求
 * 
 * 
 * 级联,静态,api层重新维护
 * 
 */
export default {
    name: 'xxSelect',
    props: {
        props: {
            type: Object,
            default: () => ({
                key: "key",
                value: "value",
                label: "label"
            })
        },
        autoLoad: {
            type: [Boolean, String],
            default: true
        },
        api: {
            type: Function,
            required: true
        }
    },
    created() {
        if (this.autoLoad === true) {
            this.loadData()
        }
    },
    data() {
        return {
            /**
             * 0:未初始化
             * 1: 加载中
             * 2: 加载成功
             * 3: 加载失败
             */
            status: 0,
            data: []
        }
    },
    methods: {
        visibleChangeHanlder() {
            if (this.status === 0) {
                this.loadData()
            }
        },
        abort() {
            // axios/fetch 取消请求
        },
        async loadData() {
            /**
             * 默认abort:prefetch
             * 请求参数一直:使用上一次请求  preload
             * 
             */
            if (this.status === 1) {
                this.abort()
            }
            try {
                this.status = 1
                const data = await this.api()
                this.data = data
                this.status = 2
            } catch (error) {
                this.status = 3
            }
        }
    }
}
</script>

技术点

组合下的继承/复用

v-bind="$attrs"v-on="$listeners" 用于继承上一个组件传递的所有属性与事件,可以叫他一生"属性/事件穿透"吗 /dog

自定义option

插槽的基础使用方式,但这里干碎了另一种方式,optionGroup,可以再来一个组件重新使用或定义二位数组的方式处理

<el-option v-for="item in data" :key="item[props.key]" :label="item[props.label]" :value="item[props.value]">
    <slot name="option" :item="item"></slot>
</el-option>

三类状态的提供

这三种状态,在element-ui这个组件库中,只能使用empty插槽处理,这里自定义即可

注:这里涉及到请求失败后,是否显示之前数据的问题,需要记录上一次请求的数据内容,比如第二次加载失败,可以选择取消,使用上一次请求的数据[看产品需求]

<template slot="empty">
    <div v-if="status === 1">
        loading...
    </div>
    <div v-else-if="status === 2">
        没有数据
    </div>
    <div v-else-if="status === 3">
        error
        <el-button @click="loadData">
            reTry
        </el-button>
    </div>
</template>

魔术数字调整

这是模型转换的简写

props: {
    type: Object,
    default: () => ({
        key: "key",
        value: "value",
        label: "label"
    })
},

生命周期的定义

接口如果交给组件管理,那他的生命周期,一般包含两种

  • 预加载: 与组件同生命周期 autoLoad= true
  • 惰性加载:展开的时候加载 autoLoad="lazy"
  • 交给父组件管理:autoLoad = false [此处未处理]
autoLoad: {
    type: [Boolean, String],
    default: true
},

通讯状态管理

我知道有人喜欢用三个属性来替代,如 isInit、loading、error,但他实际上就是一个通讯状态的属性,用三个属性处理,有8种可能,需要处理优先级等问题

如果需要属性,可以使用computed属性,而不是使用三个属性,描述一个属性的4种可能

/**
 * 0:未初始化
 * 1: 加载中
 * 2: 加载成功
 * 3: 加载失败
 */
status: 0,

惰性加载

因为本身包含初始化状态,直接挂就行了

visibleChangeHanlder() {
    if (this.status === 0) {
        this.loadData()
    }
},

通讯取消 - 略

跟使用请求的方式有关,此处忽略

abort() {
    // axios/fetch 取消请求
},

请求

这里需要处理status === 1,即正在请求的状态,包含以下几种策略

  • 取消当前请求 -- 比如滚动下拉
  • 取消上一个请求 -- 比如分页处理,这里的abort就是他
  • 复用上一个请求 -- 比如请求参数一致
  • 排队等上一个请求 -- 比如埋点 [防止6个池子吃完,业务接口无响应]
  • ...
async loadData() {
    /**
     * 默认abort:prefetch
     * 请求参数一直:使用上一次请求  preload
     * 
     */
    if (this.status === 1) {
        this.abort()
    }
    try {
        this.status = 1
        const data = await this.api()
        this.data = data
        this.status = 2
    } catch (error) {
        this.status = 3
    }
}
转载自:https://juejin.cn/post/7376472620457934883
评论
请登录