前端进阶之路

后端接口调用学习

看懂request.js,学习接口请求封装

复制代码
import store from '@/store'
import config from '@/config'
import { getToken } from '@/utils/auth'
import errorCode from '@/utils/errorCode'
import { toast, showConfirm, tansParams } from '@/utils/common'

let timeout = 10000
const baseUrl = config.baseUrl

const request = config => {
  // 是否需要设置 token
  const isToken = (config.headers || {}).isToken === false
  config.header = config.header || {}
  if (getToken() && !isToken) {
    config.header['Authorization'] = 'Bearer ' + getToken()
  }
  // get请求映射params参数
  if (config.params) {
    let url = config.url + '?' + tansParams(config.params)
    url = url.slice(0, -1)
    config.url = url
  }
  return new Promise((resolve, reject) => {
    uni.request({
        method: config.method || 'get',
        timeout: config.timeout ||  timeout,
        url: config.baseUrl || baseUrl + config.url,
        data: config.data,
        header: config.header,
        dataType: 'json'
      }).then(response => {
        let [error, res] = response
        if (error) {
          toast('后端接口连接异常')
          reject('后端接口连接异常')
          return
        }
        const code = res.data.code || 200
        const msg = errorCode[code] || res.data.msg || errorCode['default']
        if (code === 401) {
			//加这里
			const dingLogining = uni.getStorageSync('dingLogining')
          if(!dingLogining){
			  showConfirm('登录状态已过期,您可以继续留在该页面,或者重新登录?').then(res => {
			    if (res.confirm) {
			      store.dispatch('LogOut').then(res => {
			        uni.reLaunch({ url: '/pages/login' })
			      })
			    }
			  })
		  }
          reject('无效的会话,或者会话已过期,请重新登录。')
        } else if (code === 500) {
          toast(msg)
          reject('500')
        } else if (code !== 200) {
          toast(msg)
          reject(code)
        }
        resolve(res.data)
      })
      .catch(error => {
        let { message } = error
        if (message === 'Network Error') {
          message = '后端接口连接异常'
        } else if (message.includes('timeout')) {
          message = '系统接口请求超时'
        } else if (message.includes('Request failed with status code')) {
          message = '系统接口' + message.substr(message.length - 3) + '异常'
        }
        toast(message)
        reject(error)
      })
  })
}

export default request

举个例子

  1. 你(前端页面):想吃炸鸡(需要数据)。
  2. 同事写的后台(后端):做炸鸡的餐厅(提供数据)。
  3. 接口(API):点餐的菜单和送餐的骑手。
  4. utils/request.js:你手机里的外卖APP系统。它帮你自动填地址、自动用优惠券、遇到商家关门自动弹窗提示,不用你每次点外卖都自己去搞这些繁琐的事情。

第一部分:准备工具箱(引入依赖)

复制代码
import store from '@/store'       // 仓库:存着你个人的全局状态(比如登录状态)
import config from '@/config'     // 配置文件:存着外卖软件的默认设置
import { getToken } from '@/utils/auth' // 获取通行证:拿到你的VIP会员卡号(Token)
import errorCode from '@/utils/errorCode' // 错误密码本:比如 404代表找不到店,500代表店炸了
import { toast, showConfirm, tansParams } from '@/utils/common' // 常用工具:弹窗提示、确认框、参数转换工具

let timeout = 10000               // 默认如果等了10秒(10000毫秒)还不来,就超时报错
const baseUrl = config.baseUrl    // 默认的街道地址(后端服务器的统一前缀地址)

第二部分(配置请求)

复制代码
const request = config => {
  // 1. 查验身份:是不是不需要带会员卡(Token)?
  const isToken = (config.headers || {}).isToken === false
  config.header = config.header || {}
  
  // 如果你有会员卡(Token),且这个接口没有明确说"不需要带卡",那就把卡号塞进请求头(Header)里带给后端
  if (getToken() && !isToken) {
    config.header['Authorization'] = 'Bearer ' + getToken()
  }

  // 2. 整理参数:如果是GET请求(比如你要查东西),就把你要查的条件拼在网址后面
  if (config.params) {
    let url = config.url + '?' + tansParams(config.params) // 比如变成 /api/user?name=小明&age=18
    url = url.slice(0, -1)
    config.url = url
  }

第三部分(外卖小哥出发与回来(Promise 与 uni.request))

复制代码
// Promise 就是一个"承诺",它承诺不管成功还是失败,最后一定会给你个准信儿
  return new Promise((resolve, reject) => {
    // uni.request 是小程序/App真正发请求的方法(真正的外卖小哥)
    uni.request({
        method: config.method || 'get', // 请求方式(默认GET)
        timeout: config.timeout || timeout, // 等多久
        url: config.baseUrl || baseUrl + config.url, // 去哪家店
        data: config.data, // 传给后端的具体数据
        header: config.header, // 带着刚刚贴了会员卡的请求头
        dataType: 'json'
      }).then(response => {
        // 小哥回来了! response 就是他带回来的东西

第四部分(处理结果)

复制代码
let [error, res] = response
        // 1. 小哥路上出车祸了(网络根本没通)
        if (error) {
          toast('后端接口连接异常')
          reject('后端接口连接异常') // reject 代表以失败告终
          return
        }

        // 2. 拿到餐厅给的单子,看看单子上的状态码(code)
        const code = res.data.code || 200
        const msg = errorCode[code] || res.data.msg || errorCode['default']
        
        // 3. 状态码是 401(也就是你的会员卡过期了,保安不让你进)
        if (code === 401) {
          const dingLogining = uni.getStorageSync('dingLogining')
          if(!dingLogining){
            // 弹个窗问你:登录过期了,要不要重新登录?
            showConfirm('登录状态已过期...').then(res => {
              if (res.confirm) {
                // 你点了确认,清空登录状态,跳回登录页
                store.dispatch('LogOut').then(res => {
                  uni.reLaunch({ url: '/pages/login' })
                })
              }
            })
          }
          reject('无效的会话...')
          
        // 4. 状态码是 500(餐厅厨房起火了,服务器内部错误)
        } else if (code === 500) {
          toast(msg) // 弹出错误提示
          reject('500')
          
        // 5. 状态码不是 200(200代表一切顺利,其他都是有业务毛病)
        } else if (code !== 200) {
          toast(msg)
          reject(code)
        }
        
        // 6. 历经千辛万苦,外卖没问题!把热乎的饭菜交给你(resolve)
        resolve(res.data)
      })
      // 后面还有个 .catch,是处理超时等异常的,也是统一弹窗报错,就不细说了

接口定义示例

复制代码
import request from '@/utils/request'

// 查询检查任务列表
export function listTask(query) {
  return request({
    url: '/×××/×××/list',
    method: 'get',
    params: query
  })
}

使用接口获取数据

复制代码
getDetail(taskDetailId).then(response => {
					this.form = response.data
					if (this.form.images != null) {
						this.imageList = this.form.images.split(',')
					}
					this.foodTaskRectifyResultList = response.data.foodTaskRectifyResultList
					this.foodTaskRectifyResultList.forEach(item => {
						if (item.images != null) {
							item.images = item.images.split(',')
							// console.log("item.images", item.images)
						}
					})
				})

套用这个模式;

页面布局

1.上传图片/图片显示,所有手机都能实现一行三个图片,使用函数calc();

复制代码
calc((100% - 44rpx) / 3)
//在calc函数中,加号和减号两边必须有空格,不然无法正确识别;

2.uni-app框架使用的单位是rpx,如果UI是px,那么要进行单位转换,自己代码中的rpx值=750 * 元素在设计稿中的宽度 / 设计稿基准宽度;

3.要在大布局上加font-family和font-weight,不然如果你在页面内的某些布局上没有写font-family,那么这些部分的文字会随着系统调节;

4.有时候在开发者工具找来的类名,用v-deep不起作用有三种办法:1)直接用这个类名,不要v-deep;,2)用!important;3)看看是不是外面布局约束了它,将其挪出来。

5.有时候不同系统样式的显示层级会出错,用v-show这种方式控制;

控制图片上传方式只能拍照,不允许上传

使用u-upload组件,而且支持图片回显。设置:sourceType="['camera'],就只允许手机拍照上传,不管电脑那里还是可以上传的。

复制代码
<u-upload :action="action" :header="header" ref="uUpload" name="file" multiple :maxCount="10" :show-progress="false"
							:sourceType="['camera']" :max-size="5 * 1024 * 1024" upload-text="拍摄图片" v-model="submitForm.images"></u-upload>

图片上传的逻辑梳理

复制代码
<u-upload :action="action" :file-list="FileList" :header="header" ref="uUpload"
									:show-progress="false" name="file" multiple :maxCount="10" :sourceType="['camera']"
									upload-text="拍摄图片" :max-size="5 * 1024 * 1024"
									@on-success="beforeUpload()"></u-upload>

1.使用的uView的组件(链接:uView的upload

2.参数说明

参数 说明
action 上传接口地址
fileList 显示已上传的文件列表(Array)
header 上传请求头(比如 Authorization token),通常是对象(这个代码里的header是因为我上传图片时没有令牌,无法成功,所以才加的)。
fileList 显示已上传的文件列表(Array)
ref 组件引用名,如果ref="uUpload,可通过 this.$refs.uUpload 调用组件方法
fileList 显示已上传的文件列表(Array)-----用来回显的
show-progress 上传进度条
name 上传时表单字段名,后端一般按这个字段取文件
multiple 允许多选(布尔属性,写上就表示true)
maxCount 最多允许上传文件数
sourceType 文件来源,如果写上:sourceType="['camera']",代表只允许拍照,不允许从相册选
upload-text 上传按钮文字
max-size 选择单个文件的最大大小,单位 B(byte),默认不限制(假设5MB,可以写成510241024)

3.方法说明

方法 含义
on-sucess 单个文件上传成功时触发回调

4.方法逻辑

复制代码
import {getToken} from "@/utils/auth"

data(){
	FileList:[],
	action: 'https://zcs/prod-api/file/upload',
	header: {
		Authorization: "Bearer " + getToken(),
	},
}

//上传照片实际执行链路-入口
//点击不合格按钮触发open(index),open(index)会做三件事:记录当前检查项下标:this.index = index;接着打开弹窗;然后把当前项已有的Images(逗号分隔URL字符串)转成FileList回显到上传组件
open(index){
	this.inde = index
	this.popupVisible = true
	this.FileList = []  //避免上一次打开时的图片残留
	let images = this.foodTaskDetailList[index].images
	//下面的语句,判断当前检查项有没有已保存的图片数据(images是否有值)
	if(images){
		this.FileList = images.split(',').map((item,index) => ({
			url:item
		}))
	}
	//images.split(',')把字符串按逗号切开,例如"a.jpg,b.jpg"->["a.jpg","b.jpg"],split 这个方法的设计就是"把一个字符串拆成多个片段并返回数组"
	//map((item,index) => ({url:item}))遍历数组,把每一项变成对象,所以要用()包住对象字面量。因为在箭头函数里,=>{...}默认会被当成"函数体代码块",不是对象字面量
	//明明FileList就是个数组,为啥split后已经是数组了还要转换为对象呢?因为虽然FileList是Array,但是Array只规定"外层是数组",不规定元素必须是字符串,它可以是:string[]:["a.jpg","b.jpg"],也可以是object[]:[{url:"a.jpg"},"url:"b.jpg"],也可以是混合(不推荐),这里之所以map成对象,是因为u-upload的file-list通常要求的是"文件对象数组",没想至少要有url字段,组件才能回显图片。
}

//上传照片实际执行链路-照片URL什么时候写回业务数据
//async是一个异步函数
//async beforeUpload(){
	//遍历list,取response下code=200的data里的url,用","拼接
	let url = this.$ref.uUpload.lists.map(item => item.response.data.url).join(",")
	//u-upload组件内部自己维护了lists,一张图片上传成功后,会把每项的progress、response等信息回填到lists对应项里,类似于
	{
  "code": 200,
  "data": {
    "url": "https://.../xxx.jpg"
  }
}因此是 item.response.data.url
join是拼接方法,用于将数组或列表中的所有元素连接成一个字符串。元素通过制定的分割符进行分隔。
//然后写入当前检查项:
this.foodTaskDetailList[this.index].images = url
return true
}

//其余知识补充
//JavaScript是单线程语言,意味着一次只能执行一个任务,为了避免长时间运行的任务阻塞主线程,JavaScript使用异步编程模型
//async是一个异步函数,总是返回一个Promise:如果返回值不是Promise,会自动包装成resolved Promise;如果抛出异常,会返回rejected Promise。
//await智能在async函数内部使用
//async function detchData(){
	const result = await somwPromise;
	console.log(result)
}
//async会暂停async函数的执行,等待Promise完成:如果Promise被resolve,返回resolve的值;如果Promise被reject,抛出错误(可以用try/catch捕获)
//async函数里的await是,写起来串行,跑起来异步。举个例子:
async function fetchUserData() {
  try {
    const response = await fetch('https://api.example.com/user');
    if (!response.ok) {
      throw new Error('网络响应不正常');
    }
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('获取数据失败:', error);
  }
}
//这里面的const response = await fetch(...)只有执行完毕后,才能执行 const data = await response.json();也就是fetchUserData这个函数内部,代码是串行的,但是async/await并不是卡死,而是在await的同时,主线程(Event Loop)跳出async函数,已经跑去执行别的代码了,当await后面的异步操作完成时,引擎会把之前从好的进度重新推回执行栈。

**//提出疑惑**
//beforeUpload里面既然已经将组件lists的url每一个都获取了并且拼接成了逗号分割的字符串传给了this.foodTaskList[index].images,那为什么弹窗点提交时从this.$refs.uUpload.lists里面判断有没有图片
//问题答案:因为images只在on-success时更新,如果提交时只看images,那可能会造成以下情况:
//1)用户删了图片,但是代码里面没有同步清空images,结果images还有旧url,校验会误判有图;
//2)状态滞后,用户刚选图,或还在上传中,images还没写进去;
//3)上传失败是,images还是之前的旧值,只看images会把这次失败掩盖掉。
**//提出疑惑2**
//回显的FileList是images的url逗号分割而来的,但是如果images只在on-success时更新,那我删除一张图片,然后点击取消,会什么进来的时候回显正常,删掉的哪张图不在了
//问题答案:看到"删了后再进来也没了",主要是因为现在看到的很多时候是 u-upload 组件自己的内存状态(lists),不是重新从 images 全量恢复。
**//提出疑惑3**
//但是我界面回显不是依赖fileList吗,而我的fileList是images里的url按逗号分割形成的,跟Lists没关系呀
//问题答案:这跟源码有关系,u-upload组件的实现细节:不是直接用fileList渲染,而是fileList先喂给内部lists,真正渲染的是lists。
watch: {
		fileList: {
			immediate: true,
			handler(val) {
				val.map(value => {
					// 首先检查内部是否已经添加过这张图片,因为外部绑定了一个对象给fileList的话(对象引用),进行修改外部fileList
					// 时,会触发watch,导致重新把原来的图片再次添加到this.lists
					// 数组的some方法意思是,只要数组元素有任意一个元素条件符合,就返回true,而另一个数组的every方法的意思是数组所有元素都符合条件才返回true
					let tmp = this.lists.some(val => {
						return val.url == value.url;
					})
					// 如果内部没有这个图片(tmp为false),则添加到内部
					!tmp && this.lists.push({ url: value.url, error: false, progress: 100 });
				});
			}
		},
// 用户通过ref手动的形式,移除一张图片
		remove(index) {
			// 判断索引的合法范围
			if (index >= 0 && index < this.lists.length) {
				this.lists.splice(index, 1);   //可以看到移除,也是从lists当中移除的
				this.$emit('on-list-change', this.lists, this.index);
			}
		},
//提出疑问4
//所以我的代码里压根不用把images的url按逗号分割然后传到fileList里面照样能够实现回显是吗
//images -> split -> FileList 这步还是需要的,只是现在因为组件状态保留,偶尔看起来"不传也能回显"。
//同一次打开/未销毁组件时,upload内部lists还在,可能看起来"回显";
//重新进入页面、切换到别的检查项,或组件被重建后,内部lists会丢失。这是只有把后端保存的images重新转成FileList,才能稳定回显。
//测试遗漏:没有测试一个检查项上传图片后,点击取消,再进入下一个检查项的不合格页面查看回显是否正常;
//测试遗漏:没有测试一个检查项删除图片后,点击取消,再返回到首页,然后进入自查任务页面,然后查看回显是否正常。

//上传照片实际执行链路-不合格页面点击提交时做什么
//顺序检验,检验至少要有一张上传完成的图片
const uploadedList = this.$refs.uUpload.lists.filter(item => item.progress === 100);
				if (uploadedList.length === 0) {
					uni.showToast({
						title: '请拍摄不合格图片',
						icon: 'none',
						duration: 2000
					});
					return;
				}
//根据上面的疑惑解答,可知道为什么校验是要从lists里面进行校验了

//上传照片实际执行链路-真正提交到后端
if (this.form.taskId != null) {
		updateTask(this.form).then(response => {
			this.handleUpdate()
		})
}
//因为Images我们已经准备好了,所以这里直接调用后端接口就行了。
相关推荐
米丘2 小时前
vue-router 5.x RouterView 组件是如何实现?
前端
不光头强2 小时前
HashMap知识点
java·开发语言·哈希算法
神秘的猪头2 小时前
🚀 深入浅出 Event Loop:带你彻底搞懂 JS 执行机制
前端·javascript·面试
张一凡932 小时前
easy-model 实战:跨组件通信、监听与异步加载,一库搞定 React 状态难题
前端·react.js
顺风尿一寸2 小时前
Spring事务回滚探秘:从@Transactional到数据库连接的完整旅程
java·后端
用户3076752811272 小时前
《前端细节控:如何完美实现聊天窗口的“智能自动滚动”?》
前端
前端付豪2 小时前
练习单导出
前端·python·llm
焦糖玛奇朵婷2 小时前
盲盒小程序一站式开发
java·大数据·服务器·前端·小程序
yatum_20142 小时前
VirtualBox 搭建 Hadoop-2.7.3 集群完整安装总结
java·ide·eclipse