Skip to content

组件通信与表单组件(三) ​

本页对应 组件通信与表单组件,共 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 访问。

常见错误 ​

  1. 现象:请求一直不返回或页面拿不到结果。
    原因:请求拦截器里忘记 return config。
    修法:请求拦截器回调末尾必须返回 config。

  2. 现象:响应拦截器报错或页面拿到的是完整响应对象。
    原因:把 response 写成 request,或 return res 而不是业务数据。
    修法:确认使用 interceptors.response.use,并按需 return res.data。

  3. 现象:请求时传了 authorization,但最终仍使用全局值。
    原因:config.headers 被直接整体赋值,局部头信息被覆盖。
    修法:先展开原局部头信息,再追加全局配置,并注意覆盖顺序。

  4. 现象:断网或请求失败后 loading 一直不消失。
    原因:只在响应成功回调里调用 hideLoading。
    修法:响应成功和失败两个回调都要隐藏 loading。

  5. 现象:调用 uni.$toast 提示未定义。
    原因:封装文件未在入口执行,或页面未触发对应事件。
    修法:在 main.js 顶部导入执行封装文件,并确认调用位置在事件回调内。

  6. 现象:Pinia store 数据在页面中不可用或为 undefined。
    原因:defineStore 的 setup 函数忘记 return,或页面未调用 useXxxStore()。
    修法:store 内返回需要暴露的数据/方法,页面中先调用对应 useXxxStore() 再使用。

练习 ​

  1. 在请求拦截器中添加全局 authorization,并在某次请求的 headers 中传入局部 authorization,验证局部值覆盖全局值。

  2. 修改响应拦截器:从响应对象中取出 data 和 statusCode,页面只接收服务端业务数据。

  3. 为请求封装 loading:默认显示 loading,支持通过 custom.loading 局部关闭,并保证请求失败时也会隐藏。

  4. 在 utils 中封装 uni.$toast,支持传入 title、icon、mask,并在测试页面点击按钮调用验证。

  5. 使用 Pinia 定义 counter store,暴露一个响应式 count,在页面中绑定显示,并预留加减按钮修改入口。

课堂画面 ​

课堂画面 01

课堂画面 02

课堂画面 03

课堂画面 04

课堂画面 05

课堂画面 06

原视频对照 ​

#视频时长
1优医问诊-配置请求拦截器12:57
2优医咨询-配置响应拦截器06:16
3优医咨询-配置请求状态12:56
4优医咨询-全局封装utils09:30
5优医咨询-Pinia介绍03:29
6优医咨询-安装并导入项目中04:45
7优医咨询-Pinia定义Store10:18

小结 ​

  1. 请求拦截器必须返回 config,响应拦截器应返回页面真正需要的数据。
  2. 局部请求头覆盖全局请求头时,关键是保留原局部配置并控制展开顺序。
  3. loading 的完整处理要覆盖成功与失败两条路径。
  4. custom 适合承载自定义请求配置,可用于控制是否显示 loading。
  5. 全局工具挂到 uni 上后,仍需要在项目入口执行封装文件。
  6. 当前项目确定使用 Vue 3 + Pinia,不再按 Vuex 思路组织状态。
  7. Pinia 的 setup 写法中,页面要使用的数据和方法必须显式 return。
  8. 页面使用 store 时,先导入并调用 useXxxStore(),再进行绑定或操作。