主题
滚动容器与页面交互(一)
本页对应 滚动容器与页面交互,共 8 段视频,约 1 小时 28 分。
学习目标
- 能够说明登录相关接口为何无需携带 token,并在请求拦截器中通过“白名单”数组判断请求路径,实现按需携带 token
- 能够使用 uni-app 的
uni-countdown组件配合v-if/v-else与响应式变量,完成点击“获取验证码”后按钮与倒计时的切换显示 - 能够将原生
uni-countdown组件复制到自定义组件目录并二次封装,通过新增showHour、showMinute等属性控制小时、分钟及其冒号的显隐,实现仅显示秒级倒计时 - 能够监听倒计时的
timeup事件,在倒计时结束时恢复“获取验证码”按钮、更新按钮文字为“再次获取”,并通过color属性调整倒计时文字颜色 - 能够按“目录分模块”的方式封装接口调用:在
service/user.js中基于uni.request封装获取验证码与验证码登录 API,并遵循统一的xxxAPI命名规范 - 能够为表单绑定
v-model与name,配置校验规则并通过ref获取表单实例调用validate(),结合try/catch捕获校验结果并将校验返回值直接作为登录接口参数提交
知识点
学习反馈:先厘清两个技术点
本节反馈先处理两个技术点:请求白名单与 scroll-view 配置;随后把倒计时、验证码接口、登录接口和表单验证串起来。整体思路是:先明确业务前提,再决定代码放在哪、如何封装、如何调试。
白名单与请求拦截器
为什么需要:不是所有接口都需要 token。登录相关接口的作用就是获取 token,此时用户本来就没有 token,或 token 已失效,再传 token 没有意义。转写稿反复强调:跟登录相关的接口不需要 token。
怎么想:拦截器必须知道“哪些接口不需要 token”。前端和后端要沟通清楚,把不需要 token 的接口路径放进一个数组,这个数组被形象地称为“白名单”。它本质上只是数组,不是神秘机制。
怎么做:按转写稿整理,核心判断是“不在白名单里才加 token”:
js
// 白名单只放“不需要 token”的接口路径,具体路径与后端确认
const whiteList = [/* 免 token 接口路径 */]
// 请求拦截器
if (!whiteList.includes(config.url)) {
config.header.Authorization = token
}易错点:不要把所有接口都统一加 token;不要写死假 token。转写稿调试时曾写死“一对腰”,后端就把它当 token,结果返回 token 错误。正确做法是先注掉假 token,等登录成功后再补正确 token。
scroll-view 二次封装
为什么需要:不封装 scroll-view,页面也能滚动、下拉刷新,但 tabbar 线条颜色只有黑白,不灵活;底部安全区、滚动条隐藏等适配要在多个页面重复处理。
怎么想:做一个“假装是页面最大盒子”的组件,内容在盒子里滚动,视觉上仍像整页滚动。这样可统一加 tabbar 线、隐藏滚动条、适配底部小条。
怎么做:内部仍是 scroll-view,高度与页面一致,把 refresher-enabled、下拉动画等做成可配置变量,事件名也由外部传入,避免写死。
vue
<scroll-view
:style="{ height: pageHeight }"
:refresher-enabled="refreshEnable"
:refresher-triggered="triggered"
>
<slot />
</scroll-view>易错点:属性不要固定写死,否则组件失去复用价值;组件文件夹名与组件名必须对应,否则页面会出现空白、组件不加载。
倒计时组件改造
为什么需要:uni-ui 的 countdown 默认会显示天、小时、分钟、秒,但业务只想显示秒;其可配置性不足。
怎么想:不直接改原始组件库,而是复制到自定义组件目录再改,避免以后重新下载组件库覆盖代码。
怎么做:给组件补 showDay、showHour、showMinute 属性,在小时、分钟及冒号处加 v-if 控制。页面使用自定义组件:
vue
<custom-countdown
:show-day="false"
:show-hour="false"
:show-minute="false"
:time="10"
@timeup="onCountDownTimeUp"
/>倒计时结束触发 timeup,把 showCountDown 设为 false,让“获取验证码/再次获取”按钮重新显示。
易错点:复制组件后忘了改组件名与文件夹名;属性名与模板变量名不一致;time 不加冒号会被当字符串,出现“期望 number,却得到 string”的警告。
获取验证码接口封装
为什么需要:页面直接散写请求不利于维护。按模块封装 API,登录相关放在 user.js,方法统一以 API 结尾,便于识别。
怎么想:先通过 v-model 拿到手机号,再调获取验证码接口;GET 参数要用 params 传递。
怎么做:
js
export const verifyCodeAPI = (data) => {
return http.get('/code', { params: data })
}调用时传 mobile 和 type,登录场景 type 为 login。
易错点:GET 不要直接写 data,要用 params;参数必须是对象;调试时先检查 token 是否被写死;接口返回通常固定包含 code、data、message。
登录接口与表单验证整合
为什么需要:点登录要把手机号和验证码提交给后端,提交前必须校验数据,否则“传了 ABC”也会请求失败。
怎么想:表单加 model、rules,给表单项加 name,通过 formRef 获取组件实例,再调用 validate()。验证成功后的返回值恰好是表单数据,可直接作为接口参数。
怎么做:
js
const formRef = ref(null)
async function onSubmit() {
try {
const result = await formRef.value.validate()
// result 即表单数据,可传给登录接口
} catch (e) {
console.log(e)
}
}易错点:form 容易写成 from,会导致后续变量全错;validate() 前要 await,否则 catch 捕获不到;规则名、name、数据字段名要三者对应;验证码不正确时要检查是否用了注册场景的验证码,而当前是登录场景。
先明确:哪些接口不需要 token
为什么需要:项目里不是所有接口都要带 token。登录相关接口的目的就是获取 token,或者因为 token 不存在、已失效才需要登录;此时再传 token 没有意义。
怎么想:先把“哪些接口不需要 token”明确出来,再在请求拦截器里判断当前请求路径是否属于这些接口。
怎么做:把不需要 token 的接口路径放进一个数组,也就是“白名单”;请求拦截器判断请求路径是否在白名单中,不在白名单里才加 token。
易错点:白名单本质就是数组,不要被名字吓住;哪些接口需要 token 要和后端确认,不能自己猜。
js
// 白名单:不需要传 token 的接口
const whiteList = ['/login', '/register', '/code']
// 请求拦截器中:不在白名单里才加 token
if (!whiteList.includes(config.url)) {
config.header.Authorization = token
}倒计时组件的显示切换
为什么需要:点击“获取验证码”后,按钮要变成倒计时提示,避免用户连续点击。
怎么想:页面上有两个元素——获取验证码按钮和倒计时组件,用 v-if / v-else 控制显示,用一个响应式变量切换。
怎么做:给按钮加点击事件,点击后把 showCountDown 设为 true;倒计时组件用 v-if="showCountDown",按钮用 v-else。
易错点:ref 要记得导入;变量名不要写错;修改值要写 .value。
vue
<button v-else @click="onGetCode">获取验证码</button>
<uni-countdown v-if="showCountDown" :second="10" />
<script setup>
import { ref } from 'vue'
const showCountDown = ref(false)
const onGetCode = () => {
showCountDown.value = true
}
</script>二次封装倒计时:只保留秒
为什么需要:uni-countdown 默认会显示天、小时、分钟、秒,但登录验证码场景通常只想要“多少秒后再次获取”。
怎么想:不改原始组件库,复制 uni-countdown 到自己的 components 目录,改造成 custom-countdown,增加 showHour、showMinute 等属性。
怎么做:在组件里找到小时变量 H、分钟变量 I,用 v-if 控制它们以及后面的冒号;在 props 里补上 showHour、showMinute。页面里使用改造后的组件,并传 :showDay="false"、:showHour="false"、:showMinute="false"。
易错点:复制组件后,组件名和文件夹名要一致,否则页面会空白;不要直接改 uni_modules 原始组件,否则重新下载会被覆盖。
vue
<custom-countdown
:second="10"
:showDay="false"
:showHour="false"
:showMinute="false"
/>倒计时结束后的“再次获取”
为什么需要:倒计时结束后要恢复可点击状态,并提示“再次获取”,交互更自然。
怎么想:倒计时组件有 timeup 事件,结束后把 showCountDown 改回 false,按钮重新显示;同时把按钮文字从“获取验证码”改成“再次获取”。
怎么做:监听 timeup,在回调里修改显示变量和按钮文字变量。
易错点:timeup 事件名不要写错;按钮文字建议用变量控制,不要写死。
vue
<custom-countdown
v-if="showCountDown"
:second="10"
@timeup="onCountDownTimeUp"
/>
<script setup>
const btnText = ref('获取验证码')
const onCountDownTimeUp = () => {
showCountDown.value = false
btnText.value = '再次获取'
}
</script>获取验证码接口的封装与调用
为什么需要:页面里直接写请求会越来越乱,接口要按模块统一管理。
怎么想:新建 service/user.js,登录相关接口都放这里;方法名统一以 API 结尾,方便识别。
怎么做:导入封装好的 http;获取验证码一般用 get,get 参数放到 params 里;调用时传手机号和 type: 'login'。
易错点:get 参数不要直接塞在 data 里,要用 params;调试时如果还带着假 token,后端可能返回 token 错误,要先去掉;手机号没传会提示 mobile 必填。
js
import http from '@/utils/http'
export const verifyCodeAPI = (data) => {
return http.get('/code', {
params: data
})
}登录接口与表单验证整合
为什么需要:用户填完手机号和验证码后,要先校验数据格式,再提交登录接口。
怎么想:表单需要 v-model、name、rules,再通过 ref 拿到表单实例,调用 validate 验证;验证通过后把结果传给登录接口。
怎么做:给表单加 :model="formData"、:rules="rules"、ref="formRef";按钮点击时 await formRef.value.validate(),成功后再调 loginByMobileAPI(result)。
易错点:form 不要写成 from;validate 要写 .value;验证失败要用 try/catch 捕获;登录接口用 post,参数直接放对象里。
js
export const loginByMobileAPI = (data) => {
return http.post('/login', data)
}js
try {
const result = await formRef.value.validate()
await loginByMobileAPI(result)
} catch (err) {
console.log(err)
}调试时还遇到:倒计时只显示秒需要改组件;组件名和文件夹名不一致会导致组件不加载;验证码接口返回 901 要检查 token;mobile 必填要检查参数;form/from 写错会连带 code 报错。把这些点逐个排掉,倒计时和验证码登录流程就能跑通。
代码示例
1. 请求封装 + 白名单(utils/request.js)
js
// utils/request.js
// 基地址以接口文档「调试」里给出的完整地址为准,不要写死测试地址
const BASE_URL = 'http://xxx.xx.xx.xx:8000'
// 白名单:存放「不需要携带 token」的接口路径
// 具体有哪些,需要和 后端 沟通确认
const whiteList = ['/login', '/register', '/password']
const http = (options) => {
const config = { ...options }
config.header = { ...(config.header || {}) }
// 关键:取反 —— 没有在白名单里的接口(没有“特权”),才需要携带 token
if (!whiteList.includes(config.url)) {
const token = uni.getStorageSync('token')
if (token) {
config.header.Authorization = token
}
}
return new Promise((resolve, reject) => {
uni.request({
url: BASE_URL + config.url,
method: config.method || 'GET',
// GET 的参数走 params,POST 的参数走 data
data: (config.method || 'GET').toUpperCase() === 'GET' ? config.params : config.data,
header: config.header,
success: (res) => resolve(res.data), // 后端固定返回 { code, data, message }
fail: (err) => reject(err)
})
})
}
export default http要点 / 易错点
whiteList本质就是一个数组,只是名字形象(有特权 = 走后门)。不要被名词唬住。- 与登录相关的接口一律不放 token:登录的目的就是获取 token,此时 token 要么不存在、要么已失效,带了没有任何意义。
- 判断一定要取反:
!whiteList.includes(config.url)—— 没在白名单里才加 token。 - 调试时如果之前手写过
Authorization: 'xxx'的假 token,一定要注释掉,否则后端会返回 token 错误。
2. 二次封装倒计时组件(components/custom-countdown/custom-countdown.vue)
vue
<template>
<view class="uni-countdown">
<text v-if="showDay" class="uni-countdown__number">{{ d }}</text>
<text v-if="showDay" class="uni-countdown__sperator">天</text>
<!-- 小时:原来只有 showDay 能控制,这里补上 showHour -->
<text v-if="showHour" class="uni-countdown__number">{{ h }}</text>
<text v-if="showHour" class="uni-countdown__sperator">:</text>
<!-- 分钟:补上 showMinute -->
<text v-if="showMinute" class="uni-countdown__number">{{ i }}</text>
<text v-if="showMinute" class="uni-countdown__sperator">:</text>
<!-- 秒:一定要显示 -->
<text class="uni-countdown__number">{{ s }}</text>
</view>
</template>
<script>
export default {
name: 'CustomCountdown',
props: {
// 原组件自带的属性
showDay: { type: Boolean, default: true },
color: { type: String, default: '#000000' },
second: { type: Number, default: 0 },
// 我们自己新增的两个属性,必须先在这里声明,模板里才能用
showHour: { type: Boolean, default: true },
showMinute: { type: Boolean, default: true }
},
data() {
return { d: 0, h: 0, i: 0, s: 0, timer: null }
},
created() {
this.start()
},
beforeUnmount() {
clearInterval(this.timer)
},
methods: {
format(total) {
this.d = Math.floor(total / 86400)
this.h = Math.floor((total % 86400) / 3600)
this.i = Math.floor((total % 3600) / 60)
this.s = total % 60
},
start() {
let total = this.second
this.format(total)
this.timer = setInterval(() => {
if (total <= 0) {
clearInterval(this.timer)
this.$emit('time-up') // 倒计时结束,通知父组件
return
}
total--
this.format(total)
}, 1000)
}
}
}
</script>
<style scoped>
.uni-countdown { display: flex; align-items: center; }
.uni-countdown__number { font-size: 26rpx; color: #00c250; }
.uni-countdown__sperator { font-size: 26rpx; margin: 0 4rpx; }
</style>要点 / 易错点
- 不要去改
uni_modules里的原始组件:以后重新下载组件库,改动会被覆盖。拷一份到自己components目录再改。 - 新增属性
showHour / showMinute必须先在props里声明,否则模板里用不了(原组件是选项式 API)。 - 小时和分钟后面的冒号也要一起加
v-if,否则会出现「:`」这种残缺显示。 - 组件名必须和文件夹名一致(
custom-countdown),否则页面里直接写标签不会自动加载,表现为一片空白。
3. 登录页:获取验证码 + 倒计时切换 + 表单验证(pages/login/mobile.vue)
vue
<template>
<view class="mobile">
<uni-forms ref="formRef" :model="formData" :rules="rules">
<uni-forms-item label="手机号" name="mobile">
<uni-easyinput v-model="formData.mobile" placeholder="请输入手机号" />
</uni-forms-item>
<uni-forms-item label="验证码" name="code">
<uni-easyinput v-model="formData.code" placeholder="请输入验证码" />
<view class="code-btn" @click="onGetCode">
<!-- 未倒计时:显示按钮文字 -->
<text v-if="!showCountDown">{{ btnText }}</text>
<!-- 倒计时中:显示文字 + 自定义倒计时组件 -->
<view v-else class="countdown-box">
<text class="tip">多少秒之后再次获取</text>
<custom-countdown
:second="10"
:show-day="false"
:show-hour="false"
:show-minute="false"
color="#00c250"
@time-up="onCountDownTimeUp"
/>
</view>
</view>
</uni-forms-item>
</uni-forms>
<button type="primary" @click="onFormSubmit">登录</button>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { verifyCodeAPI, loginByMobileAPI } from '@/service/user.js'
const formRef = ref(null)
const formData = ref({ mobile: '', code: '' })
const showCountDown = ref(false) // 控制按钮 / 倒计时 的切换
const btnText = ref('获取验证码')
// 规则名 mobile、code 必须和 uni-forms-item 的 name、以及 formData 的字段一一对应
const rules = {
mobile: {
rules: [
{ required: true, errorMessage: '请输入手机号' },
{ pattern: '^1\\d{10}$', errorMessage: '手机号格式不正确' }
]
},
code: {
rules: [
{ required: true, errorMessage: '请输入验证码' },
{ pattern: '^\\d{6}$', errorMessage: '验证码为 6 位数字' }
]
}
}
// 点击获取验证码
const onGetCode = async () => {
if (showCountDown.value) return
showCountDown.value = true // 切换成倒计时组件
try {
// 两个参数:手机号 + 场景(登录场景传 login)
const res = await verifyCodeAPI({ mobile: formData.value.mobile, type: 'login' })
console.log(res) // { code, data, message }
} catch (e) {
console.log(e)
}
}
// 倒计时结束:隐藏倒计时,显示按钮,并改文案
const onCountDownTimeUp = () => {
showCountDown.value = false
btnText.value = '再次获取'
}
// 点击登录:先校验,再调接口
const onFormSubmit = async () => {
try {
const result = await formRef.value.validate() // 校验通过,result 就是表单数据
await loginByMobileAPI(result)
} catch (e) {
console.log(e) // 校验失败或接口失败都走这里
}
}
</script>
<style scoped>
.mobile { padding: 40rpx; }
.code-btn { position: absolute; right: 20rpx; top: 0; line-height: 70rpx; }
.countdown-box { display: flex; align-items: center; }
.tip { font-size: 26rpx; color: #00c250; margin-right: 6rpx; }
</style>要点 / 易错点
- 切换用的是
v-if / v-else+ 一个ref变量(showCountDown),点击时showCountDown.value = true,.value千万别丢。 :second="10"要加冒号才是数值;不加冒号会被当成字符串,控制台报「期望 number,收到 string」的警告。time-up事件里把showCountDown改回false,同时把按钮文字改成「再次获取」,交互才完整。- 校验三步走:①
:model="formData"② 每个uni-forms-item写name(和字段名一致) ③:rules="rules",最后用formRef.value.validate()触发。 - 常见低级错误:把
form写成from(formData写成fromDate),会连带一片报错;validate()属于组件实例,必须formRef.value.validate()。 - 校验失败会抛 Promise 异常,必须
try / catch包起来,否则控制台一片红。 - 接口封装统一放在
service/user.js,方法名以API结尾(verifyCodeAPI、loginByMobileAPI),看到名字就知道是在调接口;GET 参数用params,POST 参数用data。
常见错误
登录接口也带上了 token,后端返回 901 现象:调获取验证码接口,控制台返回 token 错误 / 未登录。 原因:请求拦截器里无条件
config.header.Authorization = ...,且演示阶段还写死了一对占位引号;登录类接口本就没有或已失效 token,传了等于传垃圾。 修法:把登录相关接口路径放进白名单数组,用请求的pathname判断,未命中白名单才加 token;调试期把写死的 Authorization 注掉,登录成功后再补真 token。白名单判断写了但没生效 现象:加进数组的接口仍然带 token,或反过来所有接口都不带。 原因:拿完整 URL(含 baseURL、query)去
includes,与数组里存的相对路径对不上;或者判断前多写了一个取反。 修法:统一用路径部分比对,打印一次实际参与比较的值确认;理清语义——"在数组里=不需要 token,不在=需要"。复制倒计时组件后页面一片空白 现象:点获取验证码没反应,组件区域空白,控制台无报错。 原因:只改了文件内容,忘了把文件夹/文件名改成与组件名一致,easycom 自动加载匹配不到。 修法:自定义目录下文件夹名、组件名、模板标签三者保持一致,改完重启 dev 服务。
直接改 uni_modules 里的源码 现象:本地跑得好好的,重新下载/更新组件库后改动全没了。 原因:改的是依赖源码目录,被覆盖。 修法:把组件整体拷到
components自定义目录再改造,页面引用自定义名。给组件传了 showHour/showMinute 但不生效 现象:模板里
v-if已写,页面仍显示小时/分钟。 原因:只改了模板,没在选项式props里声明这两个属性,值传不进来。 修法:在props中补声明(含默认值),再让v-if绑定对应变量;注意小时、分钟后面的冒号也要一并控制,否则会留下孤立的:。倒计时结束后按钮回不来 现象:倒计时走完停在 0,无法再次点击获取。 原因:没监听
time-up事件,或监听了但没把控制变量置回 false。 修法:监听time-up,将showCountDown.value = false,同时把按钮文案切成"再次获取"。get 请求参数塞进 data,后端报 mobile 必填 现象:请求发出去了,返回参数缺失。 原因:get 参数要走 query,须用
params;data是给 post 用的。 修法:http.get(url, { params: {...} }),并在页面调用时把mobile和type(如 login/register)都传全。表单验证不触发或直接抛红 现象:点登录没校验,或控制台出现未捕获的 Promise 拒绝。 原因:
ref名与模板不一致、form写成from、字段name与 rules 键名不匹配;validate()失败会 reject 而没接 catch。 修法:三处命名对齐后formRef.value.validate(),外层用try/catch(await场景),成功分支直接复用返回值当请求参数。
练习
在请求拦截器中实现 token 白名单:定义存放"无需 token 接口路径"的数组,用请求路径判断,未命中才注入 Authorization。写完后验证登录接口不再返回 901,并说明为什么登录类接口天然不需要 token。
基于 uni_modules 中的 countdown 组件做二次封装:拷贝到自定义目录并改名,新增
showHour、showMinute两个属性,控制小时/分钟及其后冒号的显隐;在页面中通过属性把倒计时裁剪成"只剩秒"。写完后自测:把组件名改错会有什么表现?完成倒计时交互闭环:点击"获取验证码"→ 按钮消失、倒计时出现且前缀显示"XX 秒后再次获取"→ 结束后倒计时消失、按钮恢复且文案变为"再次获取"。要求用一个布尔变量驱动,并说明两个元素为什么必须互斥显示。
在
service/user.js中封装两个接口:获取短信验证码(get,参数 mobile、type)与手机验证码登录(post,参数 mobile、code),命名统一以API结尾。在页面中分别调用,打印返回的三个字段,并解释 get 与 post 传参位置的差别。给登录表单补齐校验:为手机号、验证码绑定
v-model与name,配置 rules,获取表单组件实例后调用validate,用try/catch处理失败提示;成功后直接把校验返回值作为登录接口参数提交。完成后故意留空提交一次,观察并说明现象。
课堂画面






原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 学习反馈 | 16:40 |
| 2 | 优医咨询-登录倒计时组件-1 | 11:05 |
| 3 | 优医咨询-倒计时组件-2 | 11:12 |
| 4 | 优医咨询-倒计时组件-3 | 05:50 |
| 5 | 优医咨询-获取验证码接口封装 | 13:43 |
| 6 | 优医咨询-获取验证码接口调用 | 09:27 |
| 7 | 优医咨询-验证码登录接口封装 | 06:54 |
| 8 | 优医咨询-验证码登录数据验证 | 13:59 |
小结
- token 不是所有接口都要传,"哪些不传"必须由前端显式声明成数组,拦截器只做判断,不做猜测。
- 登录类接口不带 token 是逻辑必然,不是偷懒:它的目的就是拿 token,此时本地要么没有、要么已失效。
- 白名单本质就是普通数组,叫这个名字只是因为它享有"免传 token"的特权,别被术语吓住。
- 公共组件的价值在于"只改一处":tabbar 下划线、底部安全区、滚动条隐藏这类增强,封装一次即可全站复用。
- 第三方组件不满足需求时,优先"拷贝出来二次封装",而不是直接改依赖源码——可配置性靠 props,可持续性靠不改原库。
- 组件改造有两处最容易漏:属性没在 props 里声明、复制后名字没对齐导致组件加载不到;这两处排查成本高,写的时候就该对上。
- 接口调用要成体系:按模块分文件、命名带统一后缀、请求参数位置(params/data)严格按方法区分,后续维护才不混乱。
- 表单校验是提交前的闸门,校验失败走 catch、成功直接复用返回值,能省掉一次手动取值的冗余代码。