uniapp项目之小兔鲜儿小程序商城(一) 项目介绍,技术栈,小程序的基础架构,封装拦截器和请求函数

一.项目介绍和前置内容

该项目是一个B2C电商平台项目

1.重要链接

项目演示: http://erabbit.itheima.net/#/
接口文档:https://www.apifox.cn/apidoc/shared/0e6ee326-d646-41bd-9214-29dbf47648fa
项目代码(从这里开始开发):git clone http://git.itcast.cn/heimaqianduan/erabbit-uni-app-vue3-ts.git

2.技术栈
  • vue3.0组合式api
  • vue-cli项目脚手架
  • axios请求接口
  • vue-router单页路由
  • vuex状态管理
  • vuex-persiststate数据持久化
  • normalize.css初始化样式
  • @vueuse/core组合api常用工具库
  • 算法Power Set
  • dayjs日期处理小组件
  • validate表单校验

二.创建uniapp项目

两种方式创建uni-app项目:HBuilderX(都是DCloud公司旗下)和命令行

1.使用HBuilderX创建
  • 创建项目:HBuilderX:创建项目>uniapp>默认模板>vue3
  • 下载插件:工具>插件安装>安装新插件:uni-app(vue3)编译器
    在这里插入图片描述

在这里插入图片描述

  • 在微信开发者工具中运行项目:运行>微信开发者工具>选择路径
    在这里插入图片描述

  • 开启服务端口:前往微信开发者工具>设置>安全服务端口:开启
    在这里插入图片描述

  • 分离窗口并固定:开发者工具只做效果展示,只留该效果窗口即可

2.使用命令行创建
vue create -p dcloudio/uni-preset-vue my-uniapp-project
3.如何使用vscode开发uniapp项目?

vscode的优势是对ts类型的支持比较友好,本项目也将使用vscode进行uniapp开发

step1:把项目拉入vscode,开始下相关插件

uni-create-view插件:快速创建uniapp页面或组件
uni-helper插件:uni-app代码提示
uniapp小程序扩展插件:鼠标悬停查文档

step2:ts类型校验
  • 安装类型声明文件:pnpm i -D @types/wechat-miniprogram @uri-helper/uni-app-types
  • 配置tsconfig.json文件
{
    ......
    "types": [
      "@dcloudio/types",
      "miniprogram-api-typings",
      "@types/wechat-miniprogram",
      "@uni-helper/uni-app-types",
      "@uni-helper/uni-ui-types"
    ]
  },
  "vueCompilerOptions": {
    // experimentalRuntimeMode 已废弃,请升级 Vue - Official 插件至最新版本
    "plugins": [
      "@uni-helper/uni-app-types/volar-plugin"
    ]
  },
step3:设置json文件可以允许注释

vscode设置>搜"文件关联">添加:

    项                值
manifest.json        jsonc
pages.json           jsonc
4.pages.json文件的作用是什么?

配置页面的路由,导航栏,tabBar等页面类信息
*有些类似routes数组

5.示例:在项目中实现一个tabbar功能

把图标在pages/static作为静态资源放在该目录下
实现tabBar步骤:

1.pages右键>新建uniapp页面:my
2.pages/pages.json中新增tabBar配置如下:
"tabBar":{
    "list":[
        {'pagePath':'pages/index/index','text':'首页','iconPath':"xxx.png",'selectedIconPath':'xxx_selected.png'},
        {'pagePath':'pages/my/my','text':'我的'}
    ]
}

3.复制图标文件到statics文件夹中,配置图标路径
6.如何在手机上查看当前项目效果?

manifest.json配置AppID,从微信小程序配置复制
在这里插入图片描述

7.uniapp和原生小程序开发的区别?

每个页面都是vue文件,数据绑定和事件处理使用vue规范

  • 数据绑定:

属性绑定不再需要写成src='{{ url }}',直接写成vue的动态绑定属性:src='url'

  • 事件绑定:

之前:bindtap='eventName'
现在:@tap='eventName',并支持传参

  • 支持vue常用指令

三. 拉取项目代码(直接从这儿开始)

如何拉取项目代码?

项目并非从零开发,直接拉取项目代码:git clone http://git.itcast.cn/heimaqianduan/erabbit-uni-app-vue3-ts.git

拉取项目代码后需要做什么?
  • manifest.json中添加微信小程序的appid
  • 初始化:pnpm install
  • 生成 list/mp-weixin文件:pnpm dev:mp-weixin
  • 微信开发者工具中导入项目mp-weixin

四.小程序的基础架构

小程序的基础架构具体实现分为:构建页面,状态管理和数据交互

1.构建页面

构建界面分为:安装uni-ui,自动引入组件,配置ts类型

1.1.安装uni-ui组件库

安装命令:npm i @dcloudio/uni-ui

1.2.让uni-ui组件库中的组件实现自动按需导入
// pages.json
{
  // 组件自动导入
  "easycom": {
    "autoscan": true,
    "custom": {
      // uni-ui 规则如下配置  
      "^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue" 
    }
  },
  "pages": [
    // …省略
  ]
}

此时可以直接使用uni-ui的相关组件:

<uni-card>
    .....
</uni-card>
1.3.配置TS类型
  • step1:安装依赖npm i -D @uni-helper/uni-ui-types
  • step2:配置tsconfig.js

确保compilerOptions.types中含有@dcloudio/types@uni-helper/uni-ui-types,且include包含了对应的vue文件

// tsconfig.json
{
  "compilerOptions": {
    // ...
    "types": [
      "@dcloudio/types", // uni-app API 类型
      "miniprogram-api-typings", // 原生微信小程序类型
      "@uni-helper/uni-app-types", // uni-app 组件类型
      "@uni-helper/uni-ui-types" // uni-ui 组件类型  
    ]
  },
  // vue 编译器类型,校验标签类型
  "vueCompilerOptions": {
    "nativeTags": ["block", "component", "template", "slot"]
  }
}
  • step3:重启vscode,成功标志----<uni-card>有了类型声明
2.状态管理(以my页的数据持久化为例)

最终目的是设置小程序端的Pinia持久化

Pinia持久化的方案与之前的写法有些许不同:
以前的持久化主要针对的是网页端的localStorage
而小程序端的持久化针对的是小程序端的api,即:uni.setStorageSync()getsetStorageSync()

step0:安装持久化存储插件pinia-plugin-persistedstate

安装命令:pnpm i pinia-plugin-persistedstate
(Pinia 用法与 Vue3 项目完全一致,uni-app 项目仅需解决持久化插件兼容性问题。)

step1:在stores/index.ts中准备好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"
step2:在main.ts中准备好相关代码
import {createSSRApp} from 'vue'
import App from './App.vue'

//导入pinia实例
import pinia from './stores'

export function createApp(){
    //创建vue实例
    const app=createSSRApp(App)
    //使用pinia
    app.use(pinia)

    return {
        app
    }
}

*若pinia类型报错,解决方法:npm i pinia

step3:在stores/modules/member.ts中提前准备好文件用于管理会员信息
import { defineStore } from 'pinia'
import { ref } from 'vue'


// 定义 Store
export const useMemberStore = defineStore(
  'member',
  () => {
    // 会员信息
    const profile = ref<any>()


    // 保存会员信息,登录时使用
    const setProfile = (val: any) => {
      profile.value = val//用户保存的信息更新到空的profile中
    }


    // 清理会员信息,退出时使用
    const clearProfile = () => {
      profile.value = undefined
    }


    // 记得 return
    return {
      profile,
      setProfile,
      clearProfile,
    }
  },
  // TODO: 持久化
  {
    persist: true,
  },
)
step4:在src/pages/my/my.vue中提前准备好页面和代码
<script setup lang="ts">
import { useMemberStore } from '@/stores'


const memberStore = useMemberStore()
</script>


<template>
  <view class="my">
    <view>会员信息:{{ memberStore.profile }}</view>
    <button
      @tap="
        memberStore.setProfile({
          nickname: '黑马先锋',
        })
      "
      size="mini"
      plain
      type="primary"
    >
      保存用户信息
    </button>
    <button @tap="memberStore.clearProfile()" size="mini" plain type="warn">清理用户信息</button>
  </view>
</template>


<style lang="scss">
//
</style>

当前的代码可以实现数据的添加和清除,但是还没有实现持久化
一刷新页面,数据就会丢失
如何实现?

step5【重点】:改member.ts中的 persist: true语句
  // TODO: 持久化
  {
    persist: true,
  },

该语句只能在网页端实现持久化,要在小程序端实现持久化,要改正如下:

  // TODO: 持久化
  {
    persist: {
        storage:{
            getItem(key){
                uni.getStorageSync(key)
            },
            setItem(key,value){
                uni.setStorageSync(key,value)
            }
        }
    }
  },

*注:此处遇到了persist的类型报错问题,后续解决

3.数据交互

封装请求工具:拦截器和请求函数

3.1.封装拦截器

通过拦截器的封装,实现request请求和uploadFile上传文件的拦截
接口文档:https://www.apifox.cn/apidoc/shared/0e6ee326-d646-41bd-9214-29dbf47648fa

uniapp中如何添加一个拦截器(语法)?
uni.addIntercepters(STRING,OBJECT),其中,
	STRING是拦截器的名称,
	OBJECT的参数是传入具体的配置(可以写一个invoke函数,在拦截前触发)
拦截器要做什么?
  • 封装请求基地址
  • 超时时间
  • 添加请求头标识
  • 添加token
如何实现对request请求和上传文件请求的拦截?(重点)
//src/utils/http.ts

import { useMemberStore } from '@/stores'
/* 请求拦截器 */
/*
添加拦截器:拦截请求和上传文件两个接口
TODO:
  1.非http开头的url,会自动拼接baseURL
  2.请求超时
  3.添加小程序端请求头标识
  4.添加token请求头标识
*/

//基地址
const baseURL = 'http://pcapi-xiaotuxian-front-devtest.itheima.net'
// 添加拦截器:拦截请求和上传文件两个接口
const httpInterceptor = {
  // 拦截器对象
  //拦截前触发的函数,其中options就是请求的配置对象
  //它会拿到uni.request({methods:xxx,url:'xxx'})中的配置对象
  invoke(options: UniApp.RequestOptions) {
    //指定参数的类型:鼠标悬停在uni.request上获取
    //1.对于非http开头的url,会自动拼接baseURL
    if (!options.url.startsWith('http')) {
      options.url = baseURL + options.url
    }
    //2.请求超时:默认10s
    options.timeout = 10000
    //3.添加小程序端请求头标识
    options.header = {
      ...options.header, //如果存在,就保留原有header
      'source-Client': 'miniapp',
    }
    //4.添加token请求头标识:登录成功后,拿到token,放到这里
    const memberStore = useMemberStore()
    const token = memberStore.profile?.token //此处的token从memberStore,profile中获取
    if (token) {
      options.header.Authorization = token
    }
    return options
  },
}
// 拦截request请求
uni.addInterceptor('request', httpInterceptor)
// 拦截上传文件请求
uni.addInterceptor('uploadFile', httpInterceptor)
验证:拦截器是否添加成功
src/pages/my/my.vue

<script setup lang="ts">
import { useMemberStore } from '@/stores'

const memberStore = useMemberStore()
import { http } from '@/utils/http' //在src/utils/http.ts中配置了拦截器,里面有基地址
//测试接口按钮
const getData = async () => {
  // uni.request({
  //   method: 'GET',
  //   url: '/home/banner',
  // })
  //为http添加类型成功后,尝试传入一个string格式的数组,测试泛型是否生效
  //此时res的类型是:Data<string[]>,会决定res.code等数据的类型
  const res = await http<string[]>({
    method: 'GET',
    url: '/home/banner',
    header: {},
  })
  console.log('请求成功:', res.code) //code的类型成功推断为string
}
</script>

<template>
  <view class="my">
    <view>会员信息:{{ memberStore.profile }}</view>
    <!-- 模拟token -->
    <button
      @tap="
        memberStore.setProfile({
          nickname: '黑马先锋',
          token: '123456',
        })
      "
      size="mini"
      plain
      type="primary"
    >
      保存用户信息
    </button>
    <button @tap="memberStore.clearProfile()" size="mini" plain type="warn">清理用户信息</button>
    <!-- 新增一个按钮 -->
    <button @tap="getData">点击</button>
  </view>
</template>

<style lang="scss">
//
</style>

3.2.封装请求函数

封装请求函数是为了更方便地发起请求

回顾:axios函数的返回值是一个Promise对象,配置async和await可以更方便获取成功的数据

为了方便使用,我们封装的请求函数也要返回一个Promise对象

同时,由于uniapp的拦截器并不完善,因此响应拦截器的功能对类型支持并不友好,因此,前面的拦截器只完成了请求前的拦截,但响应后的拦截还没设置
我们要通过自己封装的请求函数实现之前axios响应拦截器的业务功能

对于响应拦截器,又分为成功和失败两种情况

成功时,
    提取核心数据(res.data)
    添加类型(支持泛型):uni.request函数不支持添加类型,因此要通过函数封装,自己添加泛型,通过泛型来确定后续使用的具体类型
失败时,
    处理网络错误:提示用户更换网络
    401 错误:清理用户信息,跳转登录页
    其他错误:根据后端错误信息轻提示
3.2.1.请求成功的业务处理

在这里插入图片描述

  • http.ts
/* TODO:
* 1.返回Promise对象
* 2.请求成功
* 2.1.获取核心数据res.data
* 2.2.添加类型,支持泛型
* 3.请求失败
* 3.1.网络错误:提示用户换网络
* 3.2.401错误:清理用户信息,跳转登录页面
* 3.3.其他错误:根据后端返回的message提示用户
*/
//定义一个后端返回值的类型
interface Data<T> {
  code: string //状态码:'1"
  msg: string //提示信息:'请求成功'
  result: T //核心数据类型:{{}.{},{},{},{}}
}
// 2.给http添加类型(其类型可变)--泛型:T代表任意类型
//T接收到的类型用来确定my.vue中res的类型
export const http = <T>(options: UniApp.RequestOptions) => {
  //给Promise(当前是Unknown)指定响应成功的类型
  return new Promise<Data<T>>((resolve, reject) => {
    uni.request({
      ...options,
      // 请求成功
      success(res) {
        // resolve(res.data) //1.获取核心数据res.data
        //res.data类型报错,应为string|AnyObject|ArrayBuffer中的AnyObject,并准确指定为Data<T>
        //使用类型断言,将res.data的类型断言为Data<T>
        resolve(res.data as Data<T>)
        // 此时可以去my.vue中使用res进行测试
      },
    })
  })
}
  • my.vue:使用res进行测试
//测试接口按钮
const getData = async () => {
  // uni.request({
  //   method: 'GET',
  //   url: '/home/banner',
  // })
  //为http添加类型成功后,尝试传入一个string格式的数组,测试泛型是否生效
  //此时res的类型是:Data<string[]>,会决定res.code等数据的类型
  const res = await http<string[]>({
    method: 'GET',
    url: '/home/banner',
    header: {},
  })
  console.log('请求成功:', res.code) //code的类型成功推断为string
}
  • 步骤总结
    首先内部返回一个Promise对象,方便通过asyncawait获取到数据,
    为了使用的数据更加简洁使用类型推断提取出核心数据:res.data as Data<T>
    最后再为TS项目设置类型:(通过泛型实现)
3.2.2.请求失败的业务处理

uni.request的success回调函数仅仅表示服务器响应成功,但未处理状态码,业务中使用不方便

uni.request({
    ...options,
    //响应成功
    success(res){

    },
    //响应失败
    fail(err){
    
    }
})

一有响应就走success,带来的问题:
若服务器有响应,但是响应的结果是token获取失败,此时也会走success回调,这无疑在逻辑上不准确
而axios函数仅仅只有响应状态码为2xx时才调用resolve函数,表示获取数据成功,业务中使用更准确
核心代码如下:

  if (res.statusCode >= 200 && res.statusCode < 300) {
          //请求成功:2xx
          resolve(res.data as Data<T>)
        } else if (res.statusCode === 401) {
          //请求失败:401
          //清理用户信息,跳转登录页面
          uni.showToast({
            // title:'登录过期,请重新登录',
            icon: 'none',
            title: (res.data as Data<T>).msg || '登录过期,请重新登录',
          })
        } else {
          //其他错误:根据后端返回的message提示用户
          uni.showToast({
            title: (res.data as Data<T>).msg || '请求错误',
            icon: 'none',
          })
          reject(new Error((res.data as Data<T>).msg))
        }
3.2.3.http.ts完整代码
import { useMemberStore } from '@/stores'
/* 请求拦截器 */
/*
添加拦截器:拦截请求和上传文件两个接口
TODO:
  1.非http开头的url,会自动拼接baseURL
  2.请求超时
  3.添加小程序端请求头标识
  4.添加token请求头标识
*/

//请求基地址
const baseURL = 'http://pcapi-xiaotuxian-front-devtest.itheima.net'
// 添加拦截器:拦截请求和上传文件两个接口
const httpInterceptor = {
  // 拦截器对象
  //拦截前触发的函数,其中options就是请求的配置对象
  //它会拿到uni.request({methods:xxx,url:'xxx'})中的配置对象
  invoke(options: UniApp.RequestOptions) {
    //指定参数的类型:鼠标悬停在uni.request上获取
    //1.对于非http开头的url,会自动拼接baseURL
    if (!options.url.startsWith('http')) {
      options.url = baseURL + options.url
    }
    //2.请求超时:默认10s
    options.timeout = 10000
    //3.添加小程序端请求头标识
    options.header = {
      ...options.header, //如果存在,就保留原有header
      'source-Client': 'miniapp',
    }
    //4.添加token请求头标识:登录成功后,拿到token,放到这里
    const memberStore = useMemberStore()
    const token = memberStore.profile?.token //此处的token从memberStore,profile中获取
    if (token) {
      options.header.Authorization = token
    }
    return options
  },
}
// 拦截request请求
uni.addInterceptor('request', httpInterceptor)
// 拦截上传文件请求
uni.addInterceptor('uploadFile', httpInterceptor)

/* 响应拦截器 */
/**
 * 请求函数
 * @params UniApp.RequestOptions
 * @returns Promise<any>
 * TODO:
 * 1.返回Promise对象
 * 2.请求成功
 * 2.1.获取核心数据res.data
 * 2.2.添加类型,支持泛型
 * 3.请求失败
 * 3.1.网络错误:提示用户换网络
 * 3.2.401错误:清理用户信息,跳转登录页面
 * 3.3.其他错误:根据后端返回的message提示用户
 */
//定义一个后端返回值的类型
interface Data<T> {
  code: string //状态码:'1"
  msg: string //提示信息:'请求成功'
  result: T //核心数据类型:{{}.{},{},{},{}}
}

// 2.给http添加类型(其类型可变)--泛型:T代表任意类型
//T接收到的类型用来确定my.vue中res的类型
export const http = <T>(options: UniApp.RequestOptions) => {
  //给Promise(当前是Unknown)指定响应成功的类型
  return new Promise<Data<T>>((resolve, reject) => {
    uni.request({
      ...options,
      // 请求成功
      success(res) {
        // resolve(res.data) //1.获取核心数据res.data
        //res.data类型报错,应为string|AnyObject|ArrayBuffer中的AnyObject,并准确指定为Data<T>
        //使用类型断言,将res.data的类型断言为Data<T>
        // resolve(res.data as Data<T>)
        // 此时可以去my.vue中使用res进行测试

        //优化:根据后端返回的状态码,判断请求是否成功
        if (res.statusCode >= 200 && res.statusCode < 300) {
          //请求成功:2xx
          resolve(res.data as Data<T>)
        } else if (res.statusCode === 401) {
          //请求失败:401
          //清理用户信息,跳转登录页面
          uni.showToast({
            // title:'登录过期,请重新登录',
            icon: 'none',
            title: (res.data as Data<T>).msg || '登录过期,请重新登录',
          })
        } else {
          //其他错误:根据后端返回的message提示用户
          uni.showToast({
            title: (res.data as Data<T>).msg || '请求错误',
            icon: 'none',
          })
          reject(new Error((res.data as Data<T>).msg))
        }
      },
      // 请求失败
      fail(err) {
        //提示用户换网络
        uni.showToast({
          title: '请求失败,请检查您的网络',
          icon: 'none',
        })
        reject(err)
      },
    })
  })
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端OnTheRun

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值