升级@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方式的节流组件
基本功能
下面要写一个无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…