更新@vue/cli4,用composition-api做一个节流组件

808 阅读7分钟

升级@vue/cli4

在升级@vue/cli版本4的时候遇到了各种各样的问题,不辛的是没有记录下来。只能凭借记忆中的只言片语复原回来。

npm rm -g @vue/cli
npm i -g @vue/cli

报了一个

// 使用命令清理json格式的缓存文件
npm cache clean --force
// 重新运行就安装成功了
npm i -g @vue/cli
// 查看版本,此时报了一个关于window的运行脚本权限问题(这里忘记截图了)
vue -V

通过伟大的搜索引擎找到了解决方法 PowerShell以管理员身份运行,在里面输入set-ExecutionPolicy RemoteSigned,回复Y,再次运行vue -V就可以使用了

的运行vue create xxx,在创建好的项目里面运行vue add vue-next。接下来就可以体验新版vue了

composition-api方式的节流组件

vue3文档Beta

composition-api文档

基本功能

下面要写一个无ui功能齐全的节流组件,首先要实现一个间隔函数,用来防止多次触发。例子用到验证码按钮

// components/throttle
<template>
  <div @click="start">
    <slot></slot>
  </div>
</template>
<script>
import { defineComponent, ref, onUnmounted } from 'vue'
export default defineComponent({
  name: 'throttle',
  props: {
    // 倒计时间
    seconds: {
      type: Number,
      default: 60
    }
  },
  // 数据出口
  setup (props, content) {
    // 响应式时间
    const timeRef = ref(0)
    // 定时器id
    let timer
    // 开始
    function start () {
      if (timeRef.value === 0) {
        timeRef.value = props.seconds
        content.emit('change', timeRef.value)
        startTime()
      } else {
        content.emit('ongoing', timeRef.value)
      }
    }
    // 倒计时
    function startTime () {
      clearTimeout(timer)
      timer = setTimeout(() => {
        if (timeRef.value <= 0) {
          clearTimeout(timer)
        } else {
          content.emit('change', --timeRef.value)
          startTime()
        }
      }, 1000)
    }
    // 销毁记得清除定时器
    onUnmounted(() => {
      clearTimeout(timer)
    })
    return {
      start
    }
  }
})
</script>

节流最基础的一个倒计时逻辑完成,下面父组件可以通过on接受倒计时来做相应的业务处理

// App.vue
<template>
  <div id="app">
    <throttle @ongoing="handleOngoing" @change="handleChange">
      <button>{{time ? time : '获取验证码'}}</button>
    </throttle>
  </div>
</template>

<script>
import throttle from './components/throttle'
import { defineComponent, ref } from 'vue' 
export default defineComponent({
  name: 'App',
  components: {
    throttle
  },
  setup () {
    const timeRef = ref(0)
    function handleChange (value) {
      timeRef.value = value
    }
    function handleOngoing (value) {
      console.log(`请骚等${value}秒`)
    }
    return {
      time: timeRef,
      handleOngoing,
      handleChange
    }
  }
})
</script>

点击,再点击(触发ongoing事件)。好,最基本的节流完成了

start触发权外交

思考下不足:这里我们让子组件独自处理事件在很多时候是不灵活的,例如有些情况并不是click去触发,又例如要做一些前置条件去判断是否触发。这时候把start交给父类会更好。就像收验证码需要知道手机号码是多少。 对throttle作出如下修改:

  • 把click去掉,让父组件去维护start的触发时机
  • start接受一个cd的回调函数,cd需要返回一个Boolean,true为通过,执行下去。
  • 为了使用组件时更加内聚,添加作用域插槽,这样的话父组件就可以直接在模板上使用组件的值,而不用多监听change和多一个timeRef去维护
// components/throttle
// 这里我们把click去掉
<template>
  <div>
  <!-- 插槽 prop -->
    <slot :time="timeRef"></slot>
  </div>
</template>
// components/throttle
  setup (props, content) {
  ...值
    /** 
     * 开始
     * @param {Function} cd
    */
    function start (cd) {
      if (timeRef.value === 0) {
        const b =  cd()
        if (b) {
          timeRef.value = props.seconds
          content.emit('change', timeRef.value)
          startTime()
        }
      } else {
        content.emit('ongoing', timeRef.value)
      }
    }
    ...startTime
    ...onUnmounted
    return {
      start,
      timeRef // 需要响应的值
    }
  }
// App.vue
<template>
  <div id="app">
    <input type="tel" placeholder="请输入手机号" v-model="phoneRef" maxlength="11">
    <!--使用作用域插槽-->
    <throttle ref="throttleRef" v-slot="slotProps" @ongoing="handleOngoing">
      <button @click="handleVerify">{{slotProps.time ? slotProps.time : '获取验证码'}}</button>
    </throttle>
  </div>
</template>

<script>
import throttle from './components/throttle'
import { defineComponent, ref } from 'vue' 
export default defineComponent({
  name: 'App',
  components: {
    throttle
  },
  setup () {
    const phoneRef = ref('') // 手机号码
    const throttleRef = ref(null) // 获取throttl的实例
    ...handleOngoing
    // 做验证
    function handleVerify () {
      // start转交给其他组件操作,更加灵活
      if (phoneRef.value.length === 11) {
        throttleRef.value.start(() => getCode())
      } else {
        console.warn('请输入正确的手机号码')
      }
    }
    // 假设这是个获取code的接口
    function getCode () {
      alert('发送成功')
      return true
    }
    return {
      phoneRef,
      throttleRef,
      handleOngoing,
      handleVerify
    }
  }
})
</script>

start异步问题

说回getCode这个“接口”,假设是一个异步,如果我们不做一个await之类的操作,那么start那边接受到的cd返回值就会是一个Promise,即使继续执行下去,返回的也是false。并且,为了防止调接口的时候多次点击触发cd函数,在里面添加了一个loading的值来判断

// components/throttle
// 添加一个async,然后await cd
let loading = false
async function start (cd) {
  if (timeRef.value === 0 && !loading) {
    loading = true
    const b = await cd()
    if (b) {
      timeRef.value = props.seconds
      content.emit('change', timeRef.value)
      startTime()
    }
    loading = false
  } else {
    content.emit('ongoing', timeRef.value)
  }
}

缓存功能

写到这里,已经基本完成了。但是碰到:发送验证码->倒计时->刷新->倒计时没了!!。如果是不需要后端交互,纯粹前端的话,没了的话问题不大。但是接口的时间可是实实在在的,即使没了,但是点击之后接口还是会返回时间未到的提示。身为强迫症的我,决定添加一个非必要的额外功能:缓存,用户可以采取是否开启它。

但是,缓存又引申出一个问题,缓存的key,如果只有一个启用了缓存的话还好,但是当项目有多个启用的话,又会串到一起。所以,又需要添加一个缓存id的prop。

函数

  • getCache:获取缓存
  • setCache:设置缓存
  • removeCache:清除缓存

props

  • isCache: 是否开启缓存模式
  • cacheID:缓存id。默认值0
  • cacheObject:localStorage还是sessionStorage。默认localStorage
props: {
    // 倒计时间
    seconds: {
      type: Number,
      default: 60
    },
    isCache: Boolean,
    cacheObject: {
      type: Object,
      default: () => localStorage
    },
    cacheID: {
      validator: () => true,
      default: 0,
    }
}

在这里用watch的好处是,不用在原来的start和startTime函数插入缓存逻辑

// 缓存模块
watch(() => timeRef.value, function (value) {
  if (props.isCache) {
    if (value <= 0) {
      removeCache()
    } else {
      setCache(value)
    }
  }
})
const key = 'throttle' + props.cacheID
function getCache () {
  const time = Number(props.cacheObject.getItem(key)) - Date.now()
  return time > 0 ? time / 1000 : 0
}
function setCache (seconds) {
  const time = seconds * 1000 + Date.now()
  props.cacheObject.setItem(key, time.toString())
}
function removeCache () {
  props.cacheObject.removeItem(key)
}
// 初始化
(function () {
  if (props.isCache) {
    timeRef.value = Math.round(getCache())
    startTime()
  }
  removeCache()
})()

提炼模块

composition-api带来的其中好处来了,更低成本的提炼,将面向过程方式提炼成一个个功能模块。我们在throttle目录中创建一个use.js的函数,把throttle/index.vue里面的setup时间模块和缓存模块提出来到use.js里面,组织成相应的两个函数,再export给index.vue

// components/throttle/ues.vue
import { ref } from 'vue'
export function useTime (seconds, emit) {
  const timeRef = ref(0)
  const timerRef = ref(0)
  let loading = false
  /** 
   * 开始
   * @param {Function} cd
  */
  async function start (cd) {
    if (timeRef.value === 0 && !loading) {
      loading = true
      const b = await cd()
      if (b) {
        timeRef.value = seconds
        emit('change', timeRef.value)
        startTime()
      }
      loading = false
    } else {
      emit('ongoing', timeRef.value)
    }
  }
  // 倒计时
  function startTime () {
    clearTimeout(timerRef.value)
    timerRef.value = setTimeout(() => {
      if (timeRef.value <= 0) {
        clearTimeout(timerRef.value)
      } else {
        emit('change', --timeRef.value)
        startTime()
      }
    }, 1000)
  }
  return {
    timeRef,
    timerRef,
    start,
    startTime
  }
}
export function useCache (cacheID, cacheType) {
  const key = 'throttle' + cacheID
  function getCache () {
    const time = Number(window[cacheType].getItem(key)) - Date.now()
    return time > 0 ? time / 1000 : 0
  }
  function setCache (seconds) {
    const time = seconds * 1000 + Date.now()
    window[cacheType].setItem(key, time.toString())
  }
  function removeCache () {
    window[cacheType].removeItem(key)
  }
  return {
    getCache,
    setCache,
    removeCache
  }
}
// components/throttle/index.vue
import { useTime, useCache } from './use.js'
setup (props, { emit }) {
    const {
      timeRef,
      timerRef,
      start,
      startTime
    } = useTime(props.seconds, emit)
    const { 
      getCache,
      setCache,
      removeCache
    } = useCache(props.cacheID, props.cacheType)
    // 监听
    watch(() => timeRef.value, function (value) {
      if (props.isCache) {
        if (value <= 0) {
          removeCache()
        } else {
          setCache(value)
        }
      }
    })
    // 销毁记得清除定时器
    onUnmounted(() => {
      clearTimeout(timerRef.value)
    })
    function init () {
      if (props.isCache) {
        timeRef.value = Math.round(getCache())
        startTime()
      }
      removeCache()
    }
    init()
    return {
      start,
      timeRef
    }
}

响应式props

通过提炼函数使整个组件的逻辑结构更加清晰,并且对于某部分内容的复用性也更强。但是目前碰到了一个难题,就是假如父组件传的seconds是响应式的,后面从5秒更改的10秒,throttle按目前的逻辑是无法相应更改。所以我这边创建了一个secondsRef = ref(props.second),配合watch来代理props.second。

// components/throttle/index.vue
const secondsRef = ref(props.seconds)
watch(() => props.seconds, function (value) {
  secondsRef.value = value
})
const {
  timeRef,
  timerRef,
  start,
  startTime
} = useTime(secondsRef, emit)
// components/throttle/ues.vue
export function useTime (secondsRef, emit) {
async function start (cd) {
    if (timeRef.value === 0 && !loading) {
      loading = true
      const b = await cd()
      if (b) {
        timeRef.value = secondsRef.value // 响应式的value
        emit('change', timeRef.value)
        startTime()
      }
      loading = false
    } else {
      emit('ongoing', timeRef.value)
    }
  }

结语

整个throttle就这样大功告成了,一个基础的倒计时,到功能齐全的节流,通过这个例子从中学习了composition-api的用法和思想。

下面是仓库地址,其中也是有些许改进,例如将事件传递改成vue3的v-model之类,当然,这只是抛砖引玉,能帮助到你就更好了。 github.com/hengshanMWC…