源哥带你CodeReview01

讲解在同名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)
    }
  }
}
相关推荐
小镇程序员26 分钟前
vue2 src自定义事件
前端·javascript·vue.js
炒毛豆1 小时前
vue3+echarts+ant design vue实现进度环形图
javascript·vue.js·echarts
AlgorithmAce3 小时前
Live2D嵌入前端页面
前端
nameofworld3 小时前
前端面试笔试(六)
前端·javascript·面试·学习方法·递归回溯
前端fighter3 小时前
js基本数据新增的Symbol到底是啥呢?
前端·javascript·面试
流着口水看上帝3 小时前
JavaScript完整原型链
开发语言·javascript·原型模式
guokanglun3 小时前
JavaScript数据类型判断之Object.prototype.toString.call() 的详解
开发语言·javascript·原型模式
GISer_Jing3 小时前
从0开始分享一个React项目:React-ant-admin
前端·react.js·前端框架
川石教育4 小时前
Vue前端开发子组件向父组件传参
前端·vue.js·前端开发·vue前端开发·vue组件传参
Embrace9244 小时前
为什么 Vue2会出现数据更新视图不更新 Vue3不会出现
javascript·vue.js·ecmascript