一.项目介绍和前置内容
该项目是一个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对象,方便通过async和await获取到数据,
为了使用的数据更加简洁使用类型推断提取出核心数据: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)
},
})
})
}