讲解在同名B/D上都有,主要介绍一些跟业务无关的代码技巧
注: 在CodeReview中,部分内容主观性较大,一家之言姑妄听之
本文中的业务代码抽象,实际项目中不光嵌套的多,代码量也更大
html
<template>
<div id="app">
<el-button @click="handleClick" type="primary">提交</el-button>
</div>
</template>
<script>
// 模拟接口请求
const api = ()=>{
return new Promise((resolve) => {
setTimeout(() => {
resolve()
}, 1000)
})
}
export default {
methods:{
handleClick(){
this.$confirm('确认提交吗?').then(() => {
api()
.then(()=>{
this.$message({
type: 'success',
message: '提交成功'
})
})
.catch(()=>{
this.$message({
type: 'error',
message: '提交失败'
})
})
})
.catch(() => {
this.$message({
type: 'info',
message: '提交已取消'
})
})
}
}
}
</script>
<style>
#app {
margin-top: 15%;
text-align: center;
}
</style>
Promise的回调地狱
提交按钮中 handleClick 事件,例子中用两个回调代替,实际情况比这个复杂,包括多层嵌套和额外业务描述,这里形成了Promise类的回调地狱,他包含两类问题
- Promise的回调地狱
- 没有准确的catch拦截
这里将例子在复杂一点
javascript
function handleClick(){
this.$confirm('确认提交吗?').then(() => {
api1()
.then(()=>{
// ....
api2()
.then(()=>{
// ....
api3()
.then(()=>{
// ....
this.$message({
type: 'success',
message: '提交成功'
})
})
.catch(()=>{
this.$message({
type: 'error',
message: '提交失败'
})
})
})
})
})
.catch(() => {
this.$message({
type: 'info',
message: '提交已取消'
})
})
}
传统回调地狱
此处为传统回调地狱用作对比,其中后两个函数为success和fail 【与具体api无关】
他的问题在于函数嵌套
javascript
function handleClick() {
this.$confirm('确认提交吗?', () => {
api1({}, () => {
api2({}, () => {
api3({}, () => {
this.$message({
type: 'success',
message: '提交成功'
})
}, () => {
this.$message({
type: 'error',
message: '提交失败'
})
})
})
})
}, () => {
this.$message({
type: 'info',
message: '提交已取消'
})
})
}
有些catch没有捕获,依然可能出问题
考虑下api1/api2 出bug时,提示的数据是什么
基于Promise的实现
回调地狱恶心的地方是函数嵌套,Promise是解决回调地狱问题,但并不是说只要用到,就解决了回调地狱问题,他的核心功能是Promise的状态[成功/异常]可以对外传递
Promise本意,是期望我们能够控制一组没有嵌套的函数,虽然不想改变业务逻辑,但因为catch的传递,跟前面的表现是不一样的
javascript
function handleClick() {
this.$confirm('确认提交吗?')
.catch((error) => {
throw new Error('取消提交')
})
.then(() => {
return api1()
})
.then(() => {
return api2()
})
.then(() => {
return api3()
})
.then(res => {
// res
this.$message({
type: 'success',
message: '提交成功'
})
})
.catch((error) => {
this.$message({
type: 'info',
message: error.message || '提交错误'
})
})
}
基于async/await 的实现
async/await 已经很成熟了,无脑用async/await 即可
javascript
async function handleClick() {
try {
await this.$confirm('确认提交吗?')
await api1()
await api2()
await api3()
this.$message({
type: 'success',
message: '提交成功'
})
} catch (error) {
if (error === 'cancel') {
this.$message({
type: 'error',
message: '提交已取消'
})
} else {
this.$message({
type: 'error',
message: '提交失败,请稍后再试'
})
}
}
}
异常信息重名/异常无法判断
每个第三方库都有自己的异常体系,在我们统一处理异常时,可能会产生冲突,比较好的方式是将异常拦截,并基于class的方式进行处理,这样在处理异常时,可如下操作
注: 这个项目还没有复杂到三方库异常类型冲突,只做提醒,并不修改
javascript
async function handleClick() {
try {
await this.$confirm('确认提交吗?')
await api1()
await api2()
await api3()
this.$message({
type: 'success',
message: '提交成功'
})
} catch (error) {
if (error instanceof MessageCancelError) {
this.$message({
type: 'error',
message: '提交已取消'
})
} else if (error instanceof AxiosError) {
this.$message({
type: 'error',
message: '提交失败,请稍后再试'
})
} else {
// 其他错误
}
}
}
缺少异步交互
异步操作是时间不稳定的操作,在非静默操作时,是需要通知用户正在加载/请求状态的
比如下图,就是一个展示了用户加载状态的操作方式
注1: 如果使用loading,需要再data中注册,在handler中改变,在template中使用,这三件套中的data是萌新经常忽略的点[还有起名困难,N多的loading],此处也可以继续优化
注2:在严格一点,则需要异步取消,如提交期整个页面锁死,长时间未响应,用户要中断请求
注3:这部分内容与ui/pm设计有关,CodeReview不改变产品逻辑,依然是只提醒,不修正
html
<template>
<div id="app">
<el-button @click="handleClick" :loading="loading" type="primary">提交</el-button>
</div>
</template>
<script>
// 模拟接口请求
const api = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve()
}, 1000)
})
}
export default {
data(){
return {
loading: false
}
},
methods: {
async handleClick() {
try {
await this.$confirm('确认提交吗?')
this.loading = true
await api()
this.loading = false
this.$message({
type: 'success',
message: '提交成功'
})
} catch (error) {
this.loading = false
if (error === 'cancel') {
this.$message({
type: 'info',
message: '提交已取消'
})
} else {
this.$message({
type: 'error',
message: error.message ||'提交失败'
})
}
}
}
}
}
</script>
<style>
#app {
margin-top: 15%;
text-align: center;
}
</style>
静默操作
成功/失败都不能告诉用户的异步操作行为,比如埋点/预加载/日志等操作行为
handlerClick的复用
handleClick
这种函数,在这个项目中到处都有,这类函数的特点是流程结构固定,即当我们点击提交操作时,需要包含以下步骤
- 二次确认
- 具体交互
- 通讯处理
- 异常处理
- 成功处理
这五部操作中,四个是固定的,只有固体交互是需要跟后端沟通的
在面向对象中可以基于模板/继承实现类似的操作,在函数式编程中,可以使用flow实现类似的操作,有两个主要的考虑方向
handlerError
将代码分为核心函数 + 错误处理函数,核心函数只在最外层处理ui操作,错误处理函数则处理所有异常信息
javascript
// 可复用的异常处理
function handleError(fn) {
return function (...args) {
try {
fn.call(this, ...args)
} catch (error) {
// 统一对错误进行拦截
if (error === 'cancel') {
this.$message({
type: 'info',
message: '提交已取消'
})
} else {
this.$message({
type: 'error',
message: error.message || '提交失败'
})
}
}
}
}
export default {
methods: {
handleClick: handleError(async () => {
await this.$confirm('确认提交吗?')
await api()
this.$message({
type: 'success',
message: '提交成功'
})
})
}
}
</script>
注意,如果你认可这种逻辑,一定要遵守核心函数只在最外层处理ui操作这个逻辑,这意味着以下的某些处理方式是不健康的
注:核心函数里只在对外处理ui操作,也跟重绘/重排操作有关,现在用双向绑定不用这么强调,这里特指Message系列
axios的封装问题
请求接口是核心操作的一部分,当我们在核心操作中,直接操作异常而不是对外抛出异常,会遇见以下问题
- 静默操作判断 [某一时间,突然报错]
- 伪批量接口,全部报错 [同一时间大量的错误信息]
- 文案修正 [特殊场景]
- ....
如果认可核心函数 + 异步处理的逻辑,这里需要将错误分类,然后对外抛异常,异常统一由handlerError系列处理[存在多种异常处理逻辑]
流程复用
与上面的实现类似,但思考的角度不一样,是如何使用函数式编程中的组合函数/flow处理,以实现业务流程复用
- 二次确认 【handleConfirmFlow】
- 具体交互 【自定义】
- 通讯处理 【loading/abort等】
- 异常处理 【handleConfirmFlow】
- 成功处理 【自定义】
注: 这里的业务流程比较单一,只对组合函数提醒,并没有使用类似的技巧
html
<script>
function handleConfirmFlow(fn) {
return async function (...args) {
await ElConfirm('确认提交吗?')
try {
await fn.call(this, ...args)
ElMessage({
type: 'success',
message: '提交成功'
})
} catch (error) {
// 统一对错误进行拦截
if (error === 'cancel') {
ElMessage({
type: 'info',
message: '提交已取消'
})
} else {
ElMessage({
type: 'error',
message: error.message || '提交失败'
})
}
}
}
}
export default {
methods: {
handleClick: handleConfirmFlow(async () => {
const res = await api()
// res
console.log(res)
})
}
}
</script>
语法糖/挨打系列/友尽系列
如果还想玩,可以搞语法糖,语法糖需要babel支持,基本上是被打断腿系列,在自己项目里玩玩就行了
装饰器
装饰器是面向对象里的概念,我们大JSON系列是不支持的,但我们有babel,可以将这种语法进行转换
javascript
export default {
methods: {
@handleConfirmFlow
handleClick:async function() {
const res = await api()
// res
console.log(res)
}
}
}
他最大的问题是自定义语法,对高亮,语法检查等都是冲突,比如上面就没有准确的高亮
基于注释
函数上的注释,是用来替代json,进行api组合描述的
javascript
export default {
methods: {
/**
* flow: handleConfirmFlow
*/
handleClick:async function() {
const res = await api()
// res
console.log(res)
}
}
}