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