讲解在同名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)
    }
  }
}