主题
组件通信与表单组件(三)
本页对应 组件通信与表单组件,共 7 段视频,约 1 小时 0 分。
学习目标
- 能独立配置请求拦截器,在请求头中注入
authorization,并通过对象展开保证局部请求头覆盖全局请求头。 - 能配置响应拦截器,解构并返回接口的
data与状态码,使页面请求直接获得业务数据。 - 能使用
custom参数控制 loading 的全局与局部行为,并在请求成功或失败时正确关闭 loading、配置遮罩层。 - 能将轻提示等工具方法挂载到
uni全局对象,并在入口文件引入执行后于页面中直接调用。 - 能安装并导入 Pinia,在
main.js中创建实例并通过use注册到应用。 - 能使用
defineStore定义 Store,在 Store 中声明响应式状态并导出,在页面导入后完成数据绑定与展示。
知识点
一、为什么需要请求拦截器
为什么需要: uni.request 只能发请求,不包含业务处理。项目里基地址、请求头、token、loading、错误提示会反复写;用封装库统一配置拦截器,能减少重复代码。
怎么想: 把“每次请求都要做的事”放到请求拦截器,把“每次响应都要做的事”放到响应拦截器。拦截器本质是一个函数,在请求发出前或响应回来后执行。
怎么做: 先安装并导入请求库,创建请求实例,写测试页确认请求能发出;再配置 interceptors.request.use。
易错点: request 和 response 都以 re 开头,容易写混;转写稿里就出现过把响应拦截器写成 request,运行时报 ... is not a function,要按报错行定位。
js
http.interceptors.request.use((config) => {
console.log(config)
return config
})二、请求拦截器的基本写法
为什么需要: 请求发出前要拿到本次请求的配置,例如 header、url、custom,这样才能统一加 token 或控制 loading。
怎么想: config 是请求参数对象,包含请求头等;拦截器里可以改它,但必须 return config,否则请求出不去。
怎么做: 先打印 config 观察结构,再原样返回,验证请求正常。
易错点: 忘记 return config;把 interceptors 拼错;把 request.use 写成 response.use。
js
http.interceptors.request.use((config) => {
console.log(config)
return config
})三、添加请求头:全局配置与局部覆盖
为什么需要: 登录后通常要在请求头带 authorization 等 token。全局头可统一加,但个别请求可能需要不同 token 或覆盖值。
怎么想: 全局是默认值,局部是本次请求的明确配置,应该“局部覆盖全局”。合并对象时要注意顺序:后写的属性会覆盖前面的属性。
怎么做: 在拦截器里合并 config.header,把全局默认值放前面,把本次局部 config.header 展开在后面。
易错点: 直接 config.header = { authorization: '全局token' } 会把局部头整个覆盖;合并顺序写反也会让全局覆盖局部。转写稿调试时,局部传 authorization: '2',若顺序错,请求头最后变成全局的 1。
js
http.interceptors.request.use((config) => {
config.header = {
authorization: '全局token',
...config.header
}
return config
})四、响应拦截器:只把 data 交给页面
为什么需要: 服务端返回里除了真正数据 data,还有 statusCode、config 等。页面通常只关心 data,统一在响应拦截器里剥离更清爽。
怎么想: 成功回调里解构 data 和 statusCode,返回 data;失败回调里处理错误和 loading。config 可用于无感刷新 token,但本项目不支持刷新 token,知道有即可。
怎么做: 配置 interceptors.response.use,成功返回 data,失败先隐藏 loading。
易错点: 把 response 写成 request;返回 res 而不是 data,页面还要多点一层;失败回调不写,断网后 loading 一直不消失。
js
http.interceptors.response.use(
(res) => {
const { data, statusCode } = res
return data
},
(err) => {
uni.hideLoading()
}
)五、loading 的显示、隐藏与 custom 开关
为什么需要: 请求开始显示 loading,请求结束隐藏,能避免用户误以为页面卡死;但按钮内部已有 loading 时,不希望再出现全屏 loading。
怎么想: 请求拦截器里 showLoading,响应成功和失败都 hideLoading;用 custom.loading 做开关,局部配置覆盖全局配置。
怎么做: 请求拦截器判断 config.custom?.loading !== false 再显示;请求时传 custom: { loading: false } 可关闭。
易错点: 只在成功回调隐藏 loading,断网失败时不隐藏;custom 写错位置;局部和全局开关顺序写反。转写稿中全局 loading: true、局部 loading: false,最终应不显示 loading。
js
http.interceptors.request.use((config) => {
if (config.custom?.loading !== false) {
uni.showLoading({ mask: true })
}
return config
})
http.interceptors.response.use(
(res) => {
uni.hideLoading()
const { data } = res
return data
},
(err) => {
uni.hideLoading()
}
)
// 局部关闭 loading
http.request({
url: '/test',
custom: { loading: false }
})响应拦截器要解决什么问题
为什么需要:请求拦截器负责“发请求前加东西”,响应拦截器负责“请求回来后先处理结果”。如果每个页面都自己判断状态码、自己从完整响应里取业务数据,代码会重复,页面也会混入网络层细节。
怎么想:把响应拦截器看成请求回来后的统一收口点。页面只关心“服务端真正返回的业务数据”,完整响应对象、状态码、请求配置等先在这里处理。
怎么做:仍然使用 HTTP.interceptors.response.use,注意是 response,不是 request。先写最基础版本,把完整响应打印出来并原样返回:
js
HTTP.interceptors.response.use((res) => {
console.log(res)
return res
})页面里可以用 async/await 拿结果:
js
const result = await HTTP({ url: '/test' })
console.log(result)易错点:request 和 response 都是以 re 开头,很容易写成两个 request,或者在 use 前漏掉 interceptors。转写稿里的调试过程就是:控制台提示 response 相关方法不是函数,于是回头找 use 的调用链,发现拦截器链写错;改正后刷新页面再点请求,才能继续看结果。
先看完整响应,再决定返回什么
为什么需要:不能一上来就凭感觉 return res.data,要先确认当前请求库返回的结构。转写稿中先打印 res,发现里面有一堆数据,真正需要关心的是业务数据 data,另外还有状态码 code/statusCode。
怎么想:响应对象不是页面最终要用的数据。页面要的是接口返回的业务数据;状态码用于判断接口是否成功;config 是本次请求的参数,将来做无感刷新 token 时可能会用到,但本项目不支持刷新 token,先知道有这个字段即可。
怎么做:在响应拦截器里解构出需要的数据,只把 data 返回给页面:
js
HTTP.interceptors.response.use((res) => {
const { data, code } = res.data
// code 可用于判断接口是否成功,本节先聚焦取业务数据
return data
})此时页面里的 result 接收到的就是 return data 的值,不再是完整响应。转写稿中验证方式很直接:打开页面调用处,点击请求,看 result 打印结果是否只剩服务端返回的业务数据。
易错点:解构后不能再 return res,因为 res 已经不在当前作用域;应该 return data。另外不要把 statusCode、code、data 混在一起,先按实际打印结构确认字段,再写解构。
响应拦截器的返回值会交给谁
为什么需要:理解返回值流向,才能知道页面拿到的到底是什么。转写稿中明确:页面调用请求处的 result,接收的就是响应拦截器 return 的值。
怎么想:拦截器不是“只打印一下”,它的返回值会继续传给调用方。因此 return res 时页面拿到完整响应;return data 时页面拿到业务数据。
怎么做:保持“页面只拿业务数据”的约定。页面调用处仍然正常 await:
js
const result = await HTTP({ url: '/test' })
console.log(result) // 此时应是服务端返回的业务 data易错点:如果页面里还按完整响应去取 result.data,在拦截器改成 return data 后会多取一层,导致拿不到数据。改拦截器返回值后,要同步检查页面使用处。
本节调试顺序与易错点汇总
为什么需要:响应拦截器代码很短,但写错一个单词就会让整条请求链失败,所以要有固定排查顺序。
怎么想:先确认拦截器挂载位置对不对,再确认 response 拼写,再确认 use 的回调有没有返回值,最后看页面拿到的结果是否符合预期。
怎么做:按“写基础拦截器 → 打印完整响应 → 观察字段 → 解构并只返回 data → 页面验证”的顺序推进。转写稿中先打印、再解构、再验证 result 的变化,就是完整调试过程。
易错点:一是 request/response 混淆;二是漏写 return,导致页面拿不到结果;三是解构后仍返回 res;四是页面使用处没有跟着拦截器返回值调整。把这几处对齐,响应拦截器才算配置完成。
先明确:请求状态要解决什么问题
为什么需要:发起请求后,如果页面没有任何反馈,用户不知道是在加载、卡住还是已经失败;请求结束后如果不关闭提示,又会一直挡着页面。
怎么想:把 loading 看成请求生命周期的状态:请求拦截器里“开始”,响应拦截器成功和失败分支里“结束”。
怎么做:在请求拦截器调用 uni.showLoading,在响应拦截器调用 uni.hideLoading。
易错点:只在成功响应里关闭 loading 不够,网络断开、接口报错时也必须关闭。
js
uni.showLoading({
title: '加载中...',
mask: true
})
uni.hideLoading()在请求拦截器中显示 loading
为什么需要:请求一发出就应该给用户反馈,所以显示时机应放在请求拦截器,而不是页面每次调用接口时手写。
怎么想:请求拦截器能拿到本次请求的 config,可以在这里统一处理请求头、自定义参数和 loading。
怎么做:使用 http.interceptors.request.use,在回调里判断是否需要显示 loading,然后必须 return config。
易错点:拦截器里忘记 return config,请求会发不出去;interceptors、request 拼写要准确,不要写成两个 request。
js
http.interceptors.request.use(config => {
console.log('请求参数', config)
if (config.custom?.loading) {
uni.showLoading({
title: '加载中...',
mask: true
})
}
return config
})转写稿中调试时先打印 config,可以看到请求地址、请求头等信息。后续添加 authorization 时,要注意合并顺序:局部请求头应覆盖全局请求头,不能直接把整个 headers 重新赋值,否则会把局部配置覆盖掉。
在响应拦截器中关闭 loading
为什么需要:请求结束后 loading 必须消失,否则页面会一直处于“加载中”。
怎么想:响应拦截器同样使用 interceptors.response.use,成功时关闭 loading,同时可以把服务端返回的数据简化后再交给页面。
怎么做:成功回调里先 uni.hideLoading(),再返回 res.data.data;失败回调里也要 uni.hideLoading()。
易错点:response 不要误写成 request;返回数据前要确认页面需要的是 res.data.data 还是完整响应。
js
http.interceptors.response.use(
res => {
uni.hideLoading()
return res.data.data
},
err => {
uni.hideLoading()
return Promise.reject(err)
}
)转写稿中演示了断网后请求报错,但 loading 仍然停留的问题,因此补充了第二个失败回调。失败回调建议写成普通函数形式,便于一眼看出它是函数;用箭头函数也可以,但要注意可读性。
用 custom 控制 loading 是否显示
为什么需要:不是所有请求都适合全屏 loading,有些场景希望按钮上显示加载状态,页面不要再出现遮罩。
怎么想:给请求增加一个自定义参数 custom.loading,全局可默认开启,局部调用时可传 false 覆盖全局。
怎么做:创建请求实例时配置全局 custom: { loading: true },局部请求里传 custom: { loading: false };请求拦截器中判断 config.custom?.loading。
易错点:custom 不要拼错;全局配置应放在创建实例处,局部配置放在调用请求处;局部应覆盖全局。
js
// 创建实例时配置全局
const http = new Request({
baseURL: '...',
custom: {
loading: true
}
})
// 局部请求关闭 loading
http.request({
url: '/api/test',
custom: {
loading: false
}
})请求拦截器中统一判断:
js
if (config.custom?.loading) {
uni.showLoading({
title: '加载中...',
mask: true
})
}这样全局默认显示 loading,局部传 false 就不显示;mask: true 可以加透明遮罩,防止用户误点击。易错点是判断条件写反,或者把局部参数写到错误位置。
小结与调试思路
配置请求状态的核心不是单独写 showLoading,而是把它放进请求生命周期:请求拦截器负责开始,响应拦截器成功和失败分支负责结束。调试时先打印 config 和响应结果,确认请求参数、custom、headers 是否按预期传递;遇到“response 不是一个函数”这类报错,要回到拦截器检查是否把 request 和 response 写混。最后注意:请求头合并要让局部覆盖全局,loading 判断要允许局部覆盖全局,失败分支一定要关闭 loading。
代码示例
示例一:请求拦截器 + 响应拦截器 + loading 控制(utils/request.js)
js
// utils/request.js
import Request from 'luch-request'
// 1. 创建实例:baseURL、超时、全局自定义参数
const http = new Request({
baseURL: 'https://consult-api.itheima.net',
timeout: 10000,
// 全局 custom 配置:默认显示 loading
custom: {
loading: true
}
})
// 2. 请求拦截器
http.interceptors.request.use(
(config) => {
// 局部 header 覆盖全局 header:原配置展开在前,新配置写在后
config.header = {
...config.header,
authorization: 'Bearer xxxx'
}
// 根据 custom.loading 决定是否显示 loading(局部会覆盖全局)
if (config.custom.loading) {
uni.showLoading({ title: '加载中...', mask: true })
}
// 必须 return config,否则请求发不出去
return config
},
(err) => {
return Promise.reject(err)
}
)
// 3. 响应拦截器
http.interceptors.response.use(
(res) => {
// 请求成功也要关闭 loading
uni.hideLoading()
// 只解构业务需要的字段:data 是服务端真正返回的数据,statusCode 是状态码
const { data, statusCode } = res
// 直接把 data 返回给页面,页面拿到的就是业务数据
return data
},
(err) => {
// 请求失败(断网、超时等)也必须关闭 loading
uni.hideLoading()
return Promise.reject(err)
}
)
export default http关键行与易错点
interceptors.request.use与interceptors.response.use中,request、response两个单词长得很像,很容易写错成两个request或两个response,写完先核对一遍。- 请求拦截器的回调必须
return config,否则请求发不出去;响应拦截器的成功回调要return data,页面await到的才是业务数据。 config.header = { ...config.header, authorization: 'xxx' }:展开顺序决定覆盖顺序,后面的覆盖前面的。这样写是局部 header 覆盖全局 header;若顺序颠倒,就会变成全局覆盖局部。config.custom是 luch-request 的自定义参数,全局写在new Request({ custom: {...} })里,局部写在请求方法的参数里,局部覆盖全局。- 响应拦截器的第二个参数(失败回调)不能漏,否则断网或接口报错时 loading 会一直转。
示例二:全局挂载工具方法(utils/index.js + main.js)
js
// utils/index.js
// 把方法挂到 uni 这个全局对象上,以后不用 import 就能直接用
uni.$utils = {
// 轻提示:封装 uni.showToast
toast(title = '数据加载失败...', icon = 'none', mask = true) {
uni.showToast({
title,
icon,
mask
})
}
}js
// main.js
import { createSSRApp } from 'vue'
import App from './App.vue'
// 只需让这个 js 执行一次,就会把 $utils 挂到 uni 上
import './utils/index.js'
export function createApp() {
const app = createSSRApp(App)
return { app }
}vue
<!-- 页面中直接使用,无需 import -->
<script setup>
const onTest = () => {
uni.$utils.toast('测试')
}
</script>
<template>
<button @click="onTest">测试提示</button>
</template>关键行与易错点
uni.$utils = { ... }是给全局对象uni添加属性,写法类似原生小程序往wx上挂方法。- 挂载完必须在项目入口执行一次该 JS(
main.js顶部import './utils/index.js'),否则uni.$utils是undefined。 - 调用要写在事件回调里,不能写在
setup顶层直接执行,否则页面还没渲染提示就没了。 toast的默认参数要放在形参列表里(title = '...'),不要写成title再在函数体内赋值。
示例三:Pinia 定义 Store 并在页面使用(stores/counter.js + main.js + 页面)
js
// stores/counter.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
// defineStore(名称, setup 函数)
export const useCounterStore = defineStore('counter', () => {
// setup 里可以写任意组合式 API,ref 定义的就是响应式 state
const count = ref(0)
// 必须 return 出去,页面才能拿到
return { count }
})js
// main.js
import { createSSRApp } from 'vue'
import * as Pinia from 'pinia'
import App from './App.vue'
export function createApp() {
const app = createSSRApp(App)
// 创建并安装 Pinia
app.use(Pinia.createPinia())
return { app, Pinia }
}vue
<!-- 页面中使用 -->
<script setup>
import { useCounterStore } from '@/stores/counter'
// 调用 useXxxStore() 拿到 store 实例
const store = useCounterStore()
</script>
<template>
<view class="counter">
<button>-</button>
<input :value="store.count" />
<button>+</button>
</view>
</template>
<style>
.counter {
display: flex;
align-items: center;
padding: 20rpx;
}
.counter input {
flex: 1;
text-align: center;
}
</style>关键行与易错点
defineStore第一个参数是 store 名称(建议与文件名一致),第二个参数可以是对象,也可以是 setup 函数;本节统一用 setup 函数写法。useXxxStore的use前缀和 store 名称中的use是约定,不是语法强制,但团队内要统一。- store 中定义的
ref数据必须return,否则页面拿不到。 main.js中必须app.use(Pinia.createPinia()),且 uni-app 要求return { app, Pinia },只返回app会导致 Pinia 不生效。- 页面里
useCounterStore()必须在setup中调用一次得到实例,模板中用store.count访问。
常见错误
现象:请求一直不返回或页面拿不到结果。
原因:请求拦截器里忘记return config。
修法:请求拦截器回调末尾必须返回config。现象:响应拦截器报错或页面拿到的是完整响应对象。
原因:把response写成request,或return res而不是业务数据。
修法:确认使用interceptors.response.use,并按需return res.data。现象:请求时传了
authorization,但最终仍使用全局值。
原因:config.headers被直接整体赋值,局部头信息被覆盖。
修法:先展开原局部头信息,再追加全局配置,并注意覆盖顺序。现象:断网或请求失败后 loading 一直不消失。
原因:只在响应成功回调里调用hideLoading。
修法:响应成功和失败两个回调都要隐藏 loading。现象:调用
uni.$toast提示未定义。
原因:封装文件未在入口执行,或页面未触发对应事件。
修法:在main.js顶部导入执行封装文件,并确认调用位置在事件回调内。现象:Pinia store 数据在页面中不可用或为
undefined。
原因:defineStore的 setup 函数忘记return,或页面未调用useXxxStore()。
修法:store 内返回需要暴露的数据/方法,页面中先调用对应useXxxStore()再使用。
练习
在请求拦截器中添加全局
authorization,并在某次请求的headers中传入局部authorization,验证局部值覆盖全局值。修改响应拦截器:从响应对象中取出
data和statusCode,页面只接收服务端业务数据。为请求封装 loading:默认显示 loading,支持通过
custom.loading局部关闭,并保证请求失败时也会隐藏。在
utils中封装uni.$toast,支持传入title、icon、mask,并在测试页面点击按钮调用验证。使用 Pinia 定义
counterstore,暴露一个响应式count,在页面中绑定显示,并预留加减按钮修改入口。
课堂画面






原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 优医问诊-配置请求拦截器 | 12:57 |
| 2 | 优医咨询-配置响应拦截器 | 06:16 |
| 3 | 优医咨询-配置请求状态 | 12:56 |
| 4 | 优医咨询-全局封装utils | 09:30 |
| 5 | 优医咨询-Pinia介绍 | 03:29 |
| 6 | 优医咨询-安装并导入项目中 | 04:45 |
| 7 | 优医咨询-Pinia定义Store | 10:18 |
小结
- 请求拦截器必须返回
config,响应拦截器应返回页面真正需要的数据。 - 局部请求头覆盖全局请求头时,关键是保留原局部配置并控制展开顺序。
- loading 的完整处理要覆盖成功与失败两条路径。
custom适合承载自定义请求配置,可用于控制是否显示 loading。- 全局工具挂到
uni上后,仍需要在项目入口执行封装文件。 - 当前项目确定使用 Vue 3 + Pinia,不再按 Vuex 思路组织状态。
- Pinia 的 setup 写法中,页面要使用的数据和方法必须显式
return。 - 页面使用 store 时,先导入并调用
useXxxStore(),再进行绑定或操作。