小程序的启动和打包
通过HBuilderX
点击工具栏里的文件 -> 新建 -> 项目(快捷键Ctrl+N)
完成后创建项目文件
右击pages页面,选择新建,选择目录,输入对应的文件名称,这样一个对应的文件就新建好了。
这个文件下这时是没有页面的
右击对应的文件,选择新建页面
在新弹出的页面中输入文件名称,如果勾选了右侧的同名目录,则会创建出一个index文件夹下,有一个index.vue文件
完成后点击pages.json文件,这个是uniapp的路由文件,点击yes,就可以把刚才创建的文件自动录入到这个路由中( 路由是自动导入的 )
打包
1.配置微信AppID 2.在HBuilderX工具栏,点击发行,选择小程序微信
通过vue-cli
vue3的js处理
npx degit dcloudio/uni-preset-vue#vite my-vue3-project
克隆 `dcloudio/uni-preset-vue` 仓库的 `vite` 分支到本地 `my-vue3-project` 文件夹。
npx degit dcloudio/uni-preset-vue#vite-alpha my-vue3-project
克隆同一仓库的 `vite-alpha` 分支到 **已存在的 `my-vue3-project` 文件夹**。
** vue2的处理**
需要全局安装 vue-cli `npm install -g @vue/cli`
使用正式版(对应HBuilderX最新正式版) vue create -p dcloudio/uni-preset-vue my-project
** 运行、发布uni-app**
npm run dev:mp-weixin
npm run build:mp-weixin
tabBar的讲解
这个美团小程序下方红圈内的部分就是tabBar部分
很多小程序都有这部分,在pages.json部分 color:字体默认颜色
selectedColor:字体选中时的颜色
borderStyle:tabBar上边框的颜色
backgroundColor: tabBar背景色
pagePath:点击时页面的路径
iconPath:菜单图标路径
selectedIconPath:点击菜单后图标路径
text:菜单文字
"tabBar": {
"color": "#535353",
"selectedColor": "#0bb584",
"borderStyle": "white",
"backgroundColor": "#ffffff",
"list": [{
"pagePath": "pages/index/index",
"iconPath": "static/resource/images/tab_index.png",
"selectedIconPath": "static/resource/images/tab_index_seled.png",
"text": "首页"
}, {
"pagePath": "pages/order/index",
"iconPath": "static/resource/images/tab_pub.png",
"selectedIconPath": "static/resource/images/tab_pub_seled.png",
"text": "订单"
},{
"pagePath": "pages/user/index",
"iconPath": "static/resource/images/tab_user.png",
"selectedIconPath": "static/resource/images/tab_user_seled.png",
"text": "我的"
}]
},
easycom自动导入引入和main注册
在使用 npm i @dcloudio/uni-ui 安装后,组件太多了,手动导入过于繁琐,设置自动导入
// pages.json
{ // 组件自动导入规则
"easycom": {
// 是否开启自动导入,查找components文件夹是否有uniapp的组件,有的话自动导入
"autoscan": true,
// 正则方式表达自定义组件匹配方式导入
"custom": {
// uni-ui 规则如下配置
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
}
},
// 其他内容
pages:[
// ...
]
}
在uniapp中,注册一个组件名称为compontens,在这个组件内的文件,其他文件使用时不需要引入就可以直接使用。这种方式为easycom引入
在main页面中,引入文件,在下方注册组件实例。也可以实现其他文件使用时不需要引入就可以直接使用
getSystemInfoSync和getCurrentPages().length,uni.navigateBack()
getSystemInfoSync可以根据不同的机型返回不同的数据,常用于页面适配
getCurrentPages获取页面站的数量,有时我们常常要返回页面,或者在不同页面的同一个位置显示不同的图片。
const pages = getCurrentPages()
console.log(pages,'pages')
navigateBack 关闭当前页面,返回上一页面或多级页面。
switchTab
跳转到 tabBar 页面,并关闭其他所有非 tabBar 页面,(注意这是跳转 tabBar页面专属的)。
uni.switchTab({url:'/pages/index/index'})
navigateTo 跳转到一个非 tabBar 页面。
在UniApp小程序中实现页面跳转
- 使用
uni.navigateTo方法(保留当前页面,跳转到新页面)
// 在点击事件中调用
uni.navigateTo({
url: '/pages/targetPage/targetPage' // 目标页面路径
})
2. 使用uni.redirectTo方法(关闭当前页面,跳转到新页面,重新创建页面 → ❌ 数据丢失)
uni.redirectTo({
url: '/pages/targetPage/targetPage'
})
3. 使用uni.switchTab方法(跳转到tabBar页面)
uni.switchTab({
url: '/pages/tabPage/tabPage' // 必须是已在pages.json中定义的tabBar页面
})
4. 使用uni.navigateBack()方法(关闭当前页面,跳转到新页面,数据存在)
uni.navigateBack({
url: '/pages/targetPage/targetPage'
})
跳转方式 | 页面栈变化 | 栈深度 | 页面实例 | 数据保持 |
| ------------ | ------------ | ------ | ----- | ---- |
| navigateTo | 添加新页面到栈顶 | ⬆️ 增加 | 新建 | - |
| navigateBack | 移除栈顶页面 | ⬇️ 减少 | 恢复之前的 | ✅ 保持 |
| redirectTo | 替换栈顶页面 | ➡️ 保持 | 重新创建 | ❌ 丢失 |
| reLaunch | 清空所有页面,添加新页面 | ➡️ 变为1 | 重新创建 | ❌ 丢失
小程序跳转传参
1. wx.navigateTo 跳转 + url 传参(适合简单数据)
适用于传递 字符串、数字、简单对象(需转成 JSON)。
// 当前页面(跳转并传参)
wx.navigateTo({
url: '/pages/detail/detail?id=123&name=test&data=' + encodeURIComponent(JSON.stringify({key: 'value'}))
})
新页面(detail)接收参数:
// detail.js
Page({
onLoad(options) {
const { id, name, data } = options; // 从url中解析
const parsedData = JSON.parse(decodeURIComponent(data)); // 解析对象
console.log(id); // 输出: 123
console.log(name); // 输出: "test"
console.log(parsedData); // 输出: {key: "value"}
}
})
注意:
- 参数会暴露在 URL 中,敏感数据不要用这种方式。
- 传递对象需用
JSON.stringify和encodeURIComponent处理。
分包
一、多分包配置示例*
在 pages.json 中声明多个分包,每个模块独立成包:
{
"subPackages": [
{
"root": "pages_power", // 充电模块分包
"pages": [
{"path": "index", "style": { /*...*/ }},
{"path": "detail", "style": { /*...*/ }}
]
},
{
"root": "pages_mall", // 商城模块分包
"pages": [
{"path": "home", "style": { /*...*/ }},
{"path": "goods", "style": { /*...*/ }}
]
},
{
"root": "pages_social", // 社交模块分包
"pages": [
{"path": "chat", "style": { /*...*/ }},
{"path": "profile", "style": { /*...*/ }}
]
}
]
}
* * *
### **二、多分包的核心优势**
1. **按需加载**
用户只有访问对应模块时才会下载该分包资源,比如:
- 访问 `/pages_power/index` → 下载 `pages_power` 分包
- 访问 `/pages_mall/home` → 下载 `pages_mall` 分包
1. **突破主包体积限制**
- 主包限制:2MB(微信小程序)
- 总分包限制:20MB(微信小程序)
- 每个分包最大:2MB(建议控制在 1MB 内)
1. **独立编译更新**
修改某个分包的代码后,只有该分包会重新编译,提升开发效率。
* * *
### **三、关键注意事项**
#### 1. 目录结构规范
text
复制
下载
├── pages # 主包页面
│ ├── home
│ └── ...
├── pages_power # 充电模块分包
│ ├── index.vue
│ └── detail.vue
├── pages_mall # 商城模块分包
│ ├── home.vue
│ └── ...
└── pages_social # 社交模块分包
├── chat.vue
└── ...
#### 2. 跳转分包页面
必须使用**完整路径**(包含分包目录名):
javascript
复制
下载
// 正确 uni.navigateTo({ url: '/pages_power/index' })
// 错误(无法找到页面) uni.navigateTo({ url: '/index' })
#### 3. 共享资源处理
- **组件/工具库**:
如果多个分包需要共用组件/工具,建议:
- 要么放在主包中(增加主包体积)
- 要么在每个分包中单独存放(增加重复代码但减少主包压力)
- **图片资源**:
推荐使用网络图片或通过 `copy-webpack-plugin` 复制到分包目录。
#### 4. 预加载策略
通过 `preloadRule` 提前加载高频使用的分包(微信小程序专属):
json
复制
下载
{ "preloadRule": { "pages/home/home": { "network": "all", "packages": ["pages_mall"] // 访问首页时预加载商城分包 } } }
* * *
### **四、分包优化技巧**
1. **合理划分模块**
- 高频功能放主包(如首页、登录页)
- 低频功能放分包(如设置页、帮助中心)
1. **控制分包体积**
单个分包建议 ≤1MB,可通过以下方式优化:
- 压缩图片资源
- 移除未使用的组件
- 使用小程序专用组件(如 `vant-weapp`)
1. **依赖分析**
使用 `webpack-bundle-analyzer` 分析包体积:
bash
复制
下载
```
npm run build:mp-weixin --report
```
* * *
### **五、常见问题解决方案**
#### Q1:分包后出现文件找不到错误?
- 检查 `root` 和 `path` 的拼写
- 确认文件实际存在于对应目录
#### Q2:如何跨分包共享组件?
- 方案一:升级到 UniApp 3.5+ 使用 [easycom 自动引入](https://uniapp.dcloud.io/component/easycom.html)
- 方案二:将组件声明为 [全局组件](https://uniapp.dcloud.io/collocation/pages.html#globalstyle)
#### Q3:测试时如何验证分包加载?
在微信开发者工具中:
1. 打开「调试器 → Network」
1. 过滤 `.wxapkg` 请求
1. 切换页面观察分包下载情况
uni.login(OBJECT) 和globalData,getApp()
在uni.login中有接口调用成功的回调函数 success,和接口调用失败的回调函数 fail
封装完成后,为了在每个页面都可以使用,要在App.vue页面中,使用globalData,将数据放在其中。
调用的时候通过getApp()API应用实例,返回我们需要的实例
onShow、onLoad、onLaunch、onHide
onUnload() {
// 返回上一页(包括侧滑返回、头部返回)时,若已选择证书则同步给上一页
if (this.selectedCertificates && this.selectedCertificates.length > 0) {
try {
uni.$emit("updateCer", { key: this.selectedCertificates })
} catch (error) {
console.error("返回时数据传递失败:", error)
}
}
},
onBackPress() {
// App/H5 端物理返回或手势返回时的兜底同步
if (this.selectedCertificates && this.selectedCertificates.length > 0) {
try {
uni.$emit("updateCer", { key: this.selectedCertificates })
} catch (error) {
console.error("返回键数据传递失败:", error)
}
}
return false
},
使用ts时的的uniapp的类型怎么解决呢
在uniapp开发时,使用的是js开发,这样在ts中就有类型问题,下载一个插件可以解决
npm i D @uni-helper/uni-ui-types
"types": [
"@dcloudio/types",
"miniprogram-api-typings",
"@uni-helper/uni-app-types",
"@uni-helper/uni-ui-types"
]
onLaunch - 全局初始化
- **作用域:** `App` 中定义(app.js)
- **触发时机:** **整个小程序启动时**(冷启动)或从后台切到前台**且小程序未被销毁**时,**仅触发一次**(整个小程序生命周期中只执行一次)。
- **主要用途:**
- 初始化全局数据
- 获取用户登录状态(`wx.login`)
- 获取系统信息(`wx.getSystemInfo`)
- 检查新版本(`wx.getUpdateManager`)
- **注意:** 这是整个小程序最早执行的生命周期函数。无法获取页面级数据。
onLoad - 页面首次加载
- **作用域:** `Page` 中定义(页面的 .js 文件)
- **触发时机:** **页面首次加载时触发**(一个页面实例化时),**一次页面生命周期内只执行一次**(除非页面被销毁后重新创建)。
- **参数:** 接收打开当前页面路径中的 `query` 参数(`options`)。
- **主要用途:**
- 接收路由参数(`options.query`)
- 初始化页面专用数据(`this.setData`)
- 根据参数发起页面特定的网络请求
- **注意:** 这是页面级生命周期中最早执行的函数。
onShow - 页面显示/切入前台
- **作用域:**
- **全局:** 在 `App` 中定义(`app.js`),表示**整个小程序**显示/切前台。
- **页面级:** 在 `Page` 中定义(页面的 .js 文件),表示**该特定页面**显示/切前台。
- **触发时机:**
- **全局:** 小程序**启动时**(紧跟在 `onLaunch` 之后),或小程序从**后台切换到前台**时。
- **页面级:**
- 页面**首次加载完成**后(紧跟在 `onLoad` 和 `onReady` 之后)。
- 从其他页面**返回**该页面时(例如 `wx.navigateBack`)。
- 小程序从**后台切换到前台**且当前显示的是该页面时。
- 从**Tab页切换**回到该页面时。
- **主要用途:**
- 更新需要**实时刷新**的数据(如用户信息、计时器、位置信息)。
- 每次页面展示时执行的逻辑(如统计页面曝光)。
- 从后台恢复时检查状态(全局 `onShow`)。
- **注意:** `onShow` 在一个页面的生命周期内**可以多次触发**。
onHide - 页面隐藏/切入后台
- **作用域:**
- **全局:** 在 `App` 中定义(`app.js`),表示**整个小程序**隐藏/切后台。
- **页面级:** 在 `Page` 中定义(页面的 .js 文件),表示**该特定页面**隐藏/切后台。
- **触发时机:**
- **全局:** 小程序**切到后台**时(用户点击右上角胶囊关闭、按Home键回桌面、切换到其他微信聊天窗口等)。
- **页面级:**
- 使用 `wx.navigateTo`、`wx.redirectTo`、`wx.switchTab`、`wx.reLaunch` **离开当前页面**时。
- 小程序**切到后台**且当前显示的是该页面时。
- 从当前页面**跳转到新的 Tab 页**时。
- **主要用途:**
- 停止消耗资源的操作(如清除定时器、停止动画、暂停音乐播放)。
- 保存临时状态。
- 执行页面隐藏时的清理工作。
- **注意:** `onHide` 在一个页面的生命周期内**可以多次触发**。
公共的css怎么设置
在App.vue中引入就行了
网络请求:uni.request uni.showLoading uni.getStorageSync('')
uni.showLoading:显示 loading 提示框, 需主动调用 uni.hideLoading 才能关闭提示框。
uni.getStorageSync(''):小程序的缓存
二次封装网络请求:使用uni.login调用登录的api,将登录的地址分切开前一部分,放在constructor实例 es6的class语法中,后一部分传递后,将两段代码拼接起来
class Utils {
constructor(){
this.baseUrl = 'https://code.itndedu.com/pz'
}
//获取用户信息
getUserInfo() {
// 调用登录的api
uni.login({
success:(res)=> {
console.log(res)
this.request({
url:'/auth/wxLogin',
data:{
code:res.code
},
success:res=>{
console.log(res,'res')
}
})
}
})
}
request(option = {
// 在用户没有传option时默认的数据为false
showLoading:false
}) {
// 判断url是否存在
if(!option.url) {
return false
}
if (option.showLoading) {
this.showLoading()
}
// http://159.75.169.224:7300/pz/auth/wxLogin
uni.request({
url:this.baseUrl + option.url,
data:option.data?option.data:{},
header:option.header ? option.header:{},
method:option.method? option.method:'GET',
success:(response)=>{
uni.hideLoading()
// 后端返回的数据是异常的
if(response.data.code != 10000){
// 将失败的结果返回出去
if(option.fail && typeof option.fail ==='function'){
option.fail(response)
}
}else{
// 将成功的结果返回
if(option.success && typeof option.success ==='function') {
option.success(response.data)
}
}
},
fail:response =>{
uni.hideLoading()
console.log(response)
}
})
}
// 创建加载的loading效果
showLoading() {
const isShowLoading = uni.getStorageSync('isShowLoading')
if (isShowLoading) {
uni.hideLoading()
uni.setStorageSync('isShowLoading',false)
}
uni.showLoading({
title:'加载中...',
complete:function(){
uni.setStorageSync('isShowLoading',true)
},
fail:function(){
uni.setStorageSync('isShowLoading',false)
}
})
}
}
export default new Utils()
uni.emit
通过 uni.emit在全局任意一个位置触发
下面两段代码:通过 uni.emit触发后将{msg:'页面更新'}传递到uni.on接收后console.log('监听到事件来自 update ,携带参数 msg 为:' + data.msg);打印。
uni.$on('update',function(data){
console.log('监听到事件来自 update ,携带参数 msg 为:' + data.msg);
})
uni.$emit('update',{msg:'页面更新'})
uni.chooseAddress(OBJECT)和requiredPrivateInfos
// 点击收件信息
const onAddressChange = ()=>{
uni.chooseAddress({
success:res =>{
console.log(res)
order.address.userName = res.userName
order.address.cityName = res.cityName
order.address.countyName = res.countyName
order.address.detailInfo = res.detailInfo
},
fail:res=>{
console.log(res)
}
})
}
注意,里面要配置requiredPrivateInfos
如果是使用uniapp的话则在这里配置对应的信息。
将链接转换成二维码
引入
import UQRCode from 'uqrcodejs'
原生js实现
uniapp实现
<canvas id="qrcode" canvas-id="qrcode" style="width: 300rpx;height: 300rpx;"></canvas>
// 获取uQRCode实例
const qr = new UQRCode();
// 设置二维码内容
qr.data = res.wx_code;
// 设置二维码大小,必须与canvas设置的宽高一致
qr.size = 150;
// 调用制作二维码方法
qr.make();
// 获取canvas上下文
const canvasContext = uni.createCanvasContext('qrcode'); // 如果是组件,this必须传入
// 设置uQRCode实例的canvas上下文
qr.canvasContext = canvasContext;
// 调用绘制方法将二维码图案绘制到canvas上
qr.drawCanvas();
后端返回的是二维码图片(URL 或 base64)的处理
后端返回的不一定是 短文本券码 可能是 二维码图片的 base64/URL
// 判断是否是图片来源(http(s)地址 或 base64 图片)
isImageSource(value) {
if (!value || typeof value !== 'string') return false
if (/^https?:\/\//i.test(value)) return true
if (/^data:image\/(png|jpeg|jpg);base64,/i.test(value)) return true
// 无前缀但很长的 base64 字符串也视为图片
return value.length > 1000 && /^[A-Za-z0-9+/=]+$/.test(value)
},
// 将图片绘制到 canvas(支持网络图、dataURL、纯 base64)
drawImageToCanvas(src, size) {
const draw = (path) => {
const ctx = uni.createCanvasContext("qrcode-img", this)
ctx.clearRect(0, 0, size, size)
ctx.drawImage(path, 0, 0, size, size)
ctx.draw()
}
if (/^https?:\/\//i.test(src)) {
uni.getImageInfo({
src,
success: (res) => draw(res.path),
fail: (e) => {
console.error('getImageInfo fail', e)
}
})
} else if (/^data:image\/(png|jpeg|jpg);base64,/i.test(src)) {
const base64Data = src.split(',')[1]
const fsm = uni.getFileSystemManager && uni.getFileSystemManager()
if (fsm && typeof wx !== 'undefined' && wx.env && wx.env.USER_DATA_PATH) {
const filePath = `${wx.env.USER_DATA_PATH}/qrcode_${Date.now()}.png`
fsm.writeFile({
filePath,
data: base64Data,
encoding: 'base64',
success: () => draw(filePath),
fail: (e) => { console.error('writeFile base64 fail', e) }
})
}
} else {
// 可能是不带 dataURL 头的纯 base64 字符串
const base64Data = src
const fsm = uni.getFileSystemManager && uni.getFileSystemManager()
if (fsm && typeof wx !== 'undefined' && wx.env && wx.env.USER_DATA_PATH) {
const filePath = `${wx.env.USER_DATA_PATH}/qrcode_${Date.now()}.png`
fsm.writeFile({
filePath,
data: base64Data,
encoding: 'base64',
success: () => draw(filePath),
fail: (e) => { console.error('writeFile base64 fail', e) }
})
}
}
},
地图
1.在pages.json中添加一个数据
"plugins": {
"routePlan": {
"version": "1.0.19",
"provider": "wx603853422" //注意,这里的微信id一定要是腾讯位置服务插件完成授权的
}
},
要使用插件需要后台传递具体的坐标(经度,纬度)
const map = (lng, lat) => {
let x_pi = (3.141592653589793 * 3000) / 180;
let x = lng - 0.0065;
let y = lat - 0.006;
let z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * x_pi);
let theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * x_pi);
let lngs = z * Math.cos(theta);
let lats = z * Math.sin(theta);
return {
lng: lngs,
lat: lats
}
}
@tap和@click的区别
相同点:在uniapp中两者同样是点击事件
不同点: @click是组件被点击时触发,会有约300ms的延迟(内置处理优化了); @tap是手指触摸离开时触发,没有300ms的延迟,但是会有事件穿透;
微信小程序打包流程
1.在uniapp中的manifest.json文件中微信小程序配置中要有对应的微信小程序AddId,如果是使用其他编辑器(vscode)打开,则要在源码视图中有微信小程序AppId
2.在开发时微信开发者工具使用时会点击不校验合法域名,将其取消,将后端地址改成已经上线的合法地址,
3.打开微信公共平台,找到管理/开发管理,点击修改
5.发版,点击上传,输入版本号,
6,版本管理,在微信公共平台上可以看见对应的版本
小程序的登录
在使用小程序的时候,微信是已经登录的,那么我们只需要让微信授权就可以了,成员要在开发者名单中,且将appid在开发者工具和uniapp微信小程序中配置好
必须的流程---1
1, 小程序登录的时候必须有wx.login 方法获取到用户的临时登录凭证 code。这个 code 是后续获取用户身份标识的关键。 wx.login({ success(res) { if (res.code) { // 将code发送到开发者服务器 } } });
2,encryptedData,通过@getphonenumber事件获取
3,iv,通过@getphonenumber事件获取
(注意:现在微信不再返回 encryptedData/iv,改为返回临时 code。后端需调用新接口:)
获取手机号
open-type="getPhoneNumber" @getphonenumber="ongetphonenumber"
注意:一定要添加open-type="getPhoneNumber",不然@getphonenumber="ongetphonenumber"无法触发
必要的流程---2
1,使用wx.getUserInfo获取小程序的 iv和encryptedData,可能需要rewData,signature
getUserInfo() {
// 获取用户信息
uni.getUserInfo({
// 指定返回微信的用户数据
provider: "weixin",
success: function (infoRes) {
console.log("infoRes", infoRes)
// uni.setStorageSync("encryptedData", infoRes.encryptedData)
// uni.setStorageSync("iv", infoRes.iv)
},
fail(){
alert('已取消')
}
})
},
接着请求wx.login(),获取code,
uni.login({
provider: "weixin",
success: (res) => {
console.log("login点击获取", res)
uni.setStorageSync("wxCode", res.code)
}
})
3,前端请求后端的接口,将返回的code给到后端,得到返回的code,这个code判断用户是否注册过,
3.1 6003没有注册
3.2 200注册过
4, code码是6003的时候,返回数据unionId,openId,sessionKey我们要去注册
4.1调用后台给的注册接口,传递 wx.getUserInfo 得到的数据加上unionId,openId
{
unionId:res.data.unionId?res.data.unionId:res.data.openId(unionId可能不存在)
openId
sessionKey
signature
rawData
encryptedData
iv
}
4.2注册成功后后端会返回code代码进入下一步
5, 当code是200时,判断是否绑定手机号
5.1把token保存起来
将token存储到开发者工具中
1.wx.setStorageSync() 2.wx.getStorageSync()
或者储存在pinia中
token需要持久化,使用 persist 插件
Pinia的持久化插件
import { createPinia } from 'pinia'
import persist from 'pinia-plugin-persistedstate'
// 创建 pinia 实例
const pinia = createPinia()
// 使用持久化存储插件
pinia.use(persist)
// 默认导出,给 main.ts 使用
export default pinia
// 模块统一导出
export * from './modules/member'
完成后在需要持久化的文件下完成下面的配置
获取虚拟头像和虚拟昵称等
## wx.getUserInfo
拦截器的请求
h5的前端跨域处理
request请求封装
//通过拦截器监听request这个api // uni.addInterceptor拦截器 uni.addInterceptor('request') // 拦截前触发 invoke // 拦截后触发 returnValue
uni.addInterceptor('request',{
invoke:(config)=> {
console.log(config)
}
})
config输入内容如下所示
请求拦截器
/**
* 添加拦截器:
* 拦截 request 请求
* 拦截 uploadFile 文件上传
*
* TODO:
* 1. 非 http 开头需拼接地址
* 2. 请求超时
* 3. 添加小程序端请求头标识
* 4. 添加 token 请求头标识
*/
import { useMemberStore } from '@/stores'
const baseURL = 'https://pcapi-xiaotuxian-front-devtest.itheima.net'
// 添加拦截器
const httpInterceptor = {
// 拦截前触发invoke
// 拦截后触发returnValue
invoke(options: UniApp.RequestOptions) {
console.log('请求拦截器', options)
// 1. 非 http 开头需拼接地址 startsWith:判断字符串是否以http字符串开头
if (!options.url.startsWith('http')) {
options.url = baseURL + options.url
}
// 2. 请求超时, 默认 60s
options.timeout = 10000
// 3. 添加小程序端请求头标识
// source-client 和 client-type 的区别与使用场景
// 字段名称 典型值 作用场景 示例说明
// source-client 'miniapp' 标识请求来源的具体客户端类型 微信小程序、支付宝小程序、H5 等具体平台
// client-type 'mobile' 标识请求来源的设备大类 移动端、PC 端、TV 端等宏观分类
// 'weapp' 更细分的平台标识 专指微信小程序
// 'ios'/'android' 操作系统类型 区分 iOS 和 Android 原生应用
// **使用场景**:后端可能需要根据不同的客户端返回不同的数据。例如:
// - 小程序端可能需要某些特定的字段,而Web端不需要。
// - 或者针对不同平台进行不同的业务逻辑处理(如支付方式在不同平台可能不同)。 注意请求头怎么写要和后端商量
options.header = {
// 开始的时候...options.header是空的,所以这里只会有'source-client': 'miniapp'
...options.header, //你如果有header的话,会合并到一起,这样写不会覆盖你自己的header
'source-client': 'miniapp',
}
// 4. 添加 token 请求头标识
const memberStore = useMemberStore()
// 如果 `memberStore.profile` 是 `null` 或 `undefined`,那么整个表达式就返回 `undefined`,
// 而不会尝试去访问 `token` 属性,从而避免出现因为访问 `null` 或 `undefined` 上的属性而导致的运行时错误。
// 这是一种安全访问嵌套对象属性的方式。如果用户清理了用户信息,那么 `token` 就会是 `undefined`,不会添加到请求头中。
const token = memberStore.profile?.token
// console.log('token', token)
if (token) {
// 在options.header中是没有Authorization这个属性的,这种写法可以加上Authorization属性并赋值为token
options.header.Authorization = token
}
},
}
// 拦截 request 请求
uni.addInterceptor('request', httpInterceptor)
// 拦截 uploadFile 文件上传
uni.addInterceptor('uploadFile', httpInterceptor)
响应拦截器
// 2.2 添加类型,支持泛型
// 这段代码是因为uniapp的响应拦截器不够完善,我们通过Promise来包装一下,使得我们能够更好的处理响应数据。
export const http = <T>(options: UniApp.RequestOptions) => {
// 1. 返回 Promise 对象
return new Promise<Data<T>>((resolve, reject) => {
uni.request({
...options,
// 响应成功
success(res) {
// 状态码 2xx, axios 就是这样设计的
if (res.statusCode >= 200 && res.statusCode < 300) {
// 2.1 提取核心数据 res.data
resolve(res.data as Data<T>)
} else if (res.statusCode === 401) {
// 401错误 -> 清理用户信息,跳转到登录页
const memberStore = useMemberStore()
// 清理用户信息
memberStore.clearProfile()
uni.navigateTo({ url: '/pages/login/login' })
// 标记为已拒绝,不再继续执行,回传res
reject(res)
} else {
// 其他错误 -> 根据后端错误信息轻提示
uni.showToast({
icon: 'none',
title: (res.data as Data<T>).msg || '请求错误',
})
reject(res)
}
},
// 响应失败
fail(err) {
uni.showToast({
icon: 'none',
title: '网络错误,换个网络试试',
})
reject(err)
},
})
})
}